5、free 的使命:free函数原型、释放内存、避免野指针
好,咱们接着聊。上一章我们把 malloc 和 calloc 怎么“申请”内存讲透了。那申请来的内存,用完了怎么办?
你得还回去。
不还?内存泄漏等着你。程序跑个几天,内存一点点被吃掉,最后系统扛不住,直接把你进程给杀了。我早期做嵌入式开发时就吃过这个亏——一个后台服务跑了三天,突然挂了。查了半天,发现是一个循环里 malloc 了没 free。嗯,从那以后,我每次 malloc 完,第一件事就是先把 free 写上去。
今天我们就来彻底搞懂 free 的使命。
5.1 free 函数原型
先看它的长相:
#include <stdlib.h>
void free(void *ptr);
参数只有一个:ptr —— 指向你要释放的那块内存的指针。
注意几个关键点:
ptr必须是之前由malloc、calloc或realloc返回的地址。- 如果
ptr是NULL,free什么都不做。这是标准规定的,所以你可以放心传NULL。 - 释放后,那块内存的内容变得“不确定”。别再去读它了。
free 之后,立刻把指针置为 NULL。这样就算不小心再 free 一次,也不会出问题。
5.2 释放内存:到底发生了什么?
你调用 free(ptr) 时,C 运行时库会做两件事:
- 标记那块内存为“可用”。它不会清空数据,只是把这块内存还给堆管理器。
- 可能合并相邻的空闲块,减少碎片。
说白了,就是告诉系统:“这块地我不种了,你拿去给别人用吧。” 但地里的庄稼(数据)还在,只是随时可能被新的“种植者”覆盖。
我举个例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char *p = (char*)malloc(10);
if (p == NULL) {
return 1;
}
strcpy(p, "hello");
printf("释放前: %s\n", p); // 输出 hello
free(p); // 释放内存
// 危险!释放后继续使用 p
printf("释放后: %s\n", p); // 可能输出乱码,也可能还是 hello
return 0;
}
运行这段代码,第二次 printf 可能还能打出 "hello",也可能打出乱码。为什么?因为 free 后那块内存已经被标记为“空闲”,但数据还没被覆盖。你读到的只是“残影”。
5.3 避免野指针:free 后的正确姿势
什么是野指针?就是指向“已释放”或“无效”内存的指针。你拿着它去读写,就像拿着过期的地图找路,迟早掉坑里。
怎么避免?三个字:及时置空。
free(p);
p = NULL; // 斩断指针与已释放内存的联系
这样,后面如果误用了 p,程序会立即崩溃(空指针解引用),而不是偷偷摸摸地产生随机 bug。崩溃是好事,至少你能马上定位问题。
我曾经在一个网络库中见过这样的代码:
void process_data(char *buffer) {
// ... 处理数据 ...
free(buffer);
// 忘记置 NULL
}
// 在另一个地方
char *buf = (char*)malloc(256);
process_data(buf);
if (buf != NULL) { // 这个判断是无效的!
strcpy(buf, "new data"); // 写入了已释放的内存
}
这个 bug 藏得很深。因为 buf 在 free 后没有被置 NULL,所以后面的 if 判断认为它仍然有效。结果就是往堆管理器的空闲链表里写数据,把整个堆结构破坏了。程序过一会儿才崩溃,查起来非常痛苦。
malloc,谁 free;free 完,立刻置 NULL。
5.4 常见误区与避坑指南
我总结几个新手(甚至老手)容易犯的错:
| 误区 | 说明 | 正确做法 |
|---|---|---|
| 重复 free | 对同一指针调用两次 free |
free 后立即置 NULL,第二次 free(NULL) 安全 |
| 释放栈变量 | free(&local_var) |
只释放堆内存,栈变量由系统自动管理 |
| 释放部分内存 | 对指针偏移后释放,如 free(p+1) |
必须传入 malloc 返回的原始地址 |
| 忘记释放 | 内存泄漏,程序内存持续增长 | 养成配对习惯:malloc 后立即写 free |
free 了传入的指针。结果调用方后续还在用那个指针,导致崩溃。后来我改成:要么在文档里明确说明“本函数会释放传入的内存”,要么让调用方自己释放。接口约定要清晰。
5.5 知识体系:free 的核心逻辑
下面这张图帮你理清 free 在整个动态内存管理中的位置:
从图中可以看到,free 是闭环的关键一步。它把内存还给堆管理器,让后续的 malloc 能复用。而虚线部分——野指针,就是 free 后没有置空导致的危险路径。
5.6 总结
free 的使命很简单:把借来的内存还回去。但“还”的方式决定了你的程序是健壮还是脆弱。
- 记住原型:
void free(void *ptr) - 释放后指针置
NULL,这是成本最低的防御性编程。 - 不要重复释放,不要释放栈内存,不要释放偏移后的地址。
- 配对检查:每个
malloc都要有对应的free。
嗯,关于 free 就讲这么多。下一章我们聊聊 realloc——怎么在运行时调整内存大小。不过那是后话了,先把今天的内容消化掉。