循环引用导致内存泄漏的典型现象是内存持续上涨且对象析构函数不被调用;根本原因是shared_ptr互相持有使引用计数永不归零;应由从属方使用weak_ptr并每次访问时lock()检查,避免缓存临时shared_ptr。

循环引用导致内存泄漏的典型现象
程序跑着跑着内存持续上涨,shared_ptr 管理的对象明明该销毁了却一直没调用析构函数——这是循环引用最直接的表现。常见于父子关系、观察者模式、图结构节点互相持有 shared_ptr 的场景。
根本原因:两个(或多个)shared_ptr 互相持有对方,引用计数永远不为 0,即使外部所有强引用都已释放,对象也无法析构。
- 典型错误写法:
parent->children.push_back(std::make_shared<node>(parent));</node>,子节点用shared_ptr<Node>存 parent - 调试线索:用 AddressSanitizer 或 Valgrind 检测到“still reachable”块;或在类析构函数里加日志,发现从不触发
- 注意:
weak_ptr本身不增加引用计数,但访问前必须先lock()转成临时shared_ptr,否则可能解引用空指针
weak_ptr 应该放在哪一边?
原则很简单:谁是“从属方”或“非拥有方”,就把 weak_ptr 放在那一边。比如父节点拥有子节点,子节点不应反向“拥有”父节点;观察者不拥有被观察者,只应弱引用它。
- 正确做法:子节点中用
std::weak_ptr<Parent>存父指针,访问时写if (auto p = parent.lock()) { /* 安全使用 p */ } - 别把
weak_ptr当“万能解药”乱塞——如果某处逻辑确实需要共享所有权(比如多线程任务同时持有同一资源),那它本就不该是 weak 关系 - 构造时避免隐式转换:不要写
weak_ptr<T>(shared_ptr<T>)在表达式中间,容易忽略生命周期;显式命名变量更安全
lock() 之后的 shared_ptr 是临时的,别存起来
weak_ptr::lock() 返回的是一个临时 shared_ptr,它的生命周期仅限当前作用域。把它赋给成员变量或长期缓存,等于又制造了一个新的强引用链,可能绕过 weak 设计初衷。
立即学习“C++免费学习笔记(深入)”;
- 危险操作:
class Child { std::shared_ptr<Parent> cached_parent; void update() { cached_parent = parent.lock(); } };—— 这会让 parent 永远无法释放 - 安全做法:每次需要访问时都调用
lock(),并立即检查是否为空;或把访问逻辑封装进一个 lambda,在 lock 成功后立刻执行 - 性能提醒:
lock()是原子操作,开销很小,但频繁调用仍建议缓存结果在局部变量里(不是成员变量!)
std::enable_shared_from_this 不是替代方案,而是补充工具
当你需要从对象内部安全地生成自己的 shared_ptr(比如回调函数里传自己),enable_shared_from_this 是必须的;但它和解决循环引用无关,反而用错会加剧问题。
- 常见误用:在构造函数里就调用
shared_from_this()—— 此时对象还没被shared_ptr管理,会抛std::bad_weak_ptr - 正确时机:确保对象一定已被
std::make_shared构造,且只在成员函数中调用shared_from_this() - 和 weak_ptr 的配合点:观察者注册时用
shared_from_this()传给被观察者;被观察者那边存weak_ptr<Observer>,回调前lock()判断是否还存活
真正难的不是写对 weak_ptr,而是准确判断“谁该拥有谁”。模型设计阶段就要想清楚所有权流向,否则后期补 weak_ptr 往往只是掩盖了接口职责不清的问题。










