Appearance
第5章 函数
🎯 本章学习目标
通过本章的学习,你将能够:
- 理解函数作用:知道函数用于代码复用、模块化和提高可读性
- 掌握函数五要素:返回值类型、函数名、参数列表、函数体、return
- 区分声明与定义:理解函数声明的作用,知道什么时候需要声明
- 理解作用域:掌握全局作用域、局部作用域和块作用域
- 掌握变量遮蔽:理解同名变量的遮蔽规则,会用
::访问全局 - 掌握值传递:理解值传递是复制副本,不能修改原数据
- 掌握引用传递:理解引用是别名,能直接修改实参
- 理解静态变量:掌握
static局部变量只初始化一次的特点 - 数组传参规则:知道数组传参退化为指针,二维数组列数不能省
- 识别函数错误:能找出函数声明缺失、返回局部引用等常见错误
🗺️ 本章知识图谱
函数
├── 函数基础
│ ├── 函数的作用(复用/模块化)
│ ├── 定义五要素:返回值+名字+参数+体+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 修饰的局部变量称为静态局部变量,它有三个关键特征:
- 只初始化一次——程序开始执行时初始化,不是每次函数调用都初始化
- 值保留——函数调用结束后不销毁,下次调用继续使用上次的值
- 作用域不变——仍然只能在该函数内访问,但生命周期是整个程序
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💡 引用的注意事项:
- 必须初始化——定义引用时必须指明它是谁的别名
- 不能改绑定——引用一旦绑定到一个变量,就不能再绑定到另一个变量
cppint& 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 a | int& 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 变为 215.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解析:
- 数组传参退化为指针:
arr接收nums首元素地址,函数内arr[i] *= 2直接修改原数组,nums 变为{2, 4, 6, 8} - 引用传递:
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;
}解析:
素数判断函数是函数的经典应用:
- 如果 n ≤ 1,直接返回 false(不是素数)
- 从 2 遍历到 √n,如果有一个数能整除 n,说明不是素数
- 循环结束都没找到因子,说明是素数,返回 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. 函数不能嵌套定义,但可以嵌套调用 │
└─────────────────────────────────────────────┘