更新时间: 2026-08-27
44 篇结尾留了个悬案:shared_ptr 遇到循环引用,计数归不了零,对象泄漏。std::weak_ptr 就是官方给的补丁。它是个"弱引用"——指向对象,但不增加引用计数,也就不会延长对象的寿命。
这篇要回答:weak_ptr 到底怎么打破循环?"观察者"是什么角色?以及 expired() 和 lock() 的配合为什么是标准用法?
一、weak_ptr 是什么
一句话:weak_ptr 是一个"不拥有对象"的 shared_ptr。
它可以指向 shared_ptr 管理的对象,但自己不计数。对象活没活,不由它决定;它只是"看着"。看代码:
#include <memory>
std::shared_ptr<Widget> sp = std::make_shared<Widget>();
std::weak_ptr<Widget> wp = sp; // 指向 sp 管理的对象,但不增加计数
std::cout << sp.use_count(); // 还是 1(wp 不计入)创建 weak_ptr 必须从一个 shared_ptr 构造(或者从另一个 weak_ptr 拷贝),它没有 make_weak 这样的工厂——因为它自己不拥有对象,谈不上"创建"。
二、怎么用:lock() 与 expired()
weak_ptr 不能直接解引用——它不拥有对象,对象可能已经死了,*wp 会解引用悬垂指针。标准用法是先用 lock() 尝试"提升"为 shared_ptr:
std::shared_ptr<Widget> sp = std::make_shared<Widget>();
std::weak_ptr<Widget> wp = sp;
// ... 其它代码,sp 可能已经 reset ...
if (std::shared_ptr<Widget> tmp = wp.lock()) {
// lock() 成功:对象还活着,tmp 是安全的 shared_ptr
tmp->do_something();
} else {
// 对象已被销毁
std::cout << "对象已不在了\n";
}lock() 的语义:如果对象还活着,返回一个指向它的 shared_ptr(计数 +1);如果已销毁,返回空 shared_ptr。这个"尝试提升"是原子的——不会出现"检查时活着、用的时候刚被销毁"的竞态。
expired() 是"对象是否已销毁"的检查:
if (wp.expired()) {
// 对象已销毁
}但注意:expired() 只做检查,拿到结果之后对象可能立刻被销毁(别的线程 reset 了最后的 shared_ptr)。所以"检查后再用"的模式是错的,正确姿势永远是 lock() 一步到位。
@startuml
left to right direction
skinparam nodeFontSize 13
skinparam backgroundColor #FFFFFF
rectangle "shared_ptr sp" as sp #E8F1FF {
node "指针 → Widget" as p1 #DCE9FF
node "计数: 1" as c1 #FFF3D6
}
rectangle "weak_ptr wp" as wp #FFF3D6 {
node "指针 → Widget(不计入计数)" as p2 #FFF3D6
}
rectangle "Widget 对象" as w #D9FFE2
p1 --> w
p2 --> w
note bottom of wp
wp 是观察者:
对象销毁时它自动失效,
不参与销毁决策
end note
@enduml注意图上 weak_ptr 也指向对象,但虚线/淡色表达"弱"——对象销毁时,weak_ptr 不会触发任何析构逻辑,只是自己"失效"(expired() 返回 true)。
三、打破循环引用的标准姿势
回到 44 篇的链表环:
#include <memory>
struct Node {
std::shared_ptr<Node> next;
// 关键:把环的一边换成 weak_ptr
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
// 传统写法会泄漏,但这里我们换成:
std::shared_ptr<Node> a_ptr = a;
std::weak_ptr<Node> b_weak = b;
// 构造环:a 持有 b 的 shared_ptr,b 的 next 指向 a 用 weak
// (示意:真正的环构造见下文)更标准的例子是父子关系——孩子持有指向父母的"弱引用":
struct Parent;
struct Child {
std::weak_ptr<Parent> parent; // 子→父 用 weak,打破环
};
struct Parent {
std::shared_ptr<Child> child; // 父→子 用 shared(拥有)
};
// 使用时:lock() 提升
void Child::greet() {
if (auto p = parent.lock()) {
p->hello(); // 父还活着,安全调用
}
}设计原则:"拥有关系"用 shared_ptr,"借用关系"用 weak_ptr。父拥有子,子不拥有父,只借用——这个方向性一旦定好,环自然就断了。反过来说,如果两边都互称"拥有",那就不是指针的问题,是设计本身有环,该重构。
四、C 对照:弱引用的 C 表达
C 里没有现成的弱引用,最接近的是"裸指针 + 不负责释放"的约定:
struct Node {
struct Node* next; // 强引用:拥有,负责释放
struct Node* parent; // 弱引用:不拥有,不释放(靠约定)
};| 语义 | C(裸指针+约定) | C++(weak_ptr) |
|---|---|---|
| 不增加计数 | 手动不加引用计数 | 自动不计入 |
| 对象已死 | 悬垂指针,用就崩 | expired() 可查 |
| 安全使用 | 无从保证 | lock() 原子提升 |
| 破环 | 靠"大家遵守约定" | 类型保证 |
C 的"弱引用"完全依赖人肉约定:写着"不 free"的指针,哪天被谁 free 了,别的地方还用,就是 use-after-free。weak_ptr 把"弱"写进类型:对象没了,lock() 自然返回空,不会踩到悬垂指针。
五、weak_ptr 不能做的三件事
新手容易把 weak_ptr 当万能钥匙,列出边界:
- 不能解引用:
*wp、wp->都不行(编译器报错)。要用必须lock() - 不能构造新对象:没有
make_weak,因为它不拥有 - 不延长生命周期:
use_count()不算它,对象销毁与否与它无关
@startuml
left to right direction
skinparam nodeFontSize 13
skinparam backgroundColor #FFFFFF
rectangle "对 weak_ptr 的常见误解" as myths #FFF3D6 {
node "✗ *wp 直接解引用" as m1 #FFD9D9
node "✗ wp 能防止对象销毁" as m2 #FFD9D9
node "✗ 从裸指针构造 weak_ptr" as m3 #FFD9D9
}
rectangle "正确姿势" as correct #D9FFE2 {
node "✓ lock() 提升后使用" as c1 #D9FFE2
node "✓ expired() 判断存活" as c2 #D9FFE2
node "✓ 从 shared_ptr 构造" as c3 #D9FFE2
}
myths -[hidden]down- correct
@enduml唯一一个"从裸指针构造"的例外是 std::shared_ptr 的 enable_shared_from_this——那是"对象自己拿到自己的 weak 视图"的进阶技巧,属于高手篇范畴,这里先不展开。
六、什么时候真的需要 weak_ptr
工程里 weak_ptr 的典型用武之地:
- 打破循环引用:树/图结构中子引用父,缓存中条目引用"拥有者"
- 观察者/监听器注册表:管理器持有订阅者的 weak_ptr,订阅者挂了自动失效,避免悬垂
- 缓存:缓存不拥有对象,只记录"这个 key 对应哪个对象",对象被别处销毁后缓存自动失效
- 线程任务句柄:主线程持有任务的 shared_ptr,任务内部持有主线程的 weak 引用检查主线程是否还活着
大部分业务代码其实用不到 weak_ptr——它是"补丁型"工具,遇到循环引用或者"想引用但不拥有"的场景才出手。平时能用 unique_ptr 就用 unique_ptr,要共享再上 shared_ptr,真要弱引用才轮到 weak_ptr。
七、与本站主线衔接
- 44 篇(shared_ptr):weak_ptr 是它的配套观察者
- 42 篇(RAII):weak_ptr 不参与 RAII 的"获取-释放",它是纯观察角色
- 47 篇(智能指针选型):三者的适用边界总结
- 缓存、观察者模式在
concepts/与languages/oop/中有更多展开
八、一句话总结
weak_ptr 是不增加引用计数的观察者,专治 shared_ptr 的循环引用——lock() 原子提升为 shared_ptr 才能安全使用,expired() 判断存活,核心口诀是"拥有用 shared,借用用 weak"。