6月4日 13:42

C语言内存泄漏怎么排查?五种泄漏模式和三个检测工具

C 语言没有垃圾回收,内存管理全靠程序员手动 malloc/free。这种自由度换来的代价就是内存泄漏——分配了内存却没释放,进程的内存占用只增不减。短命程序无所谓,长期运行的服务端进程泄漏几十字节,跑几天就可能吃掉几个 G。

五种常见泄漏模式

忘了 free

最直接的原因,代码写着写着就漏了:

c
void process(void) { char *buf = malloc(1024); do_something(buf); // 函数结束,buf 没释放,1KB 泄漏 }

更隐蔽的情况是提前 return:函数中间某个错误检查直接 return -1,跳过了末尾的 free。每个 return 路径都必须有对应的释放逻辑,漏一条就是泄漏。

指针覆盖

把唯一指向已分配内存的指针覆盖掉了,想 free 都找不到:

c
char *name = malloc(100); strcpy(name, "original"); name = malloc(200); // 原来的 100 字节再也找不回来

realloc 也有这个坑——如果 realloc 返回 NULL(分配失败),原指针仍然有效,但很多人写成 ptr = realloc(ptr, new_size),失败时原指针丢失,既泄漏又悬空。正确写法:

c
void *tmp = realloc(ptr, new_size); if (tmp) { ptr = tmp; } else { // ptr 仍然有效,可以继续使用或 free }

双重释放

同一块内存 free 两次,堆管理器的内部数据结构被破坏,后续的 malloc/free 行为不可预测——可能立刻崩溃,也可能跑很久才出问题:

c
free(ptr); // ... 一大段代码 ... free(ptr); // double free,堆被破坏

根本原因通常是两个模块都认为自己"拥有"这块内存的所有权。free 之后把指针置 NULL 是防御手段——free(NULL) 是安全的,不会出错:

c
free(ptr); ptr = NULL;

错误路径泄漏

函数有多个 return 点,某个错误分支忘了释放:

c
int 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

最全面的内存问题检测工具,不用改代码、不用重新编译,直接运行:

bash
valgrind --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 都支持:

bash
gcc -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 是否都关闭了?
标签:C语言