前言

C++提供了以下几种智能指针

  1. unique_ptr
  2. shared_ptr
  3. weak_ptr
  4. auto_ptr

其中auto_ptr由于过于抽象已被弃用,所以本文只讨论前三种智能指针。

unique_ptr

unique_ptr遵循独占所有权语义。被unique_ptr持有的资源不能同时被其他任何unique_ptr持有,也不应被其他任何智能指针持有。

初始化

unique_ptr大致有以下几种初始化方式。

  • 直接使用构造函数
  • 使用make_unique
  • 通过移动语义转移所有权
  • 初始化为空

对于动态开辟的数组,unique_ptr应当使用T[]模板,以便编译器调用delete[]作为删除器。或者,也可以自定义一个删除器。

删除器可以是函数指针,对象或lambda表达式。删除器的类型需要函数模板参数传入。

1
2
3
4
auto deleter = [](int * obj){
delete[] obj;
};
unique_ptr<int[],decltype(deleter)>ptr(new int[10],deleter); // decltype可推断出deleter的类型。

独占所有权

unique_ptr的拷贝构造函数和赋值运算符被显式删除。因此unique_ptr无法被复制而产生一个新的实例。这使unique_ptr的所有权不会被分离到两个unique_ptr实例。

unique_ptr可以通过移动语义来转移自身持有的资源。

1
2
3
4
// 将一个unique_ptr的资源转移到另一个unique_ptr

unique_ptr<string>ptr1 = make_unique<string>(str);
unique_ptr<string>ptr2 = move(ptr1);

shared_ptr

初始化

shared_ptr的初始化与unique_ptr类似,但是也有所不同。

  • unique_ptr类似,shared_ptr可以使用make_shared初始化,这也是较安全的初始化方式。
  • shared_ptr允许拷贝操作,这同时也会增加shared_ptr内部的引用计数器。
  • 使用移动语义可以将unique_ptr的资源转移至shared_ptr

共享资源机制

多个shared_ptr可以指向同一个对象。

shared_ptr内部维护了一个线程安全的计数器,其代表了shared_ptr指向的对象被不同shared_ptr实例引用的次数。

当该计数器归零时,shared_ptr指向的对象将被销毁,同时,对象的析构函数也会被调用。

虽然shared_ptr的引用计数是线程安全的,然而,对被shared_ptr管理的对象的操作却不是线程安全的。在多线程环境下操作shared_ptr管理的对象时,需要额外的同步机制。

shared_ptr可以指定释放资源的方式,自定义删除方法。

每个shared_ptr会创建一个控制块,包括引用计数,弱引用计数,删除器和分配器。首次创建shared_ptr时,该控制块会被创建。

控制块

控制块的内存和对象的内存同时开辟,并且为一次分配。这减少了内存开辟的次数,提高了效率。

内存块的引用计数是原子操作,保证线程安全。

只有在弱引用归0时,控制块的内存才会被释放。所以,对象和控制块的构造一定是同时的,但销毁不一定是同时的

风险

shared_ptr使用不当会导致循环引用,即对象的成员变量shared_ptr引用了对象。这导致在对象析构之前,对象的shared_ptr成员变量会一直指向shared_ptr实例,等待对象析构,然而,对象析构的条件是没有一个shared_ptr实例指向该对象。最终对象和shared_ptr实例会陷入一种类似死锁的”僵持”,最终内存无法释放,导致内存泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 两个类对象间的循环引用
class B;

class A {
public:
shared_ptr<B>ptr;
};

class B {
public:
shared_ptr<A>ptr;
};

int main() {
shared_ptr<A>ptr2 = make_shared<A>();
shared_ptr<B>ptr1 = make_shared<B>();

ptr2->ptr = ptr1;
ptr1->ptr = ptr2;

return 0;
}

将其中一个类内的shared_ptr改为weak_ptr可以解决这个问题。

weak_ptr

weak_ptr可以观察shared_ptr管理的资源,却不会增加引用计数,因此,weak_ptr可以解决循环引用的问题。

初始化

1
2
3
auto shared = std::make_shared<int>(42);
std::weak_ptr<int> weak1(shared); // 从 shared_ptr 构造
std::weak_ptr<int> weak2(weak1); // 从另一个 weak_ptr 拷贝构造

weak_ptr可以从shared_ptr构造,也可以从另一个weak_ptr拷贝。

成员函数

expired(): 用于检查资源是否已被释放,如果已释放,返回true

lock(): 会返回一个shared_ptr,只有通过这个shared_ptrweak_ptr才可以操作观察的资源。

use_count(): 返回强引用计数。

应用

利用C++提供的智能指针,我们可以刚好地管理程序内的各种资源。

例如,线程池可以用unique_ptr管理,以确保程序中线程池实例独一无二。

在多线程环境下,使用shared_ptr也可以妥善管理那些全局共享的对象,以确保该对象不会在被使用时已经是被销毁的了。