栈上的内存是「包月制」:作用域到了自动回收,省心但死板。堆是「按天租房」:想要多久住多久,但退房(free)全靠自己记。手动管理是 C 的自由,也是 C 的原罪,本章把 malloc 家族四兄弟讲透,再把我自己踩过的、教材里写着的、以及 ASan 替我抓过的内存错误整理成一张「案发记录」。原文小节编号错成 8.x,一并修正。
int *p = malloc(100 * sizeof(int)); // 100 个 int,内容是垃圾
int *q = calloc(100, sizeof(int)); // 同样大小,但全部清零
三件事必须刻进手指记忆。用 sizeof 算大小:malloc(100) 是 100 字节(约 25 个 int),写错不报错、越界等着你;永远 n * sizeof(type)。检查返回值:失败返回 NULL,直接解引用就是空指针解引用(第 1 章的案卷)。垃圾值是真的垃圾:malloc 不打扫,新拿的内存里是上一位房客的遗留,读它是未定义行为,要零初始化用 calloc(它还顺手防了乘法溢出)。
malloc 底下是什么?glibc 的分配器维护着空闲块链表,小块找堆顶(brk)、大块找内核要独立映射(mmap,操作系统课第 6 章讲过这条分工);free 把块挂回空闲链。理解这一层,「free 之后为什么指针还有值」「为什么 free(NULL) 安全」「为什么不能 free 栈上指针」全都自动有答案:free 操作的是分配器的记账结构,传一个不是它发的地址,就是把账记烂。
free(p);
p = NULL; // 老兵的习惯:退房后把钥匙销毁
free 之后 p 里还存着旧地址,但那块内存已归还,这个状态叫悬空指针:再解引用是 UB(可能读到新住客的数据),再 free 一次是 double free(堆的记账被破坏,后续 malloc 可能返回重叠的块,故障点远在案发千里之外)。置 NULL 的妙处:free(NULL) 是标准保证的无操作,后续误用要么无害要么干净地崩,把「悄悄错」变成「大声错」。
int *tmp = realloc(p, 8 * sizeof(int)); // 扩容
if (!tmp) { /* 失败:p 还有效,别覆盖! */ }
p = tmp; // 成功才换手
realloc 的行为分三种:原地扩(后面正好有地)、搬家扩(新块、拷贝、释放旧块,旧指针全部失效)、失败(返回 NULL,原块原样保留)。第三种是陷阱的产地:教科书错误写法 p = realloc(p, new); 在失败时把 p 覆盖成 NULL,原来的内存从此无人认领,泄漏 + 悬空双杀。正确姿势就是上面的「先存临时、验证再赋值」,一行顺序,人命关天。
| 案型 | 症状 | 现场特征 |
|---|---|---|
| 内存泄漏 | 跑得越久内存越肥 | 长期服务 RSS 缓涨不落;忘了 free / 异常路径跳过了 free |
| 悬空读写 | 偶发脏数据或崩 | free 后还在用;返回了指向局部对象的指针(第 2 章) |
| double free | 崩在看似无关的 malloc | 两个拥有者都以为该自己 free;free 后没置 NULL |
| 越界写堆 | 崩在很久之后的 free | 溢出写坏了堆块的元数据(块头),案发与案发现场相隔甚远 |
这些案子的共同困难是案发地不在案发现场:越界写时可能毫无动静,几百万次 malloc 之后的某一次才崩。裸肉眼看代码基本没戏,上工具:编译加 -fsanitize=address(ASan,实时抓越界、悬空、double free,报错直接带调用栈),或跑 valgrind(慢十倍但不用重编译)。我个人的建议流程:开发期全程 ASan,CI 里跑 valgrind,两者抓这类案子几乎是秒破。
与其每次默写纪律,不如写进工具函数,让错误在公共层被消化:
#include <stdlib.h>
#include <stdio.h>
void *xmalloc(size_t n) { // 检查 + 失败即退,调用方不再重复检查
void *p = malloc(n);
if (!p) { fprintf(stderr, "out of memory\n"); exit(1); }
return p;
}
(失败策略按项目定:守护进程可能选择降级重试,而不是 exit,但「绝不解引用 NULL」的底线不变。)再配两条结构级纪律:谁申请谁释放(一个模块里 malloc 和 free 最好同层出现,别让所有权跨层漂移),以及复合结构的析构函数(数据结构课第 7 章的「先逐个、后整体」)。C 没有析构函数,就把「创建/销毁成对函数」当作自己的 RAII,所有结构体类型都配一对 xxx_create/xxx_destroy,泄漏概率断崖式下降。
int **m = malloc(n * sizeof(int*)); 后每行 malloc(cols * sizeof(int)),若第 k 行分配失败,写出正确的回滚代码(把前面 k 行全释放,再释放行指针数组,返回 NULL)。