八、函数

你可能已经注意到了:前面几章里,isPrime、gcd 这些名字反复出现,但我们一直没说它们是怎么定义的。

这一页补上这部分。2 章 9 道题,题量不大,但它决定了你后面的代码是「能看」还是「一坨」。

函数解决的核心问题只有一个:同一段逻辑,不要写两遍。

  • 判素数在主循环里要调用 n 次——每次都把那五行抄一遍?
  • 求最大公约数在好几个地方都要用——复制粘贴,改一处忘一处?

函数让你「写一次,到处用」,而且用的时候只需要知道「它干什么」,不用关心「它怎么干的」。

27. 定义、调用与封装

27.1 函数的形状

返回类型 函数名(参数类型 参数名) {
    // 函数体
    return 返回值;
}

一个具体例子:

int add(int x, int y) {
    return x + y;
}

拆开看四部分:

部分 例子 说明
返回类型 int 这个函数最后交回什么类型的东西
函数名 add 你起的名字,调用时用
参数表 (int x, int y) 调用者要传进来的东西
函数体 { return x + y; } 具体做什么

调用就是「函数名 + 括号 + 实参」:

int s = add(3, 5);      // s = 8
printf("%d\n", add(10, 20));   // 30

27.2 参数是「复制」进去的

这一点必须搞明白,否则后面会莫名其妙地调不出结果。

void change(int x) {
    x = 100;            // 改的是副本
}

int main() {
    int a = 5;
    change(a);
    printf("%d\n", a);  // 还是 5,不是 100
}

传参是「复制一份进去」,函数里改的是副本,原变量不受影响。

想通过函数改变外面的变量,有两种办法:

  1. 用返回值(推荐)
  2. 用引用 int &x(后面再说)
int makeHundred() {
    return 100;
}

int main() {
    int a = 5;
    a = makeHundred();    // 用返回值改
    printf("%d\n", a);    // 100
}

第一反应应该是「用返回值」,而不是「怎么改外面的变量」。 这个思路会让你的函数更容易看懂。

第一个函数

  • max 函数 —— 题目通常要求你自己实现一个求最大值的函数

标准库里已经有 max(a, b) 了,但题目让你手写,是为了让你熟悉语法。按题目要求的函数名和参数写,别自作聪明改名字——有些 OJ 会检查你的函数签名。

27.3 把判定封装成函数

这是函数最实用的场景:一段判断逻辑,后面要用很多次。

bool isPrime(int n) {
    if (n < 2) return false;
    for (int i = 2; i <= n / i; i++) {
        if (n % i == 0) return false;
    }
    return true;
}

写完之后,主程序变得非常干净:

int cnt = 0;
for (int i = 1; i <= n; i++) {
    if (isPrime(i)) cnt++;
}
printf("%d\n", cnt);

对比一下如果没有函数,主循环里要塞进整个判素逻辑——一眼看过去不知道这段在干什么。

bool 是返回类型

bool 只有 true 和 false 两个值。判素、判回文、判闰年这类「是/否」的问题,返回类型就该是 bool。

用 int 返回 0/1 也行,但 bool 更能说明意图:读代码的人一看就知道这是个判断题。

封装判定

完全数是指「所有真因数之和等于自己」的数,比如 6 = 1+2+3。写这个函数的时候,枚举因数那部分可以复用第 17 章的技巧。

这两个函数写完,主程序应该只有五六行。 如果主程序还是很长,说明没封装干净。

27.4 多个判定组合

有些题需要判断一个数同时满足好几个条件,比如「绝对素数」:本身是素数,反过来也是素数。

bool isAbsPrime(int n) {
    if (!isPrime(n)) return false;      // 本身不是素数,直接否
    int rev = 0, t = n;
    while (t > 0) {
        rev = rev * 10 + t % 10;
        t /= 10;
    }
    return isPrime(rev);                // 反转后还是不是素数
}

注意 if (!isPrime(n)) return false; 这个写法——先排除最简单的不满足情况,避免做后面的无用功。这叫「提前返回」,比嵌套 if 清楚得多。

组合判定

这两题的关键是把每个判定拆成独立的小函数,然后组合起来。写完检查一下:你的主程序里还有没有超过 3 层的 if 嵌套?有的话就还能再拆。

第 27 章验收

  • 能写出 bool isXxx(int n) 形式的判定函数
  • 知道参数是复制进去的,改形参不影响实参
  • 会用 return false 提前返回,而不是层层嵌套
  • 主程序因为用了函数而变短了

28. 返回值、分解与递归

28.1 返回值的类型要对

函数返回什么类型,return 就应该是什么类型。类型不对会发生隐式转换,可能悄悄丢精度。

int divide(int a, int b) {
    return a / b;          // 整数除法,小数丢了
}

double divide(double a, double b) {
    return a / b;          // 这个才有小数
}

函数名可以重名,只要参数类型不同(这叫重载)。编译器会根据你传的参数自动选。

写函数时先想清楚:这个结果是整数还是小数? 想错了,函数内部算得再对,返回时也会被截断。

返回值的类型

  • 进制转换 —— 通常是把十进制转成别的进制,注意输出顺序

进制转换的常见写法是「不断取余,余数存起来,最后倒着输出」。如果写成函数,可以返回一个字符串,或者在函数里直接输出(返回类型 void)。

28.2 把大问题拆成小函数

写复杂程序时,最有效的方法是先想「这件事分几步」,每步写一个函数,最后在 main 里把它们串起来。

以「找 2 到 n 之间的亲和数」为例:

  • 第一步:求一个数的真因数和 → sumDivisors(x)
  • 第二步:判断一对数是不是亲和数 → isAmicable(a, b)
  • 第三步:枚举所有数对 → 主循环
int sumDivisors(int x) {
    int s = 0;
    for (int i = 1; i < x; i++) {
        if (x % i == 0) s += i;
    }
    return s;
}

每个函数都只有三五行的逻辑,单独看每一段都能看懂。这就是好代码的样子。

分解

326 的思路:枚举第一个素数 i,判断 n - i 是不是素数,两个都用 isPrime。主循环只有几行,因为判素已经封装好了。

28.3 递归:函数调用自己

有些问题,答案里包含着同样形式的小问题,这时候可以用递归。

最简单的例子是阶乘:

long long fact(int n) {
    if (n <= 1) return 1;        // 终止条件,必须有
    return n * fact(n - 1);      // 调用自己
}

fact(5) 会展开成 5 * fact(4) → 5 * 4 * fact(3) → …… → 5*4*3*2*1。

递归必须有两个东西

  1. 终止条件:什么时候不再调用自己(if (n <= 1) return 1;)
  2. 规模缩小:每次调用,问题要变小(n - 1)

少了任何一个,函数就会无限调用自己,直到栈溢出——程序直接崩溃(OJ 报 RE 或 Segmentation Fault)。

写递归时,第一件事就是写终止条件。

递归不是必须的。能用循环写就用循环——递归更慢、更占内存、更难调试。只有在「问题天然是嵌套的」时候才用,比如树的遍历、分治。

现在你只需要能看懂递归,不要求熟练写。

递归与迭代

  • 求 π 的值 —— 通常是级数求和,用 while 循环到精度满足

这题用循环就行,不需要递归。注意终止条件:是「某一项小于 eps」还是「和前一项的差小于 eps」,读题。

函数板块总结

9 道题不多,但要达到这个状态:

  • 看到一个「判断某性质」的需求,能立刻写出 bool isXxx(int n)
  • 主程序超过 20 行时,会本能地想「这段能不能拆成函数」
  • 知道递归必须有终止条件,且能看懂简单的递归代码

判断你函数写得好不好,有一个简单标准:把 main 里的注释全删掉,别人还能不能看懂? 能,说明函数名起对了、拆对了。

后面结构体和字符串两页,代码会越来越长。这一页练出来的拆解能力,那时候就是决定性的。

延伸阅读

服务器上有一整套 OI-Wiki(621 页),是给竞赛选手写的参考手册。下面几页跟这一页相关,但不是每一页都该现在看——标了「现在跳过」的,等以后再说。

  • 可以先看:函数 —— 声明、定义、返回值。注意它用的例子还是竞赛风格,语法本身一样
  • 现在跳过:引用 —— & 传参。这个阶段全部用值传递,等你遇到「函数里改了外面没变」的困惑再看

看不懂是正常的,不是你的问题

OI-Wiki 默认读者已经会写程序、目标是打 NOI。你现在需要的那部分,讲义里已经写了; wiki 是用来「以后查得更深」的,不是用来补课的。

© HiJudge · 内容采用 CC BY-NC-SA 4.0 许可协议            此页面修订于: 2026-11-01 03:44:10

results matching ""

    No results matching ""