LeakSanitizer(LSan)是AddressSanitizer的子组件,需编译时启用-fsanitize=address -fno-omit-frame-pointer -g -O1四标志,并在运行前设置LSAN_OPTIONS=detect_leaks=2,仅在程序正常退出时扫描未释放堆内存,不分析对象生命周期,报告中的indirectly lost常指示shared_ptr循环引用。

LeakSanitizer(LSan)不是“运行一次就出报告”的黑盒工具,而是依赖编译期插桩 + 运行末期快照的泄漏扫描机制;它只在程序正常退出(exit 或 main 返回)时触发检测,异常终止、abort()、信号杀死都会跳过扫描——这点必须前置确认,否则你会反复“没报错却实际泄漏”。
编译时必须加这四个标志,缺一不可
LSan 是 AddressSanitizer 的子组件,不能单独启用。常见错误是只加 -fsanitize=address 却漏掉关键配套:
-
-fsanitize=address:启用 ASan 基础框架,LSan 依附其上 -
-fno-omit-frame-pointer:否则调用栈丢失,你只能看到#0 0x7f... in malloc,无法定位到哪行new -
-g:没有调试信息,报告里连文件名和行号都没有 -
-O1(不是-O0,也不是-O2):-O0可能导致内联优化被禁用过度,反而干扰构造/析构函数识别;-O2+会把new/delete内联掉,栈帧消失,LSan 找不到分配源头
正确命令示例:g++ -g -O1 -fsanitize=address -fno-omit-frame-pointer leak.cpp -o leak
运行前必须设 LSAN_OPTIONS=detect_leaks=2
默认 detect_leaks=1 只报 direct leak(即完全无人引用的内存块),对 C++ 最常见的间接泄漏(如 shared_ptr 循环引用、容器持有对象但容器自身未销毁)完全静默。设成 2 才会扫描所有未释放堆块,无论是否还有指针指向它。
常见错误操作:
- 只设
ASAN_OPTIONS=detect_leaks=2—— 无效,LSan 不读这个变量 - 写成
LSAN_OPTIONS="detect_leaks=2:fast_unwind_on_malloc=0"却漏了冒号分隔符,整个变量被忽略 - 在 Docker 容器里运行却没把环境变量透传进去(比如用
docker run -e LSAN_OPTIONS=...忘加-e)
推荐启动方式:LSAN_OPTIONS=detect_leaks=2 ./leak
看懂 LSan 报告里的 indirectly lost 和控制块线索
LSan 报告中出现 indirectly lost 是循环引用泄漏的强信号。它意味着:这块内存本身有指针指向(所以不算 direct),但它所归属的对象图整体未被析构。
重点观察两点:
- 分配栈是否集中出现在
std::make_shared、std::shared_ptr或::shared_ptr std::enable_shared_from_this相关调用——这是shared_ptr环的典型痕迹 - 多个
indirectly lost块的分配位置是否相互持有(比如 A 的构造函数里 new 了 B,B 的构造函数里又持有了 A 的shared_ptr)
此时不要急着改代码,先用 valgrind --leak-check=full --track-origins=yes 对比验证:如果 valgrind 报出大量 std::_Sp_counted_base 或 std::__shared_count 分配未释放,基本可锁定为控制块残留,即循环引用已形成。
LSan 不分析对象生命周期,只做“退出快照”。真正难的是判断“该不该释放”——比如全局单例、线程局部缓存、异步回调捕获的 shared_ptr,它们可能合法存活到进程结束。这时候报告里的“泄漏”其实是误报,得靠人工结合业务逻辑交叉验证。别迷信工具输出的每一行 indirectly lost 都是 bug。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











