std::shared_ptr无内置循环引用检测机制,依赖weak_ptr打断持有链并结合ASan/valgrind定位泄漏;典型场景如父子关系中子持父需用weak_ptr,析构日志与泄漏报告协同排查。

std::shared_ptr 本身没有内置循环引用检查工具
这是最常被误解的一点:std::shared_ptr 不提供运行时循环引用检测能力。它的引用计数只管“谁还持有我”,不管“我是否间接指向了自己”。一旦形成 A → B → A 这样的 shared_ptr 持有链,引用计数永不归零,对象永远不析构——内存泄漏就发生了,且无任何警告。
所以所谓“循环引用检查工具”,实际是靠开发者主动规避 + 外部调试手段定位,而非 std::shared_ptr 自带功能。
用 std::weak_ptr 打断循环是最直接的修复方式
在可能构成闭环的持有关系中,把其中一端改为 std::weak_ptr,就能让引用计数不再计入它,从而打破循环。典型场景是父子对象、观察者-被观察者、双向链表节点。
- 父类持子类用
std::shared_ptr,子类持父类必须用std::weak_ptr - 调用
weak_ptr.lock()获取临时shared_ptr,若返回空说明父对象已销毁 - 绝不能在构造函数里用
shared_from_this()初始化同对象的shared_ptr成员(易触发隐式循环)
class Node {
public:
std::shared_ptr next;
std::weak_ptr prev; // ← 关键:这里不用 shared_ptr
};
用 AddressSanitizer + UBSan 捕获疑似循环引用导致的泄漏
虽然不能直接标出“此处有循环引用”,但 ASan 的内存泄漏报告能帮你定位长期存活却本该释放的对象。配合自定义析构日志,可反向推断问题链。
立即学习“C++免费学习笔记(深入)”;
- 编译时加
-fsanitize=address,undefined -fno-omit-frame-pointer - 运行后若输出类似
LEAK:,说明有对象未释放123byte(s) in4allocation(s) - 在类析构函数里加
std::cout ,对比哪些地址没出现 - 注意:ASan 不报告循环引用本身,只报告最终结果——泄漏;需结合对象生命周期逻辑排查
用 valgrind --leak-check=full 定位泄漏源头(Linux/macOS)
valgrind 能给出完整的分配栈回溯,比 ASan 更适合深挖长期驻留对象的创建路径。
- 运行命令:
valgrind --leak-check=full --show-leak-kinds=all ./your_program - 重点关注
definitely lost和still reachable块——后者往往是循环引用的典型表现 - 若看到某类对象的
new调用栈反复出现在多个“still reachable”块中,且它们互相持有shared_ptr,基本可锁定循环 - valgrind 无法识别
weak_ptr语义,所以你得人工对照代码判断哪条持有链该改用weak_ptr
循环引用不是语法错误,编译器不会报错,运行时也不会崩溃——它安静地吃掉内存,直到某天 OOM。真正难的不是修复,而是意识到“这个对象怎么还没析构?”背后可能藏着一个跨模块、跨线程、甚至跨 shared_ptr 生命周期的隐式闭环。











