前言

在 C 语言中,动态内存管理一直是一个非常头疼的问题,你需要自己申请内存与释放内存,一不小心就容易触发内存泄露、指针悬挂、重复释放等问题,这些问题可能还是隐性的,不容易排查。在 C++ 11 引入的三种智能指针 shared_ptrunique_ptrweak_ptr 用于自动管理动态内存,当然不规范的使用它们依然会导致一些问题。值得注意的是,在更早的 C++ 版本中的 auto_ptrC++ 11 开始被弃用,在 C++ 17 被移除,你可以使用 unique_ptr 来替代它,本文不会讲解。

本文将从两个方面介绍 shared_ptrunique_ptrweak_ptr 三种智能指针,这包括:规范使用智能指针、智能指针原理和代码复现。

shared_ptr 共享指针

创建共享指针

shared_ptr 可以使用下面的四种方法构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void deleter(int *p) {
delete p;
cout << "Free " << p << endl;
}

int main() {
std::shared_ptr<int> sp; // 未初始化, 指向 NULL
auto sp = std::make_shared<int>(10); // 使用 make_shared 初始化, 这被认为是最安全的
// 我们会在 weak_ptr 章节的最后介绍 shared_ptr 与 make_shared 的区别。
std::shared_ptr<int> sp(new auto(10)); // 直接传入申请的指针也是安全的, 但依然推荐上一种方式 (见规范使用)
std::shared_ptr<int> sp(new auto(10), deleter); // 使用自定义析构器, 第二种方式不支持传入析构器
// shared_ptr 还可被一个 unique_ptr 右值 / weak_ptr 初始化 (后面会讲到这两个智能指针)
return 0;
}

自定义析构器通常在两种情形下使用:一是确实需要重写析构器,进行其他相关操作;二是与 C 语言链接,在 C 语言中没有析构函数,这可能需要用户来绑定析构函数。

共享指针相关方法

在介绍 shared_ptr 相关方法前有必要首先简单介绍其基本原理。shared_ptr 用于在不同变量间共享资源,其底层维护了一组绑定的指针和计数器,计数器维护了其被多少变量共享,当数量为 0 时,底层指针指向的内存将被释放。

方法 功能
sp.get() 获取底层指针,返回 T *。这个操作是危险的。
sp.reset() 解除底层指针的绑定,计数器自减。
std::swap(sp1, sp2) 交换底层指针的绑定,不影响计数器。
sp.use_count() 获取计数器计数,返回 long
sp.unique() 是否是独占的,即计数器是否为 1,返回 bool
static_cast<bool>(sp) 是否绑定底层指针,shared_ptr 默认初始化时或 reset 后底层指针为 NULL
*sp / sp-> 返回底层指针的解引用,此操作前用户应该确保 sp 绑定非 NULL
sp1 = sp2 sp2 的底层指针赋值到 sp1,进行相应计数。
sp1 = std::move(sp2) sp2 的底层指针移动到 sp1,进行相应计数。

这里需要注意,移动语义与赋值语义在 shared_ptr 中是不同的(在 unique_ptr 中也不同),移动语义在完成赋值后将 reset 原智能指针。

规范使用 shared_ptr

智能指针的初衷是为用户提供自动的动态内存管理,同时也保证了程序安全。但是,如果智能指针底层指针泄露到外部,同样存在安全风险。因此,永远不要同时使用底层指针和智能指针。下面我们将辨析一些可能存在风险的操作:

1
2
3
4
int *p = new auto(10);
std::shared_ptr<int> sp(p);
// ...
// 这样的操作是合法的但也是危险的, 会出现后面 p 再被用到时可能已经被智能指针释放等问题

这也是为什么我们之前说 std::make_shared 是更安全的方案。当然,如果你像下面一样书写也是安全的。

1
2
std::shared_ptr<int> sp(new auto(10));
// new auto(10) 的返回值只存在于智能指针中, 因此还是安全的
1
2
3
4
auto sp = std::make_shared<int>(10);
int *p = sp.get();
// ...
// 这样的操作同样危险, p 同样也有可能被智能指针释放

在这里 .get 获取了 sp 的底层指针,而用户将它储存下来。这样的作法是危险的,.get 提供了一个危险的接口,它的返回值应该被立即使用,例如用于打印等等。如果你确实需要存储这个返回值,p 应该存在于尽量短小的生命周期。

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
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
// shared_ptr 代码复现
// 这个地方我偷懒没有复现 deleter
#include <bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;

template <typename T>
class SharedPtr {
private:
size_t *count = NULL;
T *ptr = NULL;

public:
SharedPtr(T *p = NULL) {
if (p == NULL)
return;
this->count = new size_t(1);
this->ptr = p;
}

SharedPtr &operator=(const SharedPtr &sp) {
if (this == &sp)
return *this;
this->reset();
this->count = sp.count;
this->ptr = sp.ptr;
if (this->count)
++*this->count;
return *this;
}

SharedPtr &operator=(SharedPtr &&sp) {
if (this == &sp)
return *this;
this->reset();
this->count = sp.count;
this->ptr = sp.ptr;
sp.count = NULL;
sp.ptr = NULL;
return *this;
}

void reset() {
if (this->ptr == NULL)
return;
if (--*this->count == 0) {
delete this->count;
delete this->ptr;
}
this->count = NULL;
this->ptr = NULL;
}

SharedPtr(const SharedPtr &sp) { this->operator=(sp); }

SharedPtr(SharedPtr &&sp) { this->operator=(std::move(sp)); }

operator bool() const { return this->ptr; }

T &operator*() { return *this->ptr; }

T *operator->() { return this->ptr; }

T *get() const { return this->ptr; }

void swap(SharedPtr &sp) {
std::swap(this->count, sp.count);
std::swap(this->ptr, sp.ptr);
}

bool unique() const { return this->count ? *this->count == 1 : false; }

long use_count() const { return this->count ? *this->count : 0; }

~SharedPtr() { this->reset(); }
};

int main() {
SharedPtr<int> sp1(new auto(10));
SharedPtr<int> sp2(new auto(11));
auto sp3 = sp1;
auto sp4 = sp2;
cout << sp1.use_count() << ' ' << sp2.use_count() << ' ' << sp3.use_count() << ' ' << sp4.use_count() << endl;

sp1 = SharedPtr<int>(std::move(sp2));
cout << sp1.use_count() << ' ' << sp2.use_count() << ' ' << sp3.use_count() << ' ' << sp4.use_count() << endl;

std::swap(sp3, sp4);
cout << sp1.use_count() << ' ' << sp2.use_count() << ' ' << sp3.use_count() << ' ' << sp4.use_count() << endl;

sp3.reset();
cout << sp1.use_count() << ' ' << sp2.use_count() << ' ' << sp3.use_count() << ' ' << sp4.use_count() << endl;

return 0;
}

unique_ptr 独占指针

创建独占指针

与共享指针不同的是,独占指针 unique_ptr 不允许拷贝,它保证了至多只有一个智能指针占有这份资源。

1
2
3
4
5
6
7
8
9
10
11
12
void deleter(int *p) {
delete p;
cout << "Free " << p << endl;
}

int main() {
std::unique_ptr<int> up; // 未初始化, 指向 NULL
auto up = std::make_unique<int>(10); // 使用 make_unique 初始化, 这被认为是最安全的
std::unique_ptr<int> up(new auto(10)); // 直接传入申请的指针也是安全的
std::unique_ptr<int, void(*)(int *)> up(new auto(10), deleter); // 使用自定义析构器
return 0;
}

这里我们注意到,对于unique_ptr 使用自定义析构器,我们必须传入析构函数模板,这与 unique_ptr 的实现原理有关。当然针对这个问题我们可以使用别名简化:

1
2
3
4
5
6
7
8
9
namespace std {
template <typename T>
using UniquePtr = unique_ptr<T, void(*)(T *)>;
}

int main() {
std::UniquePtr<int> up(new auto(10), deleter); // 使用自定义析构器
return 0;
}

独占指针相关方法

与共享指针不同的是,unique_ptr 由于是独占的,不需要维护计数器。同时它不提供拷贝构造函数与拷贝赋值函数,从而把保证独占资源。

方法 功能
up.get() 获取底层指针,返回 T *。这个操作是危险的。
up.reset() 接触底层指针的绑定,释放底层内存。
up.release() 接触底层指针的绑定,返回底层指针。不获取返回值将产生内存泄露。
std::swap(up1, up2) 交换底层指针的绑定。
static_cast<bool>(up) 是否绑定底层指针,unique_ptr 默认初始化时或 reset 后底层指针为 NULL
*up / up-> 返回底层指针的解引用,此操作前用户应该确保 up 绑定非 NULL
up1 = std::move(up2) up2 的底层指针移动到 up1

规范使用 unique_ptr

在使用 unique_ptr 时,不仅需要注意和 shared_ptr 相同的安全问题,还需要注意 release 方法存在的问题。

如果用户不接收 .release 的返回值将会造成内存泄露,因为 release 是进行指针所有权的转移,而不会释放底层内存。例如下面这个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;

namespace std {
template <typename T>
using UniquePtr = unique_ptr<T, void(*)(T *)>;
}

void deleter(int *p) {
delete p;
cout << "Free " << p << endl;
}

int main() {
std::UniquePtr<int> up(new auto(10), deleter);
up.release();
// ...
return 0;
}
// 输出: (空)

注意,这里我们使用了自定义析构器,如果内存是被 unique_ptr 释放的,那么将会输出 Free ...。而这里没有输出,说明产生了内存泄露。为了解决这个,每次使用 release 时一定要接收其返回值,这里可以使用底层指针接收,也可以使用独占指针接收。当然,我更推荐的做法是使用 unique_ptr 的移动语义,这样一定不会出现上面讨论的问题。

unique_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
56
57
58
59
60
61
62
63
64
65
66
67
// unique_ptr 代码复现
// 又偷懒没有复现 deleter
#include <bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;

template <typename T>
class UniquePtr {
private:
T *ptr = NULL;

public:
UniquePtr(T *p = NULL) : ptr(p) {}

UniquePtr &operator=(UniquePtr &&up) {
if (this == &up)

this->reset();
this->ptr = up.ptr;
up.ptr = NULL;
return *this;
};

void reset() {
if (this->ptr)
delete this->ptr;
this->ptr = NULL;
}

T *release() {
T *p = this->ptr;
this->ptr = NULL;
return p;
}

UniquePtr(UniquePtr &&up) { this->operator=(std::move(up)); }

operator bool() const { return this->ptr; }

T &operator*() { return *this->ptr; }

T *operator->() { return this->ptr; }

T *get() const { return this->ptr; }

void swap(UniquePtr &up) { std::swap(this->ptr, up.ptr); }

~UniquePtr() { this->reset(); }

UniquePtr(const UniquePtr &up) = delete;

UniquePtr &operator=(const UniquePtr &up) = delete;
};

int main() {
UniquePtr<int> up1(new auto(10));
UniquePtr<int> up2(new auto(11));
cout << (up1 ? std::to_string(*up1) : "NULL") << ' ' << (up2 ? std::to_string(*up2) : "NULL") << endl;

up2 = std::move(up1);
cout << (up1 ? std::to_string(*up1) : "NULL") << ' ' << (up2 ? std::to_string(*up2) : "NULL") << endl;

std::swap(up1, up2);
cout << (up1 ? std::to_string(*up1) : "NULL") << ' ' << (up2 ? std::to_string(*up2) : "NULL") << endl;
return 0;
}

weak_ptr 弱指针

创建弱指针

弱指针是辅助于 shared_ptr 的存在,它指向一个 shared_ptr,并对其计数器不产生影响。weak_ptr 依赖于 shared_ptr 而存在,并且不直接参与自动内存管理,因此被称为弱指针。weak_ptr 主要用于解决循环引用的问题。

1
2
3
4
5
6
int main() {
std::shared_ptr<int> sp(new auto(10));
std::weak_ptr<int> wp; // 未初始化, 指向 NULL
std::weak_ptr<int> wp(sp); // weak_ptr 必须由一个 shared_ptr 或 weak_ptr 初始化
return 0;
}

弱指针相关方法

从这张表也可以看出,weak_ptr 提供的方法也非常 “弱”,它没有 operator bool / operator* / operator-> 这些方法,所有值访问都通过 .lock 返回的 shared_ptr 间接完成。

方法 功能
wp.lock() 获取 weak_ptr 底层 shared_ptr。如果底层已弃用,返回包含 NULLshared_ptr
wp.reset() 解除与底层 shared_ptr 的绑定。
std::swap(wp1, wp2) 交换底层 shared_ptr 的绑定。
wp.use_count() 获取底层 shared_ptr 计数器计数,返回 long
wp.expired() 是否底层已弃用,即计数器是否为 0,返回 bool
wp = sp / wp1 = wp2 weak_ptr 的拷贝赋值函数和移动赋值函数是相同的。

注意,weak_ptr 未绑定 shared_ptr 的情况与底层已弃用被视为是相同的。weak_ptr 其底层事实上并非指向 shared_ptr,依然是计数器和普通指针,具体可见代码复现部分。

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
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// weak_ptr 代码复现
// 由于 shared_ptr 和 weak_ptr 需要共享计数器, 这里有两种解决方案:
// * counter 是结构体包含 shared 与 weak 计数, 底层由 shared_ptr 与 weak_ptr 共同管理.
// * counter 由 shared_ptr 自动管理, 相当于嵌套管理.
// 看了网上很多资料是前者, 看了 std 源码也是前者. 于是我决定写个后者方案?
// 评价两种: 前者时空更优, 但代码实现上复杂; 后者代码实现清晰, 但对语言要求高.
#include <bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;

template <typename T, typename Count>
class SharedPtr;

template <typename T>
class WeakPtr;

// 出于对 weak_ptr 的支持, 需要略修改
// 后者方案对主逻辑代码只需修改一处
template <typename T, typename Counter = SharedPtr<size_t, size_t *>>
class SharedPtr {
friend class WeakPtr<T>;

private:
Counter count = NULL;
T *ptr = NULL;

// 编译时推导 counter 析构器
void delete_count() {
static_assert(
std::is_same<Counter, size_t *>::value or std::is_same<Counter, SharedPtr<size_t, size_t *>>::value,
"Counter of SharedPtr must be either `size_t *` or `SharedPtr<size_t, size_t *>`.");
if constexpr (std::is_same<Counter, size_t *>::value) {
if (*this->count == 0)
delete this->count;
this->count = NULL;
} else if constexpr (std::is_same<Counter, SharedPtr<size_t, size_t *>>::value) {
this->count.reset();
}
}

public:
// ...无需修改

// 主逻辑代码只需修改此一处
void reset() {
if (this->ptr == NULL)
return;
if (--*this->count == 0) {
delete this->ptr;
}
this->ptr = NULL;
this->delete_count();
}

// ...无需修改
};

template <typename T>
class WeakPtr {
private:
SharedPtr<size_t, size_t *> count = NULL;
T *ptr = NULL;

public:
WeakPtr() = default;
WeakPtr(const SharedPtr<T> &sp) { this->operator=(sp); }
WeakPtr(SharedPtr<T> &&sp) : WeakPtr(sp) {}
WeakPtr(const WeakPtr &) = default;
WeakPtr(WeakPtr &&) = default;

WeakPtr &operator=(const SharedPtr<T> &sp) {
this->count = sp.count;
this->ptr = sp.ptr;
return *this;
}
WeakPtr &operator=(SharedPtr<T> &&sp) { return this->operator=(sp); }
WeakPtr &operator=(const WeakPtr &) = default;
WeakPtr &operator=(WeakPtr &&) = default;

void reset() {
this->count = NULL;
this->ptr = NULL;
}

SharedPtr<T> lock() const {
if (this->expired())
return NULL;
return this->ptr;
}

void swap(WeakPtr &wp) {
std::swap(this->count, wp.count);
std::swap(this->ptr, wp.ptr);
}

bool expired() const {
if (this->count == NULL)
return true;
if (*this->count == 0) {
const_cast<WeakPtr *>(this)->reset();
return true;
}
return false;
}

long use_count() const { return this->expired() ? 0 : *this->count; }
};

int main() {
SharedPtr<int> sp1(new auto(10));
SharedPtr<int> sp2(new auto(11));
auto sp3 = sp1;
auto sp4 = sp2;

WeakPtr<int> wp1(sp1);
WeakPtr<int> wp2(sp2);
cout << wp1.use_count() << ' ' << wp2.use_count() << endl;

sp1 = SharedPtr<int>(std::move(sp2));
cout << wp1.use_count() << ' ' << wp2.use_count() << endl;

std::swap(sp3, sp4);
cout << wp1.use_count() << ' ' << wp2.use_count() << endl;

sp3.reset();
cout << wp1.use_count() << ' ' << wp2.use_count() << endl;

sp1.reset();
cout << wp1.use_count() << ' ' << wp2.use_count() << endl;
sp2.reset();
cout << wp1.use_count() << ' ' << wp2.use_count() << endl;
sp3.reset();
cout << wp1.use_count() << ' ' << wp2.use_count() << endl;
sp4.reset();
cout << wp1.use_count() << ' ' << wp2.use_count() << endl;
return 0;
}

shared_ptr 与 make_shared 的区别

我们在 weak_ptr 的章节讲解 shared_ptrmake_shared 方法的区别,自然是因为两者在使用 weak_ptr 时存在区别。

前面讲到,我们可以使用 shared_ptrmake_shared 两种方法构造 shared_ptr 对象。我们建议用户使用 make_shared 构造对象,因为 make_shared 避免了暴露指针带来的安全风险。但是另一方面,make_shared 存在比 shared_ptr 更好的效率,两者在底层内存结构也是不同的。

可以参考下面这两篇文章:

why make_shared ? - Bitdewy

std::make_shared - cppreference.com