Skip to content

第5章 函数 ​


🎯 本章学习目标 ​

通过本章的学习,你将能够:

  1. 理解函数作用:知道函数用于代码复用、模块化和提高可读性
  2. 掌握函数五要素:返回值类型、函数名、参数列表、函数体、return
  3. 区分声明与定义:理解函数声明的作用,知道什么时候需要声明
  4. 理解作用域:掌握全局作用域、局部作用域和块作用域
  5. 掌握变量遮蔽:理解同名变量的遮蔽规则,会用 :: 访问全局
  6. 掌握值传递:理解值传递是复制副本,不能修改原数据
  7. 掌握引用传递:理解引用是别名,能直接修改实参
  8. 理解静态变量:掌握 static 局部变量只初始化一次的特点
  9. 数组传参规则:知道数组传参退化为指针,二维数组列数不能省
  10. 识别函数错误:能找出函数声明缺失、返回局部引用等常见错误

🗺️ 本章知识图谱 ​

函数
├── 函数基础
│   ├── 函数的作用(复用/模块化)
│   ├── 定义五要素:返回值+名字+参数+体+return
│   ├── 函数声明(原型)
│   ├── 函数调用
│   └── main 函数是唯一入口
├── 作用域与生命周期
│   ├── 全局作用域
│   ├── 局部作用域
│   ├── 块作用域
│   ├── 同名变量遮蔽
│   ├── :: 作用域运算符
│   └── static 静态局部变量
├── 参数传递
│   ├── 值传递(复制副本,不改实参)
│   ├── 引用传递(别名,直接改实参)
│   ├── 指针传递(传地址,通过*改实参)
│   ├── 数组传参(退化为指针)
│   └── 二维数组传参(列数不能省)
├── 返回值
│   ├── return 语句
│   ├── void 函数(无返回值)
│   ├── 返回值类型匹配
│   ├── 不能返回局部变量的引用
│   └── 不能返回局部变量的指针
└── 常见考点
    ├── 值传递 vs 引用传递对比
    ├── 同名变量遮蔽(必考!)
    ├── 静态局部变量
    ├── 函数声明的必要性
    └── 数组传参退化

一、为什么需要函数? ​

想象一下:如果没有函数,一个程序就是一长串指令从上到下排列——重复的逻辑要写 N 遍,上万行代码挤在 main 里,调试时想找一段逻辑简直是噩梦。

函数(Function)就是解决这些问题的利器:

作用说明生活类比
代码复用一段逻辑写一次,到处调用菜谱写一次,每次照着做
模块化把大问题拆成小问题,各个击破造车分为发动机、底盘、车身
可读性给一段逻辑取个好名字,见名知意calcScore() 比 20 行计算代码好懂
便于调试函数独立测试,出问题快速定位哪个零件坏了换哪个

🔥 高频考点: 函数是 C++ 程序的基本组成单元。一个 C++ 程序由一个或多个函数组成,其中有且仅有一个 main 函数,它是程序执行的唯一入口。


二、函数的定义、调用与声明 ​

2.1 函数定义的五要素 ​

返回值类型  函数名(参数列表)
{
    函数体(若干语句)
    return 返回值;   // 有返回值时
}

逐部分拆解:

组成部分说明示例
返回值类型函数执行完返回什么类型的数据int、double、void
函数名函数的名字,遵循标识符命名规则add、calcArea
参数列表函数接收的输入,可以有 0 个或多个int a, int b
函数体{ } 包裹的执行语句具体逻辑
return 语句返回结果并结束函数return a + b;

一个完整的例子:

cpp
#include <iostream>
using namespace std;

// 定义一个求两个整数之和的函数
int add(int a, int b)   // 返回int,函数名add,接收两个int参数
{
    int sum = a + b;    // 函数体:计算逻辑
    return sum;         // 返回结果
}

int main() {
    int result = add(3, 5);   // 调用add函数
    cout << "3 + 5 = " << result << endl;
    return 0;
}
// 输出:3 + 5 = 8

⚠️ 易错点: 函数定义时,参数列表中每个参数都必须单独写类型。int add(int a, b) 是错误的!正确写法是 int add(int a, int b)。

2.2 函数声明与定义——一句话区分 ​

  • 声明(Declaration):告诉编译器"这个函数存在",只写函数头,以分号结尾。声明也叫函数原型(Prototype)。
  • 定义(Definition):告诉编译器"这个函数做什么",包含完整的函数体。
cpp
// 函数声明(原型)—— 告诉编译器这个函数的签名(返回值类型、函数名、参数列表)
// 不包含函数体,以分号结尾
int add(int a, int b);

// 函数定义 —— 真正实现
int add(int a, int b) {
    return a + b;
}

💡 声明中参数名可以省略:int add(int, int); 和 int add(int a, int b); 完全等价。

2.3 为什么需要声明? ​

C++ 编译器从上往下逐行编译。当它在 main 中看到 add(3, 5) 时,必须已经知道 add 函数的信息(返回值类型、参数个数和类型),否则会报错。

有两种解决方案:

方案一:函数定义放在调用之前(简单直接)

cpp
#include <iostream>
using namespace std;

int add(int a, int b) {   // 定义在前面
    return a + b;
}

int main() {
    cout << add(3, 5) << endl;  // 编译器已经认识add
    return 0;
}

方案二:先声明,后定义(实际项目中更常见)

cpp
#include <iostream>
using namespace std;

int add(int a, int b);   // 函数声明(原型),告诉编译器有这个函数

int main() {
    cout << add(3, 5) << endl;  // OK,编译器知道add的签名
    return 0;
}

int add(int a, int b) {   // 函数定义在后面
    return a + b;
}

🔥 高频考点: GESP 常考"函数声明缺失导致编译错误"。记住:调用函数前,编译器必须见过该函数的声明或定义。

2.4 无返回值函数(void)与无参函数 ​

当函数只需要执行操作、不需要返回结果时,返回值类型写 void。

cpp
#include <iostream>
using namespace std;

// void函数:打印问候语,不需要返回值
void sayHello(string name) {
    cout << "你好," << name << "!" << endl;
    // 不需要return,函数执行到末尾自动返回
}

// void函数中也可以用return提前结束
void checkAge(int age) {
    if (age < 0) {
        cout << "年龄不合法!" << endl;
        return;   // 提前结束函数,后面的代码不再执行
    }
    cout << "你的年龄是:" << age << endl;
}

// 无参函数:不需要输入
void printMenu() {
    cout << "====== 菜单 ======" << endl;
    cout << "1. 开始游戏" << endl;
    cout << "2. 退出游戏" << endl;
    cout << "==================" << endl;
}

int main() {
    sayHello("小明");
    checkAge(-5);    // 输出:年龄不合法!
    checkAge(18);    // 输出:你的年龄是:18
    printMenu();     // 调用无参函数,括号不能省略
    return 0;
}

⚠️ 易错点:

  • void 函数中可以写 return;(不带值,仅表示提前结束),但不能写 return 0; 或 return 值;。
  • 即使函数没有参数,调用时括号也不能省略。printMenu; 不会调用函数,必须写 printMenu();。

2.5 函数的嵌套调用 ​

函数可以调用其他函数,形成嵌套调用链。A 调 B、B 调 C,执行流程会一层层深入,再一层层返回。

cpp
#include <iostream>
using namespace std;

// 求平方
int square(int n) {
    return n * n;
}

// 求平方和:调用square
int sumOfSquares(int a, int b) {
    return square(a) + square(b);  // 嵌套调用
}

// 求三个数的平方和:调用sumOfSquares
int sumOfThreeSquares(int a, int b, int c) {
    return sumOfSquares(a, b) + square(c);  // 多层嵌套
}

int main() {
    cout << sumOfThreeSquares(1, 2, 3) << endl;
    // 执行过程:
    // main → sumOfThreeSquares(1,2,3)
    //        → sumOfSquares(1,2)
    //           → square(1) → 返回1
    //           → square(2) → 返回4
    //           → 返回5
    //        → square(3) → 返回9
    //        → 返回5+9=14
    // 输出:14
    return 0;
}

⚠️ 易错点: C++ 不允许在函数内部定义另一个函数(不能嵌套定义),但可以在函数内部调用另一个函数(嵌套调用是允许的)。GESP 常考这个区别!

2.6 main 函数的特殊角色 ​

特点说明
程序入口程序从 main 函数开始执行,有且仅有一个
返回值返回 int 类型,return 0 表示正常结束
标准签名int main()(四级用这个即可)

🔥 高频考点: main 函数的 return 0; 可以省略,编译器会自动补上。虽然可以省略,但建议显式写出 return 0;,这是良好的编程习惯。 但其他函数(返回值类型不是 void 时)不能省略返回值。


三、形参与实参 ​

3.1 基本定义 ​

概念全称定义出现位置
形参形式参数(Parameter)函数定义时括号中声明的变量,用于接收传入的值函数定义处
实参实际参数(Argument)函数调用时括号中给出的具体值或变量函数调用处
cpp
#include <iostream>
using namespace std;

// a 和 b 是形参——函数定义时声明
int add(int a, int b) {
    return a + b;
}

int main() {
    int x = 3, y = 5;
    // x 和 y 是实参——函数调用时传入
    cout << add(x, y) << endl;  // 输出 8

    // 常量、表达式也可以作为实参
    cout << add(10, 20) << endl;       // 输出 30
    cout << add(x + 1, y * 2) << endl; // 输出 14
    return 0;
}

💡 通俗理解: 形参就像餐厅菜单上的"占位菜名"(写的是"宫保鸡丁",不是真菜),实参就是你实际点的那盘菜(真正端上桌的那份)。

3.2 形参的"局部变量"本质 ​

形参在函数被调用时才创建,函数执行完毕后立即销毁。它本质上就是函数内部的局部变量。

cpp
#include <iostream>
using namespace std;

void func(int n) {
    // n 是形参,进入函数时创建,离开函数时销毁
    cout << "函数内 n = " << n << endl;
    n = 100;  // 修改的是形参 n,不影响外部实参(值传递)
    cout << "修改后 n = " << n << endl;
}

int main() {
    int x = 10;
    func(x);  // 调用时,用 x 的值"初始化"形参 n
    // 函数结束后,n 已销毁,x 仍然是 10
    cout << "主函数 x = " << x << endl;  // 输出 10
    return 0;
}

🔥 高频考点: 形参是函数的局部变量,函数调用时创建、结束时销毁。这是理解后续"值传递不影响实参"的基础。

3.3 参数匹配规则 ​

函数调用时,实参与形参必须满足三个匹配:

匹配规则说明常见错误
个数匹配实参个数必须与形参个数一致(有默认参数除外)少传或多传参数
类型匹配实参类型应与形参类型兼容,可发生隐式类型转换double 传给 int 会截断小数
顺序匹配实参按从左到右的顺序依次传给对应形参传参顺序写反
cpp
#include <iostream>
using namespace std;

void printInfo(string name, int age, double score) {
    cout << name << ", " << age << "岁, " << score << "分" << endl;
}

int main() {
    printInfo("小明", 15, 92.5);       // ✅ 正确:个数、类型、顺序都匹配
    // printInfo("小明", 15);          // ❌ 错误:少一个参数
    // printInfo("小明", 15, 92.5, 0); // ❌ 错误:多一个参数
    printInfo("小红", 95.5, 15);       // ⚠️ 编译可能通过,但语义错误!
    // 95.5 传给 age(int),被截断为 95;15 传给 score(double),变成 15.0
    // 输出:小红, 95岁, 15分——显然不对
    return 0;
}

⚠️ 易错点: 类型不匹配时,C++ 可能进行隐式类型转换(如 double → int),编译不一定报错,但结果可能出错。考试中常给你一段有隐式转换的代码,让你判断输出。

3.4 默认参数 ​

C++ 允许为形参指定默认值,调用时如果不传该参数,就使用默认值。

cpp
#include <iostream>
using namespace std;

// b 的默认值为 10,c 的默认值为 100
int power(int a, int b = 10, int c = 100) {
    return a + b + c;
}

int main() {
    cout << power(1) << endl;        // 1 + 10 + 100 = 111
    cout << power(1, 2) << endl;     // 1 + 2 + 100 = 103
    cout << power(1, 2, 3) << endl;  // 1 + 2 + 3 = 6
    return 0;
}

🔥 核心规则:默认参数必须从右向左依次定义,不能间隔!

cpp
// ✅ 正确:默认参数从最右边连续定义
void f1(int a, int b = 1, int c = 2);

// ❌ 错误:b 没有默认值,c 却有——中间"断档"了
void f2(int a, int b = 1, int c);

// ❌ 错误:a 有默认值但 b 没有
void f3(int a = 1, int b);

默认参数通常放在**函数声明(原型)**中,定义中不再重复写:

cpp
#include <iostream>
using namespace std;

// 函数声明中指定默认参数
void greet(string name, string greeting = "你好");

int main() {
    greet("小明");           // 使用默认值
    greet("小红", "早上好");  // 覆盖默认值
    return 0;
}

// 函数定义中不再重复写默认值
void greet(string name, string greeting) {
    cout << greeting << "," << name << "!" << endl;
}

⚠️ 易错点: 同一个默认参数不能在声明和定义中重复指定,否则编译报错。声明写了默认值,定义里就不能再写。


四、作用域 ​

4.1 什么是作用域? ​

作用域(Scope) 是指程序中变量名、函数名等标识符的有效可见范围。一个变量并非在整个程序的任何地方都能访问,它只能在自己的作用域内使用。

打个比方:作用域就像房间的门——变量在自己的房间里能被看到,走出房间就看不到了。

4.2 局部作用域与全局作用域 ​

在函数内部或代码块 {} 中声明的变量,具有局部作用域(块作用域);在所有函数外部声明的变量,具有全局作用域。

cpp
#include <iostream>
using namespace std;

int g_count = 0;  // 全局变量,所有函数都能访问

void increment() {
    g_count++;  // ✅ 可以访问全局变量
}

void func() {
    int a = 10;  // a 是 func 的局部变量
    cout << a << endl;  // ✅ 可以访问
}

int main() {
    increment();
    increment();
    cout << g_count << endl;  // 输出 2
    // cout << a << endl;     // ❌ 编译错误!a 在 func 中定义,main 中不可见

    int x = 5;
    {
        int y = 20;       // y 只在这个 {} 块内有效
        cout << x << endl; // ✅ 内层可以访问外层的 x
        cout << y << endl; // ✅ 可以访问 y
    }
    // cout << y << endl;  // ❌ 编译错误!y 已超出作用域
    return 0;
}

局部变量与全局变量的核心区别:

特性局部变量全局变量
声明位置函数内或块内所有函数外部
作用域从声明到块结束从声明到文件结束
默认初值未初始化(值不确定)自动初始化为 0
生命周期函数/块执行期间整个程序运行期间
存储位置栈(stack)全局数据区

🔥 高频考点: 全局变量如果不显式初始化,系统会自动初始化为 0;而局部变量如果不初始化,值是不确定的(垃圾值)!考试常考"以下程序输出什么",关键就是看变量有没有被正确初始化。

4.3 同名变量的遮蔽(Shadowing) ​

当局部变量与全局变量同名时,在局部作用域内,局部变量会遮蔽(隐藏)全局变量。这是 GESP 四级最高频的选择题考点之一!

cpp
#include <iostream>
using namespace std;

int x = 100;  // 全局变量

int main() {
    int x = 200;  // 局部变量,与全局变量同名
    cout << x << endl;    // 输出 200——局部变量遮蔽了全局变量

    {
        int x = 300;  // 块内变量,再次遮蔽
        cout << x << endl;  // 输出 300
    }

    cout << x << endl;    // 输出 200——块内 x 已销毁,回到 main 的 x
    cout << ::x << endl;  // 输出 100——::x 表示全局作用域的 x
    return 0;
}
// 程序输出:
// 200
// 300
// 200
// 100

遮蔽规则:

  • 内层作用域的同名变量会遮蔽外层作用域的同名变量
  • "就近原则":当前作用域有,就用当前的;没有才往外找
  • 内层块结束后,外层同名变量恢复可见

如果想在局部变量遮蔽全局变量时仍然访问全局变量,可以使用作用域运算符 :::

cpp
cout << ::x << endl;  // :: 前面什么都不写,表示"最外层"的全局作用域

⚠️ 解题技巧: 遇到同名变量题目,画一张"作用域嵌套图",从内到外逐层标注每个 cout 能看到哪个变量。关键看三件事:①当前在哪个 {} 里;②当前作用域有没有同名变量;③有没有用 :: 强制访问全局。

4.4 static 静态局部变量 ​

用 static 修饰的局部变量称为静态局部变量,它有三个关键特征:

  1. 只初始化一次——程序开始执行时初始化,不是每次函数调用都初始化
  2. 值保留——函数调用结束后不销毁,下次调用继续使用上次的值
  3. 作用域不变——仍然只能在该函数内访问,但生命周期是整个程序
cpp
#include <iostream>
using namespace std;

void counter() {
    static int count = 0;  // 静态局部变量,只初始化一次
    count++;
    cout << "第" << count << "次调用" << endl;
}

int main() {
    counter();  // 输出:第1次调用
    counter();  // 输出:第2次调用(count 保留了上次的值)
    counter();  // 输出:第3次调用
    return 0;
}

对比普通局部变量,差异一目了然:

cpp
#include <iostream>
using namespace std;

void test() {
    static int s = 0;  // 只在第一次调用时执行
    int a = 0;         // 每次调用都执行
    s++;
    a++;
    cout << "s = " << s << ", a = " << a << endl;
}

int main() {
    test();  // s = 1, a = 1
    test();  // s = 2, a = 1(a 重新初始化,s 保留)
    test();  // s = 3, a = 1
    return 0;
}
特性auto 局部变量static 静态局部变量全局变量
作用域块内块内全局
生命周期块执行期间整个程序整个程序
初始化时机每次进入块时仅第一次进入时程序开始前
未初始化时值不确定自动初始化为 0自动初始化为 0
函数调用间是否保留值❌✅✅

🔥 高频考点: static 局部变量是 GESP 四级常考知识点。记住三个关键词:只初始化一次、值保留、作用域不变。考试常给出含 static 变量的函数多次调用,让你判断输出。


五、参数传递 ​

调用函数时,我们把实参的值传给形参。关键问题来了:函数内对形参的修改,会不会影响实参?答案取决于传递方式。

GESP 四级考纲要求掌握值传递和引用传递两种方式。指针传递作为值传递的特殊情况,我们也简要提及。

5.1 值传递(Pass by Value) ​

值传递时,系统会复制一份实参的值给形参。形参和实参是两个独立的变量,存放在不同的内存位置。

实参 a = 3  ──复制──▶  形参 x = 3
(地址 0x100)          (地址 0x200)

在函数内修改形参,改的是副本,实参完全不受影响。

cpp
#include <iostream>
using namespace std;

// ❌ 值传递:无法交换两个变量
void swapByValue(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    cout << "函数内:a = " << a << ", b = " << b << endl;
}

int main() {
    int x = 3, y = 7;
    cout << "调用前:x = " << x << ", y = " << y << endl;

    swapByValue(x, y);

    cout << "调用后:x = " << x << ", y = " << y << endl;
    return 0;
}
// 输出:
// 调用前:x = 3, y = 7
// 函数内:a = 7, b = 3
// 调用后:x = 3, y = 7   ← 没有交换!

为什么失败? 调用 swapByValue(x, y) 时,x 和 y 的值被复制给了 a 和 b。函数内交换的是 a、b 这两个副本,x、y 本身毫无变化。

⚠️ 易错点: 值传递是 C++ 的默认传递方式。只要形参类型不是引用(没有 &)也不是指针(没有 *),就是值传递。考试中最常见的陷阱就是给一个值传递的 swap 函数,问你实参有没有被交换。

5.2 引用传递(Pass by Reference) ​

引用(Reference)就是给已有的变量起一个别名。对引用的操作就是对原变量的操作。

cpp
int a = 10;
int& ref = a;  // ref 是 a 的别名(引用)

ref = 20;      // 等价于 a = 20
cout << a;     // 输出 20

💡 引用的注意事项:

  1. 必须初始化——定义引用时必须指明它是谁的别名
  2. 不能改绑定——引用一旦绑定到一个变量,就不能再绑定到另一个变量
cpp
int& ref;        // ❌ 编译错误!引用必须初始化
int a = 1, b = 2;
int& r = a;
r = b;           // 这是把 b 的值赋给 a,不是让 r 改绑到 b
cout << a;       // 输出 2,a 被修改了

引用传递时,形参是实参的别名,它们共享同一块内存。对形参的任何操作,直接作用于实参。

cpp
#include <iostream>
using namespace std;

// ✅ 引用传递:可以交换两个变量
void swapByRef(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 3, y = 7;
    cout << "调用前:x = " << x << ", y = " << y << endl;

    swapByRef(x, y);  // a 是 x 的别名,b 是 y 的别名

    cout << "调用后:x = " << x << ", y = " << y << endl;
    return 0;
}
// 输出:
// 调用前:x = 3, y = 7
// 调用后:x = 7, y = 3   ← 交换成功!

🔥 高频考点: 引用传递的标志是形参类型中的 & 符号。看到 int& a 就知道这是引用传递,函数内对 a 的修改会影响实参。选择题中经常对比值传递和引用传递的输出,一定要看清楚有没有 &!

5.3 值传递 vs 引用传递——核心对比 ​

对比项值传递引用传递
形参写法int aint& a
调用写法func(x)func(x)
底层机制复制实参的值实参的别名
修改形参不影响实参直接修改实参
实参要求可以是值、变量、表达式必须是变量
语法复杂度最简单简洁
适用场景只需读取,不需修改需修改实参、避免大对象复制

📌 记忆口诀: 看形参有没有 &——没有就是值传递(改了白改),有 & 是引用传递(直接改)。

来看一个混合传递的经典例子,这是考试中最常见的考法:

cpp
#include <iostream>
using namespace std;

void func(int a, int& b) {  // a 值传递,b 引用传递
    a++;
    b++;
}

int main() {
    int x = 10, y = 20;
    func(x, y);
    cout << x << " " << y;  // 输出 10 21
    return 0;
}
// 解析:
// a 是值传递,a++ 改的是副本,x 保持 10
// b 是引用传递,b++ 直接改实参 y,y 变为 21

5.4 指针传递(值传递的特殊情况) ​

考纲没有单列指针传递,但作为值传递的一种特殊形式,了解它有助于加深理解。

指针传递时,实参传递的是变量的内存地址。形参是一个指针变量,存放的是这个地址。通过解引用(*)操作,可以访问并修改地址对应的实参。

cpp
#include <iostream>
using namespace std;

// ✅ 指针传递:可以交换两个变量
void swapByPtr(int* a, int* b) {
    int temp = *a;  // 解引用:取 a 指向的值
    *a = *b;        // 将 b 指向的值赋给 a 指向的变量
    *b = temp;      // 将 temp 赋给 b 指向的变量
}

int main() {
    int x = 3, y = 7;
    swapByPtr(&x, &y);  // 传入 x 和 y 的地址
    cout << x << " " << y;  // 输出 7 3
    return 0;
}

⚠️ 深入理解: 指针传递时,形参本身也是值传递(复制了一个地址)。但因为复制的是地址,通过地址可以找到实参,所以能修改实参。如果在函数内修改指针本身(让它指向别的地方),不会影响外部的指针变量;但通过 *p 修改指向的内容,会影响实参。

5.5 数组作为函数参数 ​

数组作为函数参数时,会退化为指针——传递的不是整个数组,而是数组首元素的地址。

cpp
#include <iostream>
using namespace std;

void printSize(int arr[]) {
    cout << sizeof(arr) << endl;  // 输出 8(64位系统指针大小),不是数组总大小!
}

int main() {
    int nums[5] = {1, 2, 3, 4, 5};
    cout << sizeof(nums) << endl; // 输出 20(5 × 4字节)
    printSize(nums);
    return 0;
}

⚠️ 易错点: 数组传参退化为指针,所以函数内 sizeof(arr) 得到的是指针大小,不是数组长度。因此数组传参时通常需要额外传入数组长度。

正因为数组传参退化为指针,函数内修改 arr[i] 会直接修改原数组的元素:

cpp
#include <iostream>
using namespace std;

// 一维数组传参:三种等价写法
int sumArray(int arr[], int n) {   // 写法1:int arr[]
// int sumArray(int arr[5], int n) // 写法2:数组大小会被忽略
// int sumArray(int* arr, int n)   // 写法3:int* arr(最本质)
    int s = 0;
    for (int i = 0; i < n; i++) {
        arr[i] *= 2;   // 直接修改原数组!
        s += arr[i];
    }
    return s;
}

int main() {
    int nums[4] = {1, 2, 3, 4};
    int total = sumArray(nums, 4);  // total = 2+4+6+8 = 20

    for (int i = 0; i < 4; i++) {
        cout << nums[i] << " ";  // 输出 2 4 6 8(原数组已被修改)
    }
    cout << endl << total;       // 输出 20
    return 0;
}

二维数组传参时,必须指定第二维(列数),因为编译器需要知道每行有多少个元素才能正确计算地址。

cpp
// ✅ 正确:必须指定列数
void printMatrix(int arr[][3], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 3; j++) {
            cout << arr[i][j] << " ";
        }
        cout << endl;
    }
}

// ❌ 错误:不能省略列数
// void badFunc(int arr[][], int rows, int cols) { }

🔥 高频考点: 二维数组传参必须指定列数!考试中常出现 void func(int arr[][], int n) 让你判断对错——这是错误的。正确写法是 int arr[][列数]。


六、返回值与常见陷阱 ​

6.1 return 语句的执行机制 ​

return 会立即结束函数并返回值,return 之后的语句不会执行。

cpp
#include <iostream>
using namespace std;

int check(int n) {
    if (n < 0) {
        return -1;          // 提前返回,后面的代码不执行
    }
    cout << "n 是非负数" << endl;
    return n * 2;
}

int main() {
    cout << check(-5) << endl;  // 输出 -1(不会打印"n是非负数")
    cout << check(3) << endl;   // 先打印"n是非负数",再输出 6
    return 0;
}

6.2 ⚠️ 不能返回局部变量的引用或指针 ​

这是函数部分最经典的陷阱题!

cpp
#include <iostream>
using namespace std;

// ❌ 危险:返回局部变量的引用
int& badReturn() {
    int x = 10;       // 局部变量,函数结束后被销毁
    return x;         // 返回了一个"已死亡"变量的别名——悬空引用!
}

// ❌ 危险:返回局部变量的指针
int* badReturn2() {
    int x = 10;
    return &x;        // 同样危险!
}

// ✅ 安全:返回静态变量的引用
int& safeReturn() {
    static int x = 10;  // 静态变量不会随函数结束销毁
    return x;
}

int main() {
    int& ref = safeReturn();
    cout << ref << endl;  // 输出 10
    return 0;
}

⚠️ 核心原因: 局部变量在函数返回时内存就被回收了。如果返回它的引用或指针,外部拿到的是一个指向"已释放内存"的悬空引用/野指针,读取它的值是未定义行为,可能得到乱码或导致程序崩溃。

6.3 常见错误汇总 ​

错误类型错误示例正确做法
返回值类型不匹配int f() { return 3.14; }(截断为3)保持类型一致
void 函数 return 带值void f() { return 0; }return; 或不写
函数声明缺失定义在调用之后且未声明提前声明或调整顺序
实参个数不匹配add(1) 但需要两个参数传够参数或用默认参数
嵌套定义函数在 main 里写 void g(){}函数定义放在外部
返回局部变量引用int& f() { int x; return x; }返回值或用 static

七、📝 实战练习 ​

练习题 1(单选题) ​

题目: 以下程序运行后的输出是( )

cpp
#include <iostream>
using namespace std;

void func(int a, int& b) {
    a++;
    b++;
}

int main() {
    int x = 10, y = 20;
    func(x, y);
    cout << x << " " << y;
    return 0;
}

A. 10 20
B. 11 21
C. 10 21
D. 11 20

🔑 点击查看答案与解析

答案: C

解析:

  • 形参 a 是值传递(无 &),a++ 修改的是副本,实参 x 保持 10
  • 形参 b 是引用传递(有 &),b++ 直接修改实参 y,y 变为 21
  • 因此输出 10 21,选 C

练习题 2(单选题) ​

题目: 以下程序运行后的输出是( )

cpp
#include <iostream>
using namespace std;

int x = 5;

int main() {
    int x = 10;
    cout << x << " ";
    {
        int x = 20;
        cout << x << " ";
    }
    cout << x << " ";
    cout << ::x << endl;
    return 0;
}

A. 10 20 10 5
B. 10 20 20 5
C. 5 20 10 5
D. 10 20 10 10

🔑 点击查看答案与解析

答案: A

解析:

  • 第一个 cout << x:在 main 作用域中,局部 x = 10,输出 10
  • 第二个 cout << x:在内层 {} 中,块内 x = 20 遮蔽外层,输出 20
  • 第三个 cout << x:块已结束,块内 x 销毁,回到 main 的 x = 10,输出 10
  • 第四个 cout << ::x::: 强制访问全局变量,全局 x = 5,输出 5

输出 10 20 10 5,选 A。


练习题 3(单选题) ​

题目: 以下程序运行后的输出是( )

cpp
#include <iostream>
using namespace std;

void func(int a) {
    static int s = 0;
    s += a;
    cout << s << " ";
}

int main() {
    func(10);
    func(20);
    func(30);
    return 0;
}

A. 10 30 60
B. 10 20 30
C. 10 20 60
D. 60 60 60

🔑 点击查看答案与解析

答案: A

解析:

  • s 是静态局部变量,只初始化一次(值为 0),之后每次调用保留上次的值
  • 第一次调用 func(10):s = 0 + 10 = 10,输出 10
  • 第二次调用 func(20):s = 10 + 20 = 30,输出 30
  • 第三次调用 func(30):s = 30 + 30 = 60,输出 60

输出 10 30 60,选 A。


练习题 4(程序阅读题) ​

题目: 阅读以下程序,写出运行结果。

cpp
#include <iostream>
using namespace std;

void modify(int arr[], int n, int& sum) {
    sum = 0;
    for (int i = 0; i < n; i++) {
        arr[i] = arr[i] * 2;
        sum += arr[i];
    }
}

int main() {
    int nums[4] = {1, 2, 3, 4};
    int total;
    modify(nums, 4, total);

    for (int i = 0; i < 4; i++) {
        cout << nums[i] << " ";
    }
    cout << endl << total;
    return 0;
}
🔑 点击查看答案与解析

答案:

2 4 6 8
20

解析:

  1. 数组传参退化为指针:arr 接收 nums 首元素地址,函数内 arr[i] *= 2 直接修改原数组,nums 变为 {2, 4, 6, 8}
  2. 引用传递:sum 是引用传递(int& sum),函数内对 sum 的赋值和累加直接修改实参 total,2+4+6+8 = 20

💡 做题技巧: 本题同时考了数组传参和引用传递。数组传参时函数内修改元素会影响原数组(因为退化为指针),引用传递会修改实参。两个知识点叠加是四级考试的常见考法。


练习题 5(判断题) ​

题目: 判断以下说法是否正确。

(1)值传递时,在函数内修改形参的值不会影响实参。( )

(2)引用传递时,实参可以是常量或表达式。( )

(3)C++ 允许在一个函数内部定义另一个函数。( )

(4)静态局部变量在每次函数调用时都会重新初始化。( )

(5)二维数组作为函数参数时,可以省略第二维的大小。( )

🔑 点击查看答案与解析

答案:

题号判断解析
(1)✅ 正确值传递复制副本,修改形参不影响实参
(2)❌ 错误引用传递要求实参必须是变量,不能是常量或表达式(因为需要可取地址、可修改)
(3)❌ 错误C++ 不允许函数嵌套定义,但允许嵌套调用
(4)❌ 错误静态局部变量只初始化一次,后续调用保留上次的值
(5)❌ 错误二维数组传参必须指定列数(第二维),否则编译错误

练习题 6(单选题) ​

题目: 以下关于函数声明的说法,错误的是( ) A. 函数声明告诉编译器函数的名字、返回值类型和参数类型
B. 如果函数定义在调用之前,可以不写声明
C. 函数声明必须包含函数体
D. 函数声明也叫函数原型

🔑 点击查看答案与解析

答案: C

解析:

  • A 正确 ✅:函数声明的作用就是告诉编译器函数的接口信息
  • B 正确 ✅:函数定义在调用之前,编译器已经知道函数信息,不需要额外声明
  • C 错误 ❌:函数声明只有函数头,没有函数体,以分号结尾。函数定义才有函数体
  • D 正确 ✅:函数声明又称函数原型(Function Prototype)

🔥 高频考点:函数定义在调用之后时,必须在调用之前写函数声明,否则编译报错。


练习题 7(判断题) ​

题目: 函数可以返回局部变量的引用,因为引用只是个别名。( )

🔑 点击查看答案与解析

答案: 错误 ❌

解析: 局部变量在函数返回时就被销毁了,内存被回收。如果返回它的引用,外部拿到的是一个"悬空引用"——指向已经被释放的内存。访问这样的引用是未定义行为,可能得到乱码或崩溃。

🍎 生活比喻:就像你租了间房,退租后还把钥匙给朋友。朋友拿着钥匙去开门,房子已经不属于你了,里面可能被清空了,也可能住进了别人。


练习题 8(编程题) ​

题目: 请编写一个函数 bool isPrime(int n),判断 n 是否为素数。在主函数中输入一个整数,输出"YES"或"NO"。

提示:素数是只能被1和自身整除的大于1的自然数。

🔑 点击查看答案与解析

参考答案:

cpp
#include <iostream>
using namespace std;

bool isPrime(int n) {
    if (n <= 1) 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;
}

解析:

素数判断函数是函数的经典应用:

  1. 如果 n ≤ 1,直接返回 false(不是素数)
  2. 从 2 遍历到 √n,如果有一个数能整除 n,说明不是素数
  3. 循环结束都没找到因子,说明是素数,返回 true

💡 优化技巧:只需要检查到 √n,而不是 n-1。因为如果 n 有一个大于 √n 的因子,那对应的另一个因子一定小于 √n。


八、📊 考点总结表 ​

考点重要程度出题形式易错点
函数五要素与 return⭐⭐⭐⭐选择/程序阅读void函数不能return带值
函数声明与定义的区别⭐⭐⭐⭐⭐选择/找错定义在调用后必须先声明
作用域与变量遮蔽⭐⭐⭐⭐⭐程序阅读(必考)内层变量遮蔽外层,::访问全局
static 静态局部变量⭐⭐⭐⭐程序阅读选择题只初始化一次,保留上次值
值传递机制⭐⭐⭐⭐⭐选择/判断/程序阅读改副本不改原数据
引用传递与 & 识别⭐⭐⭐⭐⭐选择/判断/程序阅读看形参有没有&,有就是引用
值传递 vs 引用传递⭐⭐⭐⭐⭐选择/判断混合传递时逐个分析每个参数
数组传参退化为指针⭐⭐⭐⭐选择/程序填空函数内修改元素会影响原数组
二维数组传参列数⭐⭐⭐判断/选择列数不能省,行数可以省
返回局部变量引用⭐⭐⭐⭐选择/判断局部变量函数结束就销毁了

⭐ 必背速记卡 ​

┌─────────────────────────────────────────────┐
│            函数 · 必背速记卡                 │
├─────────────────────────────────────────────┤
│  1. 函数五要素:返回值+名字+参数+体+return   │
│  2. main 函数是程序执行的唯一入口            │
│  3. 定义在调用后,必须先声明(函数原型)     │
│  4. 全局变量:所有函数都能用,默认初始化为0   │
│  5. 局部变量:只在函数内有效,不初始化随机值 │
│  6. 同名变量:内层遮蔽外层,::访问全局       │
│  7. static 局部变量:只初始化一次,保留上次值│
│  8. 值传递:复制副本,改了白改               │
│  9. 引用传递:别名,直接改原数据(有&)      │
│ 10. 引用必须初始化,不能改绑定               │
│ 11. 数组传参退化为指针,修改元素影响原数组   │
│ 12. 二维数组做参数:列不能省,行可以省       │
│ 13. void 函数:无返回值,return 不带值       │
│ 14. 不能返回局部变量的引用或指针(悬空)     │
│ 15. 函数不能嵌套定义,但可以嵌套调用         │
└─────────────────────────────────────────────┘

百炼成钢,融会贯通