C++ 的 weak_ptr 是个好东西,但也是个容易让人在细节上栽跟头的工具。它不持有引用计数,所以不能像普通指针那样直接解引用,必须通过 lock() 先拿到 shared_ptr 才能安全操作。这个机制背后的逻辑是什么,又该怎么用好它,咱们来拆开聊聊。
weak_ptr 为什么不能直接访问对象
原因其实很简单:weak_ptr 本身不持有引用计数,它只是对 shared_ptr 所管理对象的一个“弱观察”。它没有重载 * 和 -> 运算符,所以直接调用 get() 或者解引用,编译阶段就会报错。
正确的做法是调用 lock() 方法,它会返回一个 shared_ptr。如果原来的对象还在,这个 shared_ptr 就是有效的;如果对象已经被释放,返回的就是空 shared_ptr,判断一下 !ptr 就能知道。
- 千万别写
wp.get()->do_something()—— 编译直接报错 - 也别写
*wp—— 同样编译不过 - 必须写成
if (auto sp = wp.lock()) { sp->do_something(); } lock()本身是线程安全的,但检查和实际使用之间仍然存在一个竞态窗口,所以“检查后立刻用”是个关键习惯
在观察者模式中如何用 weak_ptr 避免循环引用
观察者模式是循环引用的经典温床。典型场景是 Subject 持有一堆 shared_ptr,而 Observer 又持有一个指向 Subject 的 shared_ptr。这样一来,两者互相强引用,析构函数永远别想被调用。
解决办法很简单:Observer 改用 weak_ptr 来存储对 Subject 的引用。这样 Observer 不会增加 Subject 的引用计数,Subject 销毁后 Observer 仍然可以安全存在,只是下次调用 lock() 时会失败。
- Subject 侧保持
shared_ptr给外部使用,内部通知逻辑不变 - Observer 在构造时接收
shared_ptr,但内部存成weak_ptr - 每次回调前必须
if (auto subj = subject_wp.lock()) { subj->notify(...); } - 注意:不要在构造函数里把
this注册进 Subject 后,又在析构函数里反注册。此时 Subject 可能已经销毁,lock()返回空,反注册逻辑应该静默跳过
weak_ptr::lock() 和 expired() 的行为差异
expired() 是一个轻量级的检查,只读取控制块中的引用计数,不加锁也不增加计数,所以速度很快。lock() 则要原子地尝试将弱引用提升为强引用,开销略高,而且有可能失败。
- 仅判断是否失效,用
wp.expired()—— 快,适合日志、断言或快速跳过 - 需要后续使用对象,必须用
wp.lock()—— 它同时完成检查和“保活”,避免检查后对象被销毁 - 千万别写
if (!wp.expired()) { wp.lock()->do_something(); }这种代码,两次访问控制块之间,对象可能已经被其他线程销毁了,结果就是未定义行为 - 更糟的是
wp.lock().get()->do_something(),万一lock()返回空shared_ptr,get()返回空指针,解引用直接崩溃
weak_ptr 无法解决所有循环引用,注意原始指针陷阱
有人会试图用裸指针 Subject* 来替代 weak_ptr,觉得“我手动保证不 dangling 就行”。但在多线程、异步回调、延迟执行这些场景下,你根本没法确定 Subject 的生命周期边界,这种做法极其危险。
weak_ptr的价值不在于“省内存”,而在于提供一种可验证、线程安全的对象存活检查机制- 如果 Observer 的生命周期严格短于 Subject(比如栈上的临时对象),用裸指针倒也不是不行,但必须用文档明确约束,而且禁止跨函数传递或存储
- 任何涉及
std::function、lambda 捕获、消息队列延迟调用的场景,裸指针基本就等于悬垂指针 - 注意:
weak_ptr本身不防逻辑错误。比如误把不同shared_ptr创建的weak_ptr混用,控制块不一致会导致lock()永远失败
真正容易被忽略的点是:weak_ptr 必须由同一个 shared_ptr 初始化而来,跨实例赋值或从不同源头构造,会让观察彻底失效。