跳到主要内容
🔍
1.3.9已发布intermediate · 约 3 课时 · P0

动态内存管理与常见错误

正确使用 malloc/calloc/realloc/free 并识别内存问题,掌握返回值检查与所有权约定

  • 来源
  • 补充

标记说明:【来源】来自上传资料 · 【补充】课程新编 · 【纠错】按勘误表修正 · 【更新】过时内容已现代化 · 【待确认】无法可靠还原

完成标准(本章)

  • 📖 已阅读:滚动 ≥ 80% 且有效阅读 ≥ 180
  • ✏️ 已练习:小练习正确率 ≥ 60%
  • 📝 已通过测验:分数 ≥ 60
  • 🛠️ 已掌握还需完成实践任务
学习状态:未开始

1.3.9 动态内存管理与常见错误

本章来源:malloc/calloc/realloc/free 用法、堆与栈区别、内存泄漏概念来自《第一阶段讲义》24.x 节【来源】,经重新组织表述;realloc 安全模式、所有权约定、Valgrind 检测思路与全部练习为新编【补充】(约 20%)。危险行为(重复释放/释放后使用)一律静态讲解,不实际运行。平台适用性 universal。

① 学习目标

  1. 用 malloc/calloc/realloc/free 完成动态数组与动态结构体,每次分配都检查返回值
  2. 写出 realloc 安全模式(临时指针 + 判空后再赋回,避免覆盖原指针造成泄漏);
  3. 说清堆与栈在生命周期、大小、速度上的区别与选型依据;
  4. 识别四类内存问题:泄漏、重复释放、释放后使用(悬空指针)、越界写,并说出检测思路;
  5. 用 create/destroy 封装表达"谁分配谁释放"的所有权约定(实践任务)。

② 前置知识

  • 必选:1.3.6 结构体与位段(动态结构体数组、-> 访问);
  • 建议:1.1.6 GDB(检测内存问题的工具基础);1.3.1 指针(悬空指针概念已建立)。

③ 核心概念【来源】

概念说明
malloc/callocmalloc(n) 分配未初始化内存;calloc(个数, 大小) 分配并清零;都要检查返回 NULL(分配失败)
realloc调整已分配块大小(可能原地扩展、可能搬移);安全模式见 ⑥
free释放 malloc 系分配的内存;释放后指针成为悬空指针(内容未变但不可再用)
堆 vs 栈栈:编译器管理、速度快、随函数返回自动释放、大小有限(MB 级);堆:手动管理、速度慢、大小受系统内存限制、生命周期由你决定
内存泄漏分配后丢失指针、无人释放——程序长期运行内存持续增长;短命程序影响小,服务器/嵌入式长期运行是致命问题
所有权约定每个堆块要有明确的"主人":create 分配、destroy 释放成对出现;跨模块传递时文档写明谁负责释放
ValgrindLinux 内存检测工具:泄漏(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,这属于?(单选)

    ◌ 未作答

⑨ 章节测验

章节测验

5 题题库 · 随机抽 5 题 · 及格线 60 分 · 前端判分(学习自测)
开始测验 →

⑩ 实战任务

实践任务

写一个会泄漏的程序并用工具思路定位

写一个含 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/freeRTOS 专用堆接口(如 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%)