Cpp 智能指针
概述
C++标准库主要提供了三种智能指针:
shared_ptr
:共享指针,允许资源共享,在内部维持一个引用计数,复制会增加引用计数,析构则会减少引用计数,引用计数为零则释放资源unique_ptr
:独享指针,独自负责资源的管理,析构时释放资源weak_ptr
:弱共享指针,作为共享指针的辅助手段,可以指向共享指针的内容但是不参与引用计数,主要为了解决共享指针的循环引用问题
除此之外,在早期还提供了auto_ptr
,但是目前已经被废弃。下面先介绍RAII思想,然后介绍三种智能指针的使用。
简单示例与RAII
先给出使用原始指针和unique_ptr
管理资源的对比示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15{
int* p = new int(100);
// ...
delete p; // 必须记得手动释放内存
}
{
std::unique_ptr<int> up = std::make_unique<int>(200);
//...
// up 在析构时自动释放内存
}
这里利用了C++对象在离开作用域时自动调用析构函数的特点,将内存的释放操作放在对象的析构函数中,省去手动操作释放资源的麻烦。
事实上这种措施是必要的,因为C++存在的异常机制,在普通的内存管理方案中,一旦在delete p;
之前触发了异常,就会导致指针p
所对应的资源泄露,
因为异常的抛出过程只保证栈上的局部变量被逆序析构,并不会管理堆内存,因此只有把资源释放放在析构过程中,才能保证异常安全。
这里自然地引出了RAII的概念:RAII(Resource Acquisition Is Initialization)是由C++之父Bjarne Stroustrup提出的概念, 翻译为资源获取即初始化,这句话带来的直接结果是:析构时自动释放资源。
RAII这个名称起得非常随意,不需要照着原文去理解,实际上我们关注的重点不是构造而是析构过程。
一个简单的体现RAII的例子如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Buffer {
public:
explicit Buffer(size_t size) : m_data(new int[size]{}) {}
int &operator[](size_t index) { return m_data[index]; }
const int &operator[](size_t index) const { return m_data[index]; }
Buffer(const Buffer &) = delete;
Buffer(Buffer &&) = delete;
Buffer &operator=(const Buffer &) = delete;
Buffer &operator=(Buffer &&) = delete;
~Buffer() { delete[] m_data; }
private:
int *m_data = nullptr;
};
这里为了简化,我们直接禁用了所有的拷贝和移动操作,确保对象以独占的方式管理资源。
shared_ptr
创建
使用默认构造函数可以创建一个空的shared_ptr
对象
1
2
3std::shared_ptr<int> sp;
std::shared_ptr<int[]> sps;
可以直接使用原始指针初始化,通常是将new
得到的资源立刻赋值给shared_ptr
对象
1
2
3std::shared_ptr<int> sp(new int(20));
std::shared_ptr<int[]> sps(new int[10]);
使用 std::make_shared
函数也可以创建并初始化,这个语句可以避免显式的new
调用
1
2
3std::shared_ptr<int> sp = std::make_shared<int>(10);
std::shared_ptr<int[]> sps = std::make_shared<int[]>(10);
shared_ptr
对象允许被复制或移动 1
2std::shared_ptr<int> sp2 = sp; // 复制
std::shared_ptr<int> sp3 = std::move(sp2); // 移动
通过weak_ptr
对象的lock()
方法也可以创建一个shared_ptr
对象,见下文。
使用
使用星号*
可以像原始指针一样地访问和修改指针指向的资源
1
2*sp = 30;
int value = *sp;
支持直接检查当前指针是否为空(即支持向布尔类型的自动转换)
1
2
3if (sp) {
// sp 非空
}
支持对shared_ptr
指针重置(包括置空和赋予新的值)
1
2sp.reset();
sp.reset(new int(40));
可以使用get()
方法获取底层原始指针 1
int* raw_ptr = sp.get();
但是获取智能指针所对应的裸指针的做法是不建议的。
可以使用use_count()
方法获取引用计数 1
long count = sp.use_count();
这个方法的效率偏低,通常用于调试,不适合频繁调用。
原理
shared_ptr
对象除了包括一个指向资源的指针,还有一个指向附属信息的指针。通过指针管理的资源通常在堆内存中,也可以在栈内存中,见下文的讨论。附属信息必然存储在堆内存中,附属信息至少需要包括一个引用计数器(实际上还有弱引用的计数器),引用计时器的更新逻辑如下:
- 当
shared_ptr
对象被复制时,引用计数器递增; - 当
shared_ptr
对象被销毁或重置时,引用计数器递减; - 当引用计数器降为零时,指针指向的资源被释放。
对于引用计数的修改是线程安全的,但是这并不表示对
shared_ptr
管理资源的操作是线程安全的。
使用new
和std::make_shared
这两种做法有本质上的区别:
- 如果先通过
new
得到原始指针,然后传给shared_ptr
对象,通常会涉及到两次内存分配,第一次是资源本身,第二次是附属信息,而且两次获取的内存通常是不连续的。 - 如果使用
std::make_shared
函数则可以合并为一次内存分配,资源和附属信息通常在内存中连续存储,这种做法在底层实现上更加高效,在语句上也更加简洁。但是由于将两部分合并为一次内存分配,可能出现资源的假释放问题:虽然资源被析构,但是如果弱引用计数非零,系统只能对资源部分执行析构,仍然无法归还整块内存。(保证weak_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
128template <typename T>
class shared_ptr {
private:
T *m_ptr;
unsigned *m_ref_count;
public:
shared_ptr() : m_ptr(nullptr), m_ref_count(new unsigned(0)) {}
explicit shared_ptr(T *p) : m_ptr(p), m_ref_count(new unsigned(1)) {}
~shared_ptr() { release(); }
shared_ptr(const shared_ptr &other)
: m_ptr(other.m_ptr), m_ref_count(other.m_ref_count) {
++(*m_ref_count);
}
shared_ptr &operator=(const shared_ptr &other) {
if (this != &other) {
release();
m_ptr = other.m_ptr;
m_ref_count = other.m_ref_count;
++(*m_ref_count);
}
return *this;
}
shared_ptr(shared_ptr &&other) noexcept
: m_ptr(other.m_ptr), m_ref_count(other.m_ref_count) {
other.m_ptr = nullptr;
other.m_ref_count = nullptr;
}
shared_ptr &operator=(shared_ptr &&other) noexcept {
if (this != &other) {
release();
m_ptr = other.m_ptr;
m_ref_count = other.m_ref_count;
other.m_ptr = nullptr;
other.m_ref_count = nullptr;
}
return *this;
}
T *get() const { return m_ptr; }
T &operator*() const { return *m_ptr; }
T *operator->() const { return m_ptr; }
unsigned use_count() const { return *m_ref_count; }
operator bool() const { return m_ptr != nullptr; }
private:
void release() {
if ((m_ref_count != nullptr) && --(*m_ref_count) == 0) {
delete m_ptr;
delete m_ref_count;
}
}
};
template <typename T>
class shared_ptr<T[]> {
private:
T *m_ptr;
unsigned *m_ref_count;
public:
shared_ptr() : m_ptr(nullptr), m_ref_count(new unsigned(0)) {}
explicit shared_ptr(T *p) : m_ptr(p), m_ref_count(new unsigned(1)) {}
~shared_ptr() { release(); }
shared_ptr(const shared_ptr &other)
: m_ptr(other.m_ptr), m_ref_count(other.m_ref_count) {
++(*m_ref_count);
}
shared_ptr &operator=(const shared_ptr &other) {
if (this != &other) {
release();
m_ptr = other.m_ptr;
m_ref_count = other.m_ref_count;
++(*m_ref_count);
}
return *this;
}
shared_ptr(shared_ptr &&other) noexcept
: m_ptr(other.m_ptr), m_ref_count(other.m_ref_count) {
other.m_ptr = nullptr;
other.m_ref_count = nullptr;
}
shared_ptr &operator=(shared_ptr &&other) noexcept {
if (this != &other) {
release();
m_ptr = other.m_ptr;
m_ref_count = other.m_ref_count;
other.m_ptr = nullptr;
other.m_ref_count = nullptr;
}
return *this;
}
T *get() const { return m_ptr; }
T &operator*() const { return *m_ptr; }
T *operator->() const { return m_ptr; }
unsigned use_count() const { return *m_ref_count; }
operator bool() const { return m_ptr != nullptr; }
private:
void release() {
if ((m_ref_count != nullptr) && --(*m_ref_count) == 0) {
delete[] m_ptr;
delete m_ref_count;
}
}
};
weak_ptr
创建
使用默认构造函数可以得到一个空的 weak_ptr
1
std::weak_ptr<int> wp;
可以利用 shared_ptr
对象创建 weak_ptr
对象,
1
2std::shared_ptr<int> sp = std::make_shared<int>(50);
std::weak_ptr<int> wp(sp);
使用
可以使用expired()
方法检查所指向的资源是否已经过期:
- 如果所指向的资源已经不存在,则返回
true
; - 如果所指向的资源仍然存在,则返回
false
;
使用示例如下 1
2
3
4
5if (!wp.expired()) {
// 资源仍然存在
} else {
// 资源已被释放
}
可以调用lock()
方法来尝试获取指向的资源对象的shared_ptr
对象:
- 如果获取成功,会返回一个非空的指向对应资源的
shared_ptr
对象; - 如果获取失败,会返回一个空的
shared_ptr
对象。
使用示例如下 1
2
3
4
5if (auto sp = wp.lock()) {
// 资源仍然存在,sp 是一个 `shared_ptr`
} else {
// 资源已被释放
}
支持对weak_ptr
指针重置 1
wp.reset();
原理
weak_ptr
是一种不拥有资源的智能指针,它指向由
shared_ptr
管理的资源。 weak_ptr
通过内部的弱引用计数器来监视资源,但不会增加引用计数。 当所有
shared_ptr
都被销毁时,资源会被释放,但弱引用计数器仍然存在。 通过
weak_ptr
可以安全地检查资源是否仍然存在,并且可以通过
lock()
方法获取一个 shared_ptr
来使用资源。
unique_ptr
unique_ptr
的作用是以独占的方式管理一个动态分配的对象,当unique_ptr
对象超出作用域时会自动析构,它会在析构时自动删除所管理的对象。unique_ptr
对象不可复制,但可以移动,移动会转移资源的唯一所有权。
创建
使用默认构造函数可以创建一个空的unique_ptr
对象
1
2
3std::unique_ptr<int> up;
std::unique_ptr<int[]> up;
可以直接使用原始指针初始化,通常是将new
得到的资源立刻赋值给unique_ptr
对象
1
2
3std::unique_ptr<int> up(new int(20));
std::unique_ptr<int[]> up(new int[10]);
使用 std::make_unique
函数也可以创建并初始化,这个语句可以避免显式的new
调用
1
2
3std::unique_ptr<int> up = std::make_unique<int>(10);
std::unique_ptr<int[]> up = std::make_unique<int[]>(10);
(有意思的是,这个看起来非常自然的配套函数不是在C++11提供的,而是在C++14才提供)
unique_ptr
对象不允许被复制,但是可以被移动
1
std::unique_ptr<int> up2 = std::move(up); // 移动
这里的移动也包括返回值优化,例如下面的函数是可以编译通过的
1
2
3
4
5std::unique_ptr<int> func(int val)
{
std::unique_ptr<int> up(new int(val));
return up;
}
使用
使用星号*
可以像原始指针一样地访问和修改指针指向的资源
1
2*up = 30;
int value = *up;
支持直接检查当前指针是否为空(即支持向布尔类型的自动转换)
1
2
3if (up) {
// up 非空
}
支持对unique_ptr
指针重置(包括置空和赋予新的值)
1
2sp.reset();
sp.reset(new int(40));
可以使用get()
方法获取底层原始指针 1
int* raw_ptr = sp.get();
但是获取智能指针所对应的裸指针的做法是不建议的。
可以使用release()
方法在获取底层原始指针的同时,让std::unique_prt
对象主动放弃对资源的所有权,
此时我们需要负责手动释放原始指针指向的资源 1
2int* raw_ptr = up.release();
delete raw_ptr; // 需要手动删除
原理
unique_ptr
是一种独占所有权的智能指针,它确保在任意时刻只有一个
unique_ptr
拥有资源。 这意味着 unique_ptr
不允许复制,但可以移动。
当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
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
106template <typename T>
class unique_ptr {
private:
T *m_ptr;
public:
unique_ptr() : m_ptr(nullptr) {}
explicit unique_ptr(T *p) : m_ptr(p) {}
~unique_ptr() { delete m_ptr; }
unique_ptr(const unique_ptr &) = delete;
unique_ptr &operator=(const unique_ptr &) = delete;
unique_ptr(unique_ptr &&other) noexcept : m_ptr(other.m_ptr) {
other.m_ptr = nullptr;
}
unique_ptr &operator=(unique_ptr &&other) noexcept {
if (this != &other) {
delete m_ptr;
m_ptr = other.m_ptr;
other.m_ptr = nullptr;
}
return *this;
}
T *get() const { return m_ptr; }
T &operator*() const { return *m_ptr; }
T *operator->() const { return m_ptr; }
T *release() {
T *ptr = m_ptr;
m_ptr = nullptr;
return ptr;
}
void reset(T *p) {
delete m_ptr;
m_ptr = p;
}
void reset() {
delete m_ptr;
m_ptr = nullptr;
}
operator bool() const { return m_ptr != nullptr; }
};
template <typename T>
class unique_ptr<T[]> {
private:
T *m_ptr;
public:
unique_ptr() : m_ptr(nullptr) {}
explicit unique_ptr(T *p) : m_ptr(p) {}
~unique_ptr() { delete[] m_ptr; }
unique_ptr(const unique_ptr &) = delete;
unique_ptr &operator=(const unique_ptr &) = delete;
unique_ptr(unique_ptr &&other) noexcept : m_ptr(other.m_ptr) {
other.m_ptr = nullptr;
}
unique_ptr &operator=(unique_ptr &&other) noexcept {
if (this != &other) {
delete[] m_ptr;
m_ptr = other.m_ptr;
other.m_ptr = nullptr;
}
return *this;
}
T *get() const { return m_ptr; }
T &operator*() const { return *m_ptr; }
T *operator->() const { return m_ptr; }
T *release() {
T *ptr = m_ptr;
m_ptr = nullptr;
return ptr;
}
void reset(T *p) {
delete[] m_ptr;
m_ptr = p;
}
void reset() {
delete[] m_ptr;
m_ptr = nullptr;
}
operator bool() const { return m_ptr != nullptr; }
};
进阶内容
自定义删除器
对于shared_ptr
和unique_ptr
的类型参数,除了必须提供资源的类型,我们还可以提供删除器以支持自定义的删除操作,
任何可调用对象都可以作为删除器。
默认的删除操作只是相当于delete
,我们通过删除器的调用,可以在delete
前后加入更多的额外操作。
代码示例如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void customDeleter(int *ptr) {
std::cout << "Custom deleter called.\n";
delete ptr;
}
void test() {
std::unique_ptr<int, decltype(&customDeleter)> up(new int(100),
customDeleter);
std::cout << "Value: " << *up << "\n";
}
int main(){
test();
return 0;
}
运行结果如下 1
2Value: 100
Custom deleter called.
栈内存管理
shared_ptr
和unique_ptr
通常负责的是堆内存资源的管理,但是在使用自定义删除器的前提下,我们也可以用其管理栈内存中的资源。
(这种做法通常是不推荐的,但是在语法上确实是可以做到的)
使用示例如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void customDeleter(int *ptr) {
std::cout << "Custom deleter called for stack memory resource.\n";
// 不调用delete,因为它指向栈内存中的资源
}
void test() {
int value = 200;
std::unique_ptr<int, decltype(&customDeleter)> up(&value, customDeleter);
std::cout << "Value: " << *up << "\n";
}
int main() {
test();
return 0;
}
运行结果如下 1
2Value: 200
Custom deleter called for stack memory resource.
enable_shared_from_this
我们考虑一个情景:自定义类型需要将自身的this
指针打包为一个共享指针提供出去,如何实现?(对于这种需求的对象,通常不能允许在栈上构造,必须在堆上创建,否则是未定义行为,需要使用自定义删除器作为补丁,非常繁琐)
直接的做法如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A {
public:
A() = default;
explicit A(int x) : m_x(x) {}
~A() {};
// or ~A() = default;
std::shared_ptr<A> get_shared_ptr() { return std::shared_ptr<A>(this); }
private:
int m_x;
};
int main() {
std::shared_ptr<A> demo_ptr = std::make_shared<A>(10);
std::shared_ptr<A> tmp1 = demo_ptr->get_shared_ptr();
}
此时程序会报错,出现了对同一个内存的重复free或者其它问题。
问题出在方法get_shared_ptr()
中,
我们既没有通过new
也没有通过std::make_shared
创建指针,而是将现存的this
指针传给了共享指针,这导致了内存管理的冲突。
标准库提供了std::enable_shared_from_this
来解决这类问题,用其改写上文中的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A : public std::enable_shared_from_this<A>{
public:
A() = default;
explicit A(int x) : m_x(x) {}
~A() {};
// or ~A() = default;
std::shared_ptr<A> get_shared_ptr() { return shared_from_this(); }
private:
int m_x;
};
int main() {
std::shared_ptr<A> demo_ptr = std::make_shared<A>(10);
std::shared_ptr<A> tmp1 = demo_ptr->get_shared_ptr();
}
此时程序顺利执行,不会报错。
注意:
std::enable_shared_from_this<...>
是基于奇异模板递归模式实现的,必须使用public
继承。- 除了
shared_from_this()
,还有配套的weak_from_this()
方法(C++14),都是通过std::enable_shared_from_this<...>
模板类提供的。