本节摘要:栈的大小在编译期定死,数据需求在运行期才已知的场景(用户输入的规模、动态增长的表)必须用堆。malloc 从分配器手里领一块内存,free 归还——契约简单,事故却多:泄漏、悬垂指针、二次释放、堆损坏。本节从分配器的视角讲清这些事故的机器成因,给出一套可落地的防御纪律。
三种内存获取方式的边界一目了然:
| 方式 | 大小 | 时机 | 释放 | 典型失败 |
|---|---|---|---|---|
| 局部变量 | 编译期定 | 进入块 | 自动 | 大数组撑爆栈 |
| 全局/静态 | 编译期定 | 程序启动 | 永不 | 浪费内存、不可扩展 |
| malloc 家族 | 运行期任意 | 调用时 | 手动 free | 泄漏 悬垂 二次释放 |
栈上放几百 KB 的数组就可能溢出(上限常为 1 到 8MB),而"用户先告诉你 n,再处理 n 个元素"的需求只有堆能满足。基础四件套:
#include <stdlib.h> void *p1 = malloc(100); /* 要 100 字节,内容不定 */ void *p2 = calloc(25, sizeof(int)); /* 25 个 int,清零 */ void *p3 = realloc(p1, 200); /* 扩到 200 字节,旧内容保留 */ /* free(p) 归还;三者失败都返回 NULL */
标准流程与铁律:
int *data = malloc(n * sizeof(int)); if (data == NULL) { /* 铁律一:malloc 可能失败,必须检查 */ fprintf(stderr, "内存不足\n"); return -1; } /* 使用…… */ free(data); /* 铁律二:谁分配谁释放,配对出现 */ data = NULL; /* 铁律三:free 后立即置 NULL,防悬垂复用 */
malloc 不是系统调用,而是运行时库里的"二道贩子":它成块地向操作系统要内存(按页,底层是内存映射系统调用),切分成小块分给用户,free 时回收登记。为了管理,分配器在每块返回内存的前后各埋了少量元数据(块大小、空闲链表指针)——这就是"请求 100 字节实际消耗 108 字节左右"的原因。
free 怎么知道该还多少?答案就在块头的元数据里——你传给 free 的指针必须是从 malloc 家族原样拿到的地址。往前偏一字节再 free,free 把块头数据当元数据读,堆结构直接损坏——这种错误崩在很远的地方,最难查。
free 后的内存回到空闲池,大概率立即被后续 malloc 复用。于是:
int *p = malloc(sizeof(int)); *p = 42; free(p); printf("%d\n", *p); /* 未定义行为:常见两种结局 */
要么还是 42(块还没被复用,废墟残留——第 4 章的老朋友),要么是垃圾(被复用改写)。悬垂指针最毒的地方在于它经常"暂时正常",直到某次无辜的 malloc 触发复用才爆雷。

调整大小时分配器可能原地扩展,也可能搬家。搬家时旧地址失效:
int *tmp = realloc(data, new_n * sizeof(int)); if (tmp == NULL) { /* 旧 data 仍然有效!继续用旧块或清理退出 */ free(data); return -1; } data = tmp; /* 成功才接管新地址 */
直接写 data = realloc(data, ...) 是常见错误:失败时返回 NULL 覆盖旧指针,旧块地址丢失——泄漏且无法挽回。"先接临时变量"不是迂腐,是保住退路。
多函数共享一块堆内存时,最关键的问题不是"怎么 free"而是"谁负责 free"。C 没有语言层面的答案,只有工程约定:
xxx_open 配 xxx_close,malloc 配 free);Linux 内核与大型开源项目里,所有权注释是代码评审的必查项。语言没管的事,纪律来管。
⚠️ 常见坑三连:malloc 后 memset 前就用(拿到的是脏数据);free 后不置空继续用(悬垂);把栈变量地址或字符串字面量当堆指针 free(free 只认 malloc 家族的原始返回值)。
💡 关键直觉:把 malloc/free 想成"租仓库"——签约拿钥匙(检查 NULL),用完退租(free),退租后销毁钥匙副本(置 NULL),钥匙只能配对使用(一次租一次退)。堆事故全部源于钥匙管理的疏漏。
第 5 章主力收官。第 6 章把指针知识套用到它最大的应用现场:数组与字符串。