🥲Smart Pointers

参考 https://changkun.de/modern-cpp/en-us/05-pointers/

Introduction

忘了加delete?不在合适的地方加入delete?那将是一场灾难!

自动释放内存,只有类可以做到😭

于是智能指针auto_ptrunique_ptrshared_ptr来了!

将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。

再说说引用计数

基本想法是对于动态分配的对象,进行引用计数,每当增加一次对同一个对象的引用,那么引用对象的引用计数就会增加一次, 每删除一次引用,引用计数就会减一,当一个对象的引用计数减为零时,就自动删除指向的堆内存。

STL一共给我们提供了四种智能指针包括 std::shared_ptr/std::unique_ptr/std::weak_ptr,使用它们需要包含头文件 <memory>.

模板auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,并提供了另外3种解决方案。

所有的智能指针类都有一个explicit构造函数以指针作为参数。比如auto_ptr的类模板原型为:

templet<class T>
class auto_ptr {
  explicit auto_ptr(X* p = 0) ; 
  ...
}

因此不能自动将指针转换为智能指针对象,必须显式调用

shared_ptr<double> pd; 
double *p_reg = new double;
pd = p_reg;                               // not allowed (implicit conversion)
pd = shared_ptr<double>(p_reg);           // allowed (explicit conversion)
shared_ptr<double> pshared = p_reg;       // not allowed (implicit conversion)
shared_ptr<double> pshared(p_reg);        // allowed (explicit conversion

对全部三种智能指针都应避免的一点:

全局变量在堆中,智能指针pvac过期时,程序将把delete运算符用于非堆内存,这是错误的。

std::shared_ptr

它能够记录多少个 shared_ptr 共同指向一个对象,从而消除显式的调用 delete,当引用计数变为零的时候就会将对象自动删除。

但还不够,因为使用 std::shared_ptr 仍然需要使用 new 来调用,这使得代码出现了某种程度上的不对称。

std::make_shared 就能够用来消除显式的使用 new,所以std::make_shared 会分配创建传入参数中的对象, 并返回这个对象类型的std::shared_ptr指针。例如:

std::shared_ptr 可以通过 get() 方法来获取原始指针,通过 reset() 来减少一个引用计数, 并通过use_count()来查看一个对象的引用计数。例如:

std::unique_ptr

std::unique_ptr 是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全:

make_unique 并不复杂,C++11 没有提供 std::make_unique,可以自行实现:

至于为什么没有提供,C++ 标准委员会主席 Herb Sutter 在他的博客中提到原因是因为『被他们忘记了』,这里笑一笑😂。

既然是独占,换句话说就是不可复制。但是,我们可以利用 std::move 将其转移给其他的 unique_ptr,例如:

std::weak_ptr

如果你仔细思考 std::shared_ptr 就会发现依然存在着资源无法释放的问题。看下面这个例子:

运行结果是 A, B 都不会被销毁,这是因为 a,b 内部的 pointer 同时又引用了 a,b,这使得 a,b 的引用计数均变为了 2,而离开作用域时,a,b 智能指针被析构,却只能造成这块区域的引用计数减一,这样就导致了 a,b 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露,如图

解决这个问题的办法就是使用弱引用指针 std::weak_ptrstd::weak_ptr是一种弱引用(相比较而言 std::shared_ptr 就是一种强引用)。弱引用不会引起引用计数增加,当换用弱引用时候,最终的释放流程如图

在上图中,最后一步只剩下 B,而 B 并没有任何智能指针引用它,因此这块内存资源也会被释放。

std::weak_ptr 没有 * 运算符和 -> 运算符,所以不能够对资源进行操作,它可以用于检查 std::shared_ptr 是否存在,其 expired() 方法能在资源未被释放时,会返回 false,否则返回 true;除此之外,它也可以用于获取指向原始对象的 std::shared_ptr 指针,其 lock() 方法在原始对象未被释放时,返回一个指向原始对象的 std::shared_ptr 指针,进而访问原始对象的资源,否则返回nullptr

How to choose?

如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:

  • 有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;

  • 两个对象包含都指向第三个对象的指针;

  • STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr

如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr

如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。可将unique_ptr存储到STL容器,只要不调用将一个unique_ptr复制或赋给另一个算法(如sort())。例如,可在程序中使用类似于下面的代码段

其中push_back调用没有问题,因为它返回一个临时unique_ptr,该unique_ptr被赋给vp中的一个unique_ptr。另外,如果按值而不是按引用show()传递对象,for_each()将非法,因为这将导致使用一个来自vp的非临时unique_ptr初始化p1,而这是不允许的。前面说过,编译器将发现错误使用unique_ptr的企图。

unique_ptr为右值时,可将其赋给shared_ptr,这与将一个unique_ptr赋给一个需要满足的条件相同。与前面一样,在下面的代码中,make_int()的返回类型为unique_ptr<int>

模板shared_ptr包含一个显式构造函数,可用于将右值unique_ptr转换为shared_ptrshared_ptr将接管原来归unique_ptr所有的对象。

在满足unique_ptr要求的条件时,也可使用auto_ptr,但unique_ptr是更好的选择。如果你的编译器没有unique_ptr,可考虑使用Boost库提供的scoped_ptr,它与unique_ptr类似。

Last updated