C语言内存泄漏怎么排查?五种泄漏模式和三个检测工具
C 语言没有垃圾回收,内存管理全靠程序员手动 malloc/free。这种自由度换来的代价就是内存泄漏——分配了内存却没释放,进程的内存占用只增不减。短命程序无所谓,长期运行的服务端进程泄漏几十字节,跑几天就可能吃掉几个 G。
五种常见泄漏模式
忘了 free
最直接的原因,代码写着写着就漏了:
cvoid process(void) { char *buf = malloc(1024); do_something(buf); // 函数结束,buf 没释放,1KB 泄漏 }
更隐蔽的情况是提前 return:函数中间某个错误检查直接 return -1,跳过了末尾的 free。每个 return 路径都必须有对应的释放逻辑,漏一条就是泄漏。
指针覆盖
把唯一指向已分配内存的指针覆盖掉了,想 free 都找不到:
cchar *name = malloc(100); strcpy(name, "original"); name = malloc(200); // 原来的 100 字节再也找不回来
realloc 也有这个坑——如果 realloc 返回 NULL(分配失败),原指针仍然有效,但很多人写成 ptr = realloc(ptr, new_size),失败时原指针丢失,既泄漏又悬空。正确写法:
cvoid *tmp = realloc(ptr, new_size); if (tmp) { ptr = tmp; } else { // ptr 仍然有效,可以继续使用或 free }
双重释放
同一块内存 free 两次,堆管理器的内部数据结构被破坏,后续的 malloc/free 行为不可预测——可能立刻崩溃,也可能跑很久才出问题:
cfree(ptr); // ... 一大段代码 ... free(ptr); // double free,堆被破坏
根本原因通常是两个模块都认为自己"拥有"这块内存的所有权。free 之后把指针置 NULL 是防御手段——free(NULL) 是安全的,不会出错:
cfree(ptr); ptr = NULL;
错误路径泄漏
函数有多个 return 点,某个错误分支忘了释放:
cint load_config(void) { char *buf = malloc(4096); if (read_file("config", buf) < 0) { return -1; // 泄漏:buf 没 free } if (parse(buf) < 0) { free(buf); return -1; // 这里正确释放了 } free(buf); return 0; }
goto-free 模式是 C 语言中处理这种多退出点的惯用法:所有资源在函数末尾统一释放,错误路径用 goto cleanup 跳过去。
不止是内存
"内存泄漏"这个说法容易让人只盯着 malloc/free,但文件描述符(open 忘 close)、socket、临时文件、子进程(fork 忘 waitpid 导致僵尸进程)本质上都是资源泄漏,后果一样严重。文件描述符泄漏比内存泄漏更阴险——进程默认只能打开 1024 个 fd(ulimit -n),泄漏几十个就打不开新文件了。
检测工具
Valgrind
最全面的内存问题检测工具,不用改代码、不用重新编译,直接运行:
bashvalgrind --leak-check=full --show-leak-kinds=all ./my_program
输出会告诉你:哪一行 malloc 的内存没释放(definitely lost),哪些可能泄漏(indirectly lost),哪些还在用(still reachable)。重点关注 definitely lost。
缺点:慢。Valgrind 通过 JIT 模拟每条指令,程序跑起来慢 10-50 倍。不适合长时间运行的程序,通常用短测试用例跑。
AddressSanitizer (ASan)
编译器内置的检测,GCC 和 Clang 都支持:
bashgcc -fsanitize=address -g -O1 program.c -o program ./program
ASan 运行时开销比 Valgrind 小得多(约 2x),但只能检测当前编译的代码,不能检测动态库。它会捕获越界访问和 use-after-free,但内存泄漏检测靠 LeakSanitizer(LSan),需要加 -fsanitize=leak 或 ASan 默认包含。
mtrace
glibc 提供的轻量级追踪,在代码里加两行:
c#include <mcheck.h> int main(void) { mtrace(); // 开始追踪 // ... 你的代码 ... muntrace(); // 结束追踪 return 0; }
运行时设置环境变量 MALLOC_TRACE=out.txt,程序结束后用 mtrace 命令分析输出文件。比 Valgrind 轻量,但只报告未配对的 malloc/free,不能检测越界。
手动审计清单
没有工具的时候,代码审查是最后的防线:
- 每个 malloc/calloc/realloc 是否都有对应的 free?
- 每个 return 路径是否都释放了已分配的资源?
- realloc 的返回值是否用了临时变量接收?
- free 之后指针是否置 NULL?
- 错误处理分支是否遗漏了清理逻辑?
- 文件描述符和 socket 是否都关闭了?