40、内存泄漏:如何检测和避免内存泄漏?

内存泄漏,说白了就是「申请了内存,用完了不还」。你想想看,程序跑着跑着,可用内存越来越少,最后系统扛不住了,要么卡死,要么崩溃。我在嵌入式项目里遇到过好几次这种问题,排查起来真是让人头大。

今天我们就来聊聊,内存泄漏到底是怎么回事,怎么检测,怎么避免。

什么是内存泄漏?

先看一个最简单的例子:

void leak_example(void)
{
    int *p = (int *)malloc(10 * sizeof(int));
    // 用完了,但是没有 free(p)
    return;
}

每次调用这个函数,都会在堆上分配 40 个字节(假设 int 是 4 字节)。函数返回后,指针 p 丢了,这 40 个字节再也找不回来了。这就是内存泄漏。

嵌入式系统里,内存资源本来就紧张。泄漏一点点可能没事,但要是泄漏的速度比释放快,系统迟早会挂。

内存泄漏的常见原因

我总结了几种最常见的场景,你看看自己踩过几个坑:

  • 忘记释放:malloc 了,但没写对应的 free。这是最基础的错误。
  • 释放时机不对:提前释放了,后面还在用,这是野指针问题。释放晚了,或者根本没释放,就是泄漏。
  • 指针被覆盖:分配了内存,指针指向别处了,原来的地址找不到了。
  • 异常路径没处理:函数中间 return 了,后面的 free 没执行到。
  • 容器或链表未清理:动态添加的节点,程序退出前没有逐个释放。

核心原则:谁分配,谁释放。malloc 和 free 要成对出现。

如何检测内存泄漏?

检测内存泄漏,说白了就是「盯住堆内存的使用情况」。我常用的方法有这几种:

1. 代码审查

最原始但有效的方法。每看到一个 malloc,就去找对应的 free。找不到,就是问题。我个人习惯在写代码时,先写 free,再写 malloc。这样不容易忘。

2. 静态分析工具

像 PC-Lint、Cppcheck、Clang Static Analyzer 这些工具,能自动检查出一些明显的泄漏。我在项目中用 Cppcheck 比较多,它免费,而且对嵌入式代码支持不错。

3. 动态检测工具

运行时检测,效果最好。常用的有:

  • Valgrind:Linux 下的神器。跑一遍程序,它会告诉你哪里泄漏了,泄漏了多少。
  • AddressSanitizer:编译时加个选项,运行时自动检测。速度比 Valgrind 快很多。
  • 嵌入式专用工具:比如 IAR 的 C-RUN、Keil 的 RTX 内存检测。这些工具在资源受限的环境下也能用。

4. 自己写内存跟踪

在嵌入式系统里,工具链可能不支持 Valgrind。这时候我建议自己写一个简单的内存跟踪模块:

#define DEBUG_MEM

#ifdef DEBUG_MEM
    static int alloc_count = 0;
    static int free_count = 0;

    void *debug_malloc(size_t size)
    {
        void *p = malloc(size);
        if (p) alloc_count++;
        return p;
    }

    void debug_free(void *p)
    {
        if (p) free_count++;
        free(p);
    }

    void print_mem_status(void)
    {
        printf("Alloc: %d, Free: %d, Leak: %d\n",
               alloc_count, free_count, alloc_count - free_count);
    }
#else
    #define debug_malloc malloc
    #define debug_free   free
#endif

在程序的关键节点调用 print_mem_status(),如果 alloc 和 free 的差值越来越大,那肯定有泄漏。

如何避免内存泄漏?

避免泄漏,比检测泄漏更重要。我分享几个实战经验:

1. 编码规范先行

  • 每个 malloc 必须对应一个 free,写在同一个函数或模块里。
  • 如果内存要在不同模块间传递,明确谁负责释放。
  • 使用 goto 统一处理错误路径的释放(没错,嵌入式里 goto 有时是合理的)。

2. 使用智能指针(C++)或封装

如果你用 C++,直接用 std::unique_ptrstd::shared_ptr。C 语言的话,可以封装一个内存池,统一管理分配和释放。

3. 避免动态内存分配

在嵌入式系统里,我建议尽量用静态分配。说白了,就是提前算好最大需要多少内存,一次性分配好。这样根本不会泄漏。

我的建议:在资源紧张的 MCU 上,能不用 malloc 就不用。用全局数组或者静态缓冲区,既安全又高效。

4. 写单元测试

每个涉及动态内存的函数,都写单元测试。测试正常路径,也测试异常路径。我曾经因为一个异常分支没释放内存,导致产品在高温测试时频繁重启。从那以后,我再也不敢跳过异常路径的测试了。

内存泄漏检测流程图

下面这张图,是我自己总结的内存泄漏检测流程,你可以参考一下:

开始检测 1. 代码审查:找 malloc/free 对 2. 静态分析工具扫描 3. 动态检测(Valgrind/ASan) 有泄漏? 定位泄漏点 修复代码 内存安全 继续监控

实战:一个典型的泄漏场景

我记得有一次,一个同事写的通信模块,每次收到数据包就 malloc 一个缓冲区,处理完却忘了 free。测试时功能正常,但跑了一整天后,系统内存耗尽,直接死机。

排查过程是这样的:

  1. 先用 Valgrind 跑了一遍,报告了 1000 多次泄漏,全部指向同一个 malloc 调用。
  2. 查看代码,发现那个 malloc 在正常路径下确实有对应的 free。
  3. 但仔细一看,异常路径(比如校验失败)直接 return 了,free 没执行。

修复很简单,把 free 移到 return 之前,或者用 goto 统一处理。但教训很深刻:异常路径最容易藏泄漏

警告:在中断服务函数里不要用 malloc。中断上下文可能不允许阻塞,而且容易造成内存碎片。我见过有人这么干,结果系统跑着跑着就挂了。

总结一下

内存泄漏不是大问题,但累积起来就是大问题。我的经验是:

  • 写代码时就把 free 写好,别等后面补。
  • 工具能帮我们找到 80% 的泄漏,但剩下的 20% 要靠代码审查和测试。
  • 嵌入式系统里,能不用动态内存就不用。静态分配虽然不够灵活,但安全可靠。

嗯,今天就聊到这里。记住一句话:malloc 一时爽,free 火葬场。写代码时多留个心眼,后面能省很多事。

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