引言

C++ 中经常需要 new 一个对象,开辟一个内存空间,返回一个指针来操作这个内存。使用完毕之后,需要通过 delete 来释放内存空间。如果内存没有释放,那这块内存将无法再利用,导致内存泄漏。为降低人为疏忽,C++ 11 的新特性中引入了三种智能指针,来自动化地管理内存资源:

unique_ptr: 管理的资源唯一的属于一个对象,但是支持将资源移动给其他 unique_ptr 对象。当拥有所有权的 unique_ptr 对象析构时,资源即被释放。

shared_ptr: 管理的资源被多个对象共享,内部采用引用计数跟踪所有者的个数。当最后一个所有者被析构时,资源即被释放。

weak_ptr: 与 shared_ptr 配合使用,虽然能访问资源但却不享有资源的所有权,不影响资源的引用计数。有可能资源已被释放,但 weak_ptr 仍然存在。因此每次访问资源时都需要判断资源是否有效。

本文主要在循环引用的场景下探讨 shard_ptr 和 weak_ptr 原理。

循环引用

shared_ptr 通过引用计数的方式管理内存,当进行拷贝或赋值操作时,每个 shared_ptr 都会记录有多少个其他的 shared_ptr 指向相同的对象,当引用计数为 0 时,内存将被自动释放。

1
2
auto p = make_shared<int>(10); // 创建一个名为 p 的 shared_ptr,指向一个取值为 10 的 int 型对象,这个数值 10 的引用计数为 1,只有 p
auto q(p); // 创建一个名为 q 的 shared_ptr,并用 p 初始化,此时 p 和 q 指向同一个对象,此时数值 10 的引用计数为 2

当对 shared_ptr 赋予新值,或被销毁时,引用计数会递减。

1
2
auto r = make_shared<int>(20); // 创建一个名为 r 的 shared_ptr,指向一个取值为 20 的 int 型对象,这个数值 20 的引用计数为 1,只有 r
r = q; // 对 r 赋值,让 r 指向数值 10。此时数值 10 的引用计数加 1 为 3,数值 20 的引用计数减 1 位 0,数值 20 的内存将被自动释放

通常情况下 shared_ptr 可以正常运转,但是在循环引用的场景下,shared_ptr 无法正确释放内存。循环引用,顾名思义,A 指向 B,B 指向 A,在表示双向关系时,是很可能出现这种情况的,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <memory>
using namespace std;

class Son;

class Father {
public:
shared_ptr<Son> son_;
Father() {
cout << __FUNCTION__ << endl;
}
~Father() {
cout << __FUNCTION__ << endl;
}
};

class Son {
public:
shared_ptr<Father> father_;
Son() {
cout << __FUNCTION__ << endl;
}
~Son() {
cout << __FUNCTION__ << endl;
}
};

int main()
{
auto son = make_shared<Son>();
auto father = make_shared<Father>();
son->father_ = father;
father->son_ = son;
cout << "son: " << son.use_count() << endl;
cout << "father: " << father.use_count() << endl;
return 0;
}

程序的执行结果如下:

Son

Father

son: 2

father: 2

可以看到,程序分别执行了 Son 和 Father 的构造函数,但是没有执行析构函数,出现了内存泄漏。

shared_ptr 原理

shared_ptr 实际上是对裸指针进行了一层封装,成员变量除了裸指针外,还有一个引用计数,它记录裸指针被引用的次数(有多少个 shared_ptr 指向这同一个裸指针),当引用计数为 0 时,自动释放裸指针指向的资源。影响引用次数的场景包括:构造、赋值、析构。基于三个最简单的场景,实现一个 demo 版 shared_ptr 如下(实现既不严谨也不安全,仅用于阐述原理):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <memory>
using namespace std;

template<typename T>
class SharedPtr {
public:
int* counter; // 引用计数,用指针表示,多个 SharedPtr 之间可以同步修改
T* resource; // 裸指针

SharedPtr(T* resc = nullptr) { // 构造函数
cout << __PRETTY_FUNCTION__ << endl;
counter = new int(1);
resource = resc;
}

SharedPtr(const SharedPtr& rhs) { // 拷贝构造函数
cout << __PRETTY_FUNCTION__ << endl;
resource = rhs.resource;
counter = rhs.counter;
++*counter;
}

SharedPtr& operator=(const SharedPtr& rhs) { // 拷贝赋值函数
cout << __PRETTY_FUNCTION__ << endl;
--*counter; // 原来指向的资源的引用计数减 1
if (*counter == 0) {
delete counter;
delete resource;
}

resource = rhs.resource;
counter = rhs.counter;
++*counter; // 新指向的资源的引用计数加 1
}

~SharedPtr() { // 析构函数
cout << __PRETTY_FUNCTION__ << endl;
--*counter;
if (*counter == 0) {
delete counter;
delete resource;
}
}

int use_count() {
return *counter;
}
};

在循环引用示例中,用到了 make_shared 函数:

1
auto son = make_shared<Son>();  // 新建一个 Son 对象,返回指向这个 Son 对象的指针

此用法等价于:

1
2
auto son_ = new Son();  // 新建一个 Son 对象,返回指向这个对象的指针 son_
shared_ptr<Son> son(son_); // 创建一个管理 son_的 shared_ptr

代入 SharedPtr 的实现来分析示例中 main 函数的执行过程,可以得到:

1
2
3
4
auto son = make_shared<Son>();  // 调用构造函数,son.counter=1
auto father = make_shared<Father>(); // 调用构造函数,father.counter=1
son->father_ = father; // 调用赋值函数,son.counter=2
father->son_ = son; // 调用赋值函数,father.counter=2

当 main 函数执行完时,执行析构函数,此时由于 son.counter=1,father.couter=1,不满足 if 条件,不会实行 delete 命令完成资源释放,导致内存泄漏。

weak_ptr 原理

为解决循环引用的问题,仅使用 shared_ptr 是无法实现的。堡垒无法从内部攻破的时候,需要借助外力,于是有了 weak_ptr,字面意思是弱指针。为啥叫弱呢?shared_ptr A 被赋值给 shared_ptr B 时,A 的引用计数加 1;shared_ptr A 被赋值给 weak_ptr C 时,A 的引用计数不变。引用力度不够强,不足以改变引用计数,所以就弱了(个人理解,有误请指正)。

weak_ptr 在使用时,是与 shared_ptr 绑定的。基于 SharedPtr 实现来实现 demo 版的 WeakPtr,并解决循环引用的问题,全部代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include <iostream>
#include <memory>
using namespace std;

template<typename T>
class SharedPtr {
public:
int* counter;
int* weakref;
T* resource;

SharedPtr(T* resc = nullptr) {
cout << __PRETTY_FUNCTION__ << endl;
counter = new int(1);
weakref = new int(0);
resource = resc;
}

SharedPtr(const SharedPtr& rhs) {
cout << __PRETTY_FUNCTION__ << endl;
resource = rhs.resource;
counter = rhs.counter;
++*counter;
}

SharedPtr& operator=(const SharedPtr& rhs) {
cout << __PRETTY_FUNCTION__ << endl;
--*counter;
if (*counter == 0) {
delete counter;
delete resource;
}

resource = rhs.resource;
counter = rhs.counter;
++*counter;
}

~SharedPtr() {
cout << __PRETTY_FUNCTION__ << endl;
--*counter;
if (*counter == 0) {
delete counter;
delete resource;
}
}

int use_count() {
return *counter;
}
};

template<typename T>
class WeakPtr {
public:
T* resource;

WeakPtr(T* resc = nullptr) {
cout << __PRETTY_FUNCTION__ << endl;
resource = resc;
}

WeakPtr& operator=(SharedPtr<T>& ptr) {
cout << __PRETTY_FUNCTION__ << endl;
resource = ptr.resource;
++*ptr.weakref; // 赋值时引用计数 counter 不变,改变弱引用计数 weakref
}

~WeakPtr() {
cout << __PRETTY_FUNCTION__ << endl;
}
};

class Son;

class Father {
public:
SharedPtr<Son> son_;
Father() {
cout << __PRETTY_FUNCTION__ << endl;
}
~Father() {
cout << __PRETTY_FUNCTION__ << endl;
}
};

class Son {
public:
WeakPtr<Father> father_; // 将 SharedPtr 改为 WeakPtr
Son() {
cout << __PRETTY_FUNCTION__ << endl;
}
~Son() {
cout << __PRETTY_FUNCTION__ << endl;
}
};

int main()
{
auto son_ = new Son(); // 创建一个 Son 对象,返回指向 Son 对象的指针 son_
auto father_ = new Father(); // 创建一个 Father 对象,返回指向 Father 对象的指针 father_
SharedPtr<Son> son(son_); // 调用 SharedPtr 构造函数:son.counter=1, son.weakref=0
SharedPtr<Father> father(father_); // 调用 SharedPtr 构造函数:father.counter=1, father.weakref=0
son.resource->father_ = father; // 调用 WeakPtr 赋值函数:father.counter=1, father.weakref=1
father.resource->son_ = son; // 调用 SharedPtr 赋值函数:son.counter=2, son.weakref=0
cout << "son: " << son.use_count() << endl;
cout << "father: " << father.use_count() << endl;
return 0;
}

代码执行结果如下:

WeakPtr::WeakPtr(T*) [with T = Father]

Son::Son()

SharedPtr::SharedPtr(T*) [with T = Son]

Father::Father()

SharedPtr::SharedPtr(T*) [with T = Son]

SharedPtr::SharedPtr(T*) [with T = Father]

WeakPtr& WeakPtr::operator=(SharedPtr&) [with T = Father]

SharedPtr& SharedPtr::operator=(const SharedPtr&) [with T = Son]

son: 2

father: 1

SharedPtr::~SharedPtr() [with T = Father]

Father::~Father()

SharedPtr::~SharedPtr() [with T = Son]

SharedPtr::~SharedPtr() [with T = Son]

Son::~Son()

WeakPtr::~WeakPtr() [with T = Father]

可以看到 Son 对象和 Father 对象均被析构,内存泄漏的问题得到解决。析构过程解读如下:

SharedPtr::~SharedPtr() [with T = Father] # 析构 father,由于 father.couter=1,减 1 后执行 delete father_

Father::~Father() # 析构 father_,执行~Father(),进一步析构成员变量

SharedPtr::~SharedPtr() [with T = Son] # 析构 SharedPtr,此时 son.couter 减 1,son.counter=1

SharedPtr::~SharedPtr() [with T = Son] # 析构 son,由于 son.counter=1,减 1 后执行 delete son_

Son::~Son() # 析构 son_,执行~Son(),进一步析构成员变量

WeakPtr::~WeakPtr() [with T = Father] # 析构 WeakPtr

总结

  1. 尽量使用智能指针管理资源申请与释放,减少人为 new 和 delete 误操作和考虑不周的问题。
  2. 使用 make_shared 来创建 shared_ptr,如果先 new 一个对象,再用这个对象的裸指针构造一个 shared_ptr 指针,可能出现问题。shared_ptr 会自动释放资源,如果再手动 delete,释放两次那就挂了。