第 23 章 封装你的代码——函数入门
🏗️ 前情回顾:代码越来越长,
main()里一团乱麻——求最大值写了一遍又一遍,每次都要重新敲,眼睛都看花了。能不能像遥控器一样,按一个按钮就执行一段代码?这一章,我们要学会把代码"打包"成函数——写一次,用无数次。
🎯 本章目标
学完这一章,你能:
- 理解函数是什么——它为什么能让程序模块化
- 正确定义函数(返回类型、函数名、参数列表、函数体)
- 调用函数并使用
return返回值 - 区分有返回值的函数和
void函数 - 理解值传递和引用传递(
&)的区别 - 区分局部变量和全局变量,理解作用域
📖 故事引入
你家里有电视遥控器吧?上面有各种按钮:
- 按"电源"按钮 → 电视开机
- 按"音量+"按钮 → 声音变大
- 按"1"号键 → 切换到 1 频道
你不需要知道按下按钮后电路板里发生了什么——你只需要知道:按哪个按钮,得到什么效果。
编程里的函数就是这样的"遥控器按钮"。你把一段代码封装起来,给它取个名字。以后要用这段功能,直接"按按钮"(调用函数)就行,不用再写一遍。
想想你之前写过的代码——求最大值、判断素数、算阶乘……同样的逻辑,在程序里重复出现了三四次,每次都要重新敲一遍。手酸不说,万一有一处写错了,还得满世界找出来改。如果你把"求最大值"写成一个函数 max(a, b),只需要写一次,以后每次用一行 max(3, 5) 就搞定——像按遥控器按钮一样轻松。
- 输入:两个数 a 和 b
- 输出:较大的那个
- 再也不用重新敲比较逻辑——调用
max(3, 5)就得到 5
🧱 知识讲解
23.1 函数是什么
函数就是一个有名字的代码块,接收输入(参数),处理,然后返回输出(返回值)。
一个函数由四部分组成:
返回类型 函数名 (参数列表) {
// 函数体——要执行的代码
return 返回值; // 如果返回类型不是 void
}❌ 先看个反面教材——以下写法全是错的:
cppmax(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; } // ❌ 需要参数却没写
✅ 实际例子:求两个数的最大值:
int max(int a, int b) { // 返回 int,函数名 max,参数 a 和 b
if (a > b) {
return a;
} else {
return b;
}
}调用这个函数:
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
有些函数不需要返回任何东西——比如一个专门负责"打印分隔线"的函数。
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++ 的惯例是:先声明,后定义。声明告诉编译器"有这么一个函数",定义才写具体实现。
#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 引用传递
这是函数参数中最重要也最容易混淆的概念。
值传递:函数拿到的是参数的副本。在函数里修改参数,不影响原始变量。
❌ 新手常见困惑——为什么改了没生效?
cppvoid addTen(int x) { x = x + 10; } // 改了x int main() { int a = 20; addTen(a); cout << a; // 输出20!——为什么没变成30? }因为
addTen拿到的是a的复印件,复印件上改得面目全非,原件纹丝不动。
✅ 看完整例子——值传递,副本与原件互不影响:
void addTen(int x) {
x = x + 10; // 修改的是副本
cout << "函数内:" << x << endl; // 30
}
int main() {
int a = 20;
addTen(a);
cout << "函数外:" << a << endl; // 20——没变!
return 0;
}引用传递(&):函数拿到的是参数的引用(别名)。在函数里修改参数,直接修改原始变量。
✅ 加上
&就对了:cppvoid addTen(int &x) { x = x + 10; } // & 表示引用——直接操作原变量 int main() { int a = 20; addTen(a); cout << a; // 输出30!——&让函数拿到原件而不是复印件 }
void addTen(int &x) { // 注意 & ——引用
x = x + 10; // 修改的是原始变量本身
cout << "函数内:" << x << endl; // 30
}
int main() {
int a = 20;
addTen(a);
cout << "函数外:" << a << endl; // 30——变了!
return 0;
}类比:
- 值传递 = 你朋友把作业复印了一份给你参考,你在复印件上乱画,原件不变
- 引用传递 = 你朋友把作业原件给你,你在上面乱画,原件就毁了
什么时候用引用?
- 你需要函数修改传入的变量(如上例)
- 传递大对象(如长字符串、大数组)时避免复制,节省时间和内存——这时通常加
const保护:void print(const string &s)
23.5 局部变量与全局变量
局部变量:定义在函数内部(包括 main() 内部)。只在那个函数里有效。
❌ 跨函数找变量——找不到!
cppvoid func() { int x = 10; } int main() { func(); cout << x; // ❌ 编译错误!x只在func()里活着 }每个函数的变量就像每个教室的班长——只在自己教室有权威,出教室门就没人听他的了。
✅ 正确用法:
void func() {
int x = 10; // x 是 func 的局部变量
cout << x << endl;
}
int main() {
func();
// cout << x; // ❌ 错误!x 在这里不存在
return 0;
}全局变量:定义在所有函数外面。任何函数都能访问它。
int score = 100; // 全局变量
void show() {
cout << score << endl; // 能访问
}
int main() {
cout << score << endl; // 也能访问
score = 95; // 能修改
show(); // 输出 95
return 0;
}⚠️ 全局变量虽然方便,但用多了会让程序变得难以调试——任何函数都能改它,出了问题你不知道是谁改的。竞赛中通常只在特定场景(如记录状态的大数组)才用全局变量。初学阶段,尽量把变量放在需要的函数内部。
23.6 作用域
作用域 = 一个变量"活着"的范围。
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() 里多次调用,观察结果。
🦶 你踩过这些坑吗
逐一核对,看你是不是也中过招:
- [ ] 忘记 return:
int 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 处错误,请找出并改正:
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返回)各两题覆盖。。★核心(课堂必做) ◆拓展(课后练习)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | J0104 | https://hydro.ac/d/srqc/p/J0104 | 函数四要素、return、递归 |
| ◆ 拓展 | J0100 | https://hydro.ac/d/srqc/p/J0100 | 多参数、double返回、sqrt |
| ◆ 拓展 | J0103 | https://hydro.ac/d/srqc/p/J0103 | 辅助函数、数组传参、最值 |
| ◆ 拓展 | J0101 | https://hydro.ac/d/srqc/p/J0101 | bool返回、质数判断 |
| ◆ 拓展 | J0102 | https://hydro.ac/d/srqc/p/J0102 | bool返回、闰年、区间筛选 |
| ◆ 拓展 | luogu-P5735 | https://hydro.ac/p/luogu-P5735 | 多参数、浮点返回 |
| ◆ 拓展 | luogu-P5736 | https://hydro.ac/p/luogu-P5736 | bool函数、数组+函数配合 |
| ◆ 拓展 | luogu-P5737 | https://hydro.ac/p/luogu-P5737 | bool函数、闰年、区间遍历 |
💡 练习建议:先完成 1 道★核心题,确保掌握本章基本方法;再完成 7 道◆拓展题,覆盖不同变式和细节。
配套练习
共8题。函数四要素(int/bool/void/double返回)各两题覆盖。★核心(课堂必做) · ◆拓展(课后练习)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | J0104 | https://hydro.ac/d/srqc/p/J0104 | 函数四要素、return、递归 |
| ◆ 拓展 | J0100 | https://hydro.ac/d/srqc/p/J0100 | 多参数、double返回、sqrt |
| ◆ 拓展 | J0103 | https://hydro.ac/d/srqc/p/J0103 | 辅助函数、数组传参、最值 |
| ◆ 拓展 | J0101 | https://hydro.ac/d/srqc/p/J0101 | bool返回、质数判断 |
| ◆ 拓展 | J0102 | https://hydro.ac/d/srqc/p/J0102 | bool返回、闰年、区间筛选 |
| ◆ 拓展 | luogu-P5735 | https://hydro.ac/p/luogu-P5735 | 多参数、浮点返回 |
| ◆ 拓展 | luogu-P5736 | https://hydro.ac/p/luogu-P5736 | bool函数、数组+函数配合 |
| ◆ 拓展 | luogu-P5737 | https://hydro.ac/p/luogu-P5737 | bool函数、闰年、区间遍历 |
练习建议:先在课堂完成 1 道★核心题,掌握本章基本方法;课后完成 7 道◆拓展题,覆盖不同变式和细节。
自查清单:
- [ ] 我能定义一个函数,包含返回类型、函数名、参数列表、函数体
- [ ] 我会调用函数并使用返回值
- [ ] 我会写
void函数 - [ ] 我理解值传递和引用传递的区别
- [ ] 我知道什么时候用函数声明(原型)
- [ ] 我能区分局部变量和全局变量,理解作用域
🚀 下章预告:函数能接收参数了——但传进去的到底是复印件还是原件?为什么有时候改了参数,回到
main()却纹丝不动?第 24 章,我们来揭开参数传递的底层秘密——值传递与引用传递。