5.4 动态内存分配:堆的管理


5.4 动态内存分配:堆的管理

本节摘要:栈的大小在编译期定死,数据需求在运行期才已知的场景(用户输入的规模、动态增长的表)必须用堆。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 触发复用才爆雷。

图 1 堆块生命周期与三大事故

图 1 堆块生命周期与三大事故

realloc 的正确姿势

调整大小时分配器可能原地扩展,也可能搬家。搬家时旧地址失效:

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_openxxx_closemallocfree);
  • 容器持有:数据进了容器,所有权转移给容器,由容器的销毁函数统一释放;
  • 注释声明:函数返回 malloc 的指针时,文档必须写明"调用者负责 free"。

Linux 内核与大型开源项目里,所有权注释是代码评审的必查项。语言没管的事,纪律来管。

⚠️ 常见坑三连:malloc 后 memset 前就用(拿到的是脏数据);free 后不置空继续用(悬垂);把栈变量地址或字符串字面量当堆指针 free(free 只认 malloc 家族的原始返回值)。

💡 关键直觉:把 malloc/free 想成"租仓库"——签约拿钥匙(检查 NULL),用完退租(free),退租后销毁钥匙副本(置 NULL),钥匙只能配对使用(一次租一次退)。堆事故全部源于钥匙管理的疏漏。

本节要点回顾

  • 堆满足运行期才知大小的需求:栈有上限、全局不可扩展,malloc 家族补齐第三种存储期。
  • 分配器带元数据管理块:free 必须收到 malloc 家族的原始地址,偏移过的指针会损坏堆结构。
  • 三大事故:泄漏(慢性死亡)、悬垂(延时爆炸)、二次释放(当场崩溃)。
  • realloc 先接临时变量:失败时旧块仍是有效退路。
  • 所有权靠纪律:谁分配谁释放,配对函数与注释声明是 C 的官方解法。

第 5 章主力收官。第 6 章把指针知识套用到它最大的应用现场:数组与字符串。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U