10. 内存泄漏:内存泄漏的定义与危害、常见泄漏场景、使用 Valgrind 检测泄漏
内存泄漏,说白了就是“借了不还”。
你从堆上申请了一块内存,用完之后忘了释放。这块内存就永远“悬”在那里,谁也拿不走。程序跑得越久,泄漏越多,最后系统内存被吃光,程序崩溃。
我见过不少新手,甚至一些老手,都觉得“不就漏一点内存嘛,重启一下就好了”。嗯,这种想法很危险。尤其是在嵌入式系统里,内存本来就金贵,泄漏一次可能就要命。
10.1 内存泄漏的定义与危害
定义:程序中动态分配的内存,在失去所有引用后,没有被释放。这块内存既不能被程序继续使用,也无法被操作系统回收(除非进程结束)。
危害,我列几个典型的:
- 性能下降:内存越吃越多,系统开始频繁使用交换分区(swap),速度慢得像蜗牛。
- 程序崩溃:malloc 返回 NULL,程序没有做空指针检查,直接段错误。
- 系统不稳定:在嵌入式设备上,内存泄漏可能导致整个系统死机,连看门狗都救不回来。
- 难以排查:泄漏往往不是立刻爆发的,可能运行几天、几周后才出问题。这种“慢性病”最难治。
10.2 常见泄漏场景
我总结了几种最常见的“漏点”,你看看自己踩过几个坑。
10.2.1 忘记释放
最基础的错误。分配了,忘了释放。
void leak_example() {
char *buf = (char *)malloc(1024);
// 用 buf 做点事...
// 忘了 free(buf);
}
函数返回后,指针 buf 没了,那块 1024 字节的内存就永远找不回来了。
10.2.2 丢失指针
指针被重新赋值,原来的地址丢了。
void lost_pointer() {
int *p = (int *)malloc(sizeof(int) * 100);
p = (int *)malloc(sizeof(int) * 200); // 第一次分配的内存地址丢了!
free(p); // 只释放了第二次分配的
}
第一次分配的 100 个 int 空间,就这么“蒸发”了。
10.2.3 异常路径未释放
这是最隐蔽的。正常路径释放了,但异常路径没有。
void error_path_leak() {
FILE *fp = fopen("data.txt", "r");
char *buf = (char *)malloc(1024);
if (fp == NULL) {
return; // 这里忘了 free(buf)!
}
// 正常处理...
fclose(fp);
free(buf);
}
我见过一个网络服务器,就是因为这种异常路径泄漏,运行一周后内存占用从 50MB 涨到 2GB。
10.2.4 容器中的泄漏
链表、树、哈希表等数据结构,删除节点时只删了节点本身,没释放节点内部动态分配的数据。
struct Node {
char *data;
struct Node *next;
};
void delete_node(struct Node *prev) {
struct Node *del = prev->next;
prev->next = del->next;
// 忘了 free(del->data);
free(del); // data 指向的内存泄漏了
}
10.3 使用 Valgrind 检测泄漏
手动找泄漏?太累了。尤其是大型项目,代码量几十万行,靠肉眼根本看不出来。
我推荐 Valgrind。这工具我用了十几年,救过我无数次。
10.3.1 基本用法
编译时加上 -g 选项保留调试信息,然后用 Valgrind 运行程序:
gcc -g -o myapp myapp.c
valgrind --leak-check=full ./myapp
输出示例:
==12345== HEAP SUMMARY:
==12345== in use at exit: 1024 bytes in 1 blocks
==12345== total heap usage: 2 allocs, 1 frees, 2048 bytes allocated
==12345==
==12345== 1024 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2B800: malloc (vg_replace_malloc.c:299)
==12345== by 0x4005E7: leak_example (test.c:5)
==12345== by 0x4005F7: main (test.c:12)
看到 definitely lost 了吗?那就是泄漏。Valgrind 会告诉你泄漏了多少字节,在哪个文件的哪一行分配的。
10.3.2 常用选项
| 选项 | 作用 |
|---|---|
--leak-check=full |
显示每个泄漏的详细信息 |
--show-leak-kinds=all |
显示所有类型的泄漏(definite, indirect, possible, reachable) |
--track-origins=yes |
追踪未初始化值的来源(调试时很有用) |
--log-file=leak.log |
将结果输出到文件,方便分析 |
10.3.3 实战技巧
我个人的习惯是:
- 每次提交代码前,跑一遍 Valgrind。哪怕只是改了一行。
- 对于服务器程序,用
--leak-check=full --show-leak-kinds=all跑一个完整的压力测试。 - 如果泄漏报告太长,先关注
definitely lost,再处理indirectly lost。
10.4 知识体系图
核心要点:内存泄漏是 C 语言开发中最常见也最隐蔽的问题之一。记住三点:
- 每个 malloc/calloc/realloc 必须对应一个 free。
- 异常路径也要释放,别偷懒。
- Valgrind 是你的好朋友,养成习惯经常用它检查。
嗯,关于内存泄漏,我就讲这么多。你想想看,是不是自己代码里也有类似的问题?找个时间跑一遍 Valgrind,说不定能揪出几个隐藏的“炸弹”。
公众号:蓝海资料掘金营,微信deep3321