25. 内存泄漏检测:CRT调试堆
内存泄漏,说白了就是「申请了内存,但忘了还」。
在C++里,new了没delete,malloc了没free,这种问题在小型demo里可能无所谓——程序跑完操作系统会回收。但在服务端程序里,跑个几天几周,内存一点点被吃掉,最后OOM崩溃,那叫一个惨。
我个人习惯,在Windows上用Visual Studio开发时,一定会开启CRT调试堆。这玩意儿是微软内置的「内存警察」,能帮你抓到绝大多数内存泄漏。
25.1 什么是CRT调试堆
CRT是C Runtime的缩写。调试堆(Debug Heap)是CRT在Debug模式下提供的一套内存管理机制。
它做了三件事:
- 记录每次内存分配——谁、在哪、分配了多少
- 检测内存错误——比如越界写、重复释放
- 报告泄漏——程序退出时,没释放的内存会列出来
我在项目中遇到过好几次,线上服务莫名其妙内存上涨,本地Debug一跑,CRT直接告诉我「第42行new了没delete」。嗯,省了我至少两天的排查时间。
25.2 如何开启CRT调试堆
其实不需要额外安装什么。Visual Studio的Debug配置默认就带了。你只需要在代码里加几行宏定义:
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
注意顺序:_CRTDBG_MAP_ALLOC必须在#include <stdlib.h>之前定义。否则映射不生效。
然后,在程序入口(比如main函数第一行)调用:
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
这两步做完,程序退出时,如果有泄漏,输出窗口会打印类似这样的信息:
Detected memory leaks!
Dumping objects ->
{45} normal block at 0x0078A8C0, 16 bytes long.
Data: <...> CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
那个{45}是分配序号。有了它,就能定位到具体是哪次分配泄漏了。
25.3 定位泄漏的精确位置
光知道「第45次分配泄漏了」还不够。你得知道是哪行代码干的。
这时候_CRTDBG_MAP_ALLOC就派上用场了。它会把malloc、realloc、free等函数映射到带文件名和行号的调试版本。
举个例子:
// 没有 _CRTDBG_MAP_ALLOC 时,输出只有:
{45} normal block at 0x0078A8C0, 16 bytes long.
// 有了 _CRTDBG_MAP_ALLOC 后,输出变成:
{45} normal block at 0x0078A8C0, 16 bytes long.
D:\myproject\main.cpp(32) : malloc(16)
看到没?直接告诉你main.cpp第32行。这就是我为什么强调宏定义顺序不能错的原因。
25.4 用_CrtSetBreakAlloc定点断点
有时候泄漏的分配序号是固定的,比如每次运行都是{45}。这时候你可以用_CrtSetBreakAlloc让程序在分配序号为45时自动断下来。
// 在 main 开头加上
_CrtSetBreakAlloc(45);
然后F5运行,程序会在第45次内存分配时触发断点。调用堆栈里清清楚楚地显示是谁调用了malloc。我曾经用这招抓到一个藏在三层回调函数里的泄漏——那感觉,就像在草堆里找到了针。
25.5 检测内存越界
CRT调试堆还有一个隐藏技能:检测缓冲区溢出。
它在每次分配的内存前后各加了一段「哨兵字节」,默认填充为0xFD。如果程序写越界了,哨兵被破坏,CRT会在free或程序退出时报错:
HEAP CORRUPTION DETECTED: after Normal block (#45) at 0x0078A8C0.
CRT detected that the application wrote to memory after end of heap buffer.
嗯,这个错误我见过太多次了。尤其是处理字符串的时候,strcpy忘了算结尾的'\0',一不留神就越界了。
25.6 手动触发泄漏报告
有时候你不想等到程序退出才看结果。可以在关键位置手动调用:
_CrtDumpMemoryLeaks();
这个函数会立即输出当前未释放的内存。我习惯在模块卸载或单元测试结束时调用一次,确保没有残留。
但要注意:如果调用了_CrtDumpMemoryLeaks之后又分配了内存,那泄漏报告就不完整了。所以一般放在程序退出前。
25.7 一个完整的示例
把上面这些串起来,写个完整的泄漏检测模板:
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
int main() {
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
// 如果知道泄漏序号,可以取消下面这行的注释
// _CrtSetBreakAlloc(45);
// 你的业务代码
int* p = (int*)malloc(sizeof(int) * 10);
// 忘记 free(p); // 这就是泄漏
return 0;
// 程序退出时,CRT自动报告泄漏
}
运行后输出窗口会显示:
Detected memory leaks!
Dumping objects ->
{45} normal block at 0x0078A8C0, 40 bytes long.
Data: <...> CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
D:\test\main.cpp(10) : malloc(40)
Object dump complete.
看到main.cpp(10)了吗?就是第10行那个malloc。一目了然。
25.8 CRT调试堆的局限性
说实话,CRT调试堆不是万能的。有几点你得心里有数:
- 只跟踪CRT分配的内存——如果你用了
VirtualAlloc、HeapAlloc或者第三方内存池,CRT管不着。 - 性能开销大——Debug模式下每次分配都记录调用堆栈、填充哨兵,速度会慢不少。别在生产环境开。
- 不检测全局/静态对象的泄漏——全局对象的构造和析构顺序复杂,CRT有时会误报或漏报。
我曾经在一个项目里,全局单例里new了一个对象,程序退出时CRT报泄漏。但其实那个对象在进程退出时会被操作系统回收。这种「伪泄漏」需要你结合业务逻辑判断。
25.9 总结一下
CRT调试堆是Windows平台上最趁手的内存泄漏检测工具之一。它不需要额外安装,不需要配置复杂的工具链,几行宏定义就能用起来。
我个人建议:所有Windows上的C++项目,Debug配置里都默认开启CRT调试堆。养成习惯后,你会发现内存泄漏越来越少——因为每次写完代码跑一遍测试,CRT就帮你揪出来了。
- 定义
_CRTDBG_MAP_ALLOC并包含crtdbg.h - 调用
_CrtSetDbgFlag开启泄漏检测 - 用
_CrtSetBreakAlloc定位特定分配 - 用
_CrtDumpMemoryLeaks手动触发报告 - 注意:只在Debug模式下有效,Release模式自动关闭
嗯,CRT调试堆就聊到这儿。它虽然简单,但确实是我日常开发中离不开的工具。下次你遇到莫名其妙的内存上涨,不妨先试试它——说不定几分钟就找到问题了。