C 语言 malloc 与 free
更新时间:2026-08-26。本文是
languages/c/主题入门层第 51 篇。上一讲说了堆是"仓库",这一讲学两个核心操作:malloc申请、free释放。一句话的 API 背后是三个经典大坑——泄漏、悬垂、双重释放。本文把它们一次讲透。
本文要回答的问题
malloc到底要传多大?为什么总是sizeof?- 分配失败会怎样?要不要判空?
free的规则是什么?三大坑分别是什么?
一、malloc:申请内存
#include <stdlib.h>
int *p = malloc(100 * sizeof(int)); // 100 个 int 的空间三个要点:
- 参数是字节数,不是元素个数。
100个int需要100 * sizeof(int)字节; - 返回
void *,C 里可以隐式转换给任意指针类型; - 返回的内存未初始化,内容是垃圾值,用之前要赋值(想要清零用
calloc,见下一讲)。
正确写法永远带上 sizeof:
// ✅ 正确:类型和元素个数都明确
struct Student *arr = malloc(n * sizeof(struct Student));
// ❌ 错误:假设 int 是 4 字节(在 16 位/64 位平台上翻车)
int *arr = malloc(n * 4);二、判空:malloc 可能失败
malloc 失败返回 NULL,不判空直接解引用就是空指针崩溃:
int *p = malloc(1024 * 1024 * 100);
if (p == NULL) {
fprintf(stderr, "malloc failed\n");
return 1; // 或走错误处理
}规则:申请了就判空;判空了再使用。尤其是大块申请,必须检查。
三、free:释放内存
free(p); // 把内存还给堆
p = NULL; // 顺手置空,防止悬垂free 三条铁律:
| 规则 | 违反后果 |
|---|---|
| 只能 free 自己 malloc 出来的 | 非法 free → 崩溃/UB |
| 只能 free 一次 | 双重释放(double free)→ 崩溃 |
| free 后不再使用 | 悬垂指针(use-after-free)→ 数据错乱 |
free 一个栈变量(int x; free(&x);)、free 同一块两次、free 后继续 p[0] = 1——都是未定义行为,ASan 一抓一个准。
四、三大坑逐一拆解
1. 内存泄漏(memory leak):申请了不释放。
void process(int n) {
int *tmp = malloc(n * sizeof(int));
// ... 忘了 free(tmp)
}函数调用方拿到结果后,那块内存再也没人管。服务长期运行会持续吃掉内存。
2. 悬垂指针(dangling pointer):释放了还在用。
int *p = malloc(10 * sizeof(int));
free(p);
*p = 42; // ❌ use-after-free!free 只是"标记可复用",内存内容还在,但可能随时被下一次 malloc 覆盖——所以 bug 会"间歇性出现",特别难查。
3. 双重释放(double free):free 了两次。
free(p);
free(p); // ❌ double freeglibc 会检测到并直接 abort,报 free(): double free detected in tcache 2。
统一对策:free 后立刻 p = NULL;——置空后第二次 free 是 no-op(free(NULL) 合法),悬垂访问也变成确定性的空指针崩溃,好查多了。
五、谁分配谁释放:所有权约定
多函数协作时,内存的"所有权"要明确。两种常见约定:
// 约定 1:调用方分配,函数只填
void fill(int *buf, size_t n) { /* 填充 */ }
int buf[64];
fill(buf, 64); // 调用方管
// 约定 2:函数分配,调用方负责释放
char *read_line(void) {
char *s = malloc(128);
if (s == NULL) return NULL;
fgets(s, 128, stdin);
return s; // 文档写明:调用方 free
}
char *line = read_line();
/* ... */
free(line); // 按约定释放工程上常见规则:谁 malloc 谁 free;文档里写清楚"返回值需调用方释放"。
六、malloc 内部发生了什么(一览)
malloc(100) 不是每次找内核要 100 字节,而是:
- 从 glibc 的空闲链表里找一块大小合适的空闲块;
- 没有就把大块拆小 / 用
sbrk或mmap向内核要更多堆内存; - 返回块首地址,块头记录大小,供 free 时归还。
所以 malloc 并不慢到无法接受,但频繁小分配有开销——这正是内存池、对象复用等高级话题的动机。
七、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 忘判空 | 解引用 NULL 崩溃 | malloc 后必判空 |
| 忘 free | 泄漏,内存持续增长 | 谁分配谁释放 |
| free 后用 | use-after-free 间歇 bug | free 后置 NULL |
| free 两次 | abort: double free | 置 NULL + 只释放一次 |
| free 栈/静态变量 | 崩溃/UB | 只 free malloc 来的 |
| 字节数写错 | 越界写坏相邻块 | 用 sizeof 计算 |
八、与本站主线衔接
- 想要清零的分配、要扩容的分配,见calloc 与 realloc;
- 堆上动态数组的完整实现,见动态数组;
- 段错误的系统级原因,见段错误与 core dump;
- 用工具抓内存问题,见ASan 内存检测 与 Valgrind 内存检测。
一句话总结
malloc(字节数) 在堆上申请内存、失败返回 NULL 必须判空;free(p) 释放且只能释放一次、释放后置 NULL;三大坑——泄漏(忘 free)、悬垂(free 后用)、双释(free 两次)——用"谁分配谁释放 + 置空 + 检测工具"来防——堆内存是借来的东西,借了要还,还了别再碰。