5、内存管理:malloc与free的配对原则与野指针

内存管理这个话题,说大不大,说小不小。但你要是问我在嵌入式开发里踩过最深的坑是什么——我第一个想到的就是它。malloc和free,这对看似简单的搭档,用不好能把整个系统搞崩。今天咱们就好好聊聊这个。

5.1 配对原则:谁申请,谁释放

先讲个最基本的规矩:谁用malloc申请了内存,谁就得负责用free把它还回去。这听起来像废话对吧?但我见过太多代码,函数A申请了内存,函数B去释放,结果逻辑一绕,漏了或者重复释放了。

核心原则:

  • 每个malloc必须对应一个free
  • 申请和释放最好在同一个层级完成
  • 如果必须跨函数传递,一定要有明确的“所有权”文档

举个例子,我早期做项目时写过这样的代码:

// 错误示范:谁该负责释放?
void process_data(void) {
    uint8_t *buf = (uint8_t *)malloc(256);
    if (!buf) return;
    
    // 调用另一个函数处理
    handle_buffer(buf);  // 这里把buf传进去了
    // 问题:handle_buffer内部会不会free?如果不free,这里要不要free?
    // 如果两边都free,double free!如果都不free,内存泄漏!
}

你看,这就是典型的“所有权不清晰”。我个人习惯是:谁申请,谁释放。如果实在要跨函数,那就用文档或注释把规矩写死。

5.2 野指针:内存释放后的幽灵

野指针是什么?说白了就是:指针指向的内存已经被释放了,但指针变量本身还在。你想想看,这就像你家的门牌号还在,但房子已经拆了。你再按这个地址去找,找到的可能是别人的地盘,也可能是空地。

为什么会这样?

void demo(void) {
    int *p = (int *)malloc(sizeof(int));
    *p = 42;
    free(p);   // 内存释放了
    // 但p还在,它里面存的地址没变
    *p = 100;  // 危险!写入了已释放的内存
}

这段代码跑起来可能没问题,也可能立刻崩溃,更可怕的是——它可能偶尔崩溃。我在项目中遇到过这种“幽灵bug”,查了两天才定位到是野指针的问题。那感觉,真是一言难尽。

避坑指南:

我曾经在free之后忘记把指针置NULL,结果在另一个线程里又用这个指针去读写数据。系统跑了一周才崩溃,定位起来简直要命。所以我现在有个铁律:

  • free之后立即将指针置为NULL
  • 使用指针前检查是否为NULL
// 推荐做法
int *p = (int *)malloc(sizeof(int));
if (p) {
    *p = 42;
    // 使用...
    free(p);
    p = NULL;  // 关键一步!
}

// 后续使用前检查
if (p) {
    *p = 100;  // 安全,因为p已经是NULL了
}

5.3 常见陷阱与实战经验

嗯,这里我整理了几个最常见的坑,都是我自己或同事踩过的:

陷阱类型 现象 原因 解决方案
内存泄漏 系统运行越久越慢 malloc了但没free 每次malloc都配对free,用工具检测
重复释放 程序崩溃 对同一指针多次free free后置NULL,检查后再free
使用已释放内存 数据错乱或崩溃 野指针操作 free后置NULL,使用前判空
越界访问 破坏堆结构 写入超过申请的大小 严格计算大小,用sizeof

这里我特别想强调一点:嵌入式环境里,malloc/free要慎用。为什么?因为嵌入式系统内存有限,频繁的动态分配会导致碎片化。我做过一个项目,系统跑了三天后突然死机,查到最后就是堆碎片太多,malloc返回了NULL。

我的建议:

在嵌入式开发中,尽量用静态分配或内存池。如果非要用动态分配,那就:

  • 在初始化阶段一次性分配好
  • 运行期间尽量不释放和重新分配
  • 实在要动态分配,用固定大小的内存池

5.4 知识体系:一张图看懂内存管理

下面这张图是我自己总结的内存管理核心逻辑,你看完应该能有个整体把握:

内存管理核心逻辑 malloc 申请内存 成功 检查返回值 失败 返回NULL,处理错误 成功 使用内存 free 释放 + 置NULL 关键提醒 • 每次malloc必须配对free • free后立即置NULL • 使用前检查指针是否为NULL • 避免跨函数传递所有权 • 嵌入式优先用静态分配 • 考虑使用内存池 • 警惕堆碎片化 • 多线程加锁保护 • 用工具检测内存泄漏 • 不要越界访问 • 避免重复释放

5.5 总结:记住这三条

好了,说了这么多,其实核心就三条:

  1. 配对:malloc和free必须成对出现,谁申请谁释放
  2. 置空:free之后立即把指针置为NULL
  3. 检查:使用指针前检查是否为NULL,malloc后检查是否成功

这三条你要是能刻在脑子里,内存管理这块基本就不会出大问题。我这些年写过的代码,但凡出内存相关的bug,十有八九都是违反了这三条中的某一条。

最后说一句:

内存管理没有捷径,只有养成好习惯。每次写malloc的时候,就想想对应的free在哪里写。每次写free的时候,就想想指针置空了没有。久而久之,这就成了肌肉记忆。


专注资料整理