26. 内存泄漏检测:Valgrind、AddressSanitizer、CRT 调试堆、智能指针的泄漏场景
内存泄漏,说白了就是「申请了内存,但忘了还」。
我见过不少项目,跑着跑着内存占用就飙上去了,最后进程被 OOM killer 干掉。更可怕的是,有些泄漏藏得很深,只在特定路径下触发,测试环境根本发现不了。
这一章,我们来聊聊怎么逮住这些泄漏。我会介绍四种主流手段:Valgrind、AddressSanitizer、Windows 上的 CRT 调试堆,以及智能指针本身可能带来的泄漏场景。
Valgrind:Linux 下的老牌利器
Valgrind 是个工具集,其中 memcheck 工具专门检测内存问题。它的原理是模拟 CPU 执行,拦截所有内存操作指令。
说白了,它会在你的程序外面包一层「监控壳」。每次 malloc/free,它都记录在案。程序退出时,它检查有没有没释放的块。
g++ -g -O0 main.cpp -o main
valgrind --tool=memcheck --leak-check=full ./main
我个人习惯在 CI 流程里加一个 Valgrind 步骤。虽然跑起来慢(大概慢 10-20 倍),但能抓到很多隐蔽问题。
--show-leak-kinds=all 可以看到「definitely lost」和「possibly lost」两种泄漏。前者是铁证,后者可能是指针被覆盖导致的。
AddressSanitizer (ASan):编译期插桩
Valgrind 慢,对吧?那 ASan 就是它的「快版本」。
ASan 是 LLVM/GCC 内置的检测工具。它在编译时往代码里插桩,运行时用影子内存记录每一字节的状态。一旦访问越界或泄漏,立刻报错。
g++ -fsanitize=address -fno-omit-frame-pointer -g main.cpp -o main
跑起来之后,如果程序退出时还有未释放的内存,ASan 会打印泄漏报告,包含调用栈。
我遇到过最坑的一次:某个模块在 release 模式下一切正常,但一开 ASan 就崩。查了半天,发现是 memcpy 长度算错了,越界写了几个字节。release 下没触发段错误,但 ASan 直接逮住了。
CRT 调试堆:Windows 上的专属武器
如果你在 Windows 上用 MSVC 开发,CRT 调试堆是官方给的「体检工具」。
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
int main() {
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
// 你的代码
return 0;
}
程序退出时,输出窗口会打印泄漏信息。你还可以用 _CrtSetBreakAlloc(分配序号) 在指定分配处下断点,直接定位到代码行。
_CRTDBG_MAP_ALLOC,这样 new/delete 也会被映射到调试版本。配合 _CrtDumpMemoryLeaks() 可以在任意时刻手动检查。
智能指针的泄漏场景
很多人觉得用了智能指针就不会泄漏了。嗯,理想很丰满,现实很骨感。
我总结了几种常见的「智能指针泄漏」场景:
- 循环引用(shared_ptr 互指):A 持有 B 的 shared_ptr,B 持有 A 的 shared_ptr。引用计数永远降不到 0。
- 裸指针混用:把 shared_ptr 的裸指针取出来,又用这个裸指针去创建另一个 shared_ptr。两个控制块互不知情,内存释放两次或泄漏。
- 回调中的生命周期问题:lambda 捕获了 shared_ptr,但 lambda 被长期持有,导致对象无法释放。
- 容器中的 shared_ptr 未清理:往 vector/map 里塞了 shared_ptr,但容器一直不销毁,对象就一直活着。
struct Node {
std::shared_ptr<Node> next;
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a; // 循环引用,泄漏!
解决办法?用 weak_ptr 打破循环。谁「被引用」谁就用 weak_ptr,谁「引用别人」谁就用 shared_ptr。
知识体系总览
下面这张图帮你理清四种检测手段的定位和适用场景:
实战建议
说了这么多,到底怎么用?我个人的工作流是这样的:
- 本地开发:编译时开 ASan,跑单元测试。泄漏了直接看调用栈。
- 提交前:用 Valgrind 跑一遍集成测试,确保没有「possibly lost」的泄漏。
- Windows 环境:Debug 模式下开启 CRT 调试堆,配合
_CrtSetBreakAlloc定位。 - 代码审查:重点看 shared_ptr 的循环引用,以及 lambda 捕获的智能指针。
公众号:蓝海资料掘金营,微信deep3321