欢迎光临
我们一直在努力

学习C语言第十四天:函数栈帧 · 从“听不懂”到“画出图”

前言

今天是第十三天,老师讲了函数栈帧的创建和销毁。第一遍听的时候,完全懵了——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 语言规定,函数参数从右向左压栈:

  • 先压入 5(y 的值)—— esp 下移(栈向低地址生长)
  • 再压入 3(x 的值)—— esp 继续下移
  • 第 3 步:压入返回地址

    call 指令会做两件事:

  • 把下一条指令的地址(返回地址)压入栈
  • 跳转到 Add 函数的起始位置
  • 第 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(当前栈顶)
    └─────────────┘
    低地址

    注意: 栈向低地址生长,所以地址从上往下减小。

    四、函数返回时,栈帧如何销毁?

  • mov esp, ebp:让 esp 回到 ebp 的位置(释放局部变量占用的空间)
  • pop ebp:恢复 main 函数的 ebp(从栈上弹出旧的 ebp 值)
  • ret:弹出返回地址,跳回 main 继续执行
  • 至此,Add 函数的栈帧被完全销毁,局部变量 z 不复存在。

    五、理解了栈帧,你能回答这几个问题

    问题答案
    为什么局部变量只在函数内有效? 函数返回时,栈帧被销毁,局部变量一起消失
    为什么递归太深会栈溢出? 每层递归都占一块栈帧,叠太多就把栈空间撑爆了
    为什么不能返回局部变量的地址? 返回的是草稿纸上的门牌号,但草稿纸已经被撕掉了

    六、我的听课笔记(第二遍才记下来的)

    以下是第二遍听课时记的核心内容:

    • ebp 和 esp 存放的是地址,用来维护函数栈帧
    • 当有新的栈帧时,esp 会去到最上面,ebp 在新的函数栈底
    • 每一个函数调用,都要在栈区建立一个空间
    • 压栈(push):给栈顶放一个元素
    • 弹栈(pop):从栈顶删除一个元素
    • 有新的元素都是往新的栈帧上堆

    七、写在最后

    函数栈帧这一讲,第一遍听的时候我完全是懵的。听不懂不是因为笨,是因为这是第一次接触“函数调用在底层发生了什么”这个问题。第二遍听,结合画图和比喻,才终于搞懂。

    如果你也卡在这里,建议你拿张纸画一画:画一个栈,标出 ebp 和 esp 的位置,然后按步骤把参数、返回地址、局部变量填进去。画一遍,比看十遍都管用。

    苔花如米小,也学牡丹开。下次见。 (●’◡’●)


    (全文完)

    赞(0)
    未经允许不得转载:171主机测评 » 学习C语言第十四天:函数栈帧 · 从“听不懂”到“画出图”
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址