Skip to content

第 23 章 封装你的代码——函数入门


🏗️ 前情回顾:代码越来越长,main() 里一团乱麻——求最大值写了一遍又一遍,每次都要重新敲,眼睛都看花了。能不能像遥控器一样,按一个按钮就执行一段代码?这一章,我们要学会把代码"打包"成函数——写一次,用无数次。


🎯 本章目标

学完这一章,你能:

  • 理解函数是什么——它为什么能让程序模块化
  • 正确定义函数(返回类型、函数名、参数列表、函数体)
  • 调用函数并使用 return 返回值
  • 区分有返回值的函数和 void 函数
  • 理解值传递和引用传递(&)的区别
  • 区分局部变量和全局变量,理解作用域

📖 故事引入

你家里有电视遥控器吧?上面有各种按钮:

  • 按"电源"按钮 → 电视开机
  • 按"音量+"按钮 → 声音变大
  • 按"1"号键 → 切换到 1 频道

你不需要知道按下按钮后电路板里发生了什么——你只需要知道:按哪个按钮,得到什么效果。

编程里的函数就是这样的"遥控器按钮"。你把一段代码封装起来,给它取个名字。以后要用这段功能,直接"按按钮"(调用函数)就行,不用再写一遍。

想想你之前写过的代码——求最大值、判断素数、算阶乘……同样的逻辑,在程序里重复出现了三四次,每次都要重新敲一遍。手酸不说,万一有一处写错了,还得满世界找出来改。如果你把"求最大值"写成一个函数 max(a, b),只需要写一次,以后每次用一行 max(3, 5) 就搞定——像按遥控器按钮一样轻松。

  • 输入:两个数 a 和 b
  • 输出:较大的那个
  • 再也不用重新敲比较逻辑——调用 max(3, 5) 就得到 5

🧱 知识讲解

23.1 函数是什么

函数就是一个有名字的代码块,接收输入(参数),处理,然后返回输出(返回值)。

一个函数由四部分组成:

cpp
返回类型  函数名  (参数列表)  {
    // 函数体——要执行的代码
    return 返回值;    // 如果返回类型不是 void
}

先看个反面教材——以下写法全是错的:

cpp
max(int a, int b) { return a > b ? a : b; }     // ❌ 没有返回类型
int max(int a, int b) { if (a > b) return a; }   // ❌ b>=a时没有return
void max(int a, int b) { return a > b ? a : b; } // ❌ void函数不能return值
int max() { return 10; }                          // ❌ 需要参数却没写

✅ 实际例子:求两个数的最大值

cpp
int max(int a, int b) {        // 返回 int,函数名 max,参数 a 和 b
    if (a > b) {
        return a;
    } else {
        return b;
    }
}

调用这个函数

cpp
int main() {
    int result = max(10, 20);  // 调用 max,传入 10 和 20
    cout << result << endl;    // 20
    cout << max(7, 3) << endl; // 也可以直接在表达式里用 → 7
    return 0;
}

23.2 无返回值的函数:void

有些函数不需要返回任何东西——比如一个专门负责"打印分隔线"的函数。

cpp
void printLine(int n) {        // void 表示"不返回值"
    for (int i = 0; i < n; i++) {
        cout << "-";
    }
    cout << endl;
    // 没有 return
}

int main() {
    printLine(20);             // 输出 20 个短横线
    cout << "标题" << endl;
    printLine(20);
    return 0;
}

💡 void 函数里也可以写 return;(不带值),表示"提前结束这个函数"。

23.3 函数声明与定义分离

目前为止你把函数写在 main() 的上面。但实际上 C++ 的惯例是:先声明,后定义。声明告诉编译器"有这么一个函数",定义才写具体实现。

cpp
#include <iostream>
using namespace std;

// 函数声明(函数原型)——只写头部,加个分号
int max(int a, int b);
void printLine(int n);

int main() {
    cout << max(3, 5) << endl;   // 可以调用了
    printLine(10);
    return 0;
}

// 函数定义——具体实现
int max(int a, int b) {
    return (a > b) ? a : b;
}

void printLine(int n) {
    for (int i = 0; i < n; i++) cout << "-";
    cout << endl;
}

为什么要这样做?在大型项目中,声明通常放在头文件(.h)里,定义放在源文件(.cpp)里。函数声明就像"目录",编译器先扫一遍目录,知道有哪些函数,然后再看具体内容。

23.4 值传递 vs 引用传递

这是函数参数中最重要也最容易混淆的概念。

值传递:函数拿到的是参数的副本。在函数里修改参数,不影响原始变量。

新手常见困惑——为什么改了没生效?

cpp
void addTen(int x) { x = x + 10; }  // 改了x
int main() {
    int a = 20;
    addTen(a);
    cout << a;  // 输出20!——为什么没变成30?
}

因为 addTen 拿到的是 a复印件,复印件上改得面目全非,原件纹丝不动。

✅ 看完整例子——值传递,副本与原件互不影响:

cpp
void addTen(int x) {
    x = x + 10;                  // 修改的是副本
    cout << "函数内:" << x << endl; // 30
}

int main() {
    int a = 20;
    addTen(a);
    cout << "函数外:" << a << endl; // 20——没变!
    return 0;
}

引用传递(&:函数拿到的是参数的引用(别名)。在函数里修改参数,直接修改原始变量。

加上 & 就对了:

cpp
void addTen(int &x) { x = x + 10; }  // & 表示引用——直接操作原变量
int main() {
    int a = 20;
    addTen(a);
    cout << a;  // 输出30!——&让函数拿到原件而不是复印件
}
cpp
void addTen(int &x) {            // 注意 & ——引用
    x = x + 10;                  // 修改的是原始变量本身
    cout << "函数内:" << x << endl; // 30
}

int main() {
    int a = 20;
    addTen(a);
    cout << "函数外:" << a << endl; // 30——变了!
    return 0;
}

类比

  • 值传递 = 你朋友把作业复印了一份给你参考,你在复印件上乱画,原件不变
  • 引用传递 = 你朋友把作业原件给你,你在上面乱画,原件就毁了

什么时候用引用?

  1. 你需要函数修改传入的变量(如上例)
  2. 传递大对象(如长字符串、大数组)时避免复制,节省时间和内存——这时通常加 const 保护:void print(const string &s)

23.5 局部变量与全局变量

局部变量:定义在函数内部(包括 main() 内部)。只在那个函数里有效。

跨函数找变量——找不到!

cpp
void func() { int x = 10; }
int main() {
    func();
    cout << x;   // ❌ 编译错误!x只在func()里活着
}

每个函数的变量就像每个教室的班长——只在自己教室有权威,出教室门就没人听他的了。

✅ 正确用法:

cpp
void func() {
    int x = 10;                  // x 是 func 的局部变量
    cout << x << endl;
}

int main() {
    func();
    // cout << x;                // ❌ 错误!x 在这里不存在
    return 0;
}

全局变量:定义在所有函数外面。任何函数都能访问它。

cpp
int score = 100;                 // 全局变量

void show() {
    cout << score << endl;       // 能访问
}

int main() {
    cout << score << endl;       // 也能访问
    score = 95;                  // 能修改
    show();                      // 输出 95
    return 0;
}

⚠️ 全局变量虽然方便,但用多了会让程序变得难以调试——任何函数都能改它,出了问题你不知道是谁改的。竞赛中通常只在特定场景(如记录状态的大数组)才用全局变量。初学阶段,尽量把变量放在需要的函数内部。

23.6 作用域

作用域 = 一个变量"活着"的范围。

cpp
int g = 1;                       // 全局作用域

void func() {
    int x = 2;                   // func 的作用域
    cout << g << " " << x << endl;
}                                // x 在这里"死亡"

int main() {
    int y = 3;                   // main 的作用域
    func();                      // 调用 func
    cout << g << endl;           // ✅ g 是全局的
    // cout << x;                // ❌ x 只在 func 里活着
    cout << y << endl;           // ✅ y 在 main 里活着
    return 0;
}                                // y 在这里"死亡"

可以简单记住:大括号 {} 就是作用域的边界。变量在定义它的那对大括号内有效,出了大括号就消失。


✋ 动手试试

试试 1:写一个函数 square(n),返回 n 的平方。在 main() 里调用它,输出 1 到 10 的平方。

试试 2:写一个 swap(int &a, int &b) 函数,用引用传递交换两个变量的值。对比如果用值传递会发生什么。

试试 3:定义一个全局变量 counter,写两个函数 increment()show(),分别对它 +1 和输出。在 main() 里多次调用,观察结果。


🦶 你踩过这些坑吗

逐一核对,看你是不是也中过招:

  • [ ] 忘记 returnint max(int a, int b) { if (a > b) return a; } ← 当 b >= a 时没有 return,编译警告或返回垃圾值 → ✅ 确保所有路径都有 return
  • [ ] 值传递期望能修改实参:写了 void swap(int a, int b) 以为能交换——结果不行,因为是副本 → ✅ 要用引用:void swap(int &a, int &b)
  • [ ] 调用函数时写了参数类型max(int 3, int 5); ← 编译错误!→ ✅ 调用时直接写 max(3, 5);
  • [ ] 函数定义在调用之后却没有声明main() 里调用了 myFunc(),但定义在下面 → ✅ 在上面加一行声明 void myFunc();

📝 练习

基础题

1. 选择题

(1)以下哪个是合法的函数声明?
A. int func();   B. func(int a);   C. void int func();   D. function func();

(2)值传递时,函数内部修改参数会:
A. 影响原变量   B. 不影响原变量   C. 报编译错误   D. 取决于参数类型

(3)void 类型的函数意味着:
A. 没有参数   B. 没有返回值   C. 不能调用   D. 不能有参数

2. 填空题

(1)写出一个接受两个 int、返回它们和的函数声明:____________________

(2)要让函数能修改传入的参数,需要用 _____ 传递(填符号)。

(3)定义在函数内部的变量叫 _____ 变量,定义在所有函数外面的叫 _____ 变量。

3. 判断题

判断以下说法是否正确,并简述理由。

(1)函数声明(原型)和函数定义必须写在同一个位置。( )

(2)void 函数里不能写任何 return 语句。( )

4. 改错题

下面这段代码有 3 处错误,请找出并改正:

cpp
int main() {
    cout << add(3, 5) << endl;
    return 0;
}
add(int x, int y) {
    return x + y
}

提高题

5. 编程题 — 判断素数

写一个 bool isPrime(int n) 函数,判断 n 是否为素数(质数)。在 main() 里输入一个范围 [L, R],输出这个范围内所有的素数。

6. 编程题 — 数学工具包

写三个函数:

  • int gcd(int a, int b) —— 求最大公约数(用辗转相除法)
  • int lcm(int a, int b) —— 求最小公倍数
  • bool isLeap(int year) —— 判断闰年

main() 里测试它们。

(提示:lcm(a, b) = a * b / gcd(a, b)

7. 编程题 — 阶乘函数

写一个 long long factorial(int n) 函数,返回 n 的阶乘。在 main() 里输入 n(1 ≤ n ≤ 20),输出 n!。注意用 long long 防止溢出。

挑战题

8. 编程题 — 数字黑洞

"数字黑洞"是一个有趣的数学现象:任取一个各位不全相同的四位数,用它的各位数字重排出的最大数减去最小数,得到一个新数。重复这个过程,最终会落入 6174 这个"黑洞"。

写一个函数 int kaprekar(int n),对 n 做一次操作(最大排列 - 最小排列)。在 main() 里输入一个四位数,重复调用直到结果为 6174,输出整个过程。

(提示:先把四位数各位数字存进数组,排序后分别拼成最大数和最小数。)

9. 编程题 — 哥德巴赫猜想验证

哥德巴赫猜想:任意大于 2 的偶数都可以写成两个素数之和。写一个程序,输入一个偶数 n(4 ≤ n ≤ 10000),输出它是哪两个素数之和(如果有多解,输出差值最大的一组)。

(提示:先写 bool isPrime(int n) 函数,然后遍历找第一对符合条件的素数。)


🧠 本章小结

函数 = 有名字的代码块

定义:
  返回类型 函数名 (参数列表) { 函数体; return 值; }

调用:函数名(实参列表);

void → 无返回值
声明 → 告诉编译器有这个东西(写在调用之前)
定义 → 具体实现(可以写在调用之后)

参数传递:
  值传递(默认)→ 复制副本,改副本不影响原变量
  引用传递(&) → 直接操作原变量

变量:
  局部变量 → {} 内有效
  全局变量 → 所有函数都能用(慎用!)
  作用域   → {} 决定

📝 配套练习

共8题。函数四要素(int/bool/void/double返回)各两题覆盖。。★核心(课堂必做) ◆拓展(课后练习)

级别题号链接覆盖知识点
★ 核心J0104https://hydro.ac/d/srqc/p/J0104函数四要素、return、递归
◆ 拓展J0100https://hydro.ac/d/srqc/p/J0100多参数、double返回、sqrt
◆ 拓展J0103https://hydro.ac/d/srqc/p/J0103辅助函数、数组传参、最值
◆ 拓展J0101https://hydro.ac/d/srqc/p/J0101bool返回、质数判断
◆ 拓展J0102https://hydro.ac/d/srqc/p/J0102bool返回、闰年、区间筛选
◆ 拓展luogu-P5735https://hydro.ac/p/luogu-P5735多参数、浮点返回
◆ 拓展luogu-P5736https://hydro.ac/p/luogu-P5736bool函数、数组+函数配合
◆ 拓展luogu-P5737https://hydro.ac/p/luogu-P5737bool函数、闰年、区间遍历

💡 练习建议:先完成 1 道★核心题,确保掌握本章基本方法;再完成 7 道◆拓展题,覆盖不同变式和细节。


配套练习

共8题。函数四要素(int/bool/void/double返回)各两题覆盖。★核心(课堂必做) · ◆拓展(课后练习)

级别题号链接覆盖知识点
★ 核心J0104https://hydro.ac/d/srqc/p/J0104函数四要素、return、递归
◆ 拓展J0100https://hydro.ac/d/srqc/p/J0100多参数、double返回、sqrt
◆ 拓展J0103https://hydro.ac/d/srqc/p/J0103辅助函数、数组传参、最值
◆ 拓展J0101https://hydro.ac/d/srqc/p/J0101bool返回、质数判断
◆ 拓展J0102https://hydro.ac/d/srqc/p/J0102bool返回、闰年、区间筛选
◆ 拓展luogu-P5735https://hydro.ac/p/luogu-P5735多参数、浮点返回
◆ 拓展luogu-P5736https://hydro.ac/p/luogu-P5736bool函数、数组+函数配合
◆ 拓展luogu-P5737https://hydro.ac/p/luogu-P5737bool函数、闰年、区间遍历

练习建议:先在课堂完成 1 道★核心题,掌握本章基本方法;课后完成 7 道◆拓展题,覆盖不同变式和细节。

自查清单

  • [ ] 我能定义一个函数,包含返回类型、函数名、参数列表、函数体
  • [ ] 我会调用函数并使用返回值
  • [ ] 我会写 void 函数
  • [ ] 我理解值传递和引用传递的区别
  • [ ] 我知道什么时候用函数声明(原型)
  • [ ] 我能区分局部变量和全局变量,理解作用域

🚀 下章预告:函数能接收参数了——但传进去的到底是复印件还是原件?为什么有时候改了参数,回到 main() 却纹丝不动?第 24 章,我们来揭开参数传递的底层秘密——值传递与引用传递。