循环引用导致内存缓慢泄漏,需通过析构日志、use_count()检查和addresssanitizer验证;weak_ptr破环须避免lambda未lock、方向错误及多线程空解引用;重构应统一enable_shared_from_this、静态检查双向引用、约定父子指针类型。

大规模重构中一旦出现 shared_ptr 循环引用,泄漏不会报错、不崩溃、不抛异常,只会在数小时或数天后让内存缓慢爬升——这时候靠“看代码”基本无效,得用可验证的观测手段和明确的破环路径。
怎么快速确认是不是循环引用在作怪
别先改代码,先做三件事验证:
- 在疑似对象的析构函数里加
std::cout ,跑完流程看日志是否完全没输出 - 对关键类添加
use_count()打印:比如在某个操作后立即调用p->parent.use_count()和p->child.use_count(),若稳定停在 1 就高度可疑 - 用
AddressSanitizer启动程序,加上-fsanitize=leak编译选项,它能报告“未释放但无栈追踪”的堆块,这类报告往往对应循环引用锁死的对象
weak_ptr 破环时最容易踩的三个坑
weak_ptr 不是加个类型声明就万事大吉,常见误用包括:
- 在回调 lambda 中直接捕获
weak_ptr却忘了调用lock():写成[self = weak_self]() { self->do_something(); }是错的,必须写成[self = weak_self]() { if (auto p = self.lock()) p->do_something(); } - 把
weak_ptr放在父对象里,却让子对象用shared_ptr持有父对象——破环方向反了,应该让“被拥有方”用weak_ptr回指“拥有方” - 在多线程环境下,
lock()返回空shared_ptr后没处理,后续直接解引用导致 crash;必须检查返回值非空再使用
重构中如何避免边改边埋新雷
大规模替换时,最危险的是“局部改完就提交”,结果新旧引用混在一起。建议按以下顺序推进:
- 先统一加
enable_shared_from_this<t></t>到所有可能被共享持有的类,避免后续因裸指针转shared_ptr引发二次循环 - 用
clang-tidy配置cppcoreguidelines-owning-memory规则,自动标记出所有双向shared_ptr成员(如A::b_和B::a_) - 对树/图结构,强制约定:父→子用
shared_ptr,子→父一律用weak_ptr;对观察者模式,被观察者持shared_ptr,观察者持weak_ptr
为什么有些对象明明用了 weak_ptr 还不释放
这通常不是破环失败,而是控制块残留或间接强引用没断干净:
-
weak_ptr不影响强引用计数,但如果还有别的shared_ptr实例活着(比如缓存、全局 map、未清理的 lambda 捕获),对象照样不会析构 - 注意
shared_from_this()的生命周期:如果对象本身已被析构,但某个weak_ptr还在,调用lock()会返回空,但不会触发崩溃——这种“静默失效”容易掩盖真实持有者 - 控制块本身要等
weak_count == 0 && use_count == 0才释放,所以即使对象已析构,只要还有weak_ptr存活,控制块内存就还占着一小块——这不是泄漏,但会影响内存回落曲线
真正难的不是找到哪个 shared_ptr 该换成 weak_ptr,而是确认整个对象图里没有第二条隐性强引用路径。每次改完,必须用 use_count() + 析构日志双验证,不能只信“语法上看起来破环了”。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











