概述

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
19
class 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
3
std::shared_ptr<int> sp;

std::shared_ptr<int[]> sps;

可以直接使用原始指针初始化,通常是将new得到的资源立刻赋值给shared_ptr对象

1
2
3
std::shared_ptr<int> sp(new int(20));

std::shared_ptr<int[]> sps(new int[10]);

使用 std::make_shared 函数也可以创建并初始化,这个语句可以避免显式的new调用

1
2
3
std::shared_ptr<int> sp = std::make_shared<int>(10);

std::shared_ptr<int[]> sps = std::make_shared<int[]>(10);

shared_ptr对象允许被复制或移动

1
2
std::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
3
if (sp) {
// sp 非空
}

支持对shared_ptr指针重置(包括置空和赋予新的值)

1
2
sp.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管理资源的操作是线程安全的。

使用newstd::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
128
template <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
2
std::shared_ptr<int> sp = std::make_shared<int>(50);
std::weak_ptr<int> wp(sp);

使用

可以使用expired()方法检查所指向的资源是否已经过期:

  • 如果所指向的资源已经不存在,则返回true
  • 如果所指向的资源仍然存在,则返回false

使用示例如下

1
2
3
4
5
if (!wp.expired()) {
// 资源仍然存在
} else {
// 资源已被释放
}

可以调用lock()方法来尝试获取指向的资源对象的shared_ptr对象:

  • 如果获取成功,会返回一个非空的指向对应资源的shared_ptr对象;
  • 如果获取失败,会返回一个空的shared_ptr对象。

使用示例如下

1
2
3
4
5
if (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
3
std::unique_ptr<int> up;

std::unique_ptr<int[]> up;

可以直接使用原始指针初始化,通常是将new得到的资源立刻赋值给unique_ptr对象

1
2
3
std::unique_ptr<int> up(new int(20));

std::unique_ptr<int[]> up(new int[10]);

使用 std::make_unique 函数也可以创建并初始化,这个语句可以避免显式的new调用

1
2
3
std::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
5
std::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
3
if (up) {
// up 非空
}

支持对unique_ptr指针重置(包括置空和赋予新的值)

1
2
sp.reset();
sp.reset(new int(40));

可以使用get()方法获取底层原始指针

1
int* raw_ptr = sp.get();

但是获取智能指针所对应的裸指针的做法是不建议的。

可以使用release()方法在获取底层原始指针的同时,让std::unique_prt对象主动放弃对资源的所有权, 此时我们需要负责手动释放原始指针指向的资源

1
2
int* 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
106
template <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_ptrunique_ptr的类型参数,除了必须提供资源的类型,我们还可以提供删除器以支持自定义的删除操作, 任何可调用对象都可以作为删除器。 默认的删除操作只是相当于delete,我们通过删除器的调用,可以在delete前后加入更多的额外操作。

代码示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <memory>

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
2
Value: 100
Custom deleter called.

栈内存管理

shared_ptrunique_ptr通常负责的是堆内存资源的管理,但是在使用自定义删除器的前提下,我们也可以用其管理栈内存中的资源。 (这种做法通常是不推荐的,但是在语法上确实是可以做到的)

使用示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <memory>

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
2
Value: 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
#include <memory>

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

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<...>模板类提供的。