std::weak_ptr 是 C++11 引入的智能指针,它设计的初衷就是为了配合 std::shared_ptr 工作,解决它最大的痛点——循环引用(死锁)。
介绍weak_ptr
std::weak_ptr 是 C++11 中引入的一种智能指针,它与 std::shared_ptr 配合使用,能够解决 shared_ptr 引用计数所带来的问题,特别是循环引用问题。weak_ptr 允许你“观察”由 shared_ptr 管理的对象,但不会影响对象的引用计数。
weak_ptr 的特点
1、不增加引用计数:
- 与 shared_ptr 不同,weak_ptr 不会增加目标对象的引用计数。它只是一个观察者,允许访问 shared_ptr 所管理的对象,但不会阻止对象被销毁。
- 这意味着,weak_ptr 不会导致循环引用,也不会影响内存释放。
2、可以安全地转换为 shared_ptr:
- weak_ptr 可以通过 lock() 方法转换为一个有效的 shared_ptr。如果该对象仍然存在(即引用计数不为 0),则 lock() 返回一个有效的 shared_ptr,可以使用该对象。如果对象已经被销毁,lock() 会返回一个空的 shared_ptr。
3、可以检查对象是否已经被销毁:
- 通过 expired() 方法,可以检查目标对象是否已经被销毁。如果目标对象的引用计数为 0,则 expired() 会返回 true,表示该对象已被销毁。
为什么 weak_ptr 能解决循环引用问题?
在普通的 shared_ptr 使用过程中,如果对象之间存在相互引用,即两个对象通过 shared_ptr 相互引用,那么它们的引用计数会互相增加,从而造成引用计数永远不为 0的问题,这就是循环引用。
例如,假设有两个类 A 和 B,它们互相持有 shared_ptr,形成一个环:
A中有一个shared_ptr<B>,指向B。B中有一个shared_ptr<A>,指向A。
这样,在程序结束时,A 和 B 会一直保持引用,不会被销毁,因为每个对象的引用计数至少为 1。导致它们的内存永远不会释放,形成内存泄漏。
使用 weak_ptr 解决循环引用
通过将其中一个 shared_ptr 换成 weak_ptr,就可以打破这种循环引用关系。weak_ptr 不增加引用计数,它只是“观察”另一个对象,但不会阻止该对象的销毁。
- weak_ptr 不增加引用计数:它不会让对象保持在内存中,避免了循环引用。
- shared_ptr 负责销毁对象:当没有 shared_ptr 引用该对象时,引用计数为 0,对象会被销毁。
- 可以使用 lock() 转换为 shared_ptr:当需要访问对象时,可以通过 lock() 方法将 weak_ptr 转换为 shared_ptr,并确保在访问时对象没有被销。
代码示例:
这是一个经典的父节点持有子节点,子节点指回父节点的场景(例如树或链表):
错误代码(使用 shared_ptr,导致内存泄漏)
#include <iostream>
#include <memory>
class Child; // 前向声明
class Parent {
public:
std::shared_ptr<Child> child_ptr; // 指向孩子
~Parent() { std::cout << "Parent 析构" << std::endl; }
};
class Child {
public:
std::shared_ptr<Parent> parent_ptr; // 指向父亲(循环引用关键点!)
~Child() { std::cout << "Child 析构" << std::endl; }
};
int main() {
auto parent = std::make_shared<Parent>();
auto child = std::make_shared<Child>();
parent->child_ptr = child; // Parent 的引用计数变为 1
child->parent_ptr = parent; // Child 的引用计数变为 1 (循环形成)
// 函数结束,parent 和 child 离开作用域
// 引用计数减为 1,不等于 0,永远不会析构 -> 内存泄漏!
return 0;
}
// 没有任何析构函数被调用!
正确代码(使用 weak_ptr 打破循环)
#include <iostream>
#include <memory>
class Child;
class Parent {
public:
std::shared_ptr<Child> child_ptr; // 父亲拥有孩子(强引用)
~Parent() { std::cout << "Parent 析构" << std::endl; }
};
class Child {
public:
// ★ 关键:改用 weak_ptr,不增加 Parent 的引用计数
std::weak_ptr<Parent> parent_ptr;
~Child() { std::cout << "Child 析构" << std::endl; }
};
int main() {
auto parent = std::make_shared<Parent>(); // Parent 引用计数 = 1
auto child = std::make_shared<Child>(); // Child 引用计数 = 1
parent->child_ptr = child; // Child 引用计数变为 2
child->parent_ptr = parent; // Parent 引用计数不变,仍为 1(因为 weak_ptr)
// 函数结束:
// 1. parent 析构 -> Parent 引用计数 1 -> 0,Parent 内存释放
// 2. Parent 析构函数执行,内部的 child_ptr 析构 -> Child 引用计数 2 -> 1
// 3. child 析构 -> Child 引用计数 1 -> 0,Child 内存释放
return 0;
}
运行结果:
Parent 析构
Child 析构
两个对象都正确析构了。
过程解析:
你可以把引用计数想象成门上的锁:
-
错误案例:父亲拿着孩子的钥匙(+1),孩子拿着父亲的钥匙(+1)。当你试图扔掉大门钥匙(外部
shared_ptr)时,因为孩子手里还有一把父亲的钥匙,门锁计数是 1,门永远打不开,谁也出不去。 -
正确案例:孩子不再拿父亲的钥匙,而是拿了一张门禁卡(
weak_ptr)。这张卡不影响门锁计数。当你扔掉大门钥匙时,门锁计数归零,门打开了(父亲析构)。父亲出去时,扔掉了他拿着的孩子钥匙,孩子那边门锁归零,孩子也出去了。
weak_ptr 解决循环引用的原理
其底层原理依赖于两个引用计数(在 shared_ptr 的控制块中):
-
强引用计数(Use Count):记录有多少个
shared_ptr指向对象。当此数为 0 时,对象内存被销毁。 -
弱引用计数(Weak Count):记录有多少个
weak_ptr指向对象。当此数为 0 时,控制块本身被销毁。
工作原理:
-
weak_ptr只增加弱引用计数,不增加强引用计数。 -
当对象析构时(强引用计数归零),
weak_ptr会得知对象已失效(expired()返回true),但它自己仍然存在(控制块还在),直到弱引用计数归零。 -
这保证了
weak_ptr可以安全地判断对象是否存在,如果存在,则通过lock()临时提升为shared_ptr来使用对象。
总结
| 要点 | 内容 |
|---|---|
| 定位 | weak_ptr 是 shared_ptr 的“助手”,解决循环引用。 |
| 核心机制 | 不增加强引用计数,打破引用环。 |
| 使用流程 | 存在循环依赖时,将其中一方从 shared_ptr 改为 weak_ptr。 |
| 访问方式 | 不能直接访问,需要用 lock() 生成临时 shared_ptr 再访问(使用前检查空指针)。 |
| 典型场景 | 观察者模式、父子双向关联、缓存(防止过期指针)、避免悬空指针。 |
一句话牢记:weak_ptr 就像“借书证”,你有权查阅书籍(访问对象),但你不拥有这本书,图书馆(引用计数)不会因为你有借书证就不把书下架。

3万+

被折叠的 条评论
为什么被折叠?



