动态内存管理与常见错误
正确使用 malloc/calloc/realloc/free 并识别内存问题,掌握返回值检查与所有权约定
- 来源
- 补充
标记说明:【来源】来自上传资料 · 【补充】课程新编 · 【纠错】按勘误表修正 · 【更新】过时内容已现代化 · 【待确认】无法可靠还原
完成标准(本章)
- 📖 已阅读:滚动 ≥ 80% 且有效阅读 ≥ 180 秒
- ✏️ 已练习:小练习正确率 ≥ 60%
- 📝 已通过测验:分数 ≥ 60 分
- 🛠️ 已掌握还需完成实践任务
1.3.9 动态内存管理与常见错误
本章来源:malloc/calloc/realloc/free 用法、堆与栈区别、内存泄漏概念来自《第一阶段讲义》24.x 节【来源】,经重新组织表述;realloc 安全模式、所有权约定、Valgrind 检测思路与全部练习为新编【补充】(约 20%)。危险行为(重复释放/释放后使用)一律静态讲解,不实际运行。平台适用性 universal。
① 学习目标
- 用 malloc/calloc/realloc/free 完成动态数组与动态结构体,每次分配都检查返回值;
- 写出 realloc 安全模式(临时指针 + 判空后再赋回,避免覆盖原指针造成泄漏);
- 说清堆与栈在生命周期、大小、速度上的区别与选型依据;
- 识别四类内存问题:泄漏、重复释放、释放后使用(悬空指针)、越界写,并说出检测思路;
- 用 create/destroy 封装表达"谁分配谁释放"的所有权约定(实践任务)。
② 前置知识
- 必选:1.3.6 结构体与位段(动态结构体数组、-> 访问);
- 建议:1.1.6 GDB(检测内存问题的工具基础);1.3.1 指针(悬空指针概念已建立)。
③ 核心概念【来源】
| 概念 | 说明 |
|---|---|
| malloc/calloc | malloc(n) 分配未初始化内存;calloc(个数, 大小) 分配并清零;都要检查返回 NULL(分配失败) |
| realloc | 调整已分配块大小(可能原地扩展、可能搬移);安全模式见 ⑥ |
| free | 释放 malloc 系分配的内存;释放后指针成为悬空指针(内容未变但不可再用) |
| 堆 vs 栈 | 栈:编译器管理、速度快、随函数返回自动释放、大小有限(MB 级);堆:手动管理、速度慢、大小受系统内存限制、生命周期由你决定 |
| 内存泄漏 | 分配后丢失指针、无人释放——程序长期运行内存持续增长;短命程序影响小,服务器/嵌入式长期运行是致命问题 |
| 所有权约定 | 每个堆块要有明确的"主人":create 分配、destroy 释放成对出现;跨模块传递时文档写明谁负责释放 |
| Valgrind | Linux 内存检测工具:泄漏(definitely lost)、非法访问(Invalid read/write)等(命令见 ex3;本机无 Linux 未实测) |
④ 通俗解释【补充】
- 栈是"公司前台":来去登记自动完成(函数进出自动分配释放),但柜台就那么大(几 MB),放不下大件;
- 堆是"自助仓库":地方大、随便租,但退租手续必须自己办(free)——只租不退(泄漏),仓库早晚爆满;退租了还去拿货(释放后使用),拿到的可能是别人新存的东西;
- 悬空指针是"退租后还攥着的旧钥匙":钥匙还在手里(指针值没变),但房间已经不属于你——最稳妥的习惯是退租同时把钥匙折断(free 后置 NULL);
- realloc 是"换仓库":可能原地扩建(地址不变),也可能整体搬到新仓库(地址变了)——搬的过程中先在新仓库安顿好,确认成功再退旧仓(临时指针 + 判空),别先退了旧仓才发现新仓没租到(数据全丢)。
⑤ 示例代码
代码示例与验证记录
- examples/ex1-dyn-array.cmalloc/calloc/realloc 正确用法与 realloc 安全模式✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-15)编译:
gcc ex1-dyn-array.c -o d1 -std=c11 -Wall -Wextra -Wpedantic适用环境:LinuxWindows(MinGW)展开预期输出(实测)
calloc 后 arr[0..4] 全部为 0: 0 0 0 0 0 sum = 150 扩容后: 10 20 30 40 50 500 600 700
差异说明:本机实测(0 警告)。三个要点对应三行输出:calloc 清零、动态数组求和、realloc 安全模式 (tmp 判空后赋回 arr)扩容到 8 个元素
完整源码见 /code 代码示例页
- examples/ex2-struct-pool.c动态结构体数组 + create/destroy 所有权封装✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-15)编译:
gcc ex2-struct-pool.c -o d2 -std=c11 -Wall -Wextra -Wpedantic适用环境:LinuxWindows(MinGW)展开预期输出(实测)
items[0] = { id=100, name=item-0 } items[1] = { id=101, name=item-1 } items[2] = { id=102, name=item-2 }差异说明:本机实测(0 警告)。pool_create 的失败路径"分配多少清理多少"(calloc 失败先 free(p) 再返回 NULL); destroy 判 NULL 幂等——所有权成对是防泄漏的工程骨架
完整源码见 /code 代码示例页
- examples/ex3-memory-pitfalls.txt内存四类问题与检测思路(conceptual:静态讲解,不运行危险代码)概念讲解示例✓ 文档核对(非执行)· C11 7.22.3 条文 + Valgrind Memcheck 官方手册核对, 2026-08-15编译:
无(静态讲解文本;double free / use-after-free 等 UB 代码不运行)适用环境:LinuxWindows(MinGW)展开文档核对记录
(无运行输出——本示例为概念讲解,见文件内容)
差异说明:内容按 C11 7.22.3 与 Valgrind Memcheck 官方手册核对(错误类型与报告字段);本机无 Linux,Valgrind 未执行
完整源码见 /code 代码示例页
⑥ 编译与运行方法
本章示例为本机实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11):
gcc ex1-dyn-array.c -o d1 -std=c11 -Wall -Wextra -Wpedantic
d1.exe (Linux: ./d1)- ex1:calloc 清零验证、动态数组求和、realloc 安全模式(tmp 判空后赋回)真实输出;
- ex2:动态结构体数组 + create/destroy 所有权封装;
- ex3 为静态讲解(危险行为不运行),不要执行任何 double free / use-after-free 演示代码。
⑦ 常见错误
| 症状 | 原因 | 解决 |
|---|---|---|
| 程序长期运行内存持续增长 | 内存泄漏(分配未释放/指针丢失) | 所有权成对(create/destroy);Valgrind/ASan 定位(ex3) |
| realloc 后原数据"消失"或程序崩溃 | p = realloc(p, ...) 失败时返回 NULL,原指针被覆盖 → 泄漏 + 数据丢失 | 安全模式:tmp = realloc(p, n); if (tmp) p = tmp; else 处理失败(p 仍有效) |
| free 之后又用指针 | 释放后使用(use-after-free,未定义行为) | free 后置 NULL;所有权文档化;静态分析/ASan 检测 |
| 同一指针 free 两次 | 重复释放(未定义行为) | free 后置 NULL(if (p) free(p) 前提是 p 初始化为 NULL);所有权唯一 |
| malloc 返回值没检查就解引用 | 分配失败返回 NULL,解引用崩溃 | 每次分配后判 NULL 并给出错误处理路径 |
分配大小写错(如 malloc(n) 忘乘 sizeof) | 元素数与字节数混淆 | 统一 calloc(n, sizeof(T)) / malloc(n * sizeof(T));注意乘法溢出的极端场景(ex3 讲解) |
| 嵌入式程序在中断里 malloc | 堆操作不可重入/不确定延迟 | 中断上下文禁 malloc;RTOS 用专用堆接口;优先静态分配 |
⑧ 小练习
小练习
学习自测:提交后才显示答案与解析(前端判分,不作为正式考试)ex-1-3-9-1.malloc(100) 分配失败时返回什么?(单选)
◌ 未作答ex-1-3-9-2.代码审查:p = realloc(p, n); 直接覆盖原指针,主要风险是?(单选)
◌ 未作答ex-1-3-9-3.代码审查:函数内 malloc 了一块内存,函数所有返回路径中有一处在 return 前漏了 free,这属于?(单选)
◌ 未作答
⑨ 章节测验
章节测验
⑩ 实战任务
实践任务
写一个会泄漏的程序并用工具思路定位
写一个含 2-3 处内存泄漏的小程序(如循环中 malloc 后不释放、realloc 直接覆盖原指针), 运行观察内存行为;然后按本章检测思路(Valgrind 命令 + 报告解读)说明如何定位每一处泄漏 (本机无 Linux:写出完整 Valgrind 命令与预期报告含义即可,标注 documentation_checked); 最后修复全部泄漏并验证。
输入与输出
交付物:① 泄漏版源码(仅用于观察,不长期运行);② 修复版源码;③ 一份"Valgrind 命令 + 预期报告解读"说明(哪一行报 definitely lost、对应源码哪一行)。
功能要求
- 泄漏版至少包含 2 种泄漏模式(如循环泄漏 + realloc 覆盖原指针)
- 说明每处泄漏的 Valgrind 预期报告含义
- 修复版编译 0 警告、运行行为与泄漏版一致(不含泄漏)
- 明确标注"本机无 Linux,Valgrind 未执行,命令与报告含义为文档核对"
限制条件
- 危险行为(double free/use-after-free)只准静态讲解,不得运行
- 泄漏版不要长时间运行(仅验证功能正确性即可)
验收步骤(自检清单 0/4)
验收标准
- 泄漏版含 ≥2 种泄漏模式(验收步骤 1)
- Valgrind 解读正确对应源码位置(验收步骤 2)
- 修复版 0 警告、输出一致(验收步骤 3/4)
常见失败原因
- realloc 直接覆盖原指针(把泄漏当修复)
- 泄漏版用 double free 演示(危险,禁止)
- 把 Valgrind 命令写成已执行(本机无 Linux,必须 documentation_checked)
可选扩展
- 用 gdb 观察泄漏版进程的内存(heap 段地址范围随循环增长)
- 给修复版加一个"内存统计计数器"(malloc 次数 - free 次数),退出前断言为 0
完成必要清单后才能计入"已完成实践"(学习状态自动推导,不提供一键完成)
⑪ 面试问题
面试问题
realloc 为什么不能直接写 p = realloc(p, size)?正确写法是什么?高频C/C++ · medium
要点:realloc 失败返回 NULL 且原块保持不变;直接覆盖会让原指针丢失(泄漏+数据不可达)。正确写法:tmp = realloc(p, n); 判空后赋回。
realloc 尝试在原块上扩展,空间不足时分配新块、拷贝数据并释放旧块,返回新地址; 失败时返回 NULL,旧块仍有效。直接 p = realloc(p, n) 在失败时把 NULL 写进 p: 旧块地址丢失(内存泄漏),数据也无法再访问。正确模式: int *tmp = realloc(p, n * sizeof(int)); if (tmp == NULL) { /* p 仍有效:继续用旧块或 free(p) 后报错 */ } else p = tmp; 这是动态内存面试的最高频考点之一,能讲清"失败语义"与"泄漏机理"即合格。
追问:- 追问:realloc(ptr, 0) 的行为是什么?(实现定义:可能等价 free,不推荐依赖,应显式 free)
评分要点:- realloc 失败语义(NULL + 原块不变)
- 直接覆盖的泄漏机理
- 完整安全模式代码
free(p) 之后把 p 置为 NULL 有什么作用?有什么局限?高频C/C++ · medium
要点:防止对同一指针重复 free 与直接误用;但只能保护这一个指针,其他副本指针依然悬空。
free 后置 NULL 让该指针不再指向已释放内存:if (p) free(p) 的幂等写法防 double free, 解引用前判 NULL 也能拦住一部分误用(NULL 解引用会立即崩溃,比悬空指针的随机行为好定位)。 局限:如果存在多个指针指向同一块(副本/结构体字段),置 NULL 只影响其中一个, 其他副本仍是悬空指针;且 free(NULL) 本身是安全的空操作,所以关键还在所有权唯一—— 一个堆块只有一个"主人",释放后所有引用方都应视为失效(文档写明)。
追问:- 追问:free(NULL) 合法吗?(合法,C 标准规定空操作)
评分要点:- 两个作用(防重复释放/防误用)
- 副本指针局限与所有权唯一
堆和栈有什么区别?什么情况下必须用堆?高频C/C++ · medium
要点:栈自动管理、速度快、大小有限(MB 级)、生命周期随作用域;堆手动管理、大小受系统限制、生命周期由程序决定。数据量运行时才知道或超出栈容量时必须用堆。
栈:编译器自动分配释放(函数进出),分配开销极小(移动栈指针),大小有限 (Linux 默认线程栈 8MB 量级、嵌入式任务栈常只有几 KB),生命周期绑定作用域—— 函数返回即失效,因此不能把指向栈变量的指针传出去使用。 堆:malloc/calloc/realloc/free 手动管理,分配慢(维护空闲链表),容量受系统内存限制, 生命周期由程序决定——"函数返回后还要用的数据"、"运行时才知道大小的数据" (如读取文件行数后再开数组)、"超过栈容量的大对象"必须用堆。 嵌入式补充:MCU 堆通常很小且中断中禁 malloc,能静态就静态。
追问:- 追问:把指向栈变量的指针返回给调用者会发生什么?(悬空指针——栈帧回收后内存被复用)
评分要点:- 三组对比(管理/大小/生命周期)
- 必须用堆的三类场景
⑫ 延伸阅读
- 《C 程序设计语言(K&R)》8.7 存储分配——只引书名与章节名;
- Valgrind 官方文档(Memcheck 手册:definitely lost / Invalid read 的含义);
- C11 标准 7.22.3 内存管理函数(malloc/calloc/realloc/free 语义);
- 下一章预告:1.3.10 API 封装与工程目录规范——把 create/destroy 的封装思想推广到整个模块。
迁移训练(migration training)
把本章技能迁移到 嵌入式:
| 环节 | 主机(Linux/Windows) | 嵌入式(MCU/RTOS) |
|---|---|---|
| 分配接口 | malloc/calloc/realloc/free | RTOS 专用堆接口(如 pvPortMalloc);裸机优先静态分配 |
| 堆大小 | GB 级 | 通常几 KB~几 MB,极易耗尽 |
| 泄漏检测 | Valgrind/ASan | 难以运行 Valgrind:靠代码审查 + 分配/释放配对统计 + 任务栈水位监控 |
| 中断上下文 | 避免 | 禁止(不可重入、延迟不确定) |
不变的:返回值检查、所有权成对、realloc 安全模式、free 后置 NULL;要改的:接口与资源约束(嵌入式默认"能静态就静态")。
内容来源映射
| 内容部分 | 资料 | 位置 | 标记 | 说明 |
|---|---|---|---|---|
| malloc/calloc/realloc/free 用法、堆与栈区别、内存泄漏概念 | 第一阶段讲义 | 24.x 节 | 【来源】 | 正文在原资料基础上重新组织表述,未大段复制原文 |
| Valgrind 检测泄漏的完整说明与检测思路 | 第一阶段讲义 | 24.x 节 | 【补充】 | 原资料仅一句提及 Valgrind;本章给出完整检测思路(命令文档核对,本机无 Linux 未执行) |
| realloc 安全模式、所有权约定、create/destroy 封装示例、危险行为静态讲解、练习/测验/任务 | 无 | 【补充】 | 原资料无成体系练习;全部新编并本机实测(比例约 20%) |