🔷 第六章 函数
📄 一句话概括

调用一个函数="喊名字 + 递实参",然后程序跳进函数体执行,执行完再跳回调用点接着往下走。这一节把"跳进去、跳回来"这件事讲清,并交代本节三篇的分工:传值调用(6.3.1)、嵌套调用(6.3.2)、递归调用(6.3.3)。

📝 本节和前面几节的关系

6.2 讲完了"进出口"(参数与返回值),这一节开始讲"怎么用"。其实前面每个 Demo 都已经在调用函数了(cout、自己写的 printLine),只是没把它当主角。搞懂"跳进去再跳回来"这件事,第 7 章的指针、第 8 章的引用才不悬空。

一、调用语句的三要素

add(3, 5);
  1. 函数名——喊谁
  2. 实参表(圆括号里)——递什么
  3. 分号——说明这是一条语句

有返回值的函数,调用还能当表达式用(回看 3.3:表达式必有值和类型):

int s = add(3, 5) * 2;        // 调用的结果直接参与运算
cout << add(3, 5);            // 结果直接交给 cout
if (add(3, 5) > 6) { ... }    // 结果直接用于判断

这就是"函数是给代码起的名字"的真正含义——名字一旦能用,它就跟一个普通值一样到处都能用。

二、调用那一刻,程序在干什么(五步)

以 int a = square(4); 为例:

步骤 发生了什么
1 记住位置——把"我执行到哪一行了"存起来(返回地址)
2 复制实参——把 4 复制给形参 n
3 跳进去——进入 square 的函数体,从上往下执行
4 交结果——遇到 return,把值交出来
5 跳回来——回到刚才记住的那一行,继续往下

这套"记住位置、一层层记"的机制就是调用栈(call stack)。它一次性解释了两件事: - 嵌套调用为什么能一层层返回(6.3.2) - 递归为什么必须设出口(6.3.3,没出口就会"栈溢出")

一句话对照:函数是"能进去、还能出来"的代码块——这一点和 5.3.4 的 goto 完全不同(goto 跳过去就回不来,也没法把结果带回来)。

三、本节三篇的分工

小节 主题 一句话
6.3.1 传值调用 本章唯一的传参方式 实参复制给形参,函数改不动原件
6.3.2 嵌套调用 A 调 B、B 调 C 一层层进去、一层层回来
6.3.3 递归调用 函数自己调自己 必须要有出口,否则栈溢出

四、动手 Demo:看清"跳过去、回来继续"

#include <iostream>
using namespace std;

int square(int n) {
    cout << "  进入 square(" << n << ")" << endl;
    int r = n * n;
    cout << "  离开 square(" << n << "),交回 " << r << endl;
    return r;
}

int main() {
    cout << "开始" << endl;
    cout << "第一句" << endl;
    int a = square(4);
    cout << "回到 main,a = " << a << endl;
    cout << "最后一句" << endl;
    return 0;
}

看到什么算成功:

开始
第一句
  进入 square(4)
  离开 square(4),交回 16
回到 main,a = 16
最后一句

关键在顺序:square 的那两行夹在中间——这证明程序真的"跳进去、干完、又跳回来",而不是从头到尾顺序读下来。这也是"函数调用会打断顺序流"最直观的证据。

30 秒实验:在 int a = square(4); 之前和之后各加一句 cout << "标记" << endl;,观察它们把 square 的两行夹住。养成用缩进表现层次的习惯——到 6.3.2 嵌套调用时,这个习惯直接决定你能不能一眼看懂输出顺序。

五、常见误区

  1. "调用就是从文件第一行开始执行"——不是。执行从 main 开始(2.1.4),只在被调用时才跳进别的函数。
  2. "函数名不加括号也是调用"——add; 只是"提到这个名字",什么都不会发生。圆括号才是"调用"的标志。
  3. "实参和形参同名就是同一个变量"——6.2.2 已说明:两间不同的房子。
  4. "调用不花代价"——有代价:记返回地址、复制实参、建立栈帧、再拆掉。调用得极频繁的短函数会被这些开销拖慢,这正是 6.6 要讲的内联函数存在的理由。
自测三问(点开对答案) 1. 调用函数时程序做了哪五步?——记位置、复制实参、跳进去执行、交回结果、跳回调用点。 2. 有返回值的调用为什么能参与运算?——因为函数调用也是表达式(有值和类型)。 3. 为什么递归必须设出口?——每次调用都会在调用栈上"记住位置",没出口就一直堆,最后栈溢出。

相关笔记