std::weak_ptr是不增加引用计数的观察者,需通过lock()转为shared_ptr才能访问对象;用于打破循环引用,避免内存泄漏,但本身不保证线程安全。

std::weak_ptr 是什么,和 std::shared_ptr 有什么区别
它不是智能指针的“所有权持有者”,而是一个“观察者”——不增加引用计数,也不阻止对象销毁。std::weak_ptr 必须通过 lock() 升级成 std::shared_ptr 才能访问对象,而 lock() 返回一个空的 std::shared_ptr(即 .expired() == true)时,说明目标对象已被释放。
常见错误现象:std::weak_ptr::lock() 后直接解引用,没检查是否为空,导致段错误;或者误以为 weak_ptr 能延长对象生命周期。
-
weak_ptr构造时不改变shared_ptr的use_count() - 不能直接用
operator->或operator*,必须先lock() - 它的存在本身不影响对象生存期,只依赖底层控制块是否还活着
为什么循环引用会导致内存泄漏
两个 std::shared_ptr 相互持有对方所管理的对象(比如 A 持有指向 B 的 shared_ptr,B 也持有指向 A 的 shared_ptr),它们的引用计数永远 ≥1,即使外部所有 shared_ptr 都离开了作用域,控制块和对象也无法释放。
典型使用场景:树节点父子双向引用、观察者模式中被观察对象与观察者互相持有、缓存中 key 和 value 形成闭环。
立即学习“C++免费学习笔记(深入)”;
- 调试时可打印
ptr.use_count(),发现本该为 1 却是 2+,就值得怀疑 - 泄漏不是立刻可见的,常在长期运行服务中缓慢增长
- ASan/UBSan 无法捕获这种逻辑泄漏,得靠设计阶段规避
怎么用 weak_ptr 打破循环引用
把其中一端的强持有改成弱持有。比如父子关系中,子节点用 std::weak_ptr<parent></parent> 存父节点,父节点仍用 std::shared_ptr<child></child> 管理子节点。
关键点在于:谁该“负责生命周期”,谁该“仅需临时访问”。通常“拥有关系”用 shared_ptr,“非拥有关系”(如反向导航、回调上下文)用 weak_ptr。
struct Parent {
std::shared_ptr<Child> child;
};
struct Child {
std::weak_ptr<Parent> parent; // 不再是 shared_ptr
};
- 访问前必须调用
parent.lock(),并检查返回值是否非空 - 不要在构造函数里直接
lock()并保存结果——此时父对象可能还没完全构造完 - 如果频繁访问且确定生命周期安全,可临时提升一次并复用该
shared_ptr,避免重复锁开销
容易被忽略的坑:weak_ptr 不等于“线程安全”
weak_ptr 本身是线程安全的(拷贝/析构无数据竞争),但 lock() + 使用对象的过程不是原子的。多个线程同时 lock() 可能都拿到有效指针,也可能一个拿到、另一个拿到空指针——取决于对象是否恰好在此期间被释放。
更隐蔽的问题是:即使 lock() 成功,对象内容仍可能被其他线程修改,weak_ptr 不提供任何同步保障。
- 多线程下不要假设
!ptr.expired()和ptr.lock()结果一致,中间可能发生释放 - 若需强一致性,得配合 mutex 或原子标记(比如用
std::atomic_bool标记“正在销毁”) - 注意
weak_ptr的拷贝是轻量的,但lock()有原子操作开销,高频调用需权衡
真正难的不是加 weak_ptr,而是判断哪条引用链不该参与所有权管理——这得回到数据结构设计本身,而不是等内存涨了才回头改。










