NOI C++ 课程体系 / 语法入门 / 第4讲
CSP-J 入门级 · 语法入门 · 传值传引用与作用范围专题

C++ 传值参数与传引用参数、变量的作用范围 完全指南

函数传参的两种方式——传值(拷贝)与传引用(别名),以及变量在哪里声明就在哪里生效的作用范围规则,一次讲透。

6 大教学模块 2 大传参方式精讲 6 个精讲案例 5 大易错点 1 张速查表 15 道分层练习
本讲 · 第 18/56 讲传值参数与传引用参数、变量作用范围
模块 01

情景导航

用"寄快递"的故事建立直觉,理解传值(寄复印件)和传引用(寄原件)的本质区别,再给出变量作用范围的准确定义和一句好记的口诀。

故事故事引入 —— 寄快递的烦恼

小强要参加 NOI 编程比赛,老师要求每个人提交一份程序源码。小强写好后,想把文件发给三个同学检查。

他想了两种办法:

办法一:复制三份再寄。小强把源码文件复制了 3 份,每份发给一个同学。同学 A 在文件里加了注释,同学 B 改了一个变量名,同学 C 删了一行代码——但小强自己电脑上的原始文件一点没变,因为别人改的都是副本。

办法二:直接发原件。小强把原始文件直接发给三个同学,谁拿到就先改。同学 A 改了之后,文件就变成了改过的版本,同学 B 拿到的就是已经被改过的文件了。

"复制后寄的是副本,改别人的不改自己的——这就是传值;直接寄原件,谁改都影响原件——这就是传引用!"

老师笑着揭秘:函数传参的时候,C++ 默认就是"复制一份副本"传给函数(传值)。函数里怎么改副本,原变量都不会受影响。但如果用引用传递(传引用),函数拿到的就是原件的"别名",一改全改。

💡 这个故事告诉我们什么?

两种寄件方式有三个关键区别:

一、副本(传值):函数里改的是副本,原变量不变;原件(传引用):函数里改的就是原件本身,原变量跟着变。
二、副本要花时间复制大文件(效率低),原件直接给引用(效率高);
三、副本安全(不会误改),原件危险(一不小心就改了不该改的)。

程序里的函数传参也是如此。接下来我们就把"寄副本"和"寄原件"翻译成 C++ 语法。

类比生活类比 —— 寄快递、复印、日记本与共享文档

把传值、传引用和作用范围三个概念和身边的场景对一对号:

📦

复印文件 = 传值

复印后给别人改,原件不动。函数里改的是副本。

对应:void func(int x) { x = 10; }
📧

共享文档 = 传引用

多人同时编辑同一份文档,谁改大家都看到。函数里改的就是原件。

对应:void func(int& x) { x = 10; }
📝

教室里的名字 = 局部变量

在教室里叫"小明",只有教室里的人听得见。出了教室就没人知道了。

对应:函数内部声明的变量
🏟

全校广播 = 全局变量

全校广播站说的话,全校每个教室都能听到。全局变量所有函数都能用。

对应:函数外部声明的变量
定义什么是传值参数与传引用参数

函数定义时括号里的变量叫形参(形式参数),调用函数时传进去的实际值叫实参(实际参数)。C++ 提供了两种把实参交给形参的方式:

传参方式形参是什么函数内改形参会影响实参吗效率
传值(值传递)int x不会(改的是副本)大数据类型较慢
传引用(引用传递)int& x会(改的就是原件)不复制,效率高

C++ 默认是传值。想让函数修改实参的值,必须在形参前加 & 符号,声明为引用参数。

变量的作用范围(Scope)是指:变量在程序的哪个区域内"有效"、可以被访问。简单说就是——在哪里声明,就在哪里生效。

定义变量的作用范围

作用范围决定了变量"在哪里能被看见"。C++ 中有两种主要的作用范围:

变量类型声明位置作用范围生命周期
局部变量函数内部(或代码块内)从声明处到所在代码块结束函数调用时创建,返回时销毁
全局变量所有函数外部从声明处到文件末尾,所有函数都能访问程序运行期间一直存在

还有一个重要规则:局部变量会"遮蔽"全局变量——如果函数内有一个和全局变量同名的局部变量,函数内访问这个名字时用的是局部变量,全局变量被"遮住"了。

传参与作用范围口诀 传值拷贝副本去,函数改它我不变;
传引用给原件别名,函数一改全跟着变;
局部变量函数内,全局变量到处见;
同名局部遮全局,内层就近遮外层。

口诀里藏着本课的全部主角:传值、传引用、局部变量、全局变量。下面我们就把它们一个一个请出来,看清各自的"内部构造"和"执行路线"。

模块 02

知识探究

逐一拆解传值参数、传引用参数的语法和执行过程,再看作用范围规则和 6 个由浅入深的案例把常见写法一次练熟。

1传值参数(值传递)—— 副本独立,互不影响

传值(Pass by Value)是 C++ 的默认传参方式。调用函数时,实参的值被复制一份传给形参。函数里操作的是副本,对原实参没有任何影响。

void swap(int a, int b)
void:返回值类型(无返回值) swap:函数名 int a, int b:形参,传值(拷贝副本)
要点说明
形参是副本函数调用时,实参的值被复制给形参,形参和实参是两个不同的变量
函数内改形参不影响实参在函数里修改 a、b 的值,只是改了副本,原实参不变
形参随函数结束而销毁函数返回后,形参变量消失,副本被释放
基本类型传值没问题int、char、double 等小类型复制开销小,传值完全够用
大对象传值效率低数组、结构体等大对象复制耗时耗空间,应优先用传引用
传值过程
  • 调用函数时复制实参值
  • 副本进入函数体操作
  • 函数返回,副本销毁
  • 实参值保持不变
形象比喻
  • 复印文件后给别人改
  • 别人改的是复印件
  • 原件始终不动
  • 安全但浪费纸

一句话记住:传值就像复印文件——函数改的是复印件,原件纹丝不动。

2传引用参数(引用传递)—— 别名共享,一改全改

传引用(Pass by Reference)在形参前加 & 符号。形参不再是副本,而是实参的别名(别名 = 另一个名字)。函数里操作形参,就是在操作实参本身。

void swap(int& a, int& b)
void:返回值类型 swap:函数名 int& a, int& b:引用形参,是实参的别名
要点说明
形参是别名形参和实参指向同一个内存地址,是"同一个人"的两个名字
函数内改形参 = 改实参在函数里修改 a、b 的值,实参的值同步改变
不产生副本没有复制开销,对大对象(数组、结构体)效率极高
必须传变量引用形参必须绑定到变量,不能传常量或表达式(如 swap(1, 2) 是错的)
const 引用如果只想读取不想修改,可用 const int& x,既高效又安全

一句话记住:传引用就像给文件起别名——函数改别名就是改原件,一改全改。

3传值 vs 传引用 —— 一张表看明白
传值(int x)
  • 复制一份副本传给函数
  • 函数内改形参不影响实参
  • 安全,不会误改数据
  • 基本类型(int/double/char)推荐用传值
  • 大对象(数组/结构体)传值效率低
传引用(int& x)
  • 不复制,给形参一个别名
  • 函数内改形参 = 改实参
  • 效率高,不产生副本
  • 需要修改实参时用
  • 大对象传引用避免复制开销
  • 注意:可能误改实参,要谨慎使用
选择要诀:不需要改实参就用传值(安全),需要改实参或用大对象就用传引用(高效)。
4变量的作用范围

变量不是到处都能用的,它有一个"活动范围"——作用范围(Scope)。C++ 中变量的作用范围由声明位置决定:

变量类型声明位置作用范围生命周期
全局变量所有函数外部从声明处到文件末尾,所有函数都能访问程序运行全程
局部变量函数内部从声明处到函数结束(右花括号)函数调用时创建,返回时销毁
块级变量if/for/while 等代码块内从声明处到该代码块结束进入块时创建,离开块时销毁

遮蔽规则:如果局部变量和全局变量同名,函数内访问该名字时用的是局部变量,全局变量被"遮蔽"了。

C++全局变量被局部变量遮蔽示例
int x = 100;          // 全局变量 x
void func() {
    int x = 200;        // 局部变量 x,遮蔽了全局的 x
    cout << x << endl;  // 输出 200(用的是局部的)
}
int main() {
    func();              // 输出 200
    cout << x << endl;   // 输出 100(全局的 x)
    return 0;
}
5作用范围生活类比
🏟

全校公告栏 = 全局变量

全校公告栏上的通知,全校每个班级都能看到。全局变量所有函数都能访问。

📝

班级黑板 = 局部变量

黑板上的作业只在这个教室里可见,出了教室就看不到了。局部变量只在函数内可见。

📚

课堂笔记 = 块级变量

在课堂笔记里记的公式,只有这节课能用。if/for 块内声明的变量出了块就失效。

6串起来看:一次函数调用的完整旅程

把传引用交换函数跑一遍,看变量在内存中是怎么变化的。

🎬 动画演示:传引用交换 a=3, b=5 的执行过程

1
int a = 3, b = 5; —— 主函数中创建局部变量 a=3, b=5
2
swap(a, b); —— 调用 swap 函数,a 和 b 作为引用参数传入
3
int& a 成为主函数 a 的别名,int& b 成为主函数 b 的别名
4
int t = a; —— t = 3(t 是 swap 的局部变量)
5
a = b; —— 主函数的 a 变成 5(通过别名修改原件)
6
b = t; —— 主函数的 b 变成 3(通过别名修改原件)
7
函数返回,局部变量 t 销毁,但 a、b 的修改已生效
8
回到 main,cout << a << b —— 输出 5 3,交换成功!

解读:传引用让函数直接操作实参本身。如果没有 &(传值),a 和 b 在函数内只是副本,交换的只是副本,回到 main 后 a 还是 3、b 还是 5。

案例6 个由浅入深的案例

案例 1:传值交换失败 —— 副本改不动原件

题目:写一个 swap 函数交换两个整数,用传值方式。观察交换是否成功。

C++传值交换(失败版)
#include <iostream>
using namespace std;
void swap(int a, int b) {  // 传值:a 和 b 是副本
    int t = a;
    a = b;
    b = t;
}
int main() {
    int x = 3, y = 5;
    swap(x, y);
    cout << x << " " << y << endl;  // 输出什么?
    return 0;
}
运行结果
输入:无
输出:3 5(没有交换成功!)

解读:swap 函数里的 a 和 b 是 x 和 y 的副本。交换的是副本,主函数里的 x 和 y 完全没有变化。这就是传值的"安全"之处——函数改不了你。

案例 2:传引用交换成功 —— 别名直接改原件

题目:把上面的 swap 函数改成传引用方式,观察交换是否成功。

C++传引用交换(成功版)
#include <iostream>
using namespace std;
void swap(int& a, int& b) {  // 传引用:a 和 b 是别名
    int t = a;
    a = b;
    b = t;
}
int main() {
    int x = 3, y = 5;
    swap(x, y);
    cout << x << " " << y << endl;  // 输出什么?
    return 0;
}
运行结果
输入:无
输出:5 3(交换成功!)

解读:加了 & 后,a 是 x 的别名,b 是 y 的别名。函数里改 a 就是在改 x,改 b 就是在改 y。回到 main 后,x 和 y 已经被改成了 5 和 3。

案例 3:函数内修改数组 —— 数组传参的特殊性

题目:写一个函数,把数组所有元素乘以 2。观察数组传参的效果。

C++数组传参
#include <iostream>
using namespace std;
void mul2(int arr[], int n) {  // 数组名传参,实际传的是指针
    for (int i = 0; i < n; i++) arr[i] *= 2;
}
int main() {
    int a[] = {1, 2, 3};
    mul2(a, 3);
    for (int i = 0; i < 3; i++)
        cout << a[i] << " ";
    cout << endl;
    return 0;
}
运行结果
输出:2 4 6(数组元素被修改了!)

解读:数组名作为参数时,实际上传递的是数组首地址(指针),所以函数内可以直接修改数组内容。这是 C++ 数组传参的特殊之处——虽然语法上像传值,但效果上类似传引用。实际编程中,大对象推荐用 vector& 传引用。

案例 4:作用范围追踪 —— 变量在哪里可见

题目:下面程序中有多个同名变量 x,观察每个 cout 输出的是哪个 x。

C++作用范围追踪
#include <iostream>
using namespace std;
int x = 10;              // 全局变量 x = 10
void func() {
    int x = 20;           // 局部变量 x = 20(遮蔽全局)
    cout << x << " ";     // 输出 20(局部 x)
}
int main() {
    int x = 30;           // main 的局部变量 x = 30
    cout << x << " ";     // 输出 30(main 的局部 x)
    func();                 // 输出 20(func 的局部 x)
    cout << x << " ";     // 输出 30(main 的局部 x 没变)
    return 0;
}
运行结果
输出:30 20 30

解读:每个 cout 都访问自己"最近一层"的 x。main 里的是 30,func 里的是 20(遮蔽了全局的 10),调用完 func 回到 main 还是 30。全局变量 x=10 全程被遮蔽,从未被输出。

案例 5:全局变量与局部变量配合使用

题目:用全局变量记录函数被调用的次数。

C++全局变量计数
#include <iostream>
using namespace std;
int callCount = 0;      // 全局变量,初始为 0
void hello() {
    callCount++;           // 每次调用 +1
    cout << "Hello! 第" << callCount << "次调用" << endl;
}
int main() {
    hello();               // 第1次
    hello();               // 第2次
    hello();               // 第3次
    cout << "总共调用" << callCount << "次" << endl;
    return 0;
}
运行结果
Hello! 第1次调用
Hello! 第2次调用
Hello! 第3次调用
总共调用3次

解读:全局变量 callCount 在所有函数间共享,每次调用 hello() 都累加 1,函数返回后值保持不变(因为全局变量生命周期贯穿整个程序)。这是全局变量的经典用法——跨函数共享状态。

案例 6:现场捉虫 —— 三处经典错误

题目:下面这段代码藏着 3 处本课高频错误,先自己找一找。

C++待捉虫的代码
int x = 10;
void func(int x) {        // 第 1 处
    x = 20;
}
void func2(int& x) {     // 第 2 处
    x = 30;
}
int main() {
    func(x);               // x 还是 10
    func2(x);              // x 变成 30
    func2(100);            // 第 3 处
    return 0;
}
点击查看捉虫结果
第 1 处:func 用传值,x = 20 只改了副本,main 里的 x 不受影响。如果希望改实参,应改为 void func(int& x)。
第 2 处:func2 用传引用,x = 30 成功修改了实参。这是正确的写法(前提是确实需要修改实参)。
第 3 处:func2(100) 传了一个常量 100 给引用参数,这是错误的!引用必须绑定到变量,不能绑定到常量。应改为传一个变量:int tmp = 100; func2(tmp);
模块 03

实践应用

三道动手题:读代码写结果、给代码捉虫子、看要求写程序。先自己做,再点开答案对照。

实践 1 写输出

题目:传值 vs 传引用 —— 下面两个程序分别输出什么?

程序 A(传值):

C++程序 A
#include <iostream>
using namespace std;
void f(int x) { x += 10; }
int main() {
    int a = 5;
    f(a);
    cout << a << endl;
    return 0;
}

程序 B(传引用):

C++程序 B
#include <iostream>
using namespace std;
void f(int& x) { x += 10; }
int main() {
    int a = 5;
    f(a);
    cout << a << endl;
    return 0;
}
提示:仔细看 f 函数的形参有没有 & 符号,有 & 就是传引用,改的是原件;没有 & 就是传值,改的是副本。
点击查看参考答案
程序 A 输出:5。f 函数传值,x += 10 只改了副本,main 里的 a 不变。
程序 B 输出:15。f 函数传引用,x 是 a 的别名,x += 10 直接改了 a,回到 main 后 a = 15。
点评:一个 & 符号,天壤之别。记住:传值改不了原件,传引用一改全改。
实践 2 捉虫挑战

题目:下面这段代码想实现"用函数交换两个数",但输出结果没交换,找出错误并修复

C++待修复的代码
#include <iostream>
using namespace std;
void swap(int a, int b) {
    int t = a;
    a = b;
    b = t;
}
int main() {
    int x = 3, y = 5;
    swap(x, y);
    cout << x << " " << y << endl;  // 输出 3 5,没交换!
    return 0;
}
点击查看参考答案
错误原因:swap 函数的形参 a、b 是传值的(没有 &),函数内交换的只是副本,main 里的 x 和 y 不受影响。
修复方法:把形参改为引用:void swap(int& a, int& b)。这样 a 和 b 就是 x 和 y 的别名,交换的就是原件。
修复后输出:5 3(交换成功!)
实践 3 编程题

题目:写一个函数,用传引用方式同时返回两个数的和与差。

函数签名:void sumDiff(int a, int b, int& s, int& d),其中 s 返回和,d 返回差(a - b)。

提示:和与差无法通过返回值同时返回(一个函数只能 return 一个值),但用传引用参数可以同时"带回"多个结果。
点击查看参考答案
C++参考代码
#include <iostream>
using namespace std;
void sumDiff(int a, int b, int& s, int& d) {
    s = a + b;    // 通过引用参数返回和
    d = a - b;    // 通过引用参数返回差
}
int main() {
    int s, d;
    sumDiff(10, 3, s, d);
    cout << "和=" << s << " 差=" << d << endl;
    return 0;
}
示例:输入 10 和 3 → 输出"和=13 差=7"。
点评:这是传引用参数的经典应用场景——一个函数需要返回多个值时,用引用参数"带回"结果。C++ 不像 Python 可以直接 return a, b 返回元组,所以传引用是"多返回值"的主要手段。
模块 04

总结提升

把传值、传引用和作用范围浓缩成一张表、一句口诀,再用"排序交换"检验你是否真正掌握了传引用的精髓。

总结核心要点总结
概念写法特点使用场景
传值void f(int x)副本,不改原件不需要改实参时(默认)
传引用void f(int& x)别名,改原件需要修改实参或用大对象
全局变量int x;(函数外)所有函数可访问跨函数共享状态
局部变量int x;(函数内)只在函数内可见临时数据存储
遮蔽规则同名局部遮全局就近原则避免同名混淆
传参与作用范围口诀(再背一遍) 传值拷贝副本去,函数改它我不变;
传引用给原件别名,函数一改全跟着变;
局部变量函数内,全局变量到处见;
同名局部遮全局,内层就近遮外层。
拓展拓展案例 —— 排序两个数(传引用交换的实战)

排序的核心操作是"交换"。如果没有传引用,swap 函数改不了原件,排序就无法实现。

C++排序两个数(传引用版)
#include <iostream>
using namespace std;
void swap(int& a, int& b) {
    int t = a; a = b; b = t;
}
void sort2(int& a, int& b) {
    if (a > b) swap(a, b);  // 用传引用交换
}
int main() {
    int x = 5, y = 3;
    sort2(x, y);
    cout << x << " " << y << endl;  // 输出 3 5
    return 0;
}
运行结果
3 5

解读:sort2 函数接收 a、b 的引用,比较后调用 swap 交换。因为全程都是传引用,交换能穿透多层函数调用,最终 main 里的 x 和 y 被正确排序。如果 swap 用传值,这里排序就会失败。

🧠 思考与启发

传值和传引用的本质区别,其实就一句话:函数里改的是副本还是原件。副本安全但低效,原件高效但有风险。

想一想:为什么 C++ 默认用传值而不是传引用?提示:安全优先。如果默认传引用,一个不小心写的函数就可能改掉你不想改的变量,这种 bug 非常难排查。C++ 的设计哲学是"给程序员权力,但让程序员自己承担风险"——传值默认安全,想高效自己加 &。

再想想:全局变量虽然方便跨函数共享数据,但为什么好的编程风格建议尽量少用全局变量?提示:全局变量任何函数都能改,改乱了很难追踪。试试用传引用参数代替全局变量来"带回"结果,代码会更清晰、更不容易出错。

模块 05

学习检测

5 个最容易丢分的传参与作用范围陷阱、一张考前必背速查表,以及可以贴在文具盒上的知识点速记卡。

易错 1

忘记加 & 导致传引用失效

想修改实参却忘了在形参前加 &,结果函数里改的是副本,实参纹丝不动。这种 bug 最隐蔽,因为代码不会报错,只是结果不对。

错误写法
void f(int x) { x = 10; }  // 传值,改不了原件
正确写法
void f(int& x) { x = 10; }  // 传引用,改原件
口诀:想改原件加 &,忘了 & 就传值;传值改它它不改,白忙一场空欢喜。
易错 2

引用参数传了常量或表达式

引用必须绑定到变量(有存储位置的内存单元),传常量(如 100)或表达式(如 a+b)会编译报错。

错误写法
void f(int& x) { x = 10; }
f(100);       // 错误!常量不能做引用
f(a + 1);      // 错误!表达式不是变量
正确写法
int tmp = 100;
f(tmp);        // 正确!传变量
口诀:引用参数传变量,常量表达式都不行。
易错 3

全局变量与局部变量同名导致混淆

函数内声明了和全局变量同名的局部变量,函数内访问时用的是局部变量,全局变量被遮蔽。如果以为改的是全局变量,实际改的是局部变量,就会出 bug。

易误解写法
int score = 80;
void update() {
    int score = 90;  // 遮蔽了全局 score
    score += 10;     // 改的是局部的
}
// 全局 score 仍然是 80!
正确写法
int score = 80;
void update() {
    score += 10;     // 没有同名局部变量,改的是全局的
}
// 全局 score 变成 90
口诀:同名局部遮全局,想改全局别声明。
易错 4

传引用时函数外修改了不该改的变量

传引用让函数可以修改实参,如果函数内部不小心改错了变量,或者在多个函数间共享了不该共享的引用,就会造成难以追踪的 bug。

危险写法
void calc(int& x) {
    x = 0;           // 可能误改重要数据
}
安全写法
void calc(const int& x) {
    cout << x;        // 只读,不能修改
}
口诀:只读不写加 const,引用传参更安全。
易错 5

局部变量未初始化就使用

局部变量声明后不会自动初始化(不像全局变量默认初始化为 0),直接使用未初始化的局部变量会得到垃圾值。

错误写法
void f() {
    int x;           // 未初始化!
    cout << x;       // 垃圾值!
}
正确写法
void f() {
    int x = 0;      // 显式初始化
    cout << x;       // 输出 0
}
口诀:局部变量不初始化,垃圾值里埋陷阱。
速查考前速查表
概念写法特点示例提醒
传值void f(int x)副本,不改原件f(a) 不改变 a默认方式,安全
传引用void f(int& x)别名,改原件f(a) 改变 a加 &,别忘
全局变量int x;(函数外)全程可见所有函数可访问避免滥用
局部变量int x;(函数内)函数内可见函数返回即销毁记得初始化
遮蔽规则同名局部遮全局就近原则函数内用局部的避免同名
const 引用const int& x只读,高效不复制,不改大对象只读时用
📌知识点速记卡
传值 = 副本

函数内改形参不影响实参,安全但效率低(大对象)。

传引用 = 别名

形参前加 &,函数内改形参 = 改实参,高效但有风险。

全局变量

函数外声明,所有函数可访问,生命周期贯穿程序全程。

局部变量

函数内声明,只在函数内可见,函数返回即销毁。

遮蔽规则

同名局部变量会遮蔽全局变量,函数内访问用的是局部的。

const 引用

const int& x 既高效(不复制)又安全(不能改),大对象只读时首选。

引用必须传变量

不能传常量或表达式,引用必须绑定到有存储位置的变量。

局部变量要初始化

局部变量不自动初始化,使用前必须显式赋值,否则是垃圾值。

课后练习

练习作业

巩固传值、传引用和作用范围,分层练习,点击展开答案与解析。

总结练习3 道综合题
总结 1 判断题

题目:判断 —— 传引用参数的函数,实参可以是任意表达式(如 a+b、100 等)。

提示:引用必须绑定到变量,不能绑定到常量或表达式。
点击查看参考答案
答案:错误。引用参数必须绑定到变量(有存储位置的内存单元),不能传常量(如 100)或表达式(如 a+b),编译会报错。
总结 2 写输出

题目:下面程序输出什么?

C++传值 vs 传引用追踪
#include <iostream>
using namespace std;
int x = 10;
void f(int x) { x = 20; }
void g(int& x) { x = 30; }
int main() {
    int x = 5;
    f(x);
    cout << x << " ";
    g(x);
    cout << x << endl;
    return 0;
}
点击查看参考答案
答案:5 30。f(x) 传值,main 里的 x 不变(还是 5);g(x) 传引用,main 里的 x 被改成 30。注意 main 里的 x=5 遮蔽了全局的 x=10。
总结 3 编程题

题目:写一个函数,用传引用方式把一个数的各位数字反转。例如输入 123,函数执行后变量变成 321。

提示:用 % 10 取末位,/ 10 去末位,循环直到数为 0。注意:传引用让函数可以直接修改实参。
点击查看参考答案
C++参考代码
#include <iostream>
using namespace std;
void reverse(int& n) {
    int rev = 0;
    while (n > 0) {
        rev = rev * 10 + n % 10;
        n /= 10;
    }
    n = rev;  // 通过引用改实参
}
int main() {
    int x = 123;
    reverse(x);
    cout << x << endl;  // 输出 321
    return 0;
}
示例:输入 123 → 输出 321。说明:reverse 函数通过传引用直接修改实参,回到 main 后 x 的值已经被反转了。
A 组基础巩固(4 题)
A1 填空题

题目:C++ 中函数默认传参方式是 ______;要让函数修改实参的值,需要在形参前加 ______ 符号;变量在函数内部声明,它的作用范围是 ______。

点击查看参考答案
C++完整参考代码
#include <iostream>
using namespace std;

void byValue(int x) { x = 100; }   // 值传递:改的是副本
void byRef(int& x)  { x = 100; }   // 引用传递:改的就是本体

int main() {
    int a = 10;
    byValue(a);
    cout << a << " ";              // 10:值传递改不到实参
    byRef(a);
    cout << a << endl;             // 100:引用传递改到了实参
    return 0;
}

// 运行结果:10 100

答案:传值(值传递);&(引用符号);该函数内部(从声明处到函数结束)。

思路:形参前不加符号时,函数拿到的是实参的一份拷贝,改它不影响外面;加上 & 之后形参成了实参的别名,改形参就是改实参。局部变量的生命期只有一次函数调用那么长,出了函数就没了。

考点:值传递 / 引用传递的区别,以及局部变量的作用域。

易错:以为「加了 & 就要多写 &」——调用处照旧写 byRef(a) 就行,不带 &;反过来,想改实参却忘了写 &,是最常见的错。

A2 判断题

题目:判断 —— 全局变量可以在任何函数中被访问和修改。

点击查看参考答案
答案:正确(但有例外)。全局变量在所有函数外部声明,从声明处到文件末尾的所有函数都可以访问和修改它。但如果某个函数内有同名局部变量,该函数内访问的是局部变量(遮蔽规则)。

考点:全局变量的作用域——从声明处到文件末尾都可见,但会被同名局部变量遮蔽。

易错:误以为函数内同名局部变量修改的也是全局变量——遮蔽后改的只是局部变量那份。

A3 写输出

题目:下面程序输出什么?

C++传值基础
#include <iostream>
using namespace std;
void f(int x) { x = 100; }
int main() {
    int a = 10;
    f(a);
    cout << a << endl;
    return 0;
}
点击查看参考答案
C++完整参考代码
#include <iostream>
using namespace std;

void f(int x) { x = 100; }   // 形参 x 是实参 a 的副本

int main() {
    int a = 10;
    f(a);                    // a 原封不动
    cout << a << endl;       // 输出 10
    return 0;
}

// 运行结果:10

答案:10。

思路:f 用的是值传递,函数里的 x 只是 a 的副本,x = 100 改的是副本;函数一结束副本就销毁了,main 里的 a 还是 10。

考点:默认传参方式是值传递,函数内改形参不会影响实参。

易错:只看「函数里有 x = 100」就答 100。要盯住改的到底是谁——是副本还是本体。

A4 编程题

题目:写一个函数,用传引用方式把一个整数乘以 3。

提示:形参用 int& x,函数内写 x *= 3。
点击查看参考答案
C++参考代码
#include <iostream>
using namespace std;
void mul3(int& x) { x *= 3; }
int main() {
    int a = 5;
    mul3(a);
    cout << a << endl;  // 输出 15
    return 0;
}
示例:输入 5 → 输出 15。传引用让函数直接修改了实参。

思路:参数写成 int& x,函数内 x *= 3 就是在直接修改主函数的实参。

易错:参数写成 int x(传值),函数只改了副本,主函数输出仍是 5。

B 组能力提高(4 题)
B1 写输出

题目:下面程序输出什么?

C++全局与局部
#include <iostream>
using namespace std;
int a = 10;
void f() {
    int a = 20;
    cout << a << " ";
}
int main() {
    int a = 30;
    f();
    cout << a << endl;
    return 0;
}
点击查看参考答案
C++完整参考代码
#include <iostream>
using namespace std;

int a = 10;                  // 全局变量

void f() {
    int a = 20;              // 局部变量 a,把全局的 a 遮蔽了
    cout << a << " ";        // 输出 20
}

int main() {
    int a = 30;              // main 的局部变量 a,同样遮蔽全局 a
    f();
    cout << a << endl;       // 输出 30
    return 0;
}

// 运行结果:20 30

答案:20 30。

思路:就近原则——谁离得近用谁。f 函数里先声明了局部 a = 20,它把全局的 10 遮住了,所以打印 20;main 里也有自己的局部 a = 30,所以打印 30。全局变量 a = 10 全程被遮蔽,一次都没被用到。

考点:「内层遮蔽外层」的名字查找规则(局部变量优先于全局变量)。

易错:以为全局变量一定会被用上,答成 10 30 或 20 10。同名局部变量一出现,全局的那个在那个函数里就「隐身」了。

B2 推理题

题目:下面程序输出什么?

C++嵌套函数调用
#include <iostream>
using namespace std;
void f(int& x) { x += 10; }
void g(int& x) { f(x); x += 20; }
int main() {
    int a = 5;
    g(a);
    cout << a << endl;
    return 0;
}
点击查看参考答案
答案:35。g(a) 中 a 是引用,调用 f(a) 后 a 变成 15(5+10),然后 g 里 x += 20 即 a += 20,a 变成 35。全程都是引用传递,修改层层穿透。

思路:按调用顺序逐步代入:a 传引用进 f 变成 15,再进 g 加 20 得 35,全程修改都穿透回原变量。

易错:把某一层参数当成传值,就会算出 15 或 5。

B3 编程题

题目:写一个函数,用传引用方式找出数组中的最大值和最小值,分别存进两个引用参数。

函数签名:void findMaxMin(int arr[], int n, int& maxVal, int& minVal)

点击查看参考答案
C++参考代码
#include <iostream>
using namespace std;
void findMaxMin(int arr[], int n, int& maxVal, int& minVal) {
    maxVal = arr[0];
    minVal = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] > maxVal) maxVal = arr[i];
        if (arr[i] < minVal) minVal = arr[i];
    }
}
int main() {
    int a[] = {3, 1, 8, 2, 9};
    int mx, mn;
    findMaxMin(a, 5, mx, mn);
    cout << "最大=" << mx << " 最小=" << mn << endl;
    return 0;
}
示例:数组 {3, 1, 8, 2, 9} → 输出"最大=9 最小=1"。说明:通过两个引用参数同时返回最大值和最小值,避免了全局变量。

思路:maxVal、minVal 声明为引用参数,先初始化为首元素,遍历一遍更新,函数结束时实参自动带上结果。

易错:忘了在函数里先初始化 maxVal/minVal 就开始比较,用的是主函数传进来的随机值。

B4 写输出

题目:下面程序输出什么?

C++const 引用
#include <iostream>
using namespace std;
void show(const int& x) {
    cout << x << " ";
    // x = 100;  // 如果取消注释会编译报错
}
int main() {
    int a = 42;
    show(a);
    cout << a << endl;
    return 0;
}
点击查看参考答案
C++完整参考代码
#include <iostream>
using namespace std;

void show(const int& x) {       // const 引用:不复制,但也不许修改
    cout << x << " ";           // 输出 42
    // x = 100;                 // 如果取消注释会编译报错
}

int main() {
    int a = 42;
    show(a);                    // a 的值保持不变
    cout << a << endl;          // 输出 42
    return 0;
}

// 运行结果:42 42

答案:42 42。

思路:show 的参数是 const 引用,只能读不能写,a 的值全程不变,所以两处都打印 42。

考点:const 引用的两个好处——不复制(对 string、结构体这类大对象效率高)+ 不能改(安全,防止函数偷偷改你的数据)。

易错:把 const 引用当成「值传递」或「普通引用」。它比值传递省一次拷贝,又和普通引用一样能直接读到原对象;但一旦在函数里赋值,编译就会报错。

C 组思维挑战(4 题)
C1 挑战题

题目:写一个函数,用传引用方式实现三个数的排序(从小到大),不返回任何值。

函数签名:void sort3(int& a, int& b, int& c),调用后 a <= b <= c。

点击查看参考答案
C++参考代码
#include <iostream>
using namespace std;
void swap(int& a, int& b) {
    int t = a; a = b; b = t;
}
void sort3(int& a, int& b, int& c) {
    if (a > b) swap(a, b);
    if (a > c) swap(a, c);
    if (b > c) swap(b, c);
}
int main() {
    int x = 3, y = 1, z = 2;
    sort3(x, y, z);
    cout << x << " " << y << " " << z << endl;
    return 0;
}
示例:输入 3 1 2 → 输出 1 2 3。说明:三次两两比较+交换即可实现三数排序,全程传引用,交换能穿透函数调用。

思路:通过三次条件交换依次确定 a、b、c 的大小关系,swap 的参数必须是引用才能真交换。

易错:自己写的 swap 参数用了传值,交换的是副本,排序毫无效果。

C2 陷阱题

题目:下面代码有什么问题?

C++悬空引用
int& getRef() {
    int x = 10;
    return x;  // 返回局部变量的引用!
}
int main() {
    int& r = getRef();
    cout << r << endl;  // 未定义行为!
    return 0;
}
点击查看参考答案
错误原因:getRef 返回了局部变量 x 的引用。函数返回后,x 被销毁,引用 r 变成了"悬空引用",访问它属于未定义行为(可能输出垃圾值,也可能崩溃)。返回引用的函数必须返回一个生命周期足够长的变量(如全局变量、静态变量或实参引用)。

思路:函数返回了局部变量的引用,函数结束后 x 被销毁,r 成了悬空引用,访问它是未定义行为。

易错:以为“能编译、偶尔输出还正确”就没事——未定义行为可能时对时错,更难排查。

C3 编程题

题目:用全局变量和传引用两种方式分别实现"累加器",比较它们的优缺点。

累加器:每次调用函数,把一个数加到累计总和中,输出当前总和。

点击查看参考答案
C++全局变量版 vs 传引用版
#include <iostream>
using namespace std;
// 全局变量版
int total = 0;
void add_global(int v) { total += v; cout << total << " "; }

// 传引用版
void add_ref(int& total, int v) { total += v; cout << total << " "; }

int main() {
    add_global(1); add_global(2); add_global(3);
    cout << endl;
    int t = 0;
    add_ref(t, 1); add_ref(t, 2); add_ref(t, 3);
    cout << endl;
    return 0;
}
输出:1 2 3 1 3 6。比较:全局变量版简单但危险(任何函数都能改 total,难以追踪);传引用版更清晰(total 明确作为参数传递,数据流可见)。好的编程风格推荐传引用代替全局变量。

思路:全局变量版直接改 total,任何函数都能碰;传引用版把计数变量作为参数传入,作用范围更明确可控。

易错:全局版多次运行忘了清零 total,结果会越加越大;传引用版调用时忘了传变量编译都过不了。

C4 挑战题

题目:写一个函数,用传引用方式实现"冒泡排序"的一轮扫描。函数签名:void bubbleOnePass(int arr[], int n, int& swaps),其中 swaps 记录这一轮交换了多少次。

一轮冒泡:从第一个元素开始,依次比较相邻两个元素,如果前一个大于后一个就交换。用引用参数带回交换次数。

点击查看参考答案
C++参考代码
#include <iostream>
using namespace std;
void swap(int& a, int& b) {
    int t = a; a = b; b = t;
}
void bubbleOnePass(int arr[], int n, int& swaps) {
    swaps = 0;
    for (int i = 0; i < n - 1; i++) {
        if (arr[i] > arr[i+1]) {
            swap(arr[i], arr[i+1);
            swaps++;
        }
    }
}
int main() {
    int a[] = {5, 3, 1, 4, 2};
    int s;
    bubbleOnePass(a, 5, s);
    cout << "一轮后: ";
    for (int i = 0; i < 5; i++) cout << a[i] << " ";
    cout << " 交换次数: " << s << endl;
    return 0;
}
示例:数组 {5, 3, 1, 4, 2} → 一轮后 {3, 1, 4, 2, 5},交换次数 4。说明:冒泡排序的核心操作是交换(需要传引用)和计数(通过引用参数带回),两个需求都通过传引用完美解决。完整冒泡排序只需在主函数中循环调用 bubbleOnePass 即可。

思路:扫描相邻元素,逆序就交换并让引用参数 swaps 加一,一轮结束后交换次数带回主函数。

易错:函数里忘了先 swaps = 0,多次调用会把上一轮的次数累加进去。