C++智能指针
2700 Words | Read in about 13 Min | View times
Overview
在传统C++中,我们一般使用new/delete
或malloc/free
等方式来管理内存,但是由于申请内存和释放内存的过程都需要开发者自己维护,而只要是需要手动管理的逻辑,就有可能出现忘记释放或者多次释放内存的错误,这也是传统C++一致为人诟病的一个问题。在现代C++中,标准库提供了智能指针的实现,这些指针用于帮助确保程序不会出现内存和资源泄露,并具有异常安全。可以说现代C++的智能指针使得内存管理更加方便更加安全。本节内容我们将解析各类智能指针的底层实现原理。
本系列文章将包括以下领域:
本章其他内容请见 《现代C++》
你知道哪些智能指针?
shared_ptr的设计原理是什么?
如何自己设计一个智能指针?
以上问题都是C++面试中的高频题目,回答这些问题就得先搞懂智能指针背后的设计原理。
为什么需要智能指针?
首先,我们需要了解一个概念——引用计数。引用计数是为了防止内存泄漏而产生的。基本思路是对于在堆动态分配的对象进行引用计数,每当增加一次对同一个对象的引用,那么引用对象的引用计数就会增加一次;每当删除一次对同一个对象的引用,那么引用对象的引用计数就会减少一次;当引用对象的引用计数减为零时,就自动销毁指向的堆内存。引用计数能更清晰地表达一个内存资源的生命周期。
在传统C++中,需要程序员手动释放资源总是有漏洞的。因为我们可能忘记了释放而导致内存泄漏,甚至多次释放而导致异常崩溃。所以有一种惯用技法叫资源获取即初始化(RAII,Resource Acquisition Is Initialization),我们在构造对象时申请空间,在析构时释放空间。当我们有对象自由分配内存的需求时,传统C++只能使用new
和delete
去操作内存资源,而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
可以消除显示使用new
,make_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_shared
和enable_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
是空智能指针,则p2
和p3
也是空智能指针,引用计数是0
;反之,则p2
和p3
指向同一块堆内存,同时堆内存对象的引用计数都会加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_ptr
或weak_ptr
按照某种关系进行比较并返回比较结果,这种比较关系就是owner-based order
。
一个智能指针(stored_pointer
)有可能指向另一个智能指针(owned_pointer
)中的某一部分,但又要保证这两个智能指针销毁时,只对那个被指向的对象完整地析构一次,而不是两个指针分别析构一次。如果堆内存对象只有一个,但被许多智能指针指向了其中的不同部分,那么这些指针本身的地址是不同的,operator<()
可以比较它们,并且它们都不是对象的owner
,它们销毁时不会析构对象。
owner-based order
就是如果进行比较的两个智能指针指向的是同一个对象(包含继承关系),那么就认为这两个指针是“相等”的。
对于a.owner_before(b)
:
a) 如果a
与b
同为空指针,或者同时指向同一个对象(包含继承关系),就返回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_ptr
的owner_before()
用法,提供weak_ptr
的owner-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_shared
和new
方式创建shared_ptr
的区别:
需要注意的是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++标准库的实现方案是这样的:
整个实现方案中的核心是_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_ptr
和weak_ptr
的use_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++标准库。