循环引用需主动打破强引用链,而非依赖weak_ptr自动解决;只要两个对象互相持有shared_ptr即可能中招,典型场景包括父子结构、观察者模式和双向链表;weak_ptr仅应用于非拥有方,且访问前必须lock()并检查有效性。

std::shared_ptr 循环引用不是“用了 weak_ptr 就自动消失”的问题,而是必须主动识别所有权方向、在强引用链中砍掉至少一个环节——否则内存永远不释放。
怎么一眼识别代码里藏着循环引用
只要出现两个对象互相持有 std::shared_ptr,基本就中招了。典型模式包括:
- 父子结构:父节点用
std::shared_ptr持有子节点,子节点又用std::shared_ptr回指父节点 - 观察者/被观察者:观察者用
std::shared_ptr持有被观察者,被观察者又把观察者存进回调列表(且用std::shared_ptr) - 双向链表节点:每个节点都用
std::shared_ptr指向前驱和后继
编译器不会报错,运行时也无异常,但对象的析构函数永远不会被调用——你得靠日志或 Valgrind 观察到 A destroyed 和 B destroyed 从未输出,才能确认。
weak_ptr 不是“随便换一个就行”,要选对位置
关键原则:只把「非拥有方」换成 std::weak_ptr。拥有关系由数据流和生命周期语义决定,不是语法上谁先定义。
- 在树形结构中,子节点的生命周期依附于父节点,所以子节点对父节点的引用必须是
std::weak_ptr;父节点对子节点的引用保留std::shared_ptr - 在回调场景中,被观察者不应延长观察者的生命周期,所以被观察者持有的观察者指针应为
std::weak_ptr - 不能反过来:比如把父节点对子节点的引用改成
std::weak_ptr,会导致子节点在父节点还活着时就被释放
错误示例:std::weak_ptr —— 这会让父节点失去对子节点的所有权,子节点可能提前析构。
weak_ptr 访问前必须 lock(),且要检查返回值
std::weak_ptr::lock() 返回 std::shared_ptr,但它可能为空。这不是可选步骤,而是强制安全契约。
- 直接解引用
weak_ptr会编译失败,必须先lock() -
lock()成功仅表示“此刻对象还活着”,不保证后续仍存活;多线程下尤其要注意 - 不要写
if (ptr.expired()) return;然后继续用原始weak_ptr——expired()和后续操作之间存在竞态窗口
正确写法:
auto parent_ptr = parent.lock();
if (!parent_ptr) {
// 父节点已销毁,无法继续操作
return;
}
// 此时 parent_ptr 是有效的 shared_ptr,可安全使用
do_something_with(parent_ptr);
容易被忽略的控制块开销与调试盲区
std::weak_ptr 虽不增加对象引用计数,但会增加控制块(control block)的弱引用计数。这意味着:
- 即使所有
std::shared_ptr都已析构,只要还有std::weak_ptr存活,控制块本身不会释放 - Valgrind 或 ASan 可能报告“still reachable”而非“definitely lost”,容易误判为无泄漏
- 线上长期运行的服务若频繁创建/销毁带
weak_ptr的对象,控制块碎片可能成为隐性内存压力源
真正干净的解耦,不只是把 shared_ptr 换成 weak_ptr,而是要确保整个引用图是 DAG(有向无环图)——这点在复杂模块边界处最难验证。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











