概述
C++标准库主要提供了三种智能指针:(都在<memory头文件中)
shared_ptr:共享指针,允许资源共享,在内部维持一个引用计数,复制会增加引用计数,析构则会减少引用计数,引用计数为零则释放资源
unique_ptr:独享指针,独自负责资源的管理,析构时释放资源
weak_ptr:弱共享指针,作为共享指针的辅助手段,可以指向共享指针的内容但是不参与引用计数,主要为了解决共享指针的循环引用问题
除此之外,在早期还提供了auto_ptr,但是目前已经被废弃。下面先介绍RAII思想,然后介绍三种智能指针的使用。
智能指针的正确使用可以在很大程度上提供内存安全的保障,但是这其实是不够的,C++ 说到底是一个无法保证内存安全的语言。
简单示例与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);
}
|
这里利用了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;
|
支持直接检查当前指针是否为空(即支持向布尔类型的自动转换)
支持对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管理资源的操作是线程安全的。
使用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 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
可以利用 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()) { } else { }
|
支持对weak_ptr指针重置
原理
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;
|
支持直接检查当前指针是否为空(即支持向布尔类型的自动转换)
支持对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_ptr和unique_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_ptr和unique_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"; }
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() {};
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() {};
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<...>模板类提供的。
补充
注意事项
考虑shared_ptr和unique_ptr的混合使用:下面的语句是无法通过编译的
1 2
| auto u1 = std::make_unique<int>(1); std::shared_ptr<int> s1 = u1;
|
因为unique_ptr不能被赋值,只能被移动
1 2
| auto u1 = std::make_unique<int>(1); std::shared_ptr<int> s1 = std::move(u1);
|
但是如果我们提供的是unique_ptr类型的右值,情况又不太一样了。
下面的语句都是合法的
1 2 3 4
| std::shared_ptr<int> s2; s2 = std::make_unique<int>(2);
std::shared_ptr<int> s3 = std::make_unique<int>(3);
|
因为shared_ptr提供了基于unique_ptr右值的赋值和构造函数,在其中已经使用了std::move。
但是这种用法并不推荐,因为相比于直接使用shared_ptr,这里会产生额外的计算开销。
1
| std::shared_ptr<int> s3 = std::make_shared<int>(3);
|
C 语言实现 RAII
在 C 语言中,也有实现 RAII 的需求,但是由于 C 语言并没有直接提供相关语法,只能间接实现——通过宏定义或者编译器的扩展来实现。
例如使用 GCC 的 __attribute__((cleanup(...))) 扩展来实现 RAII(测试发现 clang 也支持)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <stdio.h>
void cleanup_int(int *p) { printf("cleanup: %d\n", *p); }
void demo(void) { int x __attribute__((cleanup(cleanup_int))) = 42; printf("x = %d\n", x); }
int main(void) { demo(); }
|
也可以使用宏定义来模拟实现 RAII(当然在使用时有很多注意事项,例如不能使用 break 等语句)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <stdio.h> #include <stdlib.h>
#define DEFER(cleanup) \ for (int _i = 0; !_i; (_i = 1, cleanup))
void demo(void) { int *p = malloc(sizeof *p); if (!p) return;
DEFER(free(p)) { *p = 42; printf("%d\n", *p); } }
int main(void) { demo(); }
|