C++智能指针

Share on:
2700 Words | Read in about 13 Min | View times

Overview

在传统C++中,我们一般使用new/deletemalloc/free等方式来管理内存,但是由于申请内存和释放内存的过程都需要开发者自己维护,而只要是需要手动管理的逻辑,就有可能出现忘记释放或者多次释放内存的错误,这也是传统C++一致为人诟病的一个问题。在现代C++中,标准库提供了智能指针的实现,这些指针用于帮助确保程序不会出现内存和资源泄露,并具有异常安全。可以说现代C++的智能指针使得内存管理更加方便更加安全。本节内容我们将解析各类智能指针的底层实现原理。

本系列文章将包括以下领域:

本章其他内容请见 《现代C++》

你知道哪些智能指针?

shared_ptr的设计原理是什么?

如何自己设计一个智能指针?

以上问题都是C++面试中的高频题目,回答这些问题就得先搞懂智能指针背后的设计原理。

为什么需要智能指针?

首先,我们需要了解一个概念——引用计数。引用计数是为了防止内存泄漏而产生的。基本思路是对于在堆动态分配的对象进行引用计数,每当增加一次对同一个对象的引用,那么引用对象的引用计数就会增加一次;每当删除一次对同一个对象的引用,那么引用对象的引用计数就会减少一次;当引用对象的引用计数减为零时,就自动销毁指向的堆内存。引用计数能更清晰地表达一个内存资源的生命周期。

在传统C++中,需要程序员手动释放资源总是有漏洞的。因为我们可能忘记了释放而导致内存泄漏,甚至多次释放而导致异常崩溃。所以有一种惯用技法叫资源获取即初始化(RAII,Resource Acquisition Is Initialization),我们在构造对象时申请空间,在析构时释放空间。当我们有对象自由分配内存的需求时,传统C++只能使用newdelete去操作内存资源,而C++11开始引入了智能指针,加入了引用计数的思想,使程序员不再需要关注手动释放内存了。

智能指针类型

STL的头文件<memory>中提供的智能指针都是类模板实现的,类型包括:

  • std::auto_ptr

这是C++98提供的智能指针,在C++11开始已经被废弃。这是因为auto_ptr采用copy语义来转移指针资源的所有权,同时原指针被置为NULL,这跟我们通常理解的copy行为是不一致的,通常copy行为不会引起原数据的修改。auto_ptr这种做法在使用时,非常容易引起错误。所以从C++11开始,提供了对移动语义的支持,使用了unique_ptr来代替auto_ptr

  • std::shared_ptr

共享指针,如果需要多个指针指向同一个对象,就使用std::shared_ptr。通过对同一个托管对象的引用计数来管理,当引用计数减到0后,对象就会自动被删除。

  • unique_ptr

C++11提供了move语义支持,使得资源转移通常只会在必要的场合发生,unique_ptr禁用了copy而用move代替,unique_ptr对指针资源的转移只能通过显式方式来进行,比auto_ptr更加安全可靠,所以auto_ptr在现代C++中被废除了。

如果不需要多个指针指向同一个对象,就使用std::unique_ptr

  • std::weak_ptr

weak_ptr是为了配合shared_ptr而产生的一种智能指针,如果多个指针互相指向对方引用的对象,即出现循环引用的问题,就使用std::weak_ptr。通过对引用计数对象的引用计数来管理,当这个计数减到0后,自动删除引用技术对象。注意这个引用计数和shared_ptr的引用计数目标是不一样的,在后面剖析源码实现的模块里我们再具体展开。

智能指针使用方法

简单认识了这些类型的智能指针后,我们来看一下各自的使用方法和使用场景。

std::auto_ptr

我们来看auto_ptr的使用方法,以及会出现的问题,以此来理解为什么要废弃auto_ptr

 1std::auto_ptr<std::string> movies[5] = {
 2    std::auto_ptr<std::string>(new std::string("Iron Man")),
 3    std::auto_ptr<std::string>(new std::string("Captain America")),
 4    std::auto_ptr<std::string>(new std::string("Captain Marvel")),
 5    std::auto_ptr<std::string>(new std::string("Spider Man")),
 6    std::auto_ptr<std::string>(new std::string("Thor")),
 7};
 8
 9std::auto_ptr<std::string> popular = movies[1]; //movies[1]的所有权直接移交给了popular,此时movies[1]变成了空指针
10
11std::cout << *movies[1] << std::endl; //crash!

从上面的例子可以看到,当一个auto_ptr赋值给另一个auto_ptr时,从语义上看是发生了拷贝赋值,但是其实原auto_ptr的所有权完全转移给了新的auto_ptr,导致对原auto_ptr解引用时就引发了未定义行为。一个赋值语义的操作,实际上却是进行了移动语义的操作,本身这个设计就不太正常。为了避免潜在的内存崩溃问题,C++标准也废弃了这种智能指针,而是在C++11中使用unique_ptr显式的移动语义来转移资源的所有权,用以替代auto_ptr

std::shared_ptr

1template<class T> shared_ptr;

1. 创建std::shared_ptr

std::shared_ptr<T>类模板中提供了多种构造方法:

  • 构建空指针
1//不传参
2std::shared_ptr<int> p1;
3std::shared_ptr<int> p2(nullptr);
  • 构建智能指针时明确其指向对象
1std::shared_ptr<std::string> p1(new std::string("Hello World"));
2
3int* raw = new int(10);
4std::shared_ptr<int> p2(raw);
5std::shared_ptr<int> p3(raw); //错误,同一裸指针不可同时赋值给多个shared_ptr

这种显示调用了new,却不用关心delete,这种使用上的不对称不太推荐。

  • 使用std::make_shared创建智能指针
1auto ptr = std::make_shared<std::string>("Hello World");

使用make_shared可以消除显示使用newmake_shared会分配创建传入参数中的对象,并返回这个对象类型的std::shared_ptr指针。

实际上,强烈推荐使用make_shared来创建shared_ptr,而不要用new。因为使用new创建裸指针再赋给shared_ptr,使用不当可能会让两个引用计数绑定到同一个堆内存对象上,从而导致最后二次删除引发崩溃。

另外,不要将this指针返回给shared_ptr。当希望将this指针托管给shared_ptr时,类需要继承自std::enable_shared_from_this,并且从shared_from_this()中获得shared_ptr指针。我们在后面介绍weak_ptr时会再次提到这个方法。

那么make_sharedenable_shared_from_this是怎么实现的?我们将在本文最后一个小节详细展开。

  • 拷贝构造/赋值和移动构造/赋值
1//调用拷贝构造函数
2std::shared_ptr<int> p2(p1);
3//调用拷贝赋值
4auto p3 = p2;
5
6//调用移动构造函数
7std::shared_ptr<int> p4(std::move(p3));
8//调用移动赋值
9auto p5 = std::move(p4);

这个例子中,若p1是空智能指针,则p2p3也是空智能指针,引用计数是0;反之,则p2p3指向同一块堆内存,同时堆内存对象的引用计数都会加1。p3移动给了p4后,p3将变成空智能指针,堆内存对象的引用计数不变。

  • 初始化智能指针时指定自定义的释放规则

在某些场景中需要自定义释放规则(如申请动态数组,shared_ptr默认不支持释放数组),当堆内存对象的引用计数减到0时,会调用我们指定的自定义释放规则。

 1//使用std::default_delete<T>作为释放规则
 2std::shared_ptr<int> p1(new int[10], std::default_delete<int[]>());
 3
 4//自定义释放规则
 5void myDelete(int* p) {
 6    delete[] p;
 7}
 8std::shared_ptr<int, void(*)(int*)> p2(new int[10], myDelete);
 9
10//lambda表达式释放规则
11std::shared_ptr<int, void(*)(int*)> p3(new int[10], [](int* p) { delete[] p; });
12
13//仿函数释放规则
14struct D {
15    void operator()(int* p) const {
16        delete[] p;
17    }
18};
19std::shared_ptr<int, D> p1(new int[10], D());

std::shared_ptr一共提供了如下类型的构造函数声明:

 1constexpr shared_ptr() noexcept; (1)
 2
 3constexpr shared_ptr( std::nullptr_t ) noexcept; (2)
 4
 5template< class Y >
 6explicit shared_ptr( Y* ptr ); (3)
 7
 8template< class Y, class Deleter >
 9shared_ptr( Y* ptr, Deleter d ); (4)
10
11template< class Deleter >
12shared_ptr( std::nullptr_t ptr, Deleter d ); (5)
13
14template< class Y, class Deleter, class Alloc >
15shared_ptr( Y* ptr, Deleter d, Alloc alloc ); (6)
16
17template< class Deleter, class Alloc >
18shared_ptr( std::nullptr_t ptr, Deleter d, Alloc alloc ); (7)
19
20template< class Y >
21shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept; (8)
22
23template< class Y >
24shared_ptr( shared_ptr<Y>&& r, element_type* ptr ) noexcept; (8) (since C++20)
25
26shared_ptr( const shared_ptr& r ) noexcept; (9)
27
28template< class Y >
29shared_ptr( const shared_ptr<Y>& r ) noexcept; (9)
30
31shared_ptr( shared_ptr&& r ) noexcept; (10)
32
33template< class Y >
34shared_ptr( shared_ptr<Y>&& r ) noexcept; (10)
35
36template< class Y >
37explicit shared_ptr( const std::weak_ptr<Y>& r ); (11)
38
39template< class Y >
40shared_ptr( std::auto_ptr<Y>&& r ); (12) (removed in C++17)
41
42template< class Y, class Deleter >
43shared_ptr( std::unique_ptr<Y, Deleter>&& r ); (13)

关于更多这些构造函数的类型和使用方法,可以到cppreference上详细查阅。

2. std::shared_ptr类模板方法

std::shared_ptr<T>提供了以下类模板成员方法:

  • operator=()

重载赋值运算符,使同一类型的shared_ptr可以相互赋值。

  • reset()

当函数没有实参时,则会使当前shared_ptr所指堆对象内存的引用计数减1,同时将当前对象重置为一个空指针;当为函数传递一个新申请的堆内存时,则当前shared_ptr所指对象会获得该存储空间的所有权,并且引用计数的初始值为1

  • swap()

交换两个相同类型智能指针的内容。

  • get()

获得智能指针所指对象的裸指针。

  • operator*()

重载*运算符,获取当前智能指针对象指向的数据。

  • operator->()

重载->运算符,当智能指针指向的数据类型为结构体时,可以获取其内部的指定成员。

  • use_count()

返回当前智能指针所指向堆内存对象的引用计数。

 1auto ptr = std::make_shared<std::string>("Hello World"); //引用计数初始为1
 2auto ptr2 = ptr; //引用计数+1
 3auto ptr3 = ptr; //引用计数+1
 4
 5std::string* p = ptr.get(); //获取裸指针,但不会增加引用计数
 6
 7std::cout << ptr.use_count() << ", " << ptr2.use_count() << ", " << ptr3.use_count() << std::endl; //3, 3, 3
 8
 9ptr3.reset(); //释放智能指针,引用计数-1
10std::cout << ptr.use_count() << ", " << ptr2.use_count() << ", " << ptr3.use_count() << std::endl; //2, 2, 0
11
12ptr2.reset(); //释放智能指针,引用计数-1
13std::cout << ptr.use_count() << ", " << ptr2.use_count() << ", " << ptr3.use_count() << std::endl; //1, 0, 0

那么引用计数又是如何实现的呢?为什么相同类型的shared_ptr都能知道引用计数的变化?我们将在本文最后一个小节详细展开。

  • unique()

判断当前智能指针是否唯一一个指向该堆内存对象的智能指针。

  • operator bool()

判断当前智能指针是否为空智能指针。

  • operator[]

C++17新增的方法,可以用来下标索引数组。

  • owner_before()

该成员函数用于将一个shared_ptr与另外一个shared_ptrweak_ptr按照某种关系进行比较并返回比较结果,这种比较关系就是owner-based order

一个智能指针(stored_pointer)有可能指向另一个智能指针(owned_pointer)中的某一部分,但又要保证这两个智能指针销毁时,只对那个被指向的对象完整地析构一次,而不是两个指针分别析构一次。如果堆内存对象只有一个,但被许多智能指针指向了其中的不同部分,那么这些指针本身的地址是不同的,operator<()可以比较它们,并且它们都不是对象的owner,它们销毁时不会析构对象。

owner-based order就是如果进行比较的两个智能指针指向的是同一个对象(包含继承关系),那么就认为这两个指针是“相等”的。

对于a.owner_before(b)

a) 如果ab同为空指针,或者同时指向同一个对象(包含继承关系),就返回false

b) 如果是其它情况,则用智能指针所指向的对象的地址来比较大小,若a的地址 < b的地址,则返回true,若a的地址 > b的地址,则返回false

 1class A {
 2public:
 3    A() : m_a(0) {}
 4    A(int a) : m_a(a) {}
 5private:
 6    int m_a;
 7};
 8
 9class B {
10public:
11    B() : m_b(0) {}
12    B(int b) : m_b(b) {}
13private:
14    int m_b;
15};
16
17class C : public A, public B {
18public:
19    C() {}
20    C(int a, int b) : A(a), B(b) {}
21};
22
23int main()
24{
25    std::cout << std::boolalpha;
26
27    {
28        std::shared_ptr<C> pC(new C(5, 15), [](C* p) {
29            std::cout << "delete C" << std::endl;
30            delete p;
31        });
32
33        std::shared_ptr<A> pA(pC);
34        std::shared_ptr<B> pB(pC);
35
36        std::cout << "pC address: " << pC << std::endl; //0x556938ec0eb0
37        std::cout << "pA address: " << pA << std::endl; //0x556938ec0eb0
38        std::cout << "pB address: " << pB << std::endl; //0x556938ec0eb4
39
40        //pB和pC地址虽然不同,但指向同一个对象,所以operator==()的结果是相等的
41        std::cout << "pA == pC: " << (pA == pC) << std::endl; //true
42        std::cout << "pB == pC: " << (pB == pC) << std::endl; //true
43
44        //pA、pB、pC指向同一对象(包含继承关系),都返回false
45        std::cout << "pA.owner_before(pC): " << pA.owner_before(pC) << std::endl; //false
46        std::cout << "pC.owner_before(pA): " << pC.owner_before(pA) << std::endl; //false
47        std::cout << "pB.owner_before(pC): " << pB.owner_before(pC) << std::endl; //false
48        std::cout << "pC.owner_before(pB): " << pC.owner_before(pB) << std::endl; //false
49    }
50
51    {
52        std::shared_ptr<A> pA0(nullptr);
53        auto pA1 = std::make_shared<A>(10);
54        auto pA2 = std::make_shared<A>(20);
55
56        std::cout << "pA0 address: " << pA0 << std::endl; //0
57        std::cout << "pA1 address: " << pA1 << std::endl; //0x556938ec0ee0
58        std::cout << "pA2 address: " << pA2 << std::endl; //0x556938ec0ec0
59
60        //pA1与pA2不指向同一个对象,且也不同时为空指针,则比较两者的地址大小:pA1地址 < pA2地址则返回true,pA1地址 > pA2地址则返回false
61        std::cout << "pA1.owner_before(pA2): " << pA1.owner_before(pA2) << std::endl; //false
62        std::cout << "pA2.owner_before(pA1): " << pA2.owner_before(pA1) << std::endl; //true
63
64        //pA1与pA0不指向同一个对象,且也不同时为空指针,则比较两者的地址大小:pA1地址 < pA0地址则返回true,pA1地址 > pA0地址则返回false
65        std::cout << "pA1.owner_before(pA0): " << pA1.owner_before(pA0) << std::endl; //false
66        std::cout << "pA0.owner_before(pA1): " << pA0.owner_before(pA1) << std::endl; //true
67
68        //pA2与pA0不指向同一个对象,且也不同时为空指针,则比较两者的地址大小:pA2地址 < pA0地址则返回true,pA2地址 > pA0地址则返回false
69        std::cout << "pA2.owner_before(pA0): " << pA2.owner_before(pA0) << std::endl; //false
70        std::cout << "pA0.owner_before(pA2): " << pA0.owner_before(pA2) << std::endl; //true
71    }
72}

std::unique_ptr

1template<class T> unique_ptr;

std::unique_ptr实现了独享所有权的语义。一个非空的unique_ptr总是拥有它所指向的资源。转移一个unique_ptr将会把所有权也从源指针转移给目标指针,源指针被置空。

拷贝一个unique_ptr是被禁止的,因为如果拷贝一个unique_ptr,操作结束后,这两个unique_ptr都会指向相同的资源,它们都认为自己拥有这块资源,所以都会企图释放,这样就会出现重复释放的问题。因此unique_ptr是一个仅能移动的类型,完全取代了auto_ptr

当指针析构时,它所拥有的资源也被销毁。默认情况下,资源的析构是伴随着调用unique_ptr内部的原始指针的delete操作的。

1. 创建unique_ptr

  • 构建独享指针时明确其指向对象
1std::unique_ptr<int> p1(new int(10));
2
3int* raw = new int(10);
4std::unique_ptr<int> p2(raw);
5std::unique_ptr<int> p3(p2); //错误,没有拷贝构造函数
6std::unique_ptr<int> p4 = p2; //错误,没有拷贝赋值操作
7std::unique_ptr<int> p5 = new int(10); //错误,构造函数是explicit的
  • 移动构造和移动赋值
1std::unique_ptr<int> p1(new int(10));
2
3//调用移动构造后,p1变成空指针
4std::unique_ptr<int> p2(std::move(p1));
5
6//调用移动赋值后,p2变成空指针
7std::unique_ptr<int> p3 = std::move(p2);
  • 传参和返回值

unique_ptr不可拷贝和赋值,但是C++标准支持拷贝或赋值一个即将被销毁的unique_ptr

作为返回值:

1std::unique_ptr<int> clone(int a) {
2    return std::unqiue_ptr<int>(new int(a));
3}
4
5std::unique_ptr<int> clone2(int a) {
6    std::unique_ptr<int> p(new int(a));
7    return p;
8}

作为函数参数,可以使用引用避免所有权发生转移,也可以暂时转移所有权,结束后回收:

 1void foo(std::unique_ptr<int>& ptr) {
 2    std::cout << *ptr << std::endl;
 3}
 4
 5std::unique_ptr<int> bar(std::unique_ptr<int> ptr) {
 6    std::cout << *ptr << std::endl;
 7    return ptr;
 8}
 9
10std::unique_ptr<int> p(new int(10));
11//引用传参,所有权不会变化
12foo(p); //10
13
14//值传参,暂时转移所有权,函数返回时重新拷贝给源指针
15//如果函数返回时没有接收,这块内存就发生了泄漏!
16p = bar(std::unique_ptr<int>(p.release())); //10
  • 使用std::make_unique创建指针
1//从C++14开始才支持std::make_unique
2std::unique_ptr<int> p = std::make_unique<int>(10);
3
4//C++14之前也可以自己封装一个make_unique,至于为什么C++11标准没有make_unique,C++委员会解释是因为遗忘了。
5template<typename T, typename ...Args>
6std::unique_ptr<T> make_unique(Args&& ...args) {
7    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
8}
  • 初始化智能指针时指定自定义的释放规则
 1//使用std::default_delete<T>作为释放规则
 2std::unique_ptr<int> p1(new int[10], std::default_delete<int[]>());
 3
 4//自定义释放规则
 5void myDelete(int* p) {
 6    delete[] p;
 7}
 8std::unique_ptr<int, void(*)(int*)> p2(new int[10], myDelete);
 9
10//lambda表达式释放规则
11std::unique_ptr<int, void(*)(int*)> p3(new int[10], [](int* p) { delete[] p; });
12
13//使用仿函数释放规则
14struct D {
15    void operator()(int* p) const {
16        delete[] p;
17    }
18};
19std::unique_ptr<int> p4(new int[10], D());

std::unique_ptr一共提供了如下类型的构造函数声明:

 1//members of the primary template, unique_ptr<T>
 2constexpr unique_ptr() noexcept;
 3constexpr unique_ptr( std::nullptr_t ) noexcept; (1)
 4
 5explicit unique_ptr( pointer p ) noexcept; (2)(constexpr since C++23)
 6
 7unique_ptr( pointer p, /* see below */ d1 ) noexcept; (3)(constexpr since C++23)
 8
 9unique_ptr( pointer p, /* see below */ d2 ) noexcept; (4)(constexpr since C++23)
10
11unique_ptr( unique_ptr&& u ) noexcept; (5)(constexpr since C++23)
12
13template< class U, class E >
14unique_ptr( unique_ptr<U, E>&& u ) noexcept; (6)(constexpr since C++23)
15
16template< class U >
17unique_ptr( std::auto_ptr<U>&& u ) noexcept; (7)(removed in C++17)
18
19//members of the specialization for arrays, unique_ptr<T[]>
20constexpr unique_ptr() noexcept;
21constexpr unique_ptr( std::nullptr_t ) noexcept; (1)
22
23template< class U > explicit unique_ptr( U p ) noexcept; (2)(constexpr since C++23)
24
25template< class U > unique_ptr( U p, /* see below */ d1 ) noexcept; (3)(constexpr since C++23)
26
27template< class U > unique_ptr( U p, /* see below */ d2 ) noexcept; (4)(constexpr since C++23)
28
29unique_ptr( unique_ptr&& u ) noexcept; (5)(constexpr since C++23)
30
31template< class U, class E >
32unique_ptr( unique_ptr<U, E>&& u ) noexcept; (6)(constexpr since C++23)

关于更多这些构造函数的类型和使用方法,可以到cppreference上详细查阅。

2. std::unique_ptr类模板方法

std::unique_ptr<T>提供了以下类模板成员方法:

  • operator=()

重载赋值运算符,使同一类型的unique_ptr可以互相赋值。

  • release()

返回原始指针,并释放unique_ptr对该资源的所有权。注意这里只会释放所有权,并不是释放所指向的内存,这时返回值就是对这块资源的唯一索引了,如果忘记手动释放,就会导致内存泄漏。

  • reset()

参数可以为空、内置指针,先将当前指向的堆内存对象释放掉,再用参数赋值给unique_ptr

  • swap()

交换两个unique_ptr的堆内存对象,且自定义的释放规则也会交换。

  • get()

返回原始指针。

  • get_deleter()

返回自定义的删除规则。

 1struct D {
 2    void foo() { std::cout << "D::foo" << std::endl; }
 3    void operator()(int* p) const {
 4        delete[] p;
 5    }
 6};
 7
 8std::unique_ptr<int, D> p1(new int[10], D());
 9D& deleter = p1.get_deleter();
10deleter.foo(); //D::foo
  • operator bool()

判断当前指针是否为空指针。

  • operator*() 重载*运算符,获取当前智能指针对象指向的数据。

  • operator->()

重载->运算符,当智能指针指向的数据类型为结构体时,可以获取其内部的指定成员。

  • operator[]()

C++17新增的方法,可以用来下标索引数组。

std::weak_ptr

1template<class T> weak_ptr;

weak_ptr是为了配合shared_ptr而引入的一种智能指针。说白了它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,即将一个weak_ptr绑定到一个shared_ptr,而不会改变shared_ptr的引用计数。不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,堆内存对象就会被释放。

1. 创建weak_ptr

  • 使用shared_ptr直接构造或隐式转换
1std::shared_ptr<int> sp(new int(10));
2
3//使用shared_ptr直接构造
4std::weak_ptr<int> wp(sp); //sp的引用计数不会增加
5
6//隐式转换
7std::weak_ptr<int> wp2 = sp;
  • 拷贝构造/赋值和移动构造/赋值
1//调用拷贝构造
2std::weak_ptr<int> wp3(wp2);
3//调用拷贝赋值
4auto wp4 = wp2;
5
6//调用移动构造
7std::weak_ptr<int> wp5(std::move(wp4));
8//调用移动赋值
9auto wp6 = std::move(wp5);

2. std::weak_ptr类模板方法

std::weak_ptr<T>提供了以下类模板成员方法:

  • operator=()

重载赋值运算符,使同一类型的weak_ptr可以相互赋值。

  • reset()

释放引用计数管理对象的所有权。

  • swap()

交换两个相同类型智能指针的内容。

  • use_count()

返回weak_ptr的强引用计数,即所对应的shared_ptr的引用计数。

  • expired()

判断引用计数管理对象是否已经释放。

  • lock()

创建一个新的shared_ptr指针指向相同的堆内存对象,引用计数会增加1,如果堆内存对象已被释放,则返回一个空的shared_ptr

由于weak_ptr没有重载operator*()operator->(),也没有提供get()方法,因此不可以直接通过weak_ptr来访问对象,必须通过lock()之后才能访问数据。

 1std::shared_ptr<int> sp(new int(10));
 2std::weak_ptr<int> wp(sp);
 3//sp.reset();
 4
 5if (wp.expired())
 6    std::cout << "sp already destoryed." << std::endl;
 7
 8if (std::shared_ptr<int> p = wp.lock())
 9    std::cout << *p << std::endl;
10else
11    std::cout << "wp points nothing." << std::endl;
  • owner_before()

shared_ptrowner_before()用法,提供weak_ptrowner-based order比较方法。

使用场景

我们回到设计weak_ptr的原始需求:两个智能指针互相依赖的问题。

 1#include <iostream>
 2#include <memory>
 3
 4class A;
 5class B;
 6
 7class A {
 8public:
 9    A() { std::cout << "A::A()" << std::endl; }
10    ~A() { std::cout << "A::~A()" << std::endl; }
11    std::shared_ptr<B> pb;
12};
13
14class B {
15public:
16    B() { std::cout << "B::B()" << std::endl; }
17    ~B() { std::cout << "B::~B()" << std::endl; }
18    std::shared_ptr<A> pa;
19};
20
21int main() {
22    {
23        std::shared_ptr<A> spa = std::make_shared<A>();
24        std::shared_ptr<B> spb = std::make_shared<B>();
25        spa->pb = spb;
26        spb->pa = spa;
27    }
28    return 0;
29}

两个类型互相持有对方类型的一个shared_ptr时,最终都没有执行析构,两块内存泄漏了。运行结果如下:

1A::A()
2B::B()

两个类型都改成持有对方类型的weak_ptr之后是否解决了该问题:

 1class A;
 2class B;
 3
 4class A {
 5public:
 6    A() { std::cout << "A::A()" << std::endl; }
 7    ~A() { std::cout << "A::~A()" << std::endl; }
 8    std::weak_ptr<B> pb;
 9};
10
11class B {
12public:
13    B() { std::cout << "B::B()" << std::endl; }
14    ~B() { std::cout << "B::~B()" << std::endl; }
15    std::weak_ptr<A> pa;
16};
17
18int main() {
19    {
20        std::shared_ptr<A> spa = std::make_shared<A>();
21        std::shared_ptr<B> spb = std::make_shared<B>();
22        spa->pb = spb;
23        spb->pa = spa;
24    }
25    return 0;
26}

最终双方都可以执行析构,内存正确释放,运行结果如下:

1A::A()
2B::B()
3B::~B()
4A::~A()

智能指针实现原理

以上内容只是开胃小菜,从这里开始,我们将通过源码剖析来解决之前遗留的一些疑问:

  • make_shared如何实现?和new直接创建shared_ptr的方式有什么区别?

  • enable_shared_from_this如何实现?

  • 智能指针一直在强调的引用计数如何实现?

辅助函数std::make_shared的实现原理

make_shared的作用是在分配引用计数器的空间时,同时多分配一块空间给数据用,让数据和引用计数器在连续的内存区域,对CPU缓存也友好。同时不用暴露原始指针,降低了出现多个引用计数绑定到同一个原始指针而引发二次删除的几率。

std::make_shared是一个模板函数,其源码如下:

1//<shared_ptr.h>
2template<typename _Tp, typename... _Args>
3inline shared_ptr<_Tp>
4make_shared(_Args&&... __args)
5{
6    typedef typename std::remove_cv<_Tp>::type _Tp_nc;
7    return std::allocate_shared<_Tp>(std::allocator<_Tp_nc>(),
8                    std::forward<_Args>(__args)...);
9}

它的实现依赖一个shared_ptr<T>的构造函数:

 1//<shared_ptr.h>
 2template<typename _Tp, typename _Alloc, typename... _Args>
 3inline shared_ptr<_Tp>
 4allocate_shared(const _Alloc& __a, _Args&&... __args)
 5{
 6    return shared_ptr<_Tp>(_Sp_alloc_shared_tag<_Alloc>{__a},
 7                std::forward<_Args>(__args)...);
 8}
 9
10//...
11
12template<typename _Alloc, typename... _Args>
13shared_ptr(_Sp_alloc_shared_tag<_Alloc> __tag, _Args&&... __args)
14    : __shared_ptr<_Tp>(__tag, std::forward<_Args>(__args)...)
15{ }

其中_Sp_alloc_shared_tag是一个辅助类,只是用来区分使用哪个模板构造函数。其定义如下:

1//<shared_ptr.h>
2template<typename _Alloc>
3struct _Sp_alloc_shared_tag
4{
5    const _Alloc& _M_a;
6};

shared_ptr<T>继承自__shared_ptr<T>,实际上这里的构造函数调用了基类的一个构造函数:

1//<shared_ptr_base.h>
2template<typename _Alloc, typename... _Args>
3__shared_ptr(_Sp_alloc_shared_tag<_Alloc> __tag, _Args&&... __args)
4    : _M_ptr(), _M_refcount(_M_ptr, __tag, std::forward<_Args>(__args)...)
5{
6    _M_enable_shared_from_this_with(_M_ptr);
7}

其中_M_ptr()构造了一个堆内存对象,而_M_refcount()构造了一个类型为__shared_count的引用计数对象,实际的内存分配发生在这个引用计数对象的构造上:

 1//<shared_ptr_base.h>
 2template<typename _Tp, typename _Alloc, typename... _Args>
 3__shared_count(_Tp*& __p, _Sp_alloc_shared_tag<_Alloc> __a, _Args&&... __args)
 4{
 5    typedef _Sp_counted_ptr_inplace<_Tp, _Alloc, _Lp> _Sp_cp_type;
 6    typename _Sp_cp_type::__allocator_type __a2(__a._M_a);
 7    auto __guard = std::__allocate_guarded(__a2);
 8    _Sp_cp_type* __mem = __guard.get();
 9    auto __pi = ::new (__mem)
10    _Sp_cp_type(__a._M_a, std::forward<_Args>(__args)...);
11    __guard = nullptr;
12    _M_pi = __pi;
13    __p = __pi->_M_ptr();
14}

这里这句::new (__men)实际上调用了一个placement new()对分配好的内存进行对象构造,构造的对象类型实际上是_Sp_counted_ptr_inplace对象的构造,这个类继承自_Sp_counted_base,是最终引用计数对象的实现:

 1//<shared_ptr_base.h>
 2template<typename _Tp, typename _Alloc, _Lock_policy _Lp>
 3class _Sp_counted_ptr_inplace final : public _Sp_counted_base<_Lp>
 4{
 5    class _Impl : _Sp_ebo_helper<0, _Alloc>
 6    {
 7        typedef _Sp_ebo_helper<0, _Alloc> _A_base;
 8    public:
 9        //...
10        __gnu_cxx::__aligned_buffer<_Tp> _M_storage;
11    };
12
13public:
14    template<typename... _Args>
15    _Sp_counted_ptr_inplace(_Alloc __a, _Args&&... __args)
16        : _M_impl(__a)
17    {
18        allocator_traits<_Alloc>::construct(__a, _M_ptr(),
19            std::forward<_Args>(__args)...); // might throw
20    }
21
22    //...
23private:
24    //...
25    _Impl _M_impl;
26};

从这里可看出来,数据的内存空间被合并到了引用计数对象的内从空间上了。我们用一张图来表达make_sharednew方式创建shared_ptr的区别:

make_shared

需要注意的是make_shared方法构造的shared_ptr没法指定自定义的删除规则。

std::enable_shared_from_this的实现原理

enable_shared_from_this是一种侵入式设计(intrusive),继承这个类的派生类就拥有了enable_shared_from_this提供的能力,即shared_from_this()weak_from_this()函数,这些功能“侵入”了我们的代码。

enable_shared_from_this能让一个堆对象(假设其名为t,且已被一个shared_ptr对象pt管理),安全地生成其他额外的shared_ptr实例,它们与pt共享堆对象t的所有权。可能因为编写代码不善,让同一个堆对象绑定到了两个引用计数上,最终会导致双重删除,安全性指的就是规避这种情况出现。一般来说我们不允许通过this指针直接创建shared_ptr,也是这个原因。

简单看一下使用方法:

 1struct Foo : public std::enable_shared_from_this {
 2    std::string name;
 3};
 4
 5Foo foo = new Foo();
 6
 7//sp1, sp2, wp都共用一个引用计数,只有foo被shared_ptr托管之后,shared_from_this()才能成功,否则会抛出异常
 8std::shared_ptr<Foo> sp1 = foo;
 9std::shared_ptr<Foo> sp2 = foo->shared_from_this();
10std::weak_ptr<Foo> wp = foo->weak_from_this();

接下来我们来看一下std::enable_shared_from_this是怎么做的就可以确保安全性,源码如下:

 1//<shared_ptr.h>
 2template<typename _Tp>
 3class enable_shared_from_this
 4{
 5//...
 6public:
 7    shared_ptr<_Tp> shared_from_this() { return shared_ptr<_Tp>(this->_M_weak_this); }
 8    shared_ptr<const _Tp> shared_from_this() const { return shared_ptr<const _Tp>(this->_M_weak_this); }
 9
10    weak_ptr<_Tp> weak_from_this() noexcept { return this->_M_weak_this; }
11    weak_ptr<const _Tp> weak_from_this() const noexcept { return this->_M_weak_this; }
12
13private:
14    template<typename _Tp1>
15    void _M_weak_assign(_Tp1* __p, const __shared_count<>& __n) const noexcept
16    { _M_weak_this._M_assign(__p, __n); }
17
18    //...
19    mutable weak_ptr<_Tp>  _M_weak_this;
20};

这里_M_weak_this是一个弱指针,默认没有初始化,因此当没有数据托管时,调用shared_from_this()会失败。其中mutable为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中也可以修改。

一旦进行了初始化,即执行了shared_ptr<Foo> sp1 = foo;后,将调用shared_ptr<T>的构造函数,之后_M_weak_this将被初始化:

 1//<shared_ptr.h>
 2template<typename... _Args>
 3using _Constructible = typename enable_if<
 4    is_constructible<__shared_ptr<_Tp>, _Args...>::value
 5>::type;
 6
 7template<typename _Yp, typename = _Constructible<_Yp*>>
 8explicit shared_ptr(_Yp* __p) : __shared_ptr<_Tp>(__p) { }
 9
10//<shared_ptr_base.h>
11template<typename _Yp, typename = _SafeConv<_Yp>>
12explicit __shared_ptr(_Yp* __p)
13    : _M_ptr(__p), _M_refcount(__p, typename is_array<_Tp>::type())
14{
15    static_assert( !is_void<_Yp>::value, "incomplete type" );
16    static_assert( sizeof(_Yp) > 0, "incomplete type" );
17    _M_enable_shared_from_this_with(__p);
18}

其中_M_enable_shared_from_this_with()负责将引用计数对象写入数据对象中:

 1//<shared_ptr_base.h> class __shared_ptr
 2template<typename _Yp, typename _Yp2 = typename remove_cv<_Yp>::type>
 3typename enable_if<__has_esft_base<_Yp2>::value>::type
 4_M_enable_shared_from_this_with(_Yp* __p) noexcept
 5{
 6    if (auto __base = __enable_shared_from_this_base(_M_refcount, __p))
 7        __base->_M_weak_assign(const_cast<_Yp2*>(__p), _M_refcount);
 8}
 9
10//<shared_ptr_base.h> class __enable_shared_from_this
11template<typename _Tp1>
12void _M_weak_assign(_Tp1* __p, const __shared_count<_Lp>& __n) const noexcept
13{
14    _M_weak_this._M_assign(__p, __n);
15}

其中调用的_M_weak_assign()enable_shared_from_this::_M_weak_assign(),最终实际调用的是weak_ptr基类__weak_ptr_M_assign()函数,堆内存对象还未被托管时,即引用计数为0的时候进行初始化:

 1//<shared_ptr_base.h>
 2template<typename _Tp, _Lock_policy _Lp>
 3class __weak_ptr
 4{
 5//...
 6private:
 7    void _M_assign(_Tp* __ptr, const __shared_count<_Lp>& __refcount) noexcept
 8    {
 9        if (use_count() == 0)
10        {
11            _M_ptr = __ptr;
12            _M_refcount = __refcount;
13        }
14    }
15}

至此,enable_shared_from_this中的弱指针_M_weak_this被初始化,因此将来调用shared_from_this的时候,就可以通过这个弱指针来构造对应的shared_ptr了。

引用计数的实现原理

最后我们来看一下重中之重,智能指针的引用计数是怎么实现的?

有的同学可能会说,引用计数不就是智能指针初始化的时候加1,析构的时候减1,那直接在智能指针的类中添加一个静态变量来计数不就可以了吗?我们来想一下如果使用静态变量来计数会有什么问题。

 1template<class T>
 2class MySharedPtr<T> {
 3public:
 4    MySharedPtr<T>(...) { counter++; }
 5    ~MySharedPtr<T>(...) { --counter; }
 6private:
 7    inline static int counter = 0;
 8};
 9
10class A;
11
12MySharedPtr<A> sp1(new A()); //计数+1
13MySharedPtr<A> sp2(sp1); //计数+1
14
15//以上代码都正常,可是当加上以下代码:
16MySharedPtr<A> sp3(new A()); //从这里开始就出问题了,这是一个新的A对象,被智能指针托管后,引用计数应该是从0开始,可是现在却是3

以上代码说明,用静态变量不能解决相同类型的智能指针在托管不同对象时,引用计数的计算问题,所以不可以使用静态变量来实现引用计数。

总览

C++标准库的实现方案是这样的:

smart_pointer

整个实现方案中的核心是_Sp_counted_base类,就是通过这个类来管理引用计数的。这个基类本身不保存堆内存对象的指针,而是由它的一个派生类型_Sp_counted_ptr中的_M_ptr指针来保存原始的堆内存对象。

shared_ptr其实是继承自__shared_ptr类型,其内部持有一个__shared_count类型的对象,这个对象的内部又持有一个_Sp_counted_base类型的指针_M_pi,最终指向派生类_Sp_counted_ptr的一个对象。这样一来就可以通过__shared_count对象既管理引用计数,又管理堆内存对象,得以在引用计数变成0时释放堆内存对象。

有时我们又把_M_ptr指针所指的堆内存对象称为托管对象,而把_M_pi指针所指的_Sp_counted_ptr引用计数对象称为管理对象。管理同一个托管对象的多个智能指针内部共享一个管理对象。管理对象提供两个变量_M_use_count_M_weak_count,分别通过shared_ptrweak_ptruse_count()函数返回。_M_use_count表示托管对象的引用计数,当_M_use_count减成0时,调用托管对象的析构函数销毁并释放内存。_M_weak_count表示管理对象的引用次数,当_M_weak_count减成0时,调用管理对象的析构函数销毁并释放内存。管理对象也是一个指针,是在第一次shared_ptr创建的时候new出来的,因此最后也需要析构销毁。

_M_weak_count初始值是1,当shared_ptr拷贝或移动时不会增加,只有当_M_use_count减为0的时候,_M_weak_count才会减1。除此之外,它是由weak_ptr控制的。当weak_ptr拷贝时,_M_weak_count就会增加;当weak_ptr析构时,_M_weak_count会相应减少,减到0时表明不再需要管理对象来控制托管对象了,所以管理对象也可以销毁了。

我们通过源码具体看看这两个引用计数是如何增减的。

_M_use_count的增减

  • _M_use_count的增加

对于拷贝构造:

 1//<shared_ptr_base.h> class __shared_ptr
 2template<typename _Tp, _Lock_policy _Lp>
 3class __shared_ptr : public __shared_ptr_access<_Tp, _Lp>
 4{
 5//...
 6public:
 7    using element_type = typename remove_extent<_Tp>::type;
 8    //...
 9    template<typename _Yp>
10    __shared_ptr(const __shared_ptr<_Yp, _Lp>& __r, element_type* __p) noexcept
11        : _M_ptr(__p), _M_refcount(__r._M_refcount) // never throws
12    { }
13
14//...
15
16private:
17    element_type* _M_ptr;         // Contained pointer.
18    __shared_count<_Lp> _M_refcount;    // Reference counter.
19};
20
21//<shared_ptr_base.h> class __shared_count
22template<_Lock_policy _Lp>
23class __shared_count
24{
25//...
26public:
27    //...
28    __shared_count(const __shared_count& __r) noexcept
29        : _M_pi(__r._M_pi)
30    {
31        if (_M_pi != 0)
32            _M_pi->_M_add_ref_copy();
33    }
34    //...
35};
36
37//<shared_ptr_base.h> class _Sp_counted_base
38template<>
39inline void _Sp_counted_base<_S_single>::_M_add_ref_copy()
40{
41    ++_M_use_count;
42}

每当拷贝构造一个shared_ptr,即调用基类__shared_ptr的拷贝构造函数时,会同时拷贝构造一个__shared_count对象_M_refcount。在这个计数对象的拷贝构造函数中,实际上调用了共享管理对象的基类方法_Sp_counted_base::_M_add_ref_copy(),然后_M_use_count就会自增1。这样一来所有同源的shared_ptr::use_count()的返回结果都会加1

对于拷贝赋值:

 1//<shared_ptr.h> class shared_ptr
 2template<typename _Tp>
 3class shared_ptr : public __shared_ptr<_Tp>
 4{
 5    template<typename _Arg>
 6    using _Assignable = typename enable_if<
 7        is_assignable<__shared_ptr<_Tp>&, _Arg>::value, shared_ptr&
 8    >::type;
 9
10    //...
11public:
12    //...
13    template <typename _Yp>
14    _Assignable<const shared_ptr<_Yp>&> operator=(const shared_ptr<_Yp>& __r) noexcept
15    {
16        this->__shared_ptr<_Tp>::operator=(__r);
17        return *this;
18    }
19};
20
21//<shared_ptr_base.h> __shared_ptr
22template<typename _Tp, _Lock_policy _Lp>
23class __shared_ptr : public __shared_ptr_access<_Tp, _Lp>
24{
25    //...
26
27    // Constraint for construction from shared_ptr and weak_ptr:
28    template<typename _Yp, typename _Res = void>
29    using _Compatible = typename
30    enable_if<__sp_compatible_with<_Yp*, _Tp*>::value, _Res>::type;
31
32    // Constraint for assignment from shared_ptr and weak_ptr:
33    template<typename _Yp>
34    using _Assignable = _Compatible<_Yp, __shared_ptr&>;
35
36    //...
37public:
38    //..
39    template <typename _Yp>
40    _Assignable<_Yp> operator=(const __shared_ptr<_Yp, _Lp>& __r) noexcept
41    {
42        _M_ptr = __r._M_ptr;
43        _M_refcount = __r._M_refcount;  // __shared_count::op= doesn't throw //调用__shared_count::operator=()
44        return *this;
45    }
46};
47
48//<shared_ptr_base.h> class __shared_count
49template<_Lock_policy _Lp>
50class __shared_count
51{
52//...
53public:
54    //...
55    __shared_count& operator=(const __shared_count& __r) noexcept
56    {
57        _Sp_counted_base<_Lp>* __tmp = __r._M_pi;
58        if (__tmp != _M_pi)
59        {
60            if (__tmp != 0)
61                __tmp->_M_add_ref_copy();
62            if (_M_pi != 0)
63                _M_pi->_M_release();
64            _M_pi = __tmp;
65        }
66        return *this;
67    }
68    //...
69};

每当拷贝赋值一个shared_ptr,即调用operator=()赋值操作符时,最终实际上也是调用了共享管理对象的基类方法_Sp_counted_base::_M_add_ref_copy(),从而实现_M_use_count自增。

  • _M_use_count的减少

每当shared_ptr析构时,_M_use_count相应会减少,具体的做法是:

 1//<shared_ptr_base.h> class __shared_count
 2template<_Lock_policy _Lp>
 3class __shared_count
 4{
 5//...
 6public:
 7    //...
 8    ~__shared_count() noexcept
 9    {
10        if (_M_pi != nullptr)
11            _M_pi->_M_release();
12    }
13    //...
14};
15
16//<shared_ptr_base.h> class _Sp_counted_base
17template<>
18inline void _Sp_counted_base<_S_single>::_M_release() noexcept
19{
20    if (--_M_use_count == 0)
21    {
22        _M_dispose();
23        if (--_M_weak_count == 0)
24            _M_destroy();
25    }
26}
27
28//<shared_ptr_base.h> class _Sp_counted_ptr
29template<typename _Ptr, _Lock_policy _Lp>
30class _Sp_counted_ptr final : public _Sp_counted_base<_Lp>
31{
32public:
33    //...
34    virtual void _M_dispose() noexcept { delete _M_ptr; }
35};

析构时调用了管理对象的基类方法_Sp_counted_base::_M_release(),每调用一次,_M_use_count就会减1;当减至0时,调用_M_dispose()用于销毁托管对象。

_M_weak_count的增减

  • _M_weak_count的增加

_M_weak_count_Sp_counted_base构造函数中初始化为1

1//<shared_ptr_base.h> class _Sp_counted_base
2_Sp_counted_base() noexcept : _M_use_count(1), _M_weak_count(1) {}

从上面的代码可以看出,shared_ptr在拷贝和移动时都不会增加_M_weak_count_M_weak_count的增加发生在拷贝构造weak_ptr时:

 1//<shared_ptr_base.h> class __weak_count
 2template<_Lock_policy _Lp>
 3class __weak_count
 4{
 5public:
 6    //...
 7    __weak_count(const __weak_count& __r) noexcept : _M_pi(__r._M_pi)
 8    {
 9        if (_M_pi != nullptr)
10            _M_pi->_M_weak_add_ref();
11    }
12}
13
14//<shared_ptr_base.h> class _Sp_counted_base
15template <>
16inline void _Sp_counted_base<_S_single>::_M_weak_add_ref() noexcept
17{
18    ++_M_weak_count;
19}

拷贝构造时,共享的管理对象调用基类方法_Sp_counted_base::_M_weak_add_ref()来递增_M_weak_count

  • _M_weak_count的减少

_M_weak_count的减少有两种情况:第一种情况是shared_ptr析构时最终调用了_Sp_counted_base::_M_release()_M_use_count减至0时会自动递减一次_M_weak_count;第二种情况是weak_ptr自身析构时最终调用了_Sp_counted_base::_M_weak_release()引起一次_M_weak_count递减。

第一种情况我们在shared_ptr析构的源码中已经了解了,第二种情况是这样实现的:

 1//<shared_prt_base.h> class __weak_count
 2template<_Lock_policy _Lp>
 3class __weak_count
 4{
 5public:
 6    //...
 7    ~__weak_count() noexcept
 8    {
 9        if (_M_pi != nullptr)
10            _M_pi->_M_weak_release();
11    }
12}
13
14//<shared_ptr_base.h> class _Sp_counted_base
15template <>
16inline void _Sp_counted_base<_S_single>::_M_weak_release() noexcept
17{
18    if (--_M_weak_count == 0)
19        _M_destroy();
20}
21
22//<shared_ptr_base.h> class _Sp_counted_ptr
23template<typename _Ptr, _Lock_policy _Lp>
24class _Sp_counted_ptr final : public _Sp_counted_base<_Lp>
25{
26public:
27    //...
28    virtual void _M_destroy() noexcept { delete this; }
29}

这两种情况一旦将_M_weak_count减至0,则调用_Sp_counted_base::_M_destroy()来销毁管理对象本身。

以上内容就是关于智能指针中引用计数实现的核心内容,所有源码均来自g++10.2版本的C++标准库。

Prev Post: 『C++内存对齐』
Next Post: 『C++ NULL与nullptr』