5. free函数:函数原型、释放内存、避免重复释放
内存管理这事儿,分配只是前半段。真正考验功力的,其实是释放。我见过太多项目,前期内存分配得漂漂亮亮,一到释放环节就翻车。说白了,free 函数用不好,你的程序就跟定时炸弹差不多。
5.1 函数原型
free 函数的原型特别简单,就一行:
#include <stdlib.h>
void free(void *ptr);
参数只有一个——指向你要释放内存的指针。没有返回值,也不告诉你释放成没成功。嗯,这里要注意:free 只认你从 malloc、calloc、realloc 拿到的地址。传别的地址进去,后果自负。
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(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。
5.4 核心逻辑图
下面这张图,把 free 函数的整个生命周期和注意事项串起来了:
5.5 常见陷阱总结
| 陷阱类型 | 表现 | 解决方案 |
|---|---|---|
| 重复释放 | 程序崩溃或堆损坏 | free 后立即置 NULL |
| 释放非堆内存 | 未定义行为,可能崩溃 | 只释放 malloc 族函数返回的地址 |
| 释放后继续使用 | 数据被篡改,逻辑错误 | 置 NULL + 代码审查 |
| 忘记释放 | 内存泄漏 | 谁分配谁释放,养成习惯 |
核心要点:
- free 只释放内存,不改变指针值
- free(NULL) 是安全的,但 free(非法地址) 不是
- 释放后置 NULL,是防御性编程的基本功
- double free 比内存泄漏更难排查
说实话,free 函数本身没什么技术含量。但怎么用好它,怎么不出事,这才是真功夫。我见过十年经验的工程师,照样在 double free 上翻车。所以别小看这个简单的函数,它背后藏着的是你对整个内存管理模型的理解深度。