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; 返回结果

函数调用:

1
int result = add(3, 5);

这里 35 是实参,xy 是形参。

写函数时先问一句:这个函数输入是什么、输出是什么、只负责一件什么事。如果一句话说不清,它可能就太大了。

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

函数原型中的参数名可以省略,也不必和函数定义中的参数名一致。

1
int add(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);
// a 仍然是 5
}

8.2 引用传递

引用传递让形参成为实参的别名。函数内部修改形参,会直接修改实参。

1
2
3
4
5
6
7
8
9
void change(int& x) {
x = 10;
}

int main() {
int a = 5;
change(a);
// a 变成 10
}

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
2
power(3);    // 3^2
power(3, 4); // 3^4

默认参数应从右向左设置:

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、运行栈与函数调用

每次函数调用通常会在运行栈上创建一块记录,常称为栈帧。它保存局部变量、形参、返回地址等信息。

1
2
3
main()
-> f()
-> g()

调用 g() 时,栈上大致有:

1
2
3
g 的栈帧
f 的栈帧
main 的栈帧

函数返回时,对应栈帧被释放,局部变量随之失效。

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++ 中实现模块化的基本单位,用来完成一项相对独立的功能。

概念 说明
函数 完成某项功能的代码模块
主调函数 调用其他函数的函数
被调函数 被其他函数调用的函数

调用函数前通常需要有函数声明:

1
int add(int x, int y);

调用形式:

1
int result = add(3, 5);

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
C = (F - 32) * 5 / 9

程序:

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;
}

输入 12072,输出:

1
2
最大公约数:24
最小公倍数:360

原抽取答案中的交换变量片段疑似有笔误,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;
}

输入 34,输出 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;
}

例子:

1
2
P1(2) = 2
P3(4) = 154

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、本章复盘任务

  1. 手写一个 isPrime 函数,并用 main() 完成输入输出。
  2. 分别用值传递和引用传递写一个交换函数,观察结果差异。
  3. 用递归写 sumTo(n)getPower(x, y)fib(n)
  4. 解释为什么重载函数不能只靠返回值区分。
  5. 用模板函数写一个 maxValue(x, y),同时支持 intdouble