智能指针
1️⃣ 背景
在 C++ 中,动态内存的管理通过运算符 new/ delete 来完成的:
new:在动态内存中为对象分配空间并返回一个指向该对象的指针。delete:接收一个动态对象的指针,销毁该对象,并释放与之相关的内存。
动态内存的使用很容易出现问题,因为确保在正确的时间释放内存是极其困难的 !!!
- 忘记释放内存:内存泄露
- 释放一块仍有指针引用的内存:产生引用非法内存的指针
为了更安全地使用动态内存,新标准库提供了两种智能指针来管理动态内存:
智能指针的行为类似于常规指针,区别在于它自动释放所指向的对象
shared_ptr:允许多个指针指向同一个对象。unique_ptr:只允许单个指针独占一个对象。
2️⃣ shared_ptr 类
概念
std::shared_ptr<T> 是一个类模板,采用 RAII 手法,它的对象行为像指针,但是它可以记录有多少个对象共享它所管理的内存对象。

使用
经常通过如下两种方式创建 std::shared_ptr 对象:
auto p = std::shared_ptr<T>(new T)auto p = std::make_shared<T>(T{})
原理
1 | element_type* _M_ptr; // Contained pointer. |
shared_ptr 在内部维护两个指针成员:
_M_ptr:指向所管理的对象_M_refcount:指向控制块,包括引用计数(strong_count)、弱引用计数(weak_count)、删除器(Deleter)、分配器(Alloctor)
因为 shared_ptr 之间共享相同的内存对象,因此引用计数的存储位于堆上。

生命周期
- 当
strong_count变为 0 时,会通过指向对象的指针释放掉对象 T,导致对象 T 被析构。 - 但是控制块的释放要看
weak_count:- 若
weak_count也为 0,释放控制块。 - 若
weak_count不为 0,保留控制块。
- 若
手写 std::shared_ptr<T>
1 | template <typename T> |
常考点
1️⃣ std::make_shared 的优点
- 效率性
std::make_shared通过一次内存分配,既可以同时创建对象和控制块std::shared_ptr(new T(args...))需要进行两次内存分配才可以。多次内存分配不仅会增加时间开销,还可能导致内存碎片化,降低内存的使用效率 。
- 安全性
std::make_shared将对象的构造和智能指针的初始化合并为一个操作,减少了显式使用new的需要,从而降低了因异常导致内存泄漏的风险 。std::shared_ptr(new T(args...))安全性不高,以下代码可以进行解释:1
2
3
4
5
6void processWidget(std::shared_ptr<MyClass> ptr, priority()) {
// 处理逻辑
}
// 代码中执行以下函数
processWidget(std::shared_ptr<MyClass>(new Myclass), priority());C++编译器并不一定按照固定顺序从左至右进行调用,这会导致再执行new MyClass之后,资源已经分配,但是此时先执行priority(),而priority()中出现了异常,此时就会发生异常泄露。
2️⃣ std::make_shared 的缺点
std::make_shared 会直接分配一整块大内存,里面存放着控制块,对象 T。
- 当
strong_count为 0 时:- 对象的析构函数会执行
- 但是控制块不可以释放
- 由于对象和控制块在同一块内存,这块内存就不能被
free,导致包含对象大小的内存仍然不可使用。
3️⃣ shared_ptr 的线程安全问题
针对引用计数更新, 线程安全
这里只讨论对
shared_ptr进行拷贝的情况由于此操作读写的是引用计数,而引用计数的更新是原子操作,因此这种情况是线程安全的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16std::shared_ptr<int> p = std::make_shared<int>(0);
constexpr int N = 10000;
std::vector<std::shared_ptr<int>> sp_arr1(N);
std::vector<std::shared_ptr<int>> sp_arr2(N);
void increment_count(std::vector<std::shared_ptr<int>>& sp_arr) {
for (int i = 0; i < N; i++) {
sp_arr[i] = p;
}
}
std::thread t1(increment_count, std::ref(sp_arr1));
std::thread t2(increment_count, std::ref(sp_arr2));
t1.join();
t2.join();
std::cout<< p.use_count() << std::endl; // always 20001针对修改内存区域,线程不安全
1
2
3
4
5
6
7
8
9
10
11
12std::shared_ptr<int> p = std::make_shared<int>(0);
void modify_memory() {
for (int i = 0; i < 10000; i++) {
(*p)++;
}
}
std::thread t1(modify_memory);
std::thread t2(modify_memory);
t1.join();
t2.join();
std::cout << "Final value of p: " << *p << std::endl; // possible result: 16171, not 20000针对
shared_ptr对象的指向,线程不安全1
2
3
4
5
6
7
8
9
10
11
12
13
14std::shared_ptr<int> sp = std::make_shared<int>(1);
auto modify_sp_self = [&sp]() {
for (int i = 0; i < 1000000; ++i) {
sp = std::make_shared<int>(i);
}
};
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(modify_sp_self);
}
for (auto& t : threads) {
t.join();
}
4️⃣ std::enabled_shared_from_this
场景
当前类的成员函数通过该类的 this 的指针创建一个针对于当前类的 shared_ptr 对象,即 std::shared_ptr<当前类>(this)。
当前类必须继承
std::enabled_shared_from_this,并且提供shared_from_this函数。
后果
否则会发生 double-free 问题。
代码示例
1 | class A : public std::enable_shared_from_this<A> { |
3️⃣ weak_ptr 类
概念
std::weak_ptr<T> 也是一个类模板,它是一种弱引用,指向 shared_ptr 所管理的对象,而不影响所指对象的生命周期。
这表明将
weak_ptr绑定到一个shared_ptr,不会改变shared_ptr的引用计数。不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。
相关操作
1️⃣ 读取引用对象
由于 weak_ptr 对它所绑定的 shared_ptr 所指向的对象是没有所有权的,因此不可以通过 weak_ptr 来进行解引用得到 shared_ptr 所指向的对象。这就导致若想要读取到引用对象,必须将 weak_ptr 转换成 shared_ptr。
C++ 中提供了 lock 函数来实现该功能。
- 如果对象存在,
lock()函数返回一个指向共享对象的shared_ptr - 如果对象不存在,返回一个空
shared_ptr。
2️⃣ 判断 weak_ptr 指向的对象是否存在
weak_ptr 提供了一个成员函数 expired() 来判断所指对象是否已经被释放。
- 如果所指对象已被释放,
expired()返回 true - 如果所指对象未被释放,
expired()返回 false。
3️⃣ 作为 shared_ptr 构造函数的参数
std::weak_ptr 可以作为 std::shared_ptr 的构造函数参数,但如果std::weak_ptr 指向的对象已经被释放,那么 std::shared_ptr 的构造函数会抛出 std::bad_weak_ptr 异常。
使用场景
用于缓存
weak_ptr 可以用来缓存对象,当对象被销毁时,weak_ptr 也会自动失效,不会造成野指针。
假设我们有一个 Widget 类,我们需要从文件中加载 Widget 对象,但是 Widget 对象的加载是比较耗时的。下面这个代码,表示通过文件描述符 fd 加载一个 Widget 对象
1 | std::shared_ptr<Widget> loadWidgetFromFile(int id); |
针对于代码优化,我们希望 Widget 对象可以缓存起来,当下次需要 Widget 对象时,可以直接从缓存中获取,而不需要重新加载。
这个时候,我们就可以使用 std::weak_ptr 来缓存 Widget 对象,实现快速访问。如以下代码所示:
1 | std::shared_ptr<Widget> fastLoadWidget(int id) { |
- 当对应 id 的
Widget对象已经被缓存时,cache[id].lock()会返回一个指向Widget对象的std::shared_ptr - 否则,cache[id].lock()会返回一个空的
std::shared_ptr,这就需要重新加载Widget对象。
为什么不直接存储 std::shared_ptr 呢?
因为直接存储 std::shared_ptr 会导致缓存中的对象永远不会被销毁。
- 这会导致
std::shared_ptr的引用计数永远不会为 0。
所以使用 std::weak_ptr 不会增加对象的引用计数,因此,当缓存中的对象没有被其他地方引用时,std::weak_ptr 会自动失效,从而导致缓存中的对象被销毁。
避免循环引用
循环引用的定义
循环引用是指两个或多个对象之间通过 shared_ptr 相互引用,形成了一个环,导致它们的引用计数都不为 0,从而导致内存泄漏。
4️⃣ unique_ptr 类
概念
unique_ptr<T> 也是智能指针,同时也是一个类模板,它主要表示一种 “独占所有权” 的概念,表示不允许其他 unique_ptr 指向它所指向的对象。
原理
独占权
std::unique_ptr 独占性的实现主要依赖于禁止拷贝构造以及拷贝赋值运算符
1 | unique_ptr(const unique_ptr&) = delete; |
控制权转移
通过 std::move() 将当前 unique_ptr 对资源的所有权转移给另一个 unique_ptr。
转移后,原来的 unique_ptr 将不再拥有对内存的控制权,将变为空指针。
1 | std::unique_ptr<int> p1 = std::make_unique<int>(0); |
相关操作
通过 std::make_unique 创建 unique_ptr
这是一种更加异常安全的做法,同理于 std::make_shared。
释放所有权
通过 release 函数返回对象的裸指针
1 | unique_ptr<int> p1 = make_unique<int>(1); |
重置所有权
通过 reset 函数释放所有权,并返回 nullptr
1 | unique_ptr<int> p1 = make_unique<int>(1); |
