10. 内存泄漏:内存泄漏的定义与危害、常见泄漏场景、使用 Valgrind 检测泄漏

内存泄漏,说白了就是“借了不还”。

你从堆上申请了一块内存,用完之后忘了释放。这块内存就永远“悬”在那里,谁也拿不走。程序跑得越久,泄漏越多,最后系统内存被吃光,程序崩溃。

我见过不少新手,甚至一些老手,都觉得“不就漏一点内存嘛,重启一下就好了”。嗯,这种想法很危险。尤其是在嵌入式系统里,内存本来就金贵,泄漏一次可能就要命。

10.1 内存泄漏的定义与危害

定义:程序中动态分配的内存,在失去所有引用后,没有被释放。这块内存既不能被程序继续使用,也无法被操作系统回收(除非进程结束)。

危害,我列几个典型的:

  • 性能下降:内存越吃越多,系统开始频繁使用交换分区(swap),速度慢得像蜗牛。
  • 程序崩溃:malloc 返回 NULL,程序没有做空指针检查,直接段错误。
  • 系统不稳定:在嵌入式设备上,内存泄漏可能导致整个系统死机,连看门狗都救不回来。
  • 难以排查:泄漏往往不是立刻爆发的,可能运行几天、几周后才出问题。这种“慢性病”最难治。
⚠️ 警告:我曾经在一个工业控制项目里,因为一个循环中漏了 free,导致设备运行72小时后自动重启。客户差点退货。从那以后,我对每个 malloc 都像对账一样,必须找到对应的 free。

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
💡 提示:Valgrind 会让程序运行速度慢 10-20 倍,这是正常的。不要在性能测试时用它,只在调试阶段使用。

10.4 知识体系图

内存泄漏 定义 动态内存未释放 危害 性能下降 / 崩溃 / 不稳定 常见场景 忘记释放 / 丢失指针 忘记释放 丢失指针 异常路径 容器泄漏 检测工具:Valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes

核心要点:内存泄漏是 C 语言开发中最常见也最隐蔽的问题之一。记住三点:

  1. 每个 malloc/calloc/realloc 必须对应一个 free。
  2. 异常路径也要释放,别偷懒。
  3. Valgrind 是你的好朋友,养成习惯经常用它检查。

嗯,关于内存泄漏,我就讲这么多。你想想看,是不是自己代码里也有类似的问题?找个时间跑一遍 Valgrind,说不定能揪出几个隐藏的“炸弹”。


公众号:蓝海资料掘金营,微信deep3321