告别"代码复制粘贴",学会用函数封装逻辑,让程序像搭积木一样清晰高效
从一个"重复代码"的困境出发,理解函数为什么是 C++ 中不可或缺的"代码封装"工具。
小强要写一个程序:输入 5 个学生的成绩,分别计算每个学生的平均分和等级。他不会用函数,于是写了 5 段几乎一模一样的代码,每段都重复了"求和、除以 3、判断等级"的逻辑。代码写得又长又乱,改一处要改 5 遍,差点崩溃。
老师告诉他:"把这些重复的逻辑打包成一个函数,只需要写一次,调用 5 次就行了!"
💡 生活类比:
写好一次菜谱(函数定义),每次做饭只需按步骤执行(函数调用),不用每次都重新发明做法。
定义一次,调用多次函数定义时的形参就像模具的"空腔",还没填入具体材料。它规定了函数需要什么样的"原料"。
定义时的占位符调用函数时传入的实参就是实际放进去的"材料"。不同的材料经过同一个模具,产出不同的结果。
调用时的具体值函数执行完后交出的结果。就像工厂加工完材料后交出的成品,调用者拿到成品继续下一步操作。
return 吐出的值系统学习函数的定义语法、调用方式、形参与实参的关系,以及值传递的本质。
C++ 中,函数的基本定义格式如下:
#include <iostream>
using namespace std;
// 函数定义:返回 int,函数名 add,两个形参 a 和 b
int add(int a, int b) {
int sum = a + b; // 函数体:执行求和
return sum; // 把结果交出去
}
int main() {
int x, y;
cin >> x >> y;
// 函数调用:实参 x, y 传给形参 a, b
int result = add(x, y);
cout << result << endl;
return 0;
}
int add(int a, int b);)。
这是本讲最重要的概念。很多初学者混淆形参和实参,导致"传错了值""改了没生效"等问题。
一句话记住:实参是"原件",形参是"复印件"——改复印件,原件不会变。
| 对比项 | 形参(形式参数) | 实参(实际参数) |
|---|---|---|
| 含义 | 函数定义时写的参数,是"占位符" | 函数调用时传入的具体值或变量 |
| 出现位置 | 函数定义的括号里 | 函数调用的括号里 |
| 内存分配 | 调用时才分配内存(临时变量) | 本身已有内存空间 |
| 生命周期 | 函数结束时销毁 | 在所属作用域内一直存在 |
| 类比 | 菜谱上的"适量盐" | 你实际抓的那撮盐 |
int max2(int a, int b) { // a, b 是形参
return a > b ? a : b;
}
int main() {
int x = 5, y = 8;
max2(x, y); // x, y 是实参 → 传给 a, b
max2(10, 3); // 10, 3 是实参 → 传给 a, b
max2(x + 1, y - 2); // 表达式也可以是实参
}
C++ 中函数参数默认采用值传递(pass by value)。也就是说,实参的值被"复制"了一份给形参,函数内部对形参的任何修改都不会影响实参本身。
#include <iostream>
using namespace std;
void swap_val(int a, int b) { // a, b 是 x, y 的副本
int t = a;
a = b;
b = t;
cout << "函数内: a=" << a << " b=" << b << endl;
}
int main() {
int x = 3, y = 5;
swap_val(x, y); // 传入 x, y 的副本
cout << "函数外: x=" << x << " y=" << y << endl;
return 0;
}
函数内: a=5 b=3
函数外: x=3 y=5
因为 swap_val(x, y) 调用时,系统把 x 的值 3 复制给形参 a,把 y 的值 5 复制给形参 b。函数内交换的是 a 和 b(副本),x 和 y(原件)完全没有被碰过。
类比:你给秘书一张复印件让她修改,她在复印件上改得再花,你手里的原件也不会变。
函数可以返回一个值(用 return),也可以不返回任何值(返回类型为 void)。
return 值; 交出结果int r = add(3, 5);return; 提前结束printHello();(只打印,不返回)// 有返回值:计算面积
double area(double r) {
return 3.14159 * r * r;
}
// 无返回值:打印问候
void greet(string name) {
cout << "你好, " << name << "!" << endl;
return; // 可以省略,函数体结束自动返回
}
int main() {
double s = area(5); // 有返回值,可以赋值
greet("小明"); // 无返回值,直接调用
cout << "面积=" << area(3) << endl; // 也可以直接用在表达式中
}
#include <cmath>)| 函数 | 作用 | 示例 | 结果 |
|---|---|---|---|
abs(x) | 绝对值 | abs(-5) | 5 |
pow(x, n) | x 的 n 次方 | pow(2, 3) | 8.0 |
sqrt(x) | 平方根 | sqrt(16) | 4.0 |
round(x) | 四舍五入 | round(3.6) | 4.0 |
floor(x) | 向下取整 | floor(3.9) | 3.0 |
ceil(x) | 向上取整 | ceil(3.1) | 4.0 |
fmax(a,b) | 两数最大值 | fmax(3.5, 4.2) | 4.2 |
fmin(a,b) | 两数最小值 | fmin(3.5, 4.2) | 3.5 |
#include <iostream>
#include <cmath>
using namespace std;
int main() {
cout << abs(-7) << endl; // 输出: 7
cout << pow(2, 10) << endl; // 输出: 1024
cout << sqrt(2) << endl; // 输出: 1.41421
cout << round(3.14159) << endl; // 输出: 3
}
理解函数调用的"进出"流程,是掌握函数的关键。整个过程分为 5 步:
add(x, y) 时暂停 main,准备进入函数return sum,交出结果,函数结束、临时空间销毁通过 3 道综合案例,掌握函数的定义与调用在解题中的实际应用。
题目:定义一个函数 bool isPrime(int n),判断 n 是否为素数。主函数读入一个整数,输出是否为素数。
输入样例:17
输出样例:Yes
#include <iostream>
using namespace std;
// 判断素数的函数
bool isPrime(int n) {
if (n < 2) return false;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) return false;
}
return true;
}
int main() {
int n;
cin >> n;
if (isPrime(n))
cout << "Yes" << endl;
else
cout << "No" << endl;
return 0;
}
isPrime 负责"判断"这一独立逻辑,main 只负责"输入输出"。如果后面还有其他题目也需要判断素数,直接调用 isPrime 即可,不需要再写一遍循环。题目:定义函数 long long factorial(int n),计算 n!(n 的阶乘)。主函数读入 n,输出 n!。
输入样例:5
输出样例:120
#include <iostream>
using namespace std;
long long factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
int main() {
int n;
cin >> n;
cout << factorial(n) << endl;
return 0;
}
factorial(5) → 5 × factorial(4) → 5 × 4 × factorial(3) → ... → 5 × 4 × 3 × 2 × 1 = 120。factorial(5):n=5 ≠ 1,需要 5 × factorial(4)factorial(4):需要 4 × factorial(3)factorial(3):需要 3 × factorial(2)factorial(2):需要 2 × factorial(1)factorial(1):n ≤ 1 触发终止条件,直接 return 1(递归的"底")题目:定义函数 int gcd(int a, int b),用辗转相除法求 a 和 b 的最大公约数。主函数读入两个正整数,输出它们的 GCD。
输入样例:48 18
输出样例:6
#include <iostream>
using namespace std;
// 辗转相除法求 GCD
int gcd(int a, int b) {
while (b != 0) {
int t = b;
b = a % b;
a = t;
}
return a;
}
int main() {
int a, b;
cin >> a >> b;
cout << gcd(a, b) << endl;
return 0;
}
回顾本讲核心知识点,掌握函数定义的万能模板。
| 知识点 | 要点 |
|---|---|
| 函数定义 | 返回类型 + 函数名(形参列表) { 函数体 },定义一次可调用多次 |
| 函数调用 | 函数名(实参列表),实参值复制给形参,函数暂停 main 去执行 |
| 形参 vs 实参 | 形参是定义时的占位符,实参是调用时的具体值,个数/类型/顺序必须一一对应 |
| 值传递 | 形参是实参的副本,函数内修改形参不影响实参 |
| 返回值 | 有返回值用 return 交出结果,void 表示不返回 |
| 函数声明 | 如果函数定义在 main 后面,需在 main 前加声明(如 int add(int, int);) |
// 模板:需要返回什么类型,就写什么返回类型
返回类型 函数名(形参类型 形参1, 形参类型 形参2, ...) {
// 1. 函数体:写具体的处理逻辑
// 2. 最后用 return 交出结果(void 函数不需要)
return 结果;
}
// 调用示例
int 结果 = 函数名(实参1, 实参2); // 有返回值
函数名(实参1, 实参2); // 无返回值(void)
问题:如何真正实现两个变量的交换?值传递做不到,需要用引用传递(&)。
// 值传递:形参是副本,交换无效
void swap_bad(int a, int b) {
int t = a; a = b; b = t;
}
// 引用传递:形参是实参的"别名",交换有效
void swap_good(int& a, int& b) { // 注意 & 符号
int t = a; a = b; b = t;
}
int main() {
int x = 3, y = 5;
swap_bad(x, y); // x=3, y=5 不变
swap_good(x, y); // x=5, y=3 交换成功!
}
&)让形参成为实参的"别名",对形参的修改直接作用于实参本身。这是后面要深入学习的概念,这里先知道"值传递改不了原件,引用传递可以"即可。考点很多题故意用值传递写 swap 让你判断输出——记住:值传递的 swap 一定是"假交换"。
检测本讲核心知识点,避免常见陷阱。
int add(int a, int b) {
a + b; // 没有 return!
}
int add(int a, int b) {
return a + b; // 必须 return
}
int add(int a, int b);
add(3.5); // 类型错 + 个数少
add(1, 2, 3); // 个数多
int add(int a, int b);
add(3, 5); // 类型对 + 个数对
add(10, 20); // 表达式也可以
&),这是后面的内容。void print() {
return 42; // 编译报错!
}
void print() {
cout << "Hello";
return; // 不返回值,只提前结束
}
返回类型 函数名(形参) { 函数体; return 值; }
变量 = 函数名(实参); 或 函数名(实参);
定义时的参数名,调用时才分配内存,函数结束即销毁
调用时传入的实际值,个数/类型/顺序必须与形参一一对应
形参是实参的副本,改形参不影响实参
不返回值,可以没有 return,或写 return; 提前结束
基础 → 提高 → 挑战,层层递进
#include <iostream>
using namespace std;
void f(int x, int y) {
x = x + y;
y = x - y;
x = x - y;
cout << x << " " << y << endl;
}
int main() {
int a = 3, b = 5;
f(a, b);
cout << a << " " << b << endl;
return 0;
}
#include <iostream>
using namespace std;
void f(int x, int y) { // x, y 只是 a, b 的副本
x = x + y; // x = 3 + 5 = 8
y = x - y; // y = 8 - 5 = 3
x = x - y; // x = 8 - 3 = 5
cout << x << " " << y << endl; // 函数内:输出 5 3
}
int main() {
int a = 3, b = 5;
f(a, b); // 值传递,a、b 本身不会被改动
cout << a << " " << b << endl; // 函数外:输出 3 5
return 0;
}
// 运行结果:
// 5 3
// 3 5
答案:5 3 / 3 5(第一行是函数内的输出,第二行是 main 里的输出)。
思路:先算函数内——x = 3 + 5 = 8,y = 8 − 5 = 3,x = 8 − 3 = 5,所以函数内打印「5 3」。回到 main,a、b 是原变量,值传递只把副本送进函数,它们从头到尾没被碰过,所以打印「3 5」。
考点:值传递的副本特性 + 用三步加减法交换两个数。
易错:看到函数里交换成功了,就以为外面的 a、b 也变了——这是最典型的失分点。想让函数真的改到实参,形参要写成 int& x(引用)。
int square(int n) {
n * n;
}
int main() {
cout << square(5) << endl;
}
#include <iostream>
using namespace std;
int square(int n) {
return n * n; // ← 这里必须写 return,否则返回值是随机垃圾值
}
int main() {
cout << square(5) << endl; // 输出 25
return 0;
}
// 运行结果:25
答案:函数声明返回 int,但函数体没有 return 语句。改正办法:把 n * n; 改成 return n * n;。
思路:函数签名 int square(int n) 承诺返回一个 int,可函数体里只有表达式 n * n;,这个值被算出来后又立刻丢掉了,没有送回调用处。
考点:「返回类型不是 void,函数里就必须有 return」这条硬规则。
易错:只写 n * n; 并不会报语法错误(它是合法的表达式语句),但函数的返回值是随机垃圾值,要运行时才发现,最难查。
int square(int n) 承诺返回一个 int,但函数体只有表达式 n * n;,这个值被算出来后又立刻丢弃,没有送回调用处。n * n; 不报语法错误(是合法表达式语句),但函数返回值是随机垃圾值,运行时才出问题,最难查。int countDigit(int n, int d),统计 n 中数字 d 出现的次数。例如 countDigit(122333, 3) 返回 3。#include <iostream>
using namespace std;
int countDigit(int n, int d) {
int cnt = 0;
while (n > 0) {
if (n % 10 == d) cnt++;
n /= 10;
}
return cnt;
}
int main() {
cout << countDigit(122333, 3) << endl; // 输出: 3
}
易错:循环里忘了 n /= 10,n 永远不变会死循环;条件写成 n >= 0 时 0 无法退出也一样死循环。
double avg(double a, double b, double c),返回三个数的平均值。主函数读入三个数并输出平均值(保留 2 位小数)。#include <iostream>
#include <iomanip>
using namespace std;
double avg(double a, double b, double c) {
return (a + b + c) / 3.0;
}
int main() {
double a, b, c;
cin >> a >> b >> c;
cout << fixed << setprecision(2) << avg(a, b, c) << endl;
}
易错:(a+b+c)/3 里的 3 要写成 3.0,整数除法会截断小数;输出忘了 fixed << setprecision(2) 也保不住两位小数。
(a + b + c) / 3;主函数用 cout << fixed << setprecision(2) 保留两位小数(需 #include <iomanip>)。int power(int base, int exp),计算 base 的 exp 次方(exp 为非负整数)。不用 pow 函数,用循环实现。#include <iostream>
using namespace std;
int power(int base, int exp) {
int result = 1;
for (int i = 0; i < exp; i++)
result *= base;
return result;
}
int main() {
cout << power(2, 10) << endl; // 1024
}
int r = 1; for (int i = 0; i < exp; i++) r *= base;,注意 exp = 0 时应返回 1(任何数的 0 次方为 1,循环自然不执行,r 保持 1)。#include <iostream>
using namespace std;
int f() {
cout << "A";
return 1;
}
int main() {
cout << f() << "B" << f() << endl;
}
#include <iostream>
using namespace std;
int f() {
cout << "A"; // 副作用:每调用一次就打印一个 A
return 1;
}
int main() {
cout << f() << "B" << f() << endl; // 链式输出从左往右求值
return 0;
}
// 运行结果:A1BA1
答案:A1BA1。
思路:链式输出的求值顺序是从左到右:先执行第一个 f()(函数内打印 A,返回 1)→ 打印这个 1 → 打印字符 B → 再执行第二个 f()(打印 A,返回 1)→ 打印 1,合起来就是 A1BA1。
考点:函数调用本身可以带副作用(函数里也能 cout),要按执行顺序拆开看。
易错:把两次函数调用的输出提前或合并,误答成 AAB11 或 11BA1。记住:函数一被调用,函数里的打印就先发生。
f()(函数内输出 A,返回 1)→ 输出这个 1 → 输出字符 B → 再执行第二个 f()(输出 A,返回 1)→ 输出 1,合起来是 A1BA1。int max3(int a, int b, int c),返回三个数中的最大值。要求用嵌套的 max2 函数实现(max2 返回两个数的最大值)。#include <iostream>
using namespace std;
int max2(int a, int b) {
return a > b ? a : b;
}
int max3(int a, int b, int c) {
return max2(max2(a, b), c);
}
int main() {
cout << max3(5, 9, 3) << endl; // 9
}
易错:max2 定义在 max3 之后又没有提前声明,编译时报“未声明的标识符”——被调函数要先定义或先声明。
int max2(int p, int q) { return p > q ? p : q; },再让 max3 返回 max2(max2(a, b), c)——把三个数拆成两次两两比较。int fib(int n),用递归返回第 n 个斐波那契数(fib(1)=1, fib(2)=1, fib(n)=fib(n-1)+fib(n-2))。输出前 10 个斐波那契数。#include <iostream>
using namespace std;
int fib(int n) {
if (n <= 2) return 1;
return fib(n - 1) + fib(n - 2);
}
int main() {
for (int i = 1; i <= 10; i++)
cout << fib(i) << " ";
cout << endl;
}
易错:忘了基准情况 if (n <= 2) return 1,递归没有出口直接栈溢出崩溃。
a = 1, b = 1; for (i = 3; i <= 10; i++) { c = a + b; a = b; b = c; };递归写法能体现定义式,但 fib(40) 就会非常慢(重复计算爆炸)。int reverseNum(int n),返回 n 的逆序数。例如 reverseNum(123) 返回 321。#include <iostream>
using namespace std;
int reverseNum(int n) {
int rev = 0;
while (n > 0) {
rev = rev * 10 + n % 10;
n /= 10;
}
return rev;
}
int main() {
cout << reverseNum(123) << endl; // 321
}
rev = rev * 10 + n % 10; n /= 10;,循环直到 n 为 0。bool isPalindrome(int n),判断 n 是否为回文数(正读反读一样)。利用 reverseNum 函数实现。#include <iostream>
using namespace std;
int reverseNum(int n) {
int rev = 0;
while (n > 0) {
rev = rev * 10 + n % 10;
n /= 10;
}
return rev;
}
bool isPalindrome(int n) {
return n == reverseNum(n);
}
int main() {
cout << boolalpha << isPalindrome(12321) << endl; // true
}
易错:reverseNum 里 rev = rev*10 + n%10 的顺序写反(先加后乘),翻转结果就错了。
reverseNum(n) 得到逆序数,再与原数比较是否相等:return n == reverseNum(n);#include <iostream>
using namespace std;
int f(int x) { return x * x; }
int g(int x) { return f(x) + 1; }
int main() {
cout << g(3) << endl;
}
#include <iostream>
using namespace std;
int f(int x) { return x * x; } // f(3) = 9
int g(int x) { return f(x) + 1; } // g(3) = f(3) + 1 = 10
int main() {
cout << g(3) << endl; // 输出 10
return 0;
}
// 运行结果:10
答案:10。
思路:由内向外层层代入:g(3) → f(3) + 1,而 f(x) = x * x,所以 f(3) = 9,g(3) = 9 + 1 = 10。
考点:嵌套调用(一个函数里调用另一个函数),按「先算内层、再算外层」的顺序拆解。
易错:把 g 里那一句看成 3 + 1 = 4,漏掉了 f 的加工。看到函数名就先把它的返回值算出来,再继续往外算。
g(3) → f(3) + 1,而 f(x) = x * x,所以 f(3) = 9,g(3) = 9 + 1 = 10。int digits(int n),返回 n 的位数。例如 digits(123) 返回 3,digits(7) 返回 1。#include <iostream>
using namespace std;
int digits(int n) {
int cnt = 0;
while (n > 0) {
n /= 10;
cnt++;
}
return cnt;
}
int main() {
cout << digits(12345) << endl; // 5
}
int c = 0; while (n > 0) { n /= 10; c++; } return c;if (n == 0) return 1;。int sumDigits(int n),返回 n 的各位数字之和。例如 sumDigits(123) 返回 6(1+2+3)。#include <iostream>
using namespace std;
int sumDigits(int n) {
int s = 0;
while (n > 0) {
s += n % 10;
n /= 10;
}
return s;
}
int main() {
cout << sumDigits(123) << endl; // 6
}
易错:循环里忘了 n /= 10 导致死循环;n 为 0 时函数返回 0,若题目要求 0 的数位和是 0 需注意边界。
sum += n % 10; n /= 10;int gcd(int a, int b)(辗转相除法)和 int lcm(int a, int b)(最小公倍数),利用公式 lcm(a,b) = a*b/gcd(a,b) 实现。主函数读入两个正整数,输出它们的 GCD 和 LCM。#include <iostream>
using namespace std;
int gcd(int a, int b) {
while (b != 0) {
int t = b;
b = a % b;
a = t;
}
return a;
}
int lcm(int a, int b) {
return a / gcd(a, b) * b; // 先除后乘防溢出
}
int main() {
int a, b;
cin >> a >> b;
cout << gcd(a, b) << " " << lcm(a, b) << endl;
}
lcm = a / gcd * b——先除后乘可有效避免 a*b 溢出。a * b / gcd 时,a、b 较大(如 10⁹ 量级)会先溢出再除,结果错误。int countPrime(int n),统计 1 到 n 之间有多少个素数。要求利用 isPrime 函数。#include <iostream>
using namespace std;
bool isPrime(int n) {
if (n < 2) return false;
for (int i = 2; i * i <= n; i++)
if (n % i == 0) return false;
return true;
}
int countPrime(int n) {
int cnt = 0;
for (int i = 2; i <= n; i++)
if (isPrime(i)) cnt++;
return cnt;
}
int main() {
cout << countPrime(10) << endl; // 4 (2,3,5,7)
}
易错:isPrime 从 1 开始判断会把 1 当成素数;i*i <= n 写成 i <= n 在大数时会超时。
for (i = 1; i <= n; i++) if (isPrime(i)) cnt++;