5. free函数:函数原型、释放内存、避免重复释放

内存管理这事儿,分配只是前半段。真正考验功力的,其实是释放。我见过太多项目,前期内存分配得漂漂亮亮,一到释放环节就翻车。说白了,free 函数用不好,你的程序就跟定时炸弹差不多。

5.1 函数原型

free 函数的原型特别简单,就一行:

#include <stdlib.h>

void free(void *ptr);

参数只有一个——指向你要释放内存的指针。没有返回值,也不告诉你释放成没成功。嗯,这里要注意:free 只认你从 malloccallocrealloc 拿到的地址。传别的地址进去,后果自负。

5.2 释放内存的正确姿势

我个人习惯,每次 malloc 之后,立马想好对应的 free 写在哪。别等到代码写了几百行再回头找,那时候很容易漏掉。

看个标准用法:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p = (int *)malloc(10 * sizeof(int));
    if (p == NULL) {
        // 处理分配失败
        return -1;
    }

    // 正常使用 p
    for (int i = 0; i < 10; i++) {
        p[i] = i * 2;
    }

    // 用完了,释放
    free(p);

    return 0;
}

释放之后,p 指向的地址就失效了。但 p 本身还存着那个地址值。这就是所谓的「悬空指针」。

⚠️ 重要提醒: free 之后,一定要把指针置为 NULL。否则后面不小心用了它,就是非法访问。

所以更稳妥的写法是:

free(p);
p = NULL;

我在项目中遇到过好几次,就是因为忘了置 NULL,后面代码里又判断 if (p != NULL),结果直接踩进了野指针的坑里。

5.3 避免重复释放

重复释放,也就是 double free,是 C 语言里最隐蔽的 bug 之一。为什么会这样?因为 free 不会检查这块内存是不是已经被释放过了。你连续调两次 free,第二次就会破坏堆管理器的内部结构,轻则程序崩溃,重则产生安全漏洞。

看个反面例子:

void bad_example() {
    int *data = (int *)malloc(100 * sizeof(int));
    if (data == NULL) return;

    // 第一次释放
    free(data);

    // ... 中间隔了很多代码 ...

    // 第二次释放 —— 灾难!
    free(data);
}

你想想看,这种 bug 多难排查。代码可能跑了几万次都没事,偏偏在某次特定的内存布局下炸了。我曾经花了一整个下午,就为了找一个 double free 的问题。最后发现是两个人写了不同的模块,都以为对方没释放,结果各自调了一次 free

💡 避坑指南: 我曾经在团队里定过一个规矩——谁 malloc,谁 free。释放之后立刻置 NULL。这样即使别人不小心又调了一次 free,对 NULL 指针调用 free 是安全的(C 标准规定 free(NULL) 什么都不做)。

5.4 核心逻辑图

下面这张图,把 free 函数的整个生命周期和注意事项串起来了:

free 函数核心流程 调用 free(ptr) ptr == NULL ? (是否为空指针) 安全返回 free(NULL) 合法 检查合法性 释放堆内存 ptr = NULL(推荐) 非法地址 → 崩溃 重复释放 → double free

5.5 常见陷阱总结

陷阱类型 表现 解决方案
重复释放 程序崩溃或堆损坏 free 后立即置 NULL
释放非堆内存 未定义行为,可能崩溃 只释放 malloc 族函数返回的地址
释放后继续使用 数据被篡改,逻辑错误 置 NULL + 代码审查
忘记释放 内存泄漏 谁分配谁释放,养成习惯

核心要点:

  • free 只释放内存,不改变指针值
  • free(NULL) 是安全的,但 free(非法地址) 不是
  • 释放后置 NULL,是防御性编程的基本功
  • double free 比内存泄漏更难排查

说实话,free 函数本身没什么技术含量。但怎么用好它,怎么不出事,这才是真功夫。我见过十年经验的工程师,照样在 double free 上翻车。所以别小看这个简单的函数,它背后藏着的是你对整个内存管理模型的理解深度。


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