Photo by Ryan Stone on Unsplash

在之前红黑树中使用到了智能指针,所谓智能无非就是能够自己析构释放内存,不需要像裸指针一样手动 delete。

红黑树的代码中使用的 share_ptr,还有另外两个智能指针,分别是 unique_ptr 和 weak_ptr,本篇笔记主要记录的是 share_ptr 和 weak_ptr,至于 unique_ptr 等我后面再研究研究。

share_ptr

顾名思义,share 表示这个指针的所有权是可以共享的,换句话来说就是支持拷贝,允许多个“人”使用,跟原来的裸指针差不多,同时它的共享也是安全的,在 share_ptr 中有个引用计数,用来记录有多少个持有者,如果发生了拷贝复制,就 +1,如果发生析构就 -1,当减到 0 的时候那就会被释放掉。用下面的代码来举例:

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
#include <iostream>

using namespace std;

class Node final {
public:
using this_type = Node; // 注意这里,别名改用weak_ptr
using shared_type = std::shared_ptr<this_type>;
public:
shared_type next; // 因为用了别名,所以代码不需要改动
};

int main() {

auto n1 = make_shared<Node>();
auto n2 = make_shared<Node>();

cout << "========= step 1 =========" << endl;
cout << "n1 的引用计数为 : " << n1.use_count() << endl;
cout << "n2 的引用计数为 : " << n2.use_count() << endl;

cout << "========= step 2 =========" << endl;
n1->next = n2;
cout << "n1 的引用计数为 : " << n1.use_count() << endl;
cout << "n2 的引用计数为 : " << n2.use_count() << endl;

cout << "========= step 3 =========" << endl;
const auto& n3 = n2;
cout << "n1 的引用计数为 : " << n1.use_count() << endl;
cout << "n2 的引用计数为 : " << n2.use_count() << endl;

cout << "========= step 4 =========" << endl;
auto n4 = n2;
cout << "n1 的引用计数为 : " << n1.use_count() << endl;
cout << "n2 的引用计数为 : " << n2.use_count() << endl;
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
========= step 1 =========
n1 的引用计数为 : 1
n2 的引用计数为 : 1

========= step 2 =========
n1 的引用计数为 : 1
n2 的引用计数为 : 2

========= step 3 =========
n1 的引用计数为 : 1
n2 的引用计数为 : 2

========= step 4 =========
n1 的引用计数为 : 1
n2 的引用计数为 : 3
  • step 1:我打印了刚刚创建了两个 node 的引用计数,都是 1,说明在 share_ptr 创建时,引用计数初始为 1,也表示这个只能指针对象已经存在在内存中了。
  • step 2:我将 n2 赋值给了 n1->next,出现了拷贝赋值,n2 的引用计数改变。
  • step 3:这里又将 n2 的引用给了 n3 引用并不会触发拷贝赋值,所以引用计数也没有改变(注意:引用计数和引用是两个东西,不要搞混了)
  • step 4:最后将 n2 使用默认的赋值构造,赋值给了 n4,这里就触发了拷贝赋值,n2 的引用计数就又产生变化了。

如果要直接释放两个智能指针,哪怕引用计数大于等于 1,那可以直接调用 reset() 函数,将引用计数归零即可。

weak_ptr

这个倒霉玩意儿叫做弱指针,我写红黑树的时候,一开始用的就是这个玩意儿,把我搞得很迷糊。

简单来说,这个指针是用于解决循环应用的,比如刚才的代码,可能会出现以下情况:

1
2
n1->next = n2;
n2->next = n1;

这样就导致两个 share_ptr 的引用计数相同且一直 >= 1,两个指针无法析构释放,导致内存溢出;要解决这种循环引用只需要保证两个智能指针的引用计数不相同即可。

这种时候就需要用上 weak_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
50
51
52
53
54
55
#include <iostream>

using namespace std;

class Node final {
public:
using this_type = Node; // 注意这里,别名改用weak_ptr
using shared_type = std::shared_ptr<this_type>;
public:
shared_type next; // 因为用了别名,所以代码不需要改动
};

int main() {

auto n1 = make_shared<Node>();
auto n2 = make_shared<Node>();

{
weak_ptr<Node> nw1 = n1;

if (!nw1.expired()) {
cout << "========= step 1 =========" << endl;
cout << "n1 的引用计数为 : " << n1.use_count() << endl;
cout << "n2 的引用计数为 : " << n2.use_count() << endl;
cout << "nw 的引用计数为 : " << nw1.use_count() << endl;

cout << "========= step 2 =========" << endl;
nw1.lock()->next = n2;
cout << "n1 的引用计数为 : " << n1.use_count() << endl;
cout << "n2 的引用计数为 : " << n2.use_count() << endl;
cout << "nw 的引用计数为 : " << nw1.use_count() << endl;

cout << "========= step 3 =========" << endl;
auto nw2 = nw1;
cout << "n1 的引用计数为 : " << n1.use_count() << endl;
cout << "n2 的引用计数为 : " << n2.use_count() << endl;
cout << "nw 的引用计数为 : " << nw1.use_count() << endl;

cout << "========= step 4 =========" << endl;
auto n3 = nw1.lock();
cout << "n1 的引用计数为 : " << n1.use_count() << endl;
cout << "n2 的引用计数为 : " << n2.use_count() << endl;
cout << "nw 的引用计数为 : " << nw1.use_count() << endl;
}

cout << "========= step 5 =========" << endl;
cout << "n1 的引用计数为 : " << n1.use_count() << endl;
cout << "n2 的引用计数为 : " << n2.use_count() << endl;
cout << "nw 的引用计数为 : " << nw1.use_count() << endl;
}

cout << "========= step 6 =========" << endl;
cout << "n1 的引用计数为 : " << n1.use_count() << endl;
cout << "n2 的引用计数为 : " << n2.use_count() << endl;
}

输出结果:

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
========= step 1 =========
n1 的引用计数为 : 1
n2 的引用计数为 : 1
nw 的引用计数为 : 1

========= step 2 =========
n1 的引用计数为 : 1
n2 的引用计数为 : 2
nw 的引用计数为 : 1

========= step 3 =========
n1 的引用计数为 : 1
n2 的引用计数为 : 2
nw 的引用计数为 : 1

========= step 4 =========
n1 的引用计数为 : 2
n2 的引用计数为 : 2
nw 的引用计数为 : 2

========= step 5 =========
n1 的引用计数为 : 1
n2 的引用计数为 : 2
nw 的引用计数为 : 1

========= step 6 =========
n1 的引用计数为 : 1
n2 的引用计数为 : 2
  • step 1:这个是初始化为 1,我就不说了;
  • step 2:n2 发生了拷贝赋值,所以可以看到 n2 的引用计数加一了;
  • step 3:这里我将 nw1 拷贝赋值给 nw2,可以看到 nw1n1 的引用计数并没有发生改变,印证了之前说的 weak_ptr 不会影响引用计数;
  • step 4:通过 lock() 函数将 weak_ptr 中的对象拿出来(这一步也不会导致引用计数改变),然后拷贝赋值给 n3,这里可以看到 n1nw1 的引用计数改变了;
  • step 5:离开作用域,n3 被释放掉了,n1nw1 的应用计数变回原来的样子;
  • step 6:离开作用域,nw1 被释放掉,不影响 n1 的引用计数,但是通过 nw1n2 添加到 n1->next() 的操作并没有收到影响,n2 的引用计数还是 2

当时让我很迷惑的问题

先放上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Node final {
public:
using this_type = Node;
using shared_type = std::shared_ptr<this_type>; // 注意这里!!!!!!
// using shared_type = std::weak_ptr<this_type>;
public:
shared_type next;
};

Node::shared_type createNode() {
auto retNode = make_shared<Node>();
cout << "====== retNode ======" << endl;
cout << "retNode 引用数量 : " << retNode.use_count() << endl;
return retNode;
}

int main() {
auto newNode = createNode();
cout << "====== newNode ======" << endl;
cout << "newNode 引用数量 : " << newNode.use_count() << endl;
}

Node 类中如果使用 share_ptrweak_ptr 会各自输出什么?

结果:

1
2
3
4
5
6
7
8
9
10
11
# shared_ptr
====== retNode ======
retNode 引用数量 : 1
====== newNode ======
newNode 引用数量 : 1

# weak_ptr
====== retNode ======
retNode 引用数量 : 1
====== newNode ======
newNode 引用数量 : 0

这个问题当时就挺困扰我的,retNode 的引用计数都是 1,这没问题,但是为什么 newNode 的引用数量不一样呢,都是从 createNode() 的函数作用域出来了,引用计数应该减 1,为什么会不一样呢?

揭秘

先看 weak_ptr,在 createNode() 中创建了后,引用计数 +1,出了函数作用域后 -1 变成了 0,然后又拷贝赋值给 newNode,记得上面说的吗?weak_ptr 并不会影响引用计数,所以仍然是 0

再看看 share_ptr,出了 createNode 函数作用域后,也会 -1 变成 0,但是拷贝赋值给了 newNode,这个操作导致 share_ptr 引用计数 +1了,所以最后打印结果为 1。

主要问题就出在拷贝赋值上!!!!

两种智能指针使用场景

shared_ptr: 是强引用,无论如何都需要持有共享对象的时候就用它。

weak_ptr: 是弱引用,不一定要持有对象,只是“偶尔”想去看看对象在不在。

举个不是很恰当的栗子

就拿双向链表来说吧,父子节点会保留对方的信息,但两个指针不能同时使用 share_ptr,这样就出现循环引用了,当然,你可以在删除节点的时候将关系清除,但是如果代码有 BUG,两个指针就会造成循环引用,导致内存泄漏,可以用 share_ptr 作为 next 指针,使用 weak_ptr,作为 pre 指针。

这样 share_ptrweak_ptr 指针的引用计数不同,即可解决循环引用问题。