前言
今天是第十三天,老师讲了函数栈帧的创建和销毁。第一遍听的时候,完全懵了——ESP、EBP、压栈、弹栈,感觉在听天书。第二遍硬着头皮听,做了点笔记,又追问了好几次,才终于搞懂“原来函数调用时底层是这么回事”。这篇博客不抄课件,只记录我最后搞懂的那张图和那个比喻。
摘要: 本文用“草稿纸”的比喻,通俗讲解 C 语言函数栈帧的创建与销毁。先回答“为什么局部变量只在函数内有效”,再介绍栈的 LIFO 特性与 ebp、esp 两个关键指针,随后以 Add(3, 5) 为例,逐步拆解函数调用时压参、压返回地址、保存旧 ebp、设置新 ebp、为局部变量腾空间的完整过程,并说明返回时栈帧如何销毁。最后用表格总结局部变量生命周期、递归栈溢出、返回局部变量地址等常见疑问,附上作者第二遍听课的核心笔记。
〇、先回答一个核心问题:为什么局部变量只在函数内有效?
这个问题,从学函数第一天就有人问。答案就在栈帧里:
每次调用函数,系统会在栈区给它划一块“临时草稿纸”。函数返回,这张草稿纸就被撕掉。草稿纸都没了,上面的字(局部变量)当然也没了。
这是C语言底层的规定,不是你写代码能改变的。所以,局部变量的生命周期 = 函数的生命周期。
一、什么是“栈”?
计算机内存中有一块专门用来临时存放函数调用信息的区域,叫栈。
它是一块后进先出(LIFO) 的存储空间——就像一叠盘子,最后放上去的,最先被拿走。
每次调用一个函数,系统就在栈上叠一张新草稿纸(栈帧)。函数执行完,就把最上面那张纸撕掉(销毁栈帧)。
栈帧里存放的是:
- 函数的参数
- 局部变量
- 返回地址(函数结束后回到哪里)
二、两个关键指针:ebp 和 esp
老师在讲栈帧时,绕不开两个寄存器:ebp 和 esp。第一遍听的时候,这两个东西让我最头疼。后来用比喻理解了:
- ebp(栈底指针):一根固定的钉子,钉在当前草稿纸的底部。通过它,可以准确找到参数和局部变量。
- esp(栈顶指针):一支移动的笔,随着数据不断压入和弹出而上下移动,永远指向当前草稿纸的“写到的位置”。
简而言之: ebp 是基准线,esp 是活动端。它们共同划定了一块栈帧的范围。
三、函数调用时,栈帧的变化过程(完整拆解)
以这段代码为例:
int Add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int result = Add(3, 5);
return 0;
}
第 1 步:调用前(main 正在执行)
- ebp 和 esp 指向 main 函数的栈帧
- 栈上已经有 main 的局部变量
第 2 步:压入参数(从右向左)
C 语言规定,函数参数从右向左压栈:
第 3 步:压入返回地址
call 指令会做两件事:
第 4 步:保存旧的 ebp
进入 Add 函数后,第一件事是保存调用者(main)的 ebp:
- push ebp:把 main 的 ebp 压入栈
这样等函数返回时,可以恢复 main 的栈帧位置。
第 5 步:设置新的 ebp
- mov ebp, esp:让 ebp 指向当前 esp 的位置
现在 ebp 就是 Add 函数栈帧的底部基准。
第 6 步:为局部变量腾空间
Add 函数里有一个局部变量 z:
- sub esp, 4:把 esp 向下移动 4 个字节(int 占 4 字节),为 z 腾出空间
现在 Add 的栈帧已经完整。我画了一张示意图:
高地址
┌─────────────┐
│ main 的 │
│ 栈帧 │
├─────────────┤
│ 参数 y=5 │ ← ebp + 8
├─────────────┤
│ 参数 x=3 │ ← ebp + 4
├─────────────┤
│ 返回地址 │
├─────────────┤
│ 旧的 ebp │ ← ebp(当前栈底)
├─────────────┤
│ 局部 z │ ← ebp – 4
│ │ ← esp(当前栈顶)
└─────────────┘
低地址
注意: 栈向低地址生长,所以地址从上往下减小。
四、函数返回时,栈帧如何销毁?
至此,Add 函数的栈帧被完全销毁,局部变量 z 不复存在。
五、理解了栈帧,你能回答这几个问题
| 为什么局部变量只在函数内有效? | 函数返回时,栈帧被销毁,局部变量一起消失 |
| 为什么递归太深会栈溢出? | 每层递归都占一块栈帧,叠太多就把栈空间撑爆了 |
| 为什么不能返回局部变量的地址? | 返回的是草稿纸上的门牌号,但草稿纸已经被撕掉了 |
六、我的听课笔记(第二遍才记下来的)
以下是第二遍听课时记的核心内容:
- ebp 和 esp 存放的是地址,用来维护函数栈帧
- 当有新的栈帧时,esp 会去到最上面,ebp 在新的函数栈底
- 每一个函数调用,都要在栈区建立一个空间
- 压栈(push):给栈顶放一个元素
- 弹栈(pop):从栈顶删除一个元素
- 有新的元素都是往新的栈帧上堆
七、写在最后
函数栈帧这一讲,第一遍听的时候我完全是懵的。听不懂不是因为笨,是因为这是第一次接触“函数调用在底层发生了什么”这个问题。第二遍听,结合画图和比喻,才终于搞懂。
如果你也卡在这里,建议你拿张纸画一画:画一个栈,标出 ebp 和 esp 的位置,然后按步骤把参数、返回地址、局部变量填进去。画一遍,比看十遍都管用。
苔花如米小,也学牡丹开。下次见。 (●’◡’●)
(全文完)





