11.堆-vs-栈.md 4.4 KB

堆 vs 栈

判别规则(核心,唯一要记的)

  • 一句话区分:栈靠"后进先出"精确回收 → 快、小、无碎片、系统管;堆靠"随机分配"自由存取 → 慢、大、有碎片、程序员管
  • 面试输出公式:6 维对照(分配/回收/速度/大小/碎片/生命周期)+ 1 个为什么(栈为何不碎片)

一句话:"栈是托盘,堆是仓库;托盘不乱,仓库会乱。"

抽象描述(一句话本质)

栈是"一摞托盘"——后进先出,精确回收;堆是"自由仓库"——随意存取,东拼西凑。管理方式决定了速度、大小、碎片的一切差异。

正反例(建立直觉)

  • 正例1(分配):栈 = 移动 SP 指针,编译期算好偏移,一条指令;堆 = malloc 运行时搜索空闲链表,可能要分裂/合并
  • 正例2(回收):栈 = 函数返回 SP 一拨,从顶往下收;堆 = free 手动,可能先释放中间的块 → 留空洞
  • 正例3(碎片):栈 LIFO 精确回收永不碎片(一摞盘子从顶拿,中间永不留空);堆随机分配随机释放 → 中间留洞,洞太小塞不进新申请 → 碎片化
  • 反例1:深层递归 deep(10000) 每层压帧 → 栈耗尽 → 栈溢出
  • 反例2:反复 malloc/free 同大小块不会碎片(原样复用)——这正是内存池防碎片的原理

易混对比(8 维度)

维度
谁分配 系统(函数调用) 程序员(malloc)
谁回收 系统自动 程序员(free)
速度 快(移 SP 一条指令) 慢(搜索空闲块)
大小 小(KB 级,编译期定) 大(受 RAM 总量限制)
碎片 (LIFO 精确回收) (随机分配留洞)
生命周期 函数调用期间 malloc → free
增长方向 高地址向下 低地址向上
溢出后果 栈溢出 → HardFault 堆耗尽/碎片 → malloc 返回 NULL

变体验证(3 题,全过=学会)

  1. void deep(int n){ if(n) deep(n-1); } deep(10000) → 主要消耗,可能栈溢出(每层压帧)
  2. 陷阱题:循环反复 malloc/free 同大小块不会产生碎片(free 的空闲块被下一个 malloc 原样复用)
  3. 陷阱题:全局数组 char pool[1024];静态区,既不是栈也不是堆(大数组 ≠ 堆)

口述要点(面试怎么讲)

  • 结论先行:六维对比(分配/回收/速度/大小/碎片/生命周期),核心落点在"回收能否精确控制"
  • 为什么栈不碎片:回收严格后进先出,永远从顶往底收,中间不可能留洞
  • 嵌入式为什么用内存池:① 固定大小块 → free 后任意新申请原样复用 → 无碎片 ② 分配快且时间确定(直接拿一块,不搜索不合并)——第②点对实时系统是关键
  • 栈溢出检测:FreeRTOS 用 uxTaskGetStackHighWaterMark()(创建任务时栈填已知模式,调度器定期检查剩多少)和 configCHECK_FOR_STACK_OVERFLOW + vApplicationStackOverflowHook();裸机用调试器看 SP / 栈填充模式扫描
  • 碎片失败 vs 内存不够:碎片失败可解(合并整理碎片后又能申请);真·内存不够无解(物理限制只能加内存)
  • 易错点:以为栈也碎片;把大数组当堆(是静态区);递归深就是栈问题不是堆问题

关系网络

相邻概念 和本主题的关系 孤立理解会犯的错
内存碎片 堆碎片专门深挖:怎么产生、怎么治 以为栈也碎片
RTOS 栈估算 HighWaterMark 检测栈剩余;栈大小按 word 配 以为栈溢出只能等崩溃
内存池 固定大小块 → 无碎片 + 分配确定 以为池子只是"省内存"
内存分配方式 三区里的两个动态区,本题是它们的深度对比 把"大数组"当堆(静态区!)

学习日期

2026-08-03


每主题一页,复习时只翻本目录。