3 自定义函数进阶
正在加载视频播放器…
GESP C++ 四级:自定义函数进阶
核心目标:深入掌握函数参数传递的三种方式(值传递、引用传递、指针传递),理解函数重载与默认参数的机制与限制,掌握递归函数的设计与执行流程,能熟练判断函数相关程序的输出结果。
值传递拷贝、引用传递别名、指针传递传地址;重载看参数列表、默认参数从右向左、递归必须有出口。
一、考纲定位:三级与四级的函数分界线
GESP三级对函数的要求是“掌握函数的使用”,而四级的考纲明确列出了更系统的内容:函数的定义与调用、形参与实参、作用域,以及函数参数传递的概念(值传递、引用传递、指针传递)。四级的知识目标是“掌握函数的定义、调用及函数参数传递的方法”。
三级与四级的核心区别:
| 维度 | 三级 | 四级 |
|---|---|---|
| 函数定位 | 组织代码的工具 | 模块化编程的核心机制 |
| 参数传递 | 理解值传递基本概念 | 系统掌握值、引用、指针三种传递 |
| 递归 | 不要求 | 要求掌握简单递归 |
| 重载/默认参数 | 需要理解并会判断 | |
| 作用域 | 理解局部/全局 | 深入理解作用域与变量生命周期 |
边界提醒:四级不考函数模板、内联函数的复杂使用,但需要能读懂并判断重载和默认参数相关的代码。
二、函数参数传递深度对比
这是四级函数部分最重要的考点,也是选择题和程序阅读题的高频出题点。
2.1 三种传递方式一览
| 传递方式 | 语法 | 形参与实参关系 | 函数内修改是否影响实参 | 典型考点 |
|---|---|---|---|---|
| 值传递 | void f(int x) |
形参获得实参的值副本 | ❌ 不影响 | “swap失败”类判断 |
| 引用传递 | void f(int &x) |
形参成为实参的别名 | ✅ 直接影响 | 引用与变量的绑定关系 |
| 指针传递 | void f(int *x) |
形参接收实参的地址值 | ✅ 通过解引用修改 | 解引用修改实参的判断 |
2.2 代码对比:一次看懂三种传递
void addV(int a) { a += 10; } // 值传递
void addR(int &a) { a += 10; } // 引用传递
void addP(int *a) { *a += 10; } // 指针传递
int main() {
int x = 1;
addV(x); // x 仍为 1
addR(x); // x 变为 11
addP(&x); // x 变为 21
cout << x; // 输出 21
return 0;
}
值传递中,a 仅是 x 的副本,函数内的修改不会回写。引用传递中,& 让 a 成为 x 的别名,零成本读写。指针传递中,函数内部通过 *a 解引用直达原变量。
2.3 真题示例
真题1(2026年3月) :
void fun(int a, int &b, int *c){ a += 1; b += 2; *c += 3; } int main(){ int x = 1, y = 1, z = 1; fun(x, y, &z); cout << x << " " << y << " " << z; }答案:1 3 4
解析:a为值传递,修改不影响x;b为引用传递,修改影响y;c为指针传递,*c += 3修改了z。
真题2(2024年3月) :
若函数声明为
int f(int &x) { x += 3; return x; },则对int a = 3;,以下哪个调用能改变a的值?
答案:f(a)。引用传递中形参是实参的别名,传入a即可直接修改a的值。
2.4 引用传递的关键规则
- 引用必须在定义时初始化,不能先定义后绑定。
- 引用一旦绑定,不能再绑定其他变量。
- 引用不占独立内存空间(概念上),是变量的别名。
三、函数重载
3.1 什么是函数重载
在同一作用域内,可以定义多个同名函数,只要它们的参数列表不同(参数类型、数量或顺序不同)。
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
int add(int a, int b, int c) { return a + b + c; }
调用时,编译器根据实参的类型和数量自动选择对应的版本。
3.2 重载的判定规则
| 能构成重载 | 不能构成重载 |
|---|---|
| 参数类型不同 | 仅返回值类型不同 |
| 参数数量不同 | 仅参数名不同 |
| 参数顺序不同 | — |
int f(int a); // ✅
double f(int a); // ❌ 仅返回值不同,不构成重载
int g(int a, double b); // ✅
int g(double a, int b); // ✅ 参数顺序不同,构成重载
3.3 重载的编译时机
函数重载的选择(确定调用哪一个重载函数)是在编译时完成的。编译器根据函数调用时提供的实参类型和数量来匹配。
3.4 真题示例
下列关于函数重载的说法中,正确的是( )。
A) 函数重载允许函数的返回值类型不同
B) 函数重载可以提高代码的可读性和可维护性
C) 函数重载要求参数列表必须完全相同
D) 函数重载不允许默认参数的存在
答案:B
解析:重载的区分依据是参数列表,返回值类型不同不构成重载;重载允许默认参数存在。
四、默认参数
4.1 基本概念
在函数定义时,可以为参数指定默认值。调用时如果省略该参数,就使用默认值。
int f(int x = 2) {
return x * 3;
}
int main() {
cout << f() << " " << f(4);
// 输出:6 12
}
f() 不传参数,使用默认值 x=2,返回 6;f(4) 传入 4,覆盖默认值,返回 12。
4.2 默认参数的两条铁律
铁律一:默认参数必须从右向左定义。
void f(int a, int b = 2, int c = 3); // ✅ 正确
void f(int a = 1, int b, int c = 3); // ❌ 错误:b没有默认值,c有
铁律二:在函数声明和定义中只能指定一次默认值。
// 头文件/声明中写了默认值
void f(int a, int b = 2);
// 定义中不能再写
void f(int a, int b) { ... } // ✅ 正确
void f(int a, int b = 2) { ... } // ❌ 重复定义默认值
4.3 默认参数与重载的关系
默认参数本质上可以看作一种简化的重载形式:一个带默认值的参数,相当于同时提供了一个“有该参数”的版本和一个“没有该参数”的版本。但要注意,默认参数可能导致调用歧义:
void f(int a, int b = 2);
void f(int a);
f(5); // ❌ 歧义:两个版本都匹配
五、递归函数
5.1 什么是递归
递归函数是在函数体内直接或间接调用自身的函数。
int factorial(int n) {
if (n <= 1) return 1; // 边界条件(递归出口)
return n * factorial(n - 1); // 递归式
}
5.2 递归的两要素
一个正确的递归函数必须包含两部分:
| 要素 | 作用 | 示例 |
|---|---|---|
| 边界条件(递归出口) | 定义递归何时停止 | if (n <= 1) return 1; |
| 递归式 | 定义问题如何缩小规模 | return n * factorial(n - 1); |
缺少边界条件会导致栈溢出(无限递归),缺少递归式则无法缩小问题规模。
5.3 递归的执行流程:递推与回归
以 factorial(4) 为例:
递推过程(向下):
factorial(4) → 4 * factorial(3)
→ 4 * 3 * factorial(2)
→ 4 * 3 * 2 * factorial(1)
→ 4 * 3 * 2 * 1
回归过程(向上):
factorial(1) 返回 1
factorial(2) 返回 2 * 1 = 2
factorial(3) 返回 3 * 2 = 6
factorial(4) 返回 4 * 6 = 24
递推是问题规模不断缩小的过程,回归是逐层返回计算结果的过程。对于递归而言,递推与回归,二者缺一不可。
5.4 递归 vs 迭代
| 对比项 | 递归 | 迭代(循环) |
|---|---|---|
| 代码简洁性 | 通常更简洁直观 | 稍显冗长 |
| 执行效率 | 较低(函数调用开销) | 较高 |
| 空间消耗 | 每层调用消耗栈空间 | 通常只需常数空间 |
| 风险 | 递归过深导致栈溢出 | 无栈溢出风险 |
递归函数并不总是比非递归函数更高效。在大多数情况下,递归函数的效率低于非递归函数,因为每次调用自身都会引入额外的函数调用开销(参数压栈、栈帧创建和销毁)。此外,递归可能重复计算子问题,导致时间复杂度上升。
5.5 真题示例
以下关于递归函数的说法中,错误的是( )。
A) 递归函数在函数体内直接或间接地调用自身
B) 递归函数必须有递归式和边界条件
C) 递归函数总是比非递归函数更高效
D) 递归函数如果递归深度太大,可能会导致栈溢出
答案:C
六、函数与数组、指针的进阶结合
6.1 二维数组作为函数参数
四级要求掌握二维数组的基本使用。二维数组作为参数时,列数必须在编译期可知。
| 写法 | 代码片段 | 说明 |
|---|---|---|
| 省略首维长度 | void f(int a[][4], int r) |
最常见,首维由实参决定 |
| 固定两维 | void f(int a[3][4]) |
行列都固定的小矩阵 |
| 指针+手算下标 | void f(int *a, int r, int c) |
手动计算 a[i*c+j] |
void printMatrix(int a[][4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
cout << a[i][j] << " ";
}
cout << endl;
}
}
真题示例(样题第11题) :以下哪个函数声明在调用时可以传递二维数组的名字作为参数?
答案:void BubbleSort(int a[3][4]);。
6.2 数组指针与指针数组的区分
int *p[5]; // 指针数组:数组的5个元素都是 int*
int (*p)[5]; // 数组指针:p 指向一个包含5个int的数组
int (*p)[4] = a; 中 p 是一个数组指针,指向包含4个int的一维数组。p+2 向后移动2行,*(p+2) 得到第2行的首地址,*(*(p+2)+1) 得到第2行第1列的元素。
6.3 函数返回指针(了解)
int* findMax(int arr[], int n) {
int maxIdx = 0;
for (int i = 1; i < n; i++) {
if (arr[i] > arr[maxIdx]) maxIdx = i;
}
return &arr[maxIdx];
}
注意:不要返回函数内部局部变量的地址,局部变量在函数结束后会被销毁。
七、易错点汇总
7.1 参数传递相关
- 值传递误以为能修改实参:
void f(int x) { x = 100; }调用后实参不变。 - 引用传递语法错误:声明引用参数时
&与变量名之间无空格要求,但int & x和int& x等价。 - 指针传递忘记解引用:
void f(int *p) { p = ...; }只改了函数内的局部指针,不影响外面;*p = ...;才修改指向的值。 - 引用必须初始化:
int &r;是错误的,引用定义时必须绑定到一个有效变量。
7.2 重载与默认参数相关
- 仅返回值不同不构成重载:
int f(); double f();编译错误。 - 默认参数从右向左:
void f(int a = 1, int b);错误。 - 默认参数只能指定一次:声明和定义中重复指定会编译错误。
- 默认参数导致歧义:重载与默认参数同时使用时,可能出现调用歧义。
7.3 递归相关
- 缺少边界条件:无限递归导致栈溢出。
- 递归深度过大:即使有边界条件,过深的递归也可能导致栈溢出。
- 递归效率低于迭代:不要盲目认为递归更优。
7.4 作用域相关
- 局部变量遮蔽全局变量:函数内的同名局部变量会暂时“屏蔽”全局变量。
::访问全局变量:::x可以显式访问被遮蔽的全局变量。
int x = 3;
void f(int& x) { x += 2; }
int main() {
int x = 10;
f(x);
cout << x << " " << ::x; // 输出 12 3
}
局部变量 x=10 遮蔽了全局变量 x=3;f(x) 通过引用传递修改的是局部 x,变为12;::x 显式访问全局 x,输出3。
八、速查表
| 需求 | 写法 | 关键注意 |
|---|---|---|
| 值传递 | void f(int x) |
不影响实参 |
| 引用传递 | void f(int &x) |
影响实参,必须绑定有效变量 |
| 指针传递 | void f(int *x) |
影响实参,需解引用修改 |
| 函数重载 | 同名函数,参数列表不同 | 返回值不同不构成重载 |
| 默认参数 | int f(int x = 2) |
从右向左定义,只指定一次 |
| 递归边界 | if (n <= 1) return 1; |
必须有出口 |
| 递归式 | return n * f(n-1); |
问题规模必须缩小 |
| 二维数组参数 | void f(int a[][4], int r) |
列数必须编译期可知 |
| 数组指针 | int (*p)[4] = a; |
指向整个数组 |
| 指针数组 | int *p[4]; |
元素是指针 |
| 全局变量访问 | ::x |
被局部变量遮蔽时使用 |
九、总结
自定义函数进阶是GESP四级从“写代码”到“设计代码”的关键跨越。
- 四级函数的核心是参数传递:值传递拷贝、引用传递别名、指针传递传地址。
- 值传递不影响实参,引用传递和指针传递都能修改实参。
- 引用必须在定义时初始化,且一旦绑定不能更换目标。
- 函数重载看参数列表(类型、数量、顺序),返回值不同不构成重载。
- 默认参数从右向左定义,声明和定义中只能指定一次。
- 递归函数必须有边界条件和递归式,缺一不可。
- 递归执行分“递推”和“回归”两个阶段,递推缩小规模,回归逐层返回。
- 递归不总是比迭代高效,递归深度过大可能导致栈溢出。
- 二维数组作为参数时,列数必须在编译期可知。
- 多练“三种传递方式判断”“重载/默认参数判断”“递归写结果”三类真题。
复习建议:先把三种参数传递的代码对比写熟,确保能一眼判断函数调用后实参是否改变;再练习重载和默认参数的选择题,重点理解“参数列表”和“从右向左”;最后攻克递归,从阶乘和斐波那契入手,手动模拟 f(4) 的完整递推和回归过程。考试中先看参数是值、引用还是指针,再看函数是重载还是默认参数,最后逐层跟踪变量变化。