乐趣区

关于c:重学C02-脱离指针陷阱深入浅出-C-智能指针

文章首发

【重学 C ++】02 脱离指针陷阱:深入浅出 C++ 智能指针

前言

大家好,明天是【重学 C ++】系列的第二讲,咱们来聊聊 C ++ 的智能指针。

为什么须要智能指针

在上一讲《01 C++ 如何进行内存资源管理》中,提到了对于堆上的内存资源,须要咱们手动调配和开释。治理这些资源是个技术活,一不小心,就会导致内存透露。

咱们再给两段代码,切身体验下原生指针治理内存的噩梦。

void foo(int n) {int* ptr = new int(42);
    ...
    if (n > 5) {return;}
    ...
    delete ptr;
}


void other_fn(int* ptr) {...};
void bar() {int* ptr = new int(42);
    other_fn(ptr);
    // ptr == ?
}

foo 函数中,如果入参 n > 5, 则会导致指针ptr 的内存未被正确开释,从而导致内存透露。

bar 函数中,咱们将指针 ptr 传递给了另外一个函数 other_fn,咱们无奈确定other_fn 有没有开释 ptr 内存,如果被开释了,那 ptr 将成为一个悬空指针,bar在后续还持续拜访它,会引发未定义行为,可能导致程序解体。

下面因为原生指针使用不当导致的内存透露、悬空指针问题都能够通过智能指针来轻松防止。

C++ 智能指针是一种用于治理动静分配内存的指针类。基于 RAII 设计理念,通过封装原生指针实现的。能够在资源(原生指针对应的对象)生命周期完结时主动开释内存。

C++ 规范库中,提供了两种最常见的智能指针类型,别离是 std::unique_ptrstd::shared_ptr
接下来咱们别离具体开展介绍。

吃独食的unique_ptr

std::unique_ptr 是 C++11 引入的智能指针,用于治理动态分配的内存。每个 std::unique_ptr 实例都领有对其所蕴含对象的惟一所有权,并在其生命周期完结时主动开释对象。

创立 unique_ptr 对象

咱们能够 std::unique_ptr 的构造函数或 std::make_unique 函数(C++14 反对)来创立一个 unique_ptr 对象,在超出作用域时,会主动开释所治理的对象内存。示例代码如下:

#include <memory>
#include <iostream>
class MyClass {
public:
    MyClass() {std::cout << "MyClass constructed" << std::endl;}

    ~MyClass() {std::cout << "MyClass destroyed" << std::endl;}
};
int main() {std::unique_ptr<MyClass> ptr1(new MyClass);
    // C++14 开始反对 std::make_unique
    std::unique_ptr<int> ptr2 = std::make_unique<int>(10);
    return 0;
}

代码输入:

MyClass constructed
MyClass destroyed

拜访所治理的对象

咱们能够像应用原生指针的形式一样,拜访 unique_ptr 所指向的对象。也能够通过 get 函数获取到原生指针。

MyClass* naked_ptr = ptr1.get();
std::cout << *ptr2 << std::endl; // 输入 10

开释 / 重置所治理的对象

应用 reset函数能够开释 unique_ptr 所治理的对象,并将其指针重置为 nullptr 或指定的新指针。reset` 大略实现原理如下

template<class T> 
void unique_ptr<T>::reset(pointer ptr = pointer()) noexcept { 
    // 开释指针指向的对象
    delete ptr_; 
    // 重置指针
    ptr_ = ptr;
}

该函数次要实现两件事:

  1. 开释 std::unique_ptr 所治理的对象,以防止内存透露。
  2. std::unique_ptr 重置为 nullptr 或治理另一个对象。

code show time:

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() {std::cout << "MyClass constructed" << std::endl;}

    ~MyClass() {std::cout << "MyClass destroyed" << std::endl;}
};

int main() {
    // 创立一个 std::unique_ptr 对象,指向一个 MyClass 对象
    std::unique_ptr<MyClass> ptr(new MyClass);

    // 调用 reset,将 std::unique_ptr 重置为治理另一个 MyClass 对象
    ptr.reset(new MyClass);
    return;
}

挪动所有权

一个对象资源只能同时被一个 unique_ptr 治理。当尝试把一个 unique_ptr 间接赋值给另外一个 unique_ptr 会编译报错。

#include <memory>
int main() {std::unique_ptr<int> p1 = std::make_unique<int>(42);
    std::unique_ptr<int> p2 = p1; // 编译报错
    return 0;
}

为了把一个 std::unique_ptr 对象的所有权挪动到另一个对象中,咱们必须配合 std::move 挪动函数。

#include <memory>
#include <iostream>
int main() {std::unique_ptr<int> p1 = std::make_unique<int>(42);
    std::unique_ptr<int> p2 = std::move(p1); // ok
    std::cout << *p2 << std::endl; // 42
    std::cout << (p1.get() == nullptr) << std::endl; // true
    return 0;
}

这个例子中,咱们把 p1 通过 std::move 将其治理对象的所有权转移给了 p2, 此时p2 接管了对象,而 p1 不再领有治理对象的所有权,即无奈再操作到该对象了。

乐于分享的shared_ptr

shared_ptr是 C ++11 提供的另外一种常见的智能指针,与 unique_ptr 独占对象形式不同,shared_ptr是一种共享式智能指针,容许多个 shared_ptr 指针独特领有同一个对象,采纳援用计数的形式来治理对象的生命周期。当所有的 shared_ptr 对象都销毁时,才会主动开释所治理的对象。

创立 shared_ptr 对象

同样的,C++ 也提供了 std::shared_ptr 构造函数和 std::make_shared 函数来创立 std::shared_ptr 对象。

#include <memory>
int main() {std::shared_ptr<int> p1(new int(10));
    std::shared_ptr<int> p2 = std::make_shared<int>(20);
    return;
}

多个 shared_ptr 共享一个对象

能够通过赋值操作实现多个 shared_ptr 共享一个资源对象,例如

std::shared_ptr<int>p3 = p2;

shared_ptr采纳援用计数的形式治理资源对象的生命周期,通过调配一个额定内存当计数器。

当一个新的 shared_ptr 被创立时,它对应的计数器被初始化为 1。每当赋值给另外一个 shared_ptr 共享同一个对象时,计数器值会加 1。当某个 shared_ptr 被销毁时,计数值会减 1,当计数值变为 0 时,阐明没有任何 shared_ptr 援用这个对象,会将对象进行回收。

C++ 提供了 use_count 函数来获取 std::shared_ptr 所治理对象的援用计数,例如

std::cout << "p1 use count:" << p1.use_count() << std::endl;

开释 / 重置所治理的对象

能够应用 reset 函数来开释 / 重置 shared_ptr 所治理的对象。大略实现原理如下(不思考并发场景)

void reset(T* ptr = nullptr) {if (ref_count != nullptr) {(*ref_count)--;
        if (*ref_count == 0) { 
            delete data; 
            delete ref_count; 
        } 
    } 
    data = ptr; 
    ref_count = (data == nullptr) ? nullptr : new size_t(1); 
}

data指针来存储管理的资源,指针ref_count 来存储计数器的值。

在 reset 办法中,须要缩小计数器的值,如果计数器缩小后为 0,则须要开释治理的资源,如果缩小后不为 0,则不会开释之前的资源对象。

如果 reset 指定了新的资源指针,则须要从新设置 data 和 ref_count,并将计数器初始化为 1。否则,将计数器指针置为nullptr

shared_ptr 应用注意事项

防止循环援用

因为 shared_ptr 具备共享同一个资源对象的能力,因而容易呈现循环援用的状况。例如:

struct Node {std::shared_ptr<Node> next;};

int main() {std::shared_ptr<Node> node1(new Node);
    std::shared_ptr<Node> node2(new Node); 
    node1->next = node2; 
    node2->next = node1;
}

在上述代码中,node1node2 相互援用,在析构时会发现计数器的值不为 0,不会开释所治理的对象,产生内存透露。

为了防止循环援用,能够将其中一个指针改为 weak_ptr 类型。weak_ptr也是一种智能指针,通常配合 shared_ptr 一起应用。

weak_ptr 是一种弱援用,不对所指向的对象进行计数援用,也就是说,不减少所指对象的援用计数。当所有的 shared_ptr 都析构了,不再指向该资源时,该资源会被销毁,同时对应的所有 weak_ptr 都会变成 nullptr,这时咱们就能够利用expired() 办法来判断这个 weak_ptr 是否曾经生效。

咱们能够通过 weak_ptrlock()办法来取得一个指向共享对象的 shared_ptr。如果weak_ptr 曾经生效,lock()办法将返回一个空的shared_ptr

上面是 weak_ptr 的根本应用示例:

#include <iostream>
#include <memory>

int main() {std::shared_ptr<int> sp = std::make_shared<int>(42);
    // 创立 shared_ptr 对应的 weak_ptr 指针
    std::weak_ptr<int> wp(sp);

    // 通过 lock 创立一个对应的 shared_ptr
    if (auto p = wp.lock()) {
        std::cout << "shared_ptr value:" << *p << std::endl;
        std::cout << "shared_ptr use_count:" << p.use_count() << std::endl;} else {std::cout << "wp is expired" << std::endl;}

    // 开释 shared_ptr 指向的资源,此时 weak_ptr 生效
    sp.reset();
    std::cout << "wp is expired:" <<  wp.expired() << std::endl;
    return 0;
}

代码输入如下

shared_ptr value: 42
shared_ptr use_count: 2
wp is expired: 1

回到 shared_ptr 的循环援用问题,利用 weak_ptr 不会减少 shared_ptr 的援用计数的特点,咱们将 Node.next 的类型改为weak_ptr, 防止 node1 和 node2 相互循环援用。批改后代码如下

struct Node {

std::weak_ptr<Node> next; 

};

int main() {

std::shared_ptr<Node> node1(new Node);
std::shared_ptr<Node> node2(new Node); 
node1->next = std::weak_ptr<Node>(node2); 
node2->next = std::weak_ptr<Node>(node1); ;

}


#### 防止裸指针与 `shared_ptr` 混用
先看看以下代码

int* q = new int(9);
{

std::shared_ptr<int> p(new int(10));
...
q = p.get();

}
std::cout << *q << std::endl;

`get` 函数返回 `std::shared_ptr` 所持有的指针,然而不会减少援用计数。所以在 shared_ptr 析构时,将该指针指向的对象给开释掉了,导致指针 `q` 变成一个悬空指针。#### 防止一个原始指针初始化多个 `shared_ptr`

int* p = new int(10);
std::shared_ptr<int> ptr1(p);
// error: 两个 shared_ptr 指向同一个资源,会导致反复开释
std::shared_ptr<int> ptr2(p);


## 总结
防止手动治理内存带来的繁琐和容易出错的问题。咱们明天介绍了三种智能指针:`unique_ptr`、`shared_ptr` 和 `weak_ptr`。每种智能指针都有各自的应用场景。`unique_ptr` 用于治理独占式所有权的对象,它不能拷贝但能够挪动,是最轻量级和最快的智能指针。`shared_ptr` 用于治理多个对象共享所有权的状况,它能够拷贝和挪动。`weak_ptr` 则是用来解决 `shared_ptr` 循环援用的问题。下一节,咱们将本人入手,从零实现一个 C ++ 智能指针。敬请期待

<center>- END -</center>【往期举荐】[01 C++ 如何进行内存资源管理](https://mp.weixin.qq.com/s?__biz=MzI4MTc0NDg2OQ==&amp;mid=2247484850&amp;idx=1&amp;sn=56524ae728e1968ce38a7a210430ddf5&amp;chksm=eba5c138dcd2482e4cc50983ed93a9dcbfc03f495603d11f0efb7c4ae349e843d3230db4bccc&token=2022782435&lang=zh_CN#rd)
退出移动版