# 4.1 自定义函数和递归

---

## 一、这一节到底学什么？

这一节不要求你一下子把函数和递归全背下来。

你只要先抓住下面 4 件事就够了：

1. 知道什么时候要把一段代码单独写成函数。
2. 会写最基础的自定义函数定义。
3. 能顺着流程图读懂“函数被调用时发生了什么”。
4. 初步看懂递归是怎样一层一层调用、再一层一层返回的。

整份讲义里，我们会尽量用能直接代数字的小例子来讲，不走太空的定义路线。

---

## 二、为什么要写函数，而不是一直往下写？

先看一种很常见的情况：

- 你要多次做同一件事。
- 这件事本身可以起一个名字。
- 把它拆出去以后，主程序会更清楚。

比如你要处理一张全班成绩表。

题目要求你完成下面这些事：

- 读入每个同学的语文、数学、英语成绩。
- 计算每个同学的总分。
- 计算每个同学的平均分。
- 判断每个同学是否及格。
- 最后输出全班总分前 3 名。

如果你完全不用函数，程序很可能会变成这样：

- 主程序里一大段是在算总分。
- 后面又来一大段是在算平均分。
- 再后面又来一大段是在判断是否及格。
- 如果输出前 3 名时还要反复比较、交换，主程序会更长。

这样写会有几个明显问题：

- 主程序会越来越像一团挤在一起的步骤，不容易看出主线。
- 同样的计算规则如果写了好几次，改错时很容易漏掉某一处。
- 过几天再回头看，你很难一下分清哪一段在“算分数”，哪一段在“判及格”，哪一段在“排前 3 名”。

这时如果把任务拆开：

- 写一个函数专门求总分。
- 写一个函数专门求平均分。
- 写一个函数专门判断是否及格。

主程序就会清楚很多。

所以你可以把“一段完成固定任务的代码”单独拿出来，给它起个名字，这就是函数。

你可以先把函数理解成：

> 一个能完成某项任务的小工具。

主程序需要它时，就“叫它来做事”。

---

## 三、最基础的函数长什么样？

最常见的写法是：

```cpp
返回值类型 函数名(参数列表)
{
    函数体
}
```

先不用被名字吓住，直接把这 3 部分拆开看：

先配一个很具体的例子：

```cpp
int total_score(int chinese, int math, int english)
{
    int sum = chinese + math + english;
    return sum;
}
```

这个函数做的事非常明确：

- 给它语文、数学、英语 3 个分数。
- 它帮你算出总分。
- 最后把总分交回来。

### 1. 返回值类型

表示这个函数做完以后，要交回什么类型的结果。

例如：

- `int`：交回一个整数。
- `double`：交回一个小数。
- `bool`：交回真或假。
- `void`：不交回结果。

在刚才那个例子里：

- `total_score` 最后交回来的是总分。
- 总分通常是整数。
- 所以这里写 `int`。

### 2. 函数名

就是这个函数的名字。

名字最好能让人一眼看出用途，比如：

- `add`
- `is_even`
- `sum_to_n`

如果是给六年级同学自己写，名字能看懂比“高大上”更重要。

像：

- `total_score`
- `print_line`
- `sum_to_n`

都比随便写成 `f1`、`abc` 更容易读。

### 3. 参数列表

表示调用这个函数时，要给它哪些数据。

例如前面的成绩函数：

```cpp
int total_score(int chinese, int math, int english)
```

意思就是：

- 这个函数叫 `total_score`。
- 它需要 3 个整数，分别叫 `chinese`、`math`、`english`。
- 它最后会返回一个整数，也就是总分。

如果把真实数字代进去，比如：

- 语文 92 分
- 数学 87 分
- 英语 95 分

那么这个函数的任务就是算：

$$
92 + 87 + 95 = 274
$$

---

## 四、函数的意义：同一件事，只写一次

先看一个场景。

你需要算 3 个同学各自的总分：

- 小明：语文 92，数学 87，英语 95
- 小红：语文 78，数学 85，英语 80
- 小刚：语文 88，数学 90，英语 76

如果不用函数，主程序会这样写：

```cpp
int sum1 = 92 + 87 + 95;   // 小明的总分
int sum2 = 78 + 85 + 80;   // 小红的总分
int sum3 = 88 + 90 + 76;   // 小刚的总分
```

如果有 30 个同学，就要写 30 行。

而且"加 3 个数"这件事，每次都要重复写一遍。

这时可以定义一个函数，把"怎么加"这件事单独写好：

```cpp
int total_score(int chinese, int math, int english)
{
    int sum = chinese + math + english;
    return sum;
}
```

然后在主函数里，每个同学用一行搞定：

```cpp
int sum1 = total_score(92, 87, 95);   // 小明的总分，得到 274
int sum2 = total_score(78, 85, 80);   // 小红的总分，得到 243
int sum3 = total_score(88, 90, 76);   // 小刚的总分，得到 254
```

"怎么算总分"只定义了一次，却被叫了 3 次。

这就是函数最核心的意义：

> 把一件事的做法写一次，想用多少次就叫多少次。

### 用流程图看清楚"调用"发生了什么

下面这张图展示的是：主函数叫了 `total_score` 两次，每次都跳出去执行函数，结束后再回来继续。

```mermaid
flowchart LR
    subgraph M["主函数 main"]
        A([开始]) --> B["准备小明的分数\n92, 87, 95"]
        B --> C["调用①"]
        C --> H["sum1 = 274"]
        H --> I["准备小红的分数\n78, 85, 80"]
        I --> J["调用②"]
        J --> K["sum2 = 243"]
        K --> L[/输出 sum1, sum2/]
        L --> Z([结束])
    end

    subgraph F["函数 total_score（只写了一次）"]
        D["收到参数"] --> E["sum = chinese + math + english"]
        E --> G["return sum"]
    end

    C -- "传入 92,87,95" --> D
    G -- "返回 274" --> H
    J -- "传入 78,85,80" --> D
    G -- "返回 243" --> K
```

注意流程图里的关键点：

- 每次调用，都会"跳进"函数那个框里执行。
- 函数执行完以后，再"跳回"主函数继续往下走。
- 函数本身的代码只写了一份，但被用了两次。

---

## 五、值传递到底是什么意思？

这是这一节最重要的基础概念。

看下面这组代码：

```cpp
int add_bonus(int score)
{
    score = score + 10;
    return score;
}

int main()
{
    int math = 87;
    int new_score = add_bonus(math);
}
```

很多同学第一眼会担心：

- 函数里把 `score` 改了，那外面的 `math` 会不会也变？

这一节里，答案是：

> 不会。

因为我们现在只讲值传递。

调用 `add(x, y)` 时，发生的是：

调用 `add_bonus(math)` 时，发生的是：

- 把 `math` 当前的值 87 交给函数里的 `score`。
- 函数内部拿到的是一份新的 87。

所以：

- `score` 和 `math` 不是同一个变量。

你可以把它理解成：

- 主程序把这个分数抄了一份给函数。
- 函数只是在自己的小房间里处理这份副本。
- 函数结束以后，主程序里的原变量不受影响。

### 用一组数据走一遍

当 `math = 87` 时：

- 进入函数时，`score = 87`。
- 执行 `score = score + 10` 后，`score = 97`。
- 返回 97。

但主函数里的：

- `math` 还是 87。

最后会变成：

- `math = 87`
- `new_score = 97`

也就是说：

- 原来的分数没被直接改掉。
- 只是函数帮你算出了“加 10 分以后”的新结果。

这一点一定要和“函数内部变量的变化”区分开。

---

## 六、函数调用时，流程图应该怎么看？

先看一个非常基础的例子：主函数调用 `total_score(chinese, math, english)`。

```mermaid
flowchart TD
    A([开始]) --> B["chinese = 92, math = 87, english = 95"]
    B --> C["调用 total_score(chinese, math, english)"]
    C --> D["把 92, 87, 95 交给函数里的 3 个参数"]
    D --> E["sum = 92 + 87 + 95"]
    E --> F["return 274"]
    F --> G["ans 得到返回值"]
    G --> H[/输出 ans/]
    H --> I([结束])
```

这张图里最重要的是中间 3 步：

1. 调用函数。
2. 把实参的值交给形参。
3. 函数算完以后，把结果返回。

### 这里有两个名字要分清

- 实参：调用时真正传进去的东西，比如 `chinese`、`math`、`english`。
- 形参：函数定义里接收数据的变量，比如 `chinese`、`math`、`english` 这 3 个参数名。

你不需要死背这两个词，但一定要明白它们不是同一个层面的名字。

---

## 七、把“主函数”和“被调用函数”分开看，会更清楚

如果你总觉得函数调用时会乱，可以把它想成两块：

- 主函数负责“发任务”。
- 被调用函数负责“做任务”。

看这张图：

```mermaid
flowchart LR
    subgraph M[主函数 main]
        A[准备 3 个分数] --> B["调用 total_score(...)"]
        C[接住返回值 ans] --> D[/输出 ans/]
    end

    subgraph F[函数 total_score]
        E[收到 3 个分数] --> F1[把 3 个分数加起来]
        F1 --> G[返回总分]
    end

    B --> E
    G --> C
```

这样看会更像真实过程：

- 主函数把任务送进去。
- `add` 做完再把结果送回来。

> 函数不是“贴在原地执行的一行代码”，而是一次完整的小过程。

---

## 八、什么时候用 `void` 函数？

不是所有函数都一定要“返回一个结果”。

比如下面这个函数：

```cpp
void print_line()
{
    cout << "----------" << endl;
}
```

它的任务只是输出一行分隔线。

所以：

- 它做事。
- 但它不需要交回一个数。

这时就可以写成 `void`。

你可以把它理解成：

> `void` 函数负责“做动作”，不负责“交答案”。

如果把它放进主函数里：

```cpp
print_line();
cout << "欢迎来到成绩统计程序" << endl;
print_line();
```

它的作用就很直观：

- 不是算分数。
- 不是算答案。
- 只是帮你把输出排得更整齐。

---

## 九、递归到底是什么？

递归只有一句核心定义：

> 函数在自己的函数体里调用自己。

第一次看到这句话，很多同学会立刻觉得：

- “那不是会一直套下去吗？”

所以学递归时，最关键的不是先背代码，而是先抓住两件事：

1. 一定要有“停止条件”。
2. 每次递归都要朝着停止条件靠近。

如果没有停止条件，函数就会没完没了地继续调用。

如果虽然有停止条件，但每次都没有更接近它，那也一样会出问题。

---

## 十、先看一个最简单的递归函数

题目：求 1 到 n 的和。

我们把它写成：

```cpp
int sum_to_n(int n)
{
    if (n == 1)
    {
        return 1;
    }
    return n + sum_to_n(n - 1);
}
```

这段代码可以读成：

- 如果 `n == 1`，那答案就是 1，直接返回。
- 如果 `n > 1`，那么
  `1 + 2 + ... + n`
  就等于
  `n + (1 + 2 + ... + (n - 1))`。

所以它把原问题拆成了一个更小的问题：

- `sum_to_n(n)`
- 变成
- `n + sum_to_n(n - 1)`

这就是递归最常见的思路：

> 大问题，拆成一个同类但更小的问题。

你可以把它想成一堆叠起来的小卡片：

- 要算前 4 张卡片上的数字和。
- 可以先拿最上面这一张 4。
- 剩下的问题就变成“前 3 张卡片的和是多少”。

这样就从“4 张”慢慢变成“3 张”“2 张”“1 张”。

---

## 十一、递归流程图怎么画？

先看结构图：

```mermaid
flowchart TD
    A(["进入 sum_to_n(n)"]) --> B{n == 1 吗？}
    B -- 是 --> C[return 1]
    B -- 否 --> D["计算 sum_to_n(n - 1)"]
    D --> E[返回 n + 子问题结果]
    C --> F([结束本层调用])
    E --> F
```

这张图里，你必须抓住两条路：

- 一条是“直接结束”的路，也就是停止条件。
- 一条是“继续缩小问题”的路，也就是递归调用。

没有前者，停不下来。

没有后者，就不是递归。

---

## 十二、拿 `sum_to_n(4)` 手推一遍，最重要

只看代码，很多同学会晕。

这时最稳的方法就是：

> 拿一个很小的数，自己顺着调用过程走一遍。

### 第一步：往下调用

```text
sum_to_n(4)
= 4 + sum_to_n(3)

sum_to_n(3)
= 3 + sum_to_n(2)

sum_to_n(2)
= 2 + sum_to_n(1)

sum_to_n(1)
= 1
```

看到这里，终于碰到了停止条件。

### 第二步：往回返回

```text
sum_to_n(1) = 1
sum_to_n(2) = 2 + 1 = 3
sum_to_n(3) = 3 + 3 = 6
sum_to_n(4) = 4 + 6 = 10
```

所以最后答案是 10。

### 再用表格看一遍

| 这一层在算谁？ | 它先等谁？ | 等到以后自己返回多少？ |
|:---:|:---:|:---:|
| `sum_to_n(4)` | 等 `sum_to_n(3)` | `4 + 6 = 10` |
| `sum_to_n(3)` | 等 `sum_to_n(2)` | `3 + 3 = 6` |
| `sum_to_n(2)` | 等 `sum_to_n(1)` | `2 + 1 = 3` |
| `sum_to_n(1)` | 不用再等 | `1` |

这张表很重要，因为它把“先等谁、再返回多少”写得很清楚。

---

## 十三、递归最容易绕晕的地方，不在“往下”，而在“往回”

很多同学能接受：

- `sum_to_n(4)` 去叫 `sum_to_n(3)`。
- `sum_to_n(3)` 再去叫 `sum_to_n(2)`。

但一到返回时就乱了。

原因通常是没有意识到：

> 每一层函数调用，都在等自己的“下一层”先把结果交回来。

也就是说：

- `sum_to_n(4)` 不能立刻算完。
- 它必须先等 `sum_to_n(3)` 返回。
- `sum_to_n(3)` 又得先等 `sum_to_n(2)`。
- 一直等到最小那层先返回。

所以递归过程常常像两段：

1. 一层一层往下走。
2. 一层一层往上交答案。

---

## 十四、把递归看成“排队等结果”，会更好懂

再看一张图：

```mermaid
flowchart TD
    A["sum_to_n(4)"] --> B["等待 sum_to_n(3)"]
    B --> C["等待 sum_to_n(2)"]
    C --> D["等待 sum_to_n(1)"]
    D --> E["sum_to_n(1) 返回 1"]
    E --> F["sum_to_n(2) 返回 3"]
    F --> G["sum_to_n(3) 返回 6"]
    G --> H["sum_to_n(4) 返回 10"]
```

这张图要表达的重点不是代码细节，而是“顺序”：

- 先不断往下等。
- 再从最底层开始往回交结果。

你甚至可以把它读成下面这几句话：

- 4 这一层先别急，先去问 3。
- 3 这一层也先别急，先去问 2。
- 2 这一层再去问 1。
- 1 最简单，直接回答：我是 1。
- 然后答案一层一层往回传。

如果你能把这张图说顺，说明你已经抓住递归最核心的运行过程了。

---

## 十五、值传递放到递归里，还是一样的

递归虽然看起来更复杂，但参数传递规则没有变。

例如：

- 调用 `sum_to_n(4)` 时，这一层的 `n` 是 4。
- 它再调用 `sum_to_n(3)` 时，下一层会有一个新的 `n`，值是 3。
- 再下一层调用 `sum_to_n(2)` 时，又会有一个新的 `n`，值是 2。

所以：

- 每一层都有自己的 `n`。
- 它们不是同一个变量。
- 只是名字都叫 `n` 而已。

这也是值传递的一部分：

> 每次调用，都会得到这一层自己的那份参数值。

---

## 十六、写递归时，先问自己这两个问题

在真正动手写递归前，最好先问：

### 1. 什么时候停？

也就是停止条件是什么。

例如：

- `n == 1` 时停。
- `n == 0` 时停。

### 2. 怎么把问题变小？

也就是下一次调用时，参数怎么变化。

例如：

- `n - 1`
- `n / 2`

如果这两个问题答不出来，递归代码通常也写不稳。

---

## 十七、最常见的错误

下面这些错非常常见：

1. 写了递归调用，却没有停止条件。
2. 虽然写了停止条件，但永远到不了它。
3. 只会往下套，不会分析怎么往回返回。
4. 把“这一层的参数”和“下一层的参数”混成同一个东西。
5. 以为函数里的参数改了，主函数里的变量也会跟着改。

第 5 条在这一节里尤其要避免，因为本节只讲值传递。

---

## 十八、这一节最该记住的几句话

1. 函数就是把一段固定任务单独拿出来，起个名字反复用。
2. 最基础的函数定义要看清：返回值类型、函数名、参数列表。
3. 调用函数时，主程序是“发任务”，函数是“做任务”。
4. 本节只讲值传递，所以函数拿到的是一份新的值，不会直接改掉外面的原变量。
5. 递归就是函数调用自己。
6. 递归必须同时具备两件事：停止条件、缩小问题。
7. 读递归最稳的方法，是拿小数据手推，先看往下调用，再看往回返回。

---

## 十九、课堂练习

### 练习 1

写一个函数 `int square(int x)`，返回 `x * x`。

要求：

- 先说清楚参数是什么。
- 再说清楚返回值是什么。

### 练习 2

主函数中有 `a = 6`，调用 `triple(a)`，其中函数内部把形参乘以 3。

问题：

- 函数返回什么？
- 主函数里的 `a` 会不会变？

### 练习 3

把 `sum_to_n(5)` 的递归调用过程手写出来，写清楚：

- 往下调用到了哪一层。
- 往回返回时每层结果是多少。

如果这 3 题都能自己说顺，这一节就算真正入门了。