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 参数传递相关

  1. 值传递误以为能修改实参:void f(int x) { x = 100; } 调用后实参不变。
  2. 引用传递语法错误:声明引用参数时 & 与变量名之间无空格要求,但 int & x 和 int& x 等价。
  3. 指针传递忘记解引用:void f(int *p) { p = ...; } 只改了函数内的局部指针,不影响外面;*p = ...; 才修改指向的值。
  4. 引用必须初始化:int &r; 是错误的,引用定义时必须绑定到一个有效变量。

7.2 重载与默认参数相关

  1. 仅返回值不同不构成重载:int f(); double f(); 编译错误。
  2. 默认参数从右向左:void f(int a = 1, int b); 错误。
  3. 默认参数只能指定一次:声明和定义中重复指定会编译错误。
  4. 默认参数导致歧义:重载与默认参数同时使用时,可能出现调用歧义。

7.3 递归相关

  1. 缺少边界条件:无限递归导致栈溢出。
  2. 递归深度过大:即使有边界条件,过深的递归也可能导致栈溢出。
  3. 递归效率低于迭代:不要盲目认为递归更优。

7.4 作用域相关

  1. 局部变量遮蔽全局变量:函数内的同名局部变量会暂时“屏蔽”全局变量。
  2. :: 访问全局变量:::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四级从“写代码”到“设计代码”的关键跨越。

  1. 四级函数的核心是参数传递:值传递拷贝、引用传递别名、指针传递传地址。
  2. 值传递不影响实参,引用传递和指针传递都能修改实参。
  3. 引用必须在定义时初始化,且一旦绑定不能更换目标。
  4. 函数重载看参数列表(类型、数量、顺序),返回值不同不构成重载。
  5. 默认参数从右向左定义,声明和定义中只能指定一次。
  6. 递归函数必须有边界条件和递归式,缺一不可。
  7. 递归执行分“递推”和“回归”两个阶段,递推缩小规模,回归逐层返回。
  8. 递归不总是比迭代高效,递归深度过大可能导致栈溢出。
  9. 二维数组作为参数时,列数必须在编译期可知。
  10. 多练“三种传递方式判断”“重载/默认参数判断”“递归写结果”三类真题。

复习建议:先把三种参数传递的代码对比写熟,确保能一眼判断函数调用后实参是否改变;再练习重载和默认参数的选择题,重点理解“参数列表”和“从右向左”;最后攻克递归,从阶乘和斐波那契入手,手动模拟 f(4) 的完整递推和回归过程。考试中先看参数是值、引用还是指针,再看函数是重载还是默认参数,最后逐层跟踪变量变化。