初赛专题(十二):函数与递归 —— 传值传引用、调用树与经典递归模板

本讲定位:CSP-J 初赛系列的地基讲。七年真题统计(见 cspj-exam-analysis-2019-2025.md)里,递归类程序段出了 4 段(分形生成、汉诺塔、递归展开求值、递归 vs 递推),分布均匀、折算全卷权重约 $10 \pm 4$ 分;选择题”递归求值”2025 年出到 4 分;“递归不终止”是阅读程序判断题五大陷阱之一(2021、2024 两考)。更重要的是:第 09 讲阅读程序、第 10 讲完善程序都默认你”会读递归”——读不懂递归,DP 段、汉诺塔段、gcd 段全部无从下手。本讲把这块地基一次打牢。

初赛考递归的三种姿势,本讲各配专门小节:选择题手算(给递归定义求 $f(某值)$)、阅读程序(画调用树、精确数调用次数、判断会不会不终止)、完善程序(挖递归出口和递归调用的参数)。

前置知识:数组、for 循环、第 01 讲的栈(函数调用本质上就是栈)。

适合对象:CSP-J 初赛。标注 ⭐ 的小节为进阶内容,第一遍学习可以跳过。


1. 函数:把程序拆成零件

1.1 定义与调用

#include<iostream>
using namespace std;

int maxOfThree(int a, int b, int c) {   // 返回值类型 函数名(参数表)
    int m = a;
    if (b > m) m = b;
    if (c > m) m = c;
    return m;                           // return:带着结果回到调用处
}

int main() {
    cout << maxOfThree(3, 7, 5) << endl;   // 输出 7
    return 0;
}

三个组成部分逐一点名:

一条初赛常考的细节:声明了返回 int 的函数必须在所有路径上 returnmain 函数例外,不写 return 0; 也合法。

1.2 传值:函数里改的是”复印件”

实参复制给形参之后,形参就是一份独立的复印件——函数内部怎么改,都动不到调用处的原件。经典的翻车现场:

void swapWrong(int a, int b) {   // a、b 是 x、y 的复印件
    int t = a; a = b; b = t;     // 交换的是复印件
}

int main() {
    int x = 1, y = 2;
    swapWrong(x, y);
    cout << x << " " << y << endl;   // 仍然输出 1 2!
    return 0;
}

这道”为什么 swap 不起作用”是选择题与判断题的常客。记住结论:传值调用下,函数对形参的任何修改在函数结束时全部作废

1.3 传引用 &:函数里改的是”本尊”

在形参类型后面加 &,形参就不再是复印件,而是实参的别名——改它就是改原变量:

void swapRight(int &a, int &b) {   // a 就是 x、b 就是 y 的别名
    int t = a; a = b; b = t;
}

int main() {
    int x = 1, y = 2;
    swapRight(x, y);
    cout << x << " " << y << endl;   // 1 2 变成了 2 1
    return 0;
}

阅读程序头号陷阱就藏在这里:函数签名里那个不起眼的 &,决定了”调用之后外面的变量变没变”。读程序先扫一眼参数表——int n 是复印件,int &n 是本尊,vector<int> v 是把整个容器复制一份(还很慢),vector<int> &v 才是原容器。2022 年起选择题的 C++ 语法题(每年恒定 4 分)就爱在引用和指针上做文章。

数组是例外:数组名作参数时传的是首地址,函数内改 a[i] 就是改原数组——不加 & 也是”本尊”。这一条和上面 & 的规则合起来,覆盖了初赛程序里所有”函数改外面”的情况。