)
C 函数调用时参数、返回地址、局部变量等信息会被放到“调用栈”中管理。但要特别注意“参数一定先入栈返回值一定后入栈”这种说法并不准确。在现代 C 中具体放在哪里取决于编译器、CPU架构和调用约定。很多参数甚至根本不会入栈而是通过寄存器传递。为了让小学生先理解“函数调用栈”这个核心思想我们可以先用一个简化版模型来学习。一、先把函数调用想象成“打电话”比如int add(int a, int b) { int c a b; return c; } int main() { int x add(10, 20); }可以把add(10, 20)想象成小明main()给add()打电话“帮我算一下 10 20”那么main()需要告诉add()几件事情我要调用哪个函数参数是多少——10、20算完以后回到哪里算出来的结果放在哪里这些信息都需要被计算机记录下来。这时候就轮到**栈Stack**登场了。二、什么是“调用栈”可以把栈想象成一摞盘子┌───────────┐ │ 盘子 3 │ ← 后放 ├───────────┤ │ 盘子 2 │ ├───────────┤ │ 盘子 1 │ ← 先放 └───────────┘特点后进先出LIFO也就是最后放进去的 ↓ 最先拿出来函数调用的时候也会形成类似的结构。这个东西通常叫调用栈Call Stack三、main() 调用 add() 发生了什么看代码int add(int a, int b) { int c a b; return c; } int main() { int x add(10, 20); }程序刚开始调用栈 ┌─────────────┐ │ main() │ └─────────────┘现在main()执行int x add(10, 20);相当于“暂停一下 main我去执行 add。”于是系统需要建立一个新的栈帧Stack Frame。四、什么是栈帧可以把栈帧理解成每调用一次函数程序就给这个函数准备一个“小房间”。例如┌──────────────────┐ │ add() 的小房间 │ │ │ │ 参数 a │ │ 参数 b │ │ 局部变量 c │ │ 返回位置 │ └──────────────────┘ ┌──────────────────┐ │ main() 的小房间 │ │ │ │ x │ └──────────────────┘这个“小房间”就是栈帧。五、参数变量会不会进入栈这是今天最容易产生误解的地方。例如int add(int a, int b) { int c a b; return c; }我们先使用传统的、简化的栈模型来理解调用 add(10, 20) 栈 ┌─────────────────┐ │ 返回地址 │ ├─────────────────┤ │ b 20 │ ├─────────────────┤ │ a 10 │ ├─────────────────┤ │ c │ └─────────────────┘于是可以粗略理解为参数 ↓ 建立栈帧 ↓ 执行函数 ↓ 产生返回值 ↓ 返回调用者但是这只是帮助初学者理解的模型。现代 CPU 上可能完全不是这样。六、现代 C参数可能根本不进栈例如int add(int a, int b) { return a b; }编译器可能把a b直接放进 CPU 的寄存器。例如在某些 x86-64 调用约定中整数参数可能通过寄存器传递。于是可能变成CPU寄存器 ┌──────────────┐ │ 寄存器1 10 │ ← a ├──────────────┤ │ 寄存器2 20 │ ← b └──────────────┘而不是栈 ┌──────────────┐ │ a 10 │ ├──────────────┤ │ b 20 │ └──────────────┘所以要告诉同学们“函数参数一定入栈”是早期教学模型不是现代 C 的绝对规则。七、那返回值呢比如int add(int a, int b) { return a b; }调用int x add(10, 20);计算10 20 ↓ 30那么30放哪里现代计算机中返回值通常通过寄存器返回。例如add() 计算 10 20 30 ↓ CPU返回值寄存器 ↓ 30然后int x add(10, 20);把这个结果保存到x。八、所以“谁先入栈”这个问题应该怎么回答如果是为了给小学生建立传统调用栈模型可以这样理解调用函数 ↓ 准备参数 ↓ 保存返回位置 ↓ 建立函数栈帧 ↓ 执行函数 ↓ 产生返回值 ↓ 恢复调用者 ↓ 继续执行但如果问“参数变量和返回值哪个一定先入栈”正确答案是都不能说“一定入栈”。因为现代 C 的实际实现通常是参数 ↓ 可能使用寄存器 ↓ 函数执行 ↓ 返回值 ↓ 通常使用寄存器返回而不是简单地参数 → 入栈 返回值 → 入栈九、那返回地址是什么这个反而是理解函数调用最关键的东西。比如int main() { int x add(10, 20); cout Hello; }执行add(10, 20);CPU必须知道“add 执行完以后我应该回到 main 的哪里”所以需要保存一个东西返回地址可以想象成main() ↓ 调用 add() ↓ ┌───────────────┐ │ 返回地址 │ │ “回 main 这里” │ └───────────────┘ ↓ 执行 add() ↓ return ↓ 回到 main()这就是函数能够“返回”的重要原因。十、我们完整模拟一次代码int add(int a, int b) { int c a b; return c; } int main() { int x add(10, 20); }第1步进入 main调用栈 ┌──────────────┐ │ main() │ └──────────────┘第2步main 调用 addadd(10, 20);系统需要记录我要调用 add 参数 a 10 b 20 将来要回来简化成┌──────────────────┐ │ add() 栈帧 │ │ │ │ 返回位置 │ │ a 10 │ │ b 20 │ │ c │ ├──────────────────┤ │ main() 栈帧 │ │ x │ └──────────────────┘第3步执行 addint c a b;于是a 10 b 20 10 20 ↓ c 30栈帧可以理解为┌──────────────────┐ │ add() │ │ │ │ a 10 │ │ b 20 │ │ c 30 │ ├──────────────────┤ │ main() │ │ x │ └──────────────────┘十一、执行 return 30return c;也就是return 30;这里发生两件重要的事情① 把返回值准备好例如返回值 30通常放入规定的返回值寄存器。② add 的栈帧被销毁┌──────────────────┐ │ add() │ ← 消失 └──────────────────┘ ┌──────────────────┐ │ main() │ │ x │ └──────────────────┘十二、然后回到 mainmain得到了30于是int x add(10, 20);最终x 30整个过程可以画成main() │ │ add(10,20) ↓ ┌───────────────┐ │ add() │ │ │ │ a 10 │ │ b 20 │ │ c 30 │ └───────────────┘ │ │ return 30 ↓ main() │ ↓ x 30十三、一个非常重要的概念参数变量 ≠ 参数这也是很多小学生容易混淆的地方。比如int add(int a, int b)这里a b叫形参形式参数而add(10, 20);里面10 20叫实参实际参数可以理解成实参 形参 10 ─────────────→ a 20 ─────────────→ b所以add(10, 20);调用的时候实际上是10 → a 20 → b十四、一个很有意思的问题参数为什么“看起来像变量”例如void test(int a) { a 100; } int main() { int x 10; test(x); cout x; }结果10为什么因为main x 10 │ │ 传值 ↓ test a 10此时实际上是x ─────→ 10 a ─────→ 10两个变量。所以a 100;只修改了a。x 10 a 100十五、如果使用引用呢例如void test(int a) { a 100; } int main() { int x 10; test(x); cout x; }这次a │ │ └────────→ x所以a 100;实际上修改的是x因此输出100这就可以把前面学过的引用、指针、栈全部串起来了。十六、最后给同学们一张“函数调用地图”建议把下面这张图记住函数调用 │ ↓ ┌─────────────────┐ │ main() │ │ │ │ add(10,20) │ └────────┬────────┘ │ │ 调用 ↓ ┌─────────────────┐ │ add() │ │ │ │ a 10 │ │ b 20 │ │ c 30 │ └────────┬────────┘ │ │ return 30 ↓ ┌─────────────────┐ │ main() │ │ │ │ x 30 │ └─────────────────┘十七、同学们记住这5句话① 每调用一个函数可以想象成“开一个小房间”这个小房间就是栈帧② 函数执行结束小房间就收起来所以局部变量int c;一般也随着这个函数结束而失效。③ 函数必须知道“回来以后去哪儿”所以需要返回地址④ 参数不一定真的放进栈现代 C 中参数可能通过寄存器传递。⑤ 返回值也不一定放进栈现代 C 中返回值通常通过寄存器返回。十八、最容易考的一句话如果以后孩子在 GESP / CSP-J 里看到“函数调用一定会把所有参数压入栈中。”不要马上选对。应该想到❌不一定。因为现代编译器会根据CPU架构 调用约定 编译器优化决定参数到底放寄存器 还是放栈最后总结可以把函数调用想成main() │ │ 调用 ↓ 准备参数 / 保存必要信息 │ ↓ 进入函数 │ ↓ 建立或使用栈帧 │ ↓ 执行函数 │ ↓ 产生返回值 │ ↓ 恢复调用者状态 │ ↓ 回到 main()而对于我们本课的核心问题“返回值和参数变量哪个先入栈”最严谨的答案是没有固定的“参数先入栈、返回值后入栈”规则。现代 C 中参数和返回值都可能通过寄存器传递传统教材中的“参数入栈、返回值……”主要是为了帮助理解函数调用栈的简化模型。大家要牢牢记住“函数调用 开一个栈帧函数结束 栈帧退场参数负责把信息送进去返回值负责把结果带回来返回地址负责告诉程序‘回来以后继续执行哪里’。”