1、资料清单
| 资料 |
内容 |
入口 |
| 教材 |
《C++语言程序设计(第4版)》Chap3,原书页码 80-113 |
抽取 Markdown 下载 |
| 课件 |
Chap3 课件,位于本地郑莉 C++ 资料目录 |
本地资料夹 |
| 例题源代码 |
C++V5 源代码 |
下载 |
| VS2019 工程 |
C++V5 VS2019 solution |
下载 |
本章的定位:把 Chap2 中写在 main() 里的顺序逻辑拆成可以复用、可以测试、可以组合的函数。主线是函数定义 → 函数调用 → 参数传递 → 引用 → 重载/默认参数/内联 → 递归与模板。
2、颜色标注
| 颜色 |
含义 |
使用位置 |
| 蓝色 |
核心术语、定义 |
函数原型、形参、实参、引用 |
| 绿色 |
推荐写法、主线 |
小函数、单一职责、返回值设计 |
| 黄色 |
易错点、边界条件 |
引用绑定、递归出口、整数除法 |
| 红色 |
不建议做法 |
大段逻辑全塞进 main()、无出口递归 |
3、学习目标
| 目标 |
要会到什么程度 |
| 函数定义 |
会写返回类型、函数名、形参列表、函数体 |
| 函数声明 |
理解函数原型为什么要放在调用前 |
| 参数传递 |
分清值传递、引用传递、const 引用 |
| 引用 |
知道引用是别名,初始化后不能改绑 |
| 函数重载 |
会用参数个数或类型区分同名函数 |
| 默认参数 |
会写默认形参,知道默认值从右向左设置 |
| 递归 |
会写递归出口和递归推进 |
| 模板函数 |
会写简单 template <typename T> 函数 |
4、章节目录
| 小节 |
内容 |
本节关键词 |
| 3.1 |
函数的定义与使用 |
函数原型、返回值、主调/被调函数 |
| 3.2 |
函数调用 |
实参、形参、调用表达式 |
| 3.3 |
参数传递 |
值传递、引用传递、const 引用 |
| 3.4 |
内联函数 |
inline、调用开销、适用场景 |
| 3.5 |
默认形参 |
默认值、从右向左 |
| 3.6 |
函数重载 |
同名函数、参数匹配 |
| 3.7 |
系统函数 |
标准库函数、头文件 |
| 3.8 |
运行栈与函数调用 |
栈帧、局部变量、返回地址 |
5、函数的基本结构
函数把一段具有明确功能的代码封装起来,方便复用和测试。
1 2 3 4
| 返回类型 函数名(形参列表) { 函数体 return 返回值; }
|
例子:
1 2 3
| int add(int x, int y) { return x + y; }
|
| 部分 |
说明 |
int |
返回类型 |
add |
函数名 |
int x, int y |
形参列表 |
return x + y; |
返回结果 |
函数调用:
这里 3 和 5 是实参,x 和 y 是形参。
写函数时先问一句:这个函数输入是什么、输出是什么、只负责一件什么事。如果一句话说不清,它可能就太大了。
6、函数声明与函数原型
如果函数定义写在调用语句之后,需要先声明函数原型。
1 2 3 4 5 6 7 8 9 10 11 12
| #include <iostream>
int add(int x, int y);
int main() { std::cout << add(3, 5) << std::endl; return 0; }
int add(int x, int y) { return x + y; }
|
函数原型告诉编译器:
| 信息 |
例子 |
| 函数名 |
add |
| 返回类型 |
int |
| 参数个数 |
2 个 |
| 参数类型 |
int, int |
函数原型中的参数名可以省略,也不必和函数定义中的参数名一致。
7、主调函数与被调函数
| 概念 |
含义 |
| 主调函数 |
发起调用的函数 |
| 被调函数 |
被调用执行的函数 |
例如:
1 2 3 4 5 6 7 8
| int square(int x) { return x * x; }
int main() { int y = square(5); return 0; }
|
在这次调用关系里,main() 是主调函数,square() 是被调函数。一个函数在不同关系里可以既是主调函数,也可以是被调函数。
8、参数传递
8.1 值传递
值传递会把实参的值复制给形参。函数内部修改形参,不会影响实参。
1 2 3 4 5 6 7 8 9
| void change(int x) { x = 10; }
int main() { int a = 5; change(a); }
|
8.2 引用传递
引用传递让形参成为实参的别名。函数内部修改形参,会直接修改实参。
1 2 3 4 5 6 7 8 9
| void change(int& x) { x = 10; }
int main() { int a = 5; change(a); }
|
8.3 const 引用
如果参数很大,不想复制,又不想让函数修改它,可以使用 const 引用。
1 2 3
| void printName(const std::string& name) { std::cout << name << std::endl; }
|
| 传参方式 |
是否复制 |
能否修改实参 |
常见用途 |
| 值传递 |
是 |
否 |
小对象、只读数据 |
| 引用传递 |
否 |
是 |
需要修改实参 |
const 引用 |
否 |
否 |
大对象只读传入 |
引用必须在定义时初始化,并且初始化后不能再改绑到另一个变量。r = other; 不是让引用改绑,而是把 other 的值赋给引用所绑定的对象。
9、内联函数、默认参数与重载
9.1 内联函数
内联函数用 inline 修饰,意图是让编译器在调用点展开函数体,减少调用开销。
1 2 3
| inline int square(int x) { return x * x; }
|
适合内联的函数通常很短、调用频繁、逻辑简单。
inline 是给编译器的建议,不是强制命令。现代编译器会自己判断是否展开。复杂函数、递归函数通常不适合强行内联。
9.2 默认参数
1 2 3 4 5 6 7
| int power(int x, int y = 2) { int result = 1; for (int i = 0; i < y; ++i) { result *= x; } return result; }
|
调用:
默认参数应从右向左设置:
1
| void f(int a, int b = 1, int c = 2);
|
9.3 函数重载
函数重载指多个函数同名,但参数列表不同。
1 2 3 4 5 6 7
| int maxValue(int a, int b) { return a > b ? a : b; }
double maxValue(double a, double b) { return a > b ? a : b; }
|
重载靠参数个数、参数类型、参数顺序区分,不能只靠返回值区分。
10、递归
递归是函数直接或间接调用自身。
递归必须有两部分:
| 部分 |
作用 |
| 递归出口 |
什么时候停止 |
| 递归推进 |
每次调用都更接近出口 |
例子:求 1 + 2 + ... + n。
1 2 3 4 5 6
| int sumTo(int n) { if (n <= 1) { return n; } return n + sumTo(n - 1); }
|
递归没有出口,或者递归参数没有向出口靠近,就会无限调用,最终导致栈溢出。
11、运行栈与函数调用
每次函数调用通常会在运行栈上创建一块记录,常称为栈帧。它保存局部变量、形参、返回地址等信息。
调用 g() 时,栈上大致有:
函数返回时,对应栈帧被释放,局部变量随之失效。
12、本章代码复盘
下面这个程序串起函数、引用传递、const 引用、默认参数和递归:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <iostream> #include <string>
void printTitle(const std::string& title) { std::cout << "\n== " << title << " ==\n"; }
void swapValue(int& x, int& y) { int temp = x; x = y; y = temp; }
int power(int base, int exponent = 2) { if (exponent == 0) { return 1; } return base * power(base, exponent - 1); }
int main() { printTitle("引用传递"); int a = 3; int b = 5; swapValue(a, b); std::cout << "a = " << a << ", b = " << b << '\n';
printTitle("默认参数与递归"); std::cout << "power(3) = " << power(3) << '\n'; std::cout << "power(3, 4) = " << power(3, 4) << '\n';
return 0; }
|
13、课后习题答案
以下答案按现代 C++ 重新整理,所有程序题都使用 #include <iostream> 和 int main()。
3-1 C++ 中的函数是什么?主调函数和被调函数是什么
函数是 C++ 中实现模块化的基本单位,用来完成一项相对独立的功能。
| 概念 |
说明 |
| 函数 |
完成某项功能的代码模块 |
| 主调函数 |
调用其他函数的函数 |
| 被调函数 |
被其他函数调用的函数 |
调用函数前通常需要有函数声明:
调用形式:
3-2 观察引用程序输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <iostream>
int main() { int intOne; int& rSomeRef = intOne;
intOne = 5; std::cout << "intOne: " << intOne << '\n'; std::cout << "rSomeRef: " << rSomeRef << '\n';
int intTwo = 8; rSomeRef = intTwo;
std::cout << "intOne: " << intOne << '\n'; std::cout << "intTwo: " << intTwo << '\n'; std::cout << "rSomeRef: " << rSomeRef << '\n';
return 0; }
|
输出:
1 2 3 4 5
| intOne: 5 rSomeRef: 5 intOne: 8 intTwo: 8 rSomeRef: 8
|
关键理解:rSomeRef 从定义开始就是 intOne 的别名。rSomeRef = intTwo; 不是让引用改绑到 intTwo,而是把 intTwo 的值赋给 intOne。
3-3 比较值调用和引用调用
| 传参方式 |
形参是否是副本 |
修改形参是否影响实参 |
适合场景 |
| 值调用 |
是 |
否 |
小数据、只读传入 |
| 引用调用 |
否 |
是 |
需要修改实参 |
值调用是单向传递;引用调用让形参成为实参的别名。
3-4 什么是内联函数?有哪些特点
内联函数是用 inline 修饰的函数。
1 2 3
| inline int square(int x) { return x * x; }
|
特点:
| 特点 |
说明 |
| 可能在调用处展开 |
减少函数调用开销 |
| 适合短小函数 |
函数体过大会增加代码体积 |
| 只是建议 |
编译器可以选择不内联 |
| 定义通常放在调用前 |
让编译器看到函数体 |
教材里提到内联函数不适合复杂控制结构,这个规则可以理解为“不要把复杂函数写成内联”。
3-5 函数原型、函数定义、函数调用中的参数名必须一致吗
不必一致。函数匹配主要看参数位置、类型和个数,不看参数名。
1 2 3 4 5
| int add(int, int);
int add(int a, int b) { return a + b; }
|
3-6 重载函数通过什么区分
函数重载通过参数列表区分:
| 可区分 |
例子 |
| 参数个数不同 |
print(int) 与 print(int, int) |
| 参数类型不同 |
print(int) 与 print(double) |
| 参数顺序不同 |
f(int, double) 与 f(double, int) |
不能只靠返回值区分。
3-7 编写除法函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include <iostream>
short divider(unsigned short a, unsigned short b) { if (b == 0) { return -1; } return static_cast<short>(a / b); }
int main() { unsigned short one; unsigned short two;
std::cout << "Enter two numbers.\nNumber one: "; std::cin >> one; std::cout << "Number two: "; std::cin >> two;
short answer = divider(one, two);
if (answer != -1) { std::cout << "Answer: " << answer << std::endl; } else { std::cout << "Error, can't divide by zero!" << std::endl; }
return 0; }
|
3-8 华氏温度转摄氏温度
公式:
程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <iostream>
double fahrenheitToCelsius(double fahrenheit) { return (fahrenheit - 32.0) * 5.0 / 9.0; }
int main() { double f; std::cout << "请输入华氏温度:"; std::cin >> f;
std::cout << "摄氏温度为:" << fahrenheitToCelsius(f) << std::endl;
return 0; }
|
3-9 判断一个数是否是质数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include <iostream>
bool isPrime(int n) { if (n < 2) { return false; }
for (int divisor = 2; divisor * divisor <= n; ++divisor) { if (n % divisor == 0) { return false; } }
return true; }
int main() { int n; std::cout << "请输入一个整数:"; std::cin >> n;
if (isPrime(n)) { std::cout << n << " 是质数。" << std::endl; } else { std::cout << n << " 不是质数。" << std::endl; }
return 0; }
|
3-10 求最大公约数和最小公倍数
使用欧几里得算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| #include <iostream>
int gcd(int a, int b) { while (b != 0) { int r = a % b; a = b; b = r; } return a; }
int lcm(int a, int b) { return a / gcd(a, b) * b; }
int main() { int a; int b;
std::cout << "请输入一个正整数:"; std::cin >> a; std::cout << "请输入另一个正整数:"; std::cin >> b;
std::cout << a << " 和 " << b << " 的最大公约数是:" << gcd(a, b) << std::endl; std::cout << a << " 和 " << b << " 的最小公倍数是:" << lcm(a, b) << std::endl;
return 0; }
|
输入 120 和 72,输出:
原抽取答案中的交换变量片段疑似有笔误,j = i; 会把两个数变成一样。这里直接使用欧几里得算法,不需要先手动交换。
3-11 什么是嵌套调用和递归调用
| 概念 |
说明 |
| 嵌套调用 |
函数 A 调用函数 B,函数 B 又调用函数 C |
| 递归调用 |
函数直接或间接调用自身 |
例子:
1 2 3 4 5 6 7 8
| void a() { b(); }
int factorial(int n) { if (n <= 1) return 1; return n * factorial(n - 1); }
|
3-12 递归求 1 + 2 + ... + n
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <iostream>
int sumTo(int n) { if (n <= 1) { return n; } return n + sumTo(n - 1); }
int main() { int n; std::cout << "请输入一个正整数:"; std::cin >> n;
std::cout << "从 1 累加到 " << n << " 的和为:" << sumTo(n) << std::endl;
return 0; }
|
输入 100,结果为 5050。
3-13 递归函数 GetPower
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include <iostream>
long getPower(int x, int y) { if (y == 0) { return 1; } return x * getPower(x, y - 1); }
int main() { int number; int power;
std::cout << "Enter a number: "; std::cin >> number; std::cout << "To what power? "; std::cin >> power;
std::cout << number << " to the " << power << "th power is " << getPower(number, power) << std::endl;
return 0; }
|
输入 3 和 4,输出 81。
3-14 递归求 Fibonacci 数列
定义:
1 2 3
| fib(1) = 1 fib(2) = 1 fib(n) = fib(n - 1) + fib(n - 2), n > 2
|
程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <iostream>
long long fib(int n) { if (n <= 0) { return 0; } if (n == 1 || n == 2) { return 1; } return fib(n - 1) + fib(n - 2); }
int main() { int n; std::cout << "请输入 n:"; std::cin >> n;
std::cout << "fib(" << n << ") = " << fib(n) << std::endl;
return 0; }
|
朴素递归 Fibonacci 会重复计算很多子问题,适合观察递归过程,不适合大规模计算。后续可以用循环或动态规划优化。
3-15 递归求勒让德多项式
递推公式:
1 2 3
| P0(x) = 1 P1(x) = x Pn(x) = ((2n - 1)xPn-1(x) - (n - 1)Pn-2(x)) / n
|
程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include <iostream>
double legendre(int n, double x) { if (n == 0) { return 1.0; } if (n == 1) { return x; }
return ((2 * n - 1) * x * legendre(n - 1, x) - (n - 1) * legendre(n - 2, x)) / n; }
int main() { int n; double x;
std::cout << "请输入正整数 n:"; std::cin >> n; std::cout << "请输入 x:"; std::cin >> x;
std::cout << "P" << n << "(" << x << ") = " << legendre(n, x) << std::endl;
return 0; }
|
例子:
3-16 使用模板函数实现 Swap(x, y)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include <iostream>
template <typename T> void swapValue(T& x, T& y) { T temp = x; x = y; y = temp; }
int main() { int j = 1; int k = 2; double v = 3.14; double w = 4.35;
std::cout << "j = " << j << " k = " << k << '\n'; std::cout << "v = " << v << " w = " << w << '\n';
swapValue(j, k); swapValue(v, w);
std::cout << "After swap:\n"; std::cout << "j = " << j << " k = " << k << '\n'; std::cout << "v = " << v << " w = " << w << '\n';
return 0; }
|
输出:
1 2 3 4 5
| j = 1 k = 2 v = 3.14 w = 4.35 After swap: j = 2 k = 1 v = 4.35 w = 3.14
|
14、本章复盘任务
- 手写一个
isPrime 函数,并用 main() 完成输入输出。
- 分别用值传递和引用传递写一个交换函数,观察结果差异。
- 用递归写
sumTo(n)、getPower(x, y)、fib(n)。
- 解释为什么重载函数不能只靠返回值区分。
- 用模板函数写一个
maxValue(x, y),同时支持 int 和 double。