shared_ptr与weak_ptr
std::shared_ptr:共享所有权的管理者
std::shared_ptr 是一种拥有共享所有权的智能指针。这意味着多个 shared_ptr 实例可以共同拥有和管理同一个动态分配的对象。当最后一个指向该对象的 shared_ptr 被销毁或重置时,该对象会被自动释放。
核心理念:引用计数
shared_ptr 的核心机制是引用计数(Reference Counting)。它通过一个“控制块”来实现这一机制。
控制块(Control Block)是一个与被管理对象分离的内存块, 存储在堆上。当第一个 shared_ptr 创建时,控制块也随之被创建。它包含以下关键信息:
- 强引用计数(Strong Reference Count):记录有多少个 shared_ptr 正指向同一个对象。
- 弱引用计数(Weak Reference Count):记录有多少个 weak_ptr 正在“观察”这个对象。
- 指向被管理对象的指针。
- (可选)自定义删除器的指针。
一般来说, 引用计数和控制块的工作流程如下:
- 当一个新的 shared_ptr 创建或通过拷贝构造或拷贝赋值指向一个已有对象时,强引用计数 +1。
- 当一个 shared_ptr 被销毁(例如离开作用域)、被重置(reset())或指向其他对象时,强引用计数 -1。
- 当强引用计数变为 0 时,shared_ptr 会自动调用删除器(默认为 delete)来释放被管理的对象。
- 当弱引用计数和强引用计数都变为 0 时,控制块本身才会被释放。
这种引用计数的加减还是线程安全的:shared_ptr 的引用计数增减操作是原子的,这意味着在多线程环境下,仅对 shared_ptr 对象本身进行拷贝、赋值和销毁是线程安全的,不会导致引用计数出错。但它并不保护被管理对象本身,如果多线程要修改对象内容,仍需手动加锁。
创建 shared_ptr
函数模板 std::make_shared 的函数原型如下:
1 | namespace std { |
1 |
|
对于std::shared_ptr, 还有一些需要了解但是不太常用的接口: - 通过 get() 方法来获取原始指针,适用于和旧版C语言库等只接受原始指针的接口交互 - 通过 reset() 来减少一个强引用计数, 适用于提前终止对资源的管理 - 通过use_count()来查看一个对象的引用计数。
共享所有权
shared_ptr是支持拷贝的智能指针, 这意味着多个shared_ptr实例可以共享同一个对象的所有权。每当一个shared_ptr被拷贝时,引用计数会增加;当一个shared_ptr被销毁或重置时,引用计数会减少。
另外, shared_ptr 也支持移动语义(move semantics),这允许你将所有权从一个 shared_ptr 转移到另一个,而不会增加引用计数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 // 创建一个 shared_ptr
std::shared_ptr<MyClass> sp1 = std::make_shared<MyClass>();
std::cout << "sp1 创建后, 引用计数: " << sp1.use_count() << std::endl; // 输出: 1
{
// sp2 通过拷贝 sp1 创建
std::shared_ptr<MyClass> sp2 = sp1;
std::cout << "sp2 创建后, 引用计数: " << sp1.use_count() << std::endl; // 输出: 2
sp2->greet(); // 可以像普通指针一样使用
} // sp2 在这里离开作用域并被销毁
std::cout << "sp2 销毁后, 引用计数: " << sp1.use_count() << std::endl; // 输出: 1
// sp1 在 main 函数结束时销毁,引用计数变为0,MyClass 对象被析构
优点: - 自动管理内存,有效防止内存泄漏。 - 易于使用,可以像普通指针一样操作。 - 明确了资源的“共享所有权”语义。
但是, 如果只使用std::shared_ptr, 会存在一个难以察觉的陷阱: 循环引用(Circular Reference), 这是 shared_ptr 最大的问题,也是 weak_ptr 存在的原因。
循环引用(Circular Reference)
如下是一个双向链表的实现代码
1 |
|
这导致两个 Node 对象的引用计数都无法归零,析构函数永远不会被调用,内存泄漏发生。
这里区分一下: 栈上的 shared_ptr 变量被销毁了, 但是堆上的 Node 对象并没有被销毁, 因为它们的引用计数都没有归零。
shared_from_this()
它的作用是: 在类的成员函数中安全地获取一个指向自己(this)的 std::shared_ptr 智能指针。
先看一个错误示范:
1 |
|
p1->foo();时,
std::shared_ptr<A> p(this);会新建一个
shared_ptr,管理同一个对象,但它和外部的 p1
不是同一个控制块,各自有独立的引用计数。
当 p 离开作用域时(foo() 结束),p 的引用计数归零,析构对象,对象内存被释放。
但外部的 p1 还以为对象存在,继续访问已被释放的内存,导致未定义行为(通常是崩溃)
正确的做法是让类 A 继承自
std::enable_shared_from_this<A>,这样就可以使用
shared_from_this() 方法来获取一个指向自己的 shared_ptr:
1 |
|
其内部实现大致如下:
1 | class enable_shared_from_this { |
一般来说, 只有当类的对象确实会被 shared_ptr 管理时,才应该使用 enable_shared_from_this,否则调用 shared_from_this() 会抛出异常。适用于需要在类的成员函数中获取 shared_ptr 的场景,比如实现观察者模式、回调函数等。
std::weak_ptr:打破循环引用的观察者
std::weak_ptr 是一种非拥有型的智能指针。它像一个“观察者”,可以指向一个由 shared_ptr 管理的对象,但不会增加对象的强引用计数。
也就是说, 这种指针是一种弱引用:
- weak_ptr 的存在不会影响对象的生命周期。它只是监视对象是否存在。
- 它指向与 shared_ptr 相同的控制块,并通过创建和销毁来增减控制块中的弱引用计数。
- 你不能直接通过 weak_ptr 访问对象(没有 * 或 -> 操作符),因为对象可能随时被销毁。
创建 weak_ptr 和访问对象(核心操作:lock())
weak_ptr 只能从 shared_ptr 或另一个 weak_ptr 间接构造, 而不能构造一个来指向某个新对象
1 | std::shared_ptr<int> sp = std::make_shared<int>(10); |
weak_ptr 最重要的函数是 lock()。它会安全地检查被观察对象是否存在:
- 如果对象仍然存在,lock() 会返回一个指向该对象的有效的 shared_ptr,并使对象的强引用计数 +1。
- 如果对象已被销毁,lock() 会返回一个空的 shared_ptr。
这种“检查并获取”的模式是原子操作,保证了线程安全。
1 | std::weak_ptr<MyClass> weak_p; |
还有个常用的函数: std::weak_ptr::expired(), 用于判断当前 weak_ptr 是否已经失效,也就是它所观察的对象是否已经被销毁。如果返回 true, 表示对象已经被销毁,weak_ptr 不再指向有效资源; 返回 false则表示对象仍然存在,可以尝试通过 lock() 获取一个有效的 shared_ptr。
结合起来上面的代码还可以修改为:
1 | if (!weak_ptr.expired()) { |
解决循环引用
只需将循环引用链中的任意一环从 shared_ptr 改为 weak_ptr 即可。通常,我们会选择从属关系中的“子”指向“父”的指针(在双向链表中通常是 prev 指针)改为 weak_ptr。
1 |
|
当 create_correct_list 函数结束时,栈上的 node1 和 node2 被销毁。因为 node1 销毁,Node(10) 的强引用计数从 1 降为 0。Node(10) 对象被析构, 导致其成员 next(一个指向 Node(20) 的 shared_ptr)被销毁。这导致 Node(20) 的强引用计数从 2 降为 1。
随后,node2 销毁,Node(20) 的强引用计数从 1 降为 0。Node(20) 对象被析构。最终结果是, 所有节点都被正确地、依次地销毁。
总结对比
| 特性 | std::shared_ptr | std::weak_ptr |
|---|---|---|
| 所有权 | 拥有(共享所有权) | 不拥有(观察者) |
| 引用计数 | 创建/销毁/拷贝会改变强引用计数 | 创建/销毁/拷贝会改变弱引用计数 |
| 生命周期 | 它的存在会延长对象的生命周期 | 不影响对象的生命周期 |
| 直接访问 | 可以,通过 * 和 -> 操作符 |
不可以 |
| 安全访问方式 | 直接使用 | 必须调用 lock() 获取一个临时的 shared_ptr |
| 主要用途 | 管理具有共享所有权的动态资源 | 1. 打破 shared_ptr 的循环引用 2. 实现缓存系统 3. 观察者模式 |
| 如何创建 | std::make_shared 或从原始指针构造 |
从 shared_ptr 或其他 weak_ptr 构造 |
智能指针这种技术并不新奇,在很多语言中都是一种常见的技术,现代 C++ 将这项技术引进,在一定程度上消除了 new/delete 的滥用,是一种更加成熟的编程范式。