这一章在干嘛?
数组长度必须在编译期写死,运行时才知道要多少空间怎么办?答案是堆。本章讲 malloc 家族的使用规则和五大经典内存错误——每一项都值得用一生去避免。
11.1 malloc 与 free:基本契约
#include <stdlib.h>
int *arr = malloc(n * sizeof *arr); /* 按元素个数算,不写死类型名 */
if (arr == NULL) { /* 分配可能失败! */
/* 处理失败:报错/降级/退出 */
}
/* ... 使用 arr[0] ~ arr[n-1] ... */
free(arr); /* 归还系统;arr 之后不可再使用 */
arr = NULL; /* 防悬空:顺手置空 */
| 函数 | 参数 / 行为 |
|---|---|
| malloc(n) | 分配 n 字节,内容不初始化(是垃圾值) |
| calloc(k, size) | 分配 k×size 字节并清零 |
| realloc(p, n) | 把 p 指向的块调整为 n 字节:可能原地扩/缩,可能搬新址(内容保留),p 为 NULL 时等价 malloc |
| free(p) | 释放;p 必须是 malloc/calloc/realloc 的原样返回值 |
malloc 返回 void*:
赋给其他类型指针时 C 自动转换,不必强转(强转反而可能掩盖「忘了 stdlib.h」的错误——旧式声明把返回值当 int 截断)。写 sizeof *arr 而不是 sizeof(int),改类型时不用改这行。
11.2 realloc 与使用要点
/* 扩容惯用法:用临时变量接,失败时原指针原块都完好 */
int *tmp = realloc(arr, new_n * sizeof *arr); /* 可能原地扩,也可能搬新址 */
if (tmp == NULL) {
/* 失败:原块 arr 原样保留、未释放,可继续用或走错误处理 */
} else {
arr = tmp; /* 成功才更新指针 */
}
使用动态内存的三条纪律:① 分配即检查(NULL 判断);② 初始化别偷懒(malloc 内容是垃圾,需要零值用 calloc 或 memset);③ 释放后不回头(不读、不写、不再 free,指针要么置 NULL 要么出作用域)。
11.3 五大经典内存错误
| 错误 | 长相 | 后果 / 对策 |
|---|---|---|
| 内存泄漏 | 申请了从不 free;指针被覆盖再也找不到块 | 长期运行程序内存暴涨。对策:每个 malloc 配对 review,出函数前检查所有出口 |
| 悬空指针 | free 后继续用 p;返回栈上局部变量地址 | 读垃圾/写坏别人内存。对策:free 后置 NULL;永不返回局部变量地址 |
| 越界读写 | arr[n](合法到 n-1);malloc(4) 写 5 字节 | 踩坏堆管理结构,崩溃点远离案发点。对策:边界常挂心,工具(ASan/valgrind)常跑 |
| 重复释放 | 同一块 free 两次 | 堆结构损坏。对策:free 后置 NULL(free(NULL) 本身无害) |
| 释放非堆指针 | free 栈变量/静态区/偏移过的地址 | 未定义行为。对策:free 只接 malloc 家族的原样返回值 |
所有权思维:
每块堆内存问一句「谁负责释放?」——同一函数内配对最简单;跨函数传递时在接口注释里写明归属(调用方释放还是被调方释放)。职责不清是泄漏的总根源。
1. malloc 和 calloc 的两个差别?
① 参数形式:malloc 按总字节数,calloc 按元素个数×单个大小(自动防 int 溢出);② 初始化:malloc 内容为垃圾值,calloc 全部清零。需要零初始化选 calloc,性能敏感且随后必然整体写入选 malloc。
2. 为什么 realloc 要用临时变量接返回值?
realloc 可能搬移到新地址,原指针作废;若直接 arr = realloc(arr, n),一旦返回 NULL(失败),原指针被覆盖、原内存无法释放——泄漏。用 tmp 接住,成功才更新 arr,失败时原数据与原指针都完好。
3. 悬空指针怎么产生、怎么防?
free 之后指针值不变但目标已归还系统,继续解引用就是悬空访问;返回局部变量地址同理。防:free 后立即置 NULL;绝不返回栈上变量地址;接口注释写明内存所有权。