Skip to content

第 24 章 复印还是原件——值传递与引用


🏗️ 前情回顾:函数能接收参数了——但传进去的到底是复印件还是原件?为什么上章末尾 addTen(a) 调用后 a 还是 20?老师说加个 & 就能改——这 & 到底在内存里干了什么?这一章,我们把"参数如何传入函数"这个问题彻底讲透。


🎯 本章目标

学完这一章,你能:

  • 从内存角度理解值传递和引用传递的本质区别
  • 熟练使用 & 写出能修改实参的引用传递函数
  • 使用 const & 实现"只读高效传参"
  • 根据场景判断该用值传递还是引用传递
  • 写出正确的 swap 交换函数,并能解释两种写法为什么一个行一个不行
  • 理解数组作为参数时"退化成指针"的特性
  • 避开忘记 &、误改原变量等常见陷阱

📖 故事引入

小明和小红是同桌。有一天他们互相检查作业:

  • 方案 A:小明把作业复印了一份给小红。小红在复印件上批改、划线、做标记。小明的原件呢?完好无损——复印件上的任何改动都影响不到原件。

  • 方案 B:小明直接把原件递给小红。小红在上面写写画画……等拿回来,原件已经面目全非。

这就是值传递和引用传递最核心的区别。在 C++ 中,每当你调用一个函数,你都在做类似的选择:是把变量的副本传进去,还是把变量本身传进去?

上一章你已经看到了表象——加个 & 就能"修改成功"。这一章,我们钻到内存里看看,这些数字到底是怎么流转的。


🧱 知识讲解

24.1 值传递:拷贝一份,各走各路

C++ 默认的参数传递方式是值传递。这意味着:调用函数时,形参会得到一份实参的完整副本。函数内部操作的是副本,和原变量毫无关系。

最常见的新手误解——"我明明在函数里改了呀!"

cpp
void change(int x) { x = 999; }  // 以为能改
int main() {
    int a = 10;
    change(a);
    cout << a;  // 还是10——为什么?!
}

因为 change 拿到的是复印件。你在复印件上写"999",原件上的"10"纹丝不动。

✅ 我们用一段代码来验证背后发生了什么:

cpp
void tryChange(int x) {
    cout << "函数内:x 的地址 = " << &x << endl;   // 看看地址
    x = 999;
}

int main() {
    int a = 10;
    cout << "main 里:a 的地址 = " << &a << endl;
    tryChange(a);
    cout << "调用后 a = " << a << endl;            // 10——没变!
    return 0;
}

运行结果类似:

main 里:a 的地址 = 0x7ffd1a3b4c1c
函数内:x 的地址 = 0x7ffd1a3b4bec
调用后 a = 10

注意看:两个地址不一样ax 住在内存的不同位置。x = 999 只是修改了 x 那块内存,a 那块内存纹丝不动。

内存模型(值传递)

调用前:  a ─── [10]  (地址: 0x4c1c)

调用时:  a ─── [10]  (地址: 0x4c1c)  ← 原变量,不改
          x ─── [10]  (地址: 0x4bec)  ← 副本,改这里
                
x = 999: a ─── [10]  (不变!)
          x ─── [999] (副本变了)

💡 类比:就像游戏里的"存档"。你复制了一份存档发给朋友,朋友在他的电脑上玩得热火朝天——你自己的存档进度还是老样子。两份存档是独立的。

什么时候用值传递?当参数是 intchardouble 这类小数据,且你不需要修改原变量时,值传递最安全、最直接。


24.2 引用传递:直接操作原变量

在参数类型后面加一个 &,就成了引用传递。引用不是副本——它是原变量的别名。修改引用,就是修改原变量。

cpp
void tryChange(int &x) {         // 注意这个 &
    cout << "函数内:x 的地址 = " << &x << endl;
    x = 999;
}

int main() {
    int a = 10;
    cout << "main 里:a 的地址 = " << &a << endl;
    tryChange(a);
    cout << "调用后 a = " << a << endl;   // 999——真的变了!
    return 0;
}

运行结果:

main 里:a 的地址 = 0x7ffd1a3b4c1c
函数内:x 的地址 = 0x7ffd1a3b4c1c
调用后 a = 999

这一次,两个地址完全一样x 根本不是新创建的变量,它只是 a 的另一个名字。

内存模型(引用传递)

调用时:  a/x ─── [10]  (同一个地址!)
                
x = 999: a/x ─── [999] (直接改原内存)

💡 类比:就像你给文件创建了一个快捷方式。无论是双击快捷方式还是双击原文件,打开的都是同一个文件。引用的关系就是"快捷方式→原文件"。

关键语法

  • 函数定义:void func(int &param) — 声明这里是引用
  • 函数调用:func(a) — 直接传变量名,不需要额外的 &
  • 调用时不能传字面量:func(5) ❌ — 5 没有地址,无法引用

24.3 const 引用:只看不碰

有时候你希望享受引用传递的效率(不用复制),但又不想函数"手贱"修改你的数据。这时候用 const 引用——用 & 避免复制,用 const 锁住权限。

cpp
// 只想打印,不想修改——加 const 保护
void show(const string &s) {
    cout << "内容是:" << s << endl;
    // s = "hello";   // ❌ 编译错误!const 不允许修改
}

int main() {
    string name = "Alice";
    show(name);                // ✅ 高效:不复制整个字符串
    show("Bob");               // ✅ const 引用可以绑定字面量(临时对象)
    return 0;
}

为什么 const 引用这么常用?

想象传一个 10KB 的字符串给函数:

  • 值传递:void show(string s) → 复制整个 10KB 字符串,慢!
  • const 引用:void show(const string &s) → 只传一个"指向原数据的引用",快!
cpp
// 推荐写法 —— 高效且安全
void process(const vector<int> &data) {  
    cout << "数据大小:" << data.size() << endl;
    // 只读 data,绝不修改
}

🧠 记忆口诀const & = "借来看,不借走"——效率高,又不怕误修改。


24.4 何时用值、何时用引用?决策指南

场景推荐方式原因
参数是 intchardouble 等小类型,不需要修改值传递复制成本极低
需要函数修改参数的值引用传递 &必须直接操作原变量
参数是大对象(stringvector),只读const &避免复制大对象,加了只读保护
参数是数组自动退化为指针(见 25.6)数组永远不会被值传递复制

示例:综合运用

cpp
int square(int x) { return x * x; }          // 小类型 → 值传递

void swap(int &a, int &b) { /* ... */ }       // 要修改 → 引用

void print(const string &msg) { /* ... */ }   // 大对象只读 → const 引用

24.5 经典对比:swap 的两种写法

没有比 swap(交换两个变量的值)更能说明值传递和引用传递区别的例子了。下面两种写法,只有一种能成功。

写法一:值传递(失败版)

cpp
void swapWrong(int a, int b) {   // 值传递——拿到的是副本
    int temp = a;
    a = b;
    b = temp;
    cout << "swap内:a=" << a << " b=" << b << endl;  // 副本交换了
}

int main() {
    int x = 10, y = 20;
    swapWrong(x, y);
    cout << "main内:x=" << x << " y=" << y << endl;  // 10 20 —— 没变!
    return 0;
}
swap内:a=20 b=10    ← 副本确实交换了
main内:x=10 y=20    ← 但原变量纹丝不动!

写法二:引用传递(成功版)

cpp
void swapRight(int &a, int &b) { // 引用——直接操作原变量
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 10, y = 20;
    swapRight(x, y);
    cout << "x=" << x << " y=" << y << endl;  // 20 10 —— 成功了!
    return 0;
}

你可以把这两种写法都敲进编译器里跑一遍,亲自感受它们的差异——加不加 &,天差地别


24.6 数组作参数:默认传地址

数组是个"特殊公民"。当你把数组作为函数参数时,它从来不会走值传递——编译器会悄悄把它"退化"成一个指向首元素的指针。

cpp
void fillArray(int arr[], int n) {   // arr 实际上是个 int* 指针
    for (int i = 0; i < n; i++) {
        arr[i] = i * 10;             // 直接修改原数组!
    }
}

int main() {
    int a[5] = {0};
    fillArray(a, 5);                 // 传数组名 = 传首地址
    for (int i = 0; i < 5; i++)
        cout << a[i] << " ";         // 0 10 20 30 40 —— 原数组被改了!
    return 0;
}

背后的真相void fillArray(int arr[], int n) 被编译器理解为 void fillArray(int *arr, int n)。arr 是一个指针,指向 main 里数组 a 的起始位置。修改 arr[i] 就是修改 a[i]

这就是为什么很多函数需要额外传一个参数表示数组长度(上面的 n)——因为 arr 只是一个指针,它记不住数组有多长。

cpp
// 等价写法 —— 下面这两种声明完全一样
void func(int arr[], int n);
void func(int *arr,  int n);     // 显式写成指针更诚实

24.7 常见陷阱一览

陷阱 1:忘记 &,修改无效

这是新手最常见的错误——以为函数能修改变量,结果什么都没改变。

void addBonus(int score) → 函数里 score += 10,出来以后还是原值
void addBonus(int &score) → 别忘了 &

陷阱 2:想传 const & 却写成了值传递

void countVowels(string s) → 每次调用都复制一遍整个字符串,白白浪费性能
void countVowels(const string &s) → 不复制,还防误改

陷阱 3:试图给引用传字面量

void addTen(int &x) 然后调用 addTen(5) → 编译错误!字面量 5 没有地址
✅ 如果确实要接受字面量,改成 const int &x,或改回值传递

陷阱 4:引用传递后误改了不想改的变量

❌ 不小心把只读参数声明成了普通引用,函数里意外修改了调用者的数据
✅ 只要不需要修改,就加 const——让编译器帮你把关

陷阱 5:数组参数里写了数组大小

void print(int arr[10]) → 编译器会忽略 10 这个数字,arr 还是指针
✅ 正确做法:void print(int arr[], int n),把长度作为单独的参数传入


✋ 动手试试

试试 1:写一个 increment(int &x) 函数,它接收一个引用参数,把参数加 1。在 main() 里调用它 3 次,观察变量的变化。

试试 2:写一个 reset(int &a, int &b) 函数同时把两个参数置零。调用它验证效果。

试试 3:写一个 reverse(int arr[], int n) 函数,翻转数组。只传数组名和长度,不返回任何值——观察 main() 里的数组是否真的被翻转了。

试试 4:对比三种传参方式,写三个版本的函数分别用值传递、引用传递、const 引用传递。在每个函数里尝试修改参数,看编译器会报什么错。


🦶 你踩过这些坑吗

逐一核对,看你是不是也中过招:

  • [ ] swap 写成了值传递void swap(int a, int b) → 调用后原变量纹丝不动,这是踩坑人数最多的错误之一 → ✅ void swap(int &a, int &b)
  • [ ] 给普通引用传字面量:定义了 void show(int &x),调用时写 show(42) → ❌ C++ 不允许字面量绑定到非 const 引用 → ✅ 改用 void show(const int &x)
  • [ ] 以为数组参数会复制:写 void init(int arr[100]) 并期望函数收到一个独立的数组副本 → ❌ 数组不会复制,arr 就是原数组 → ✅ 想保护原数组需手动在函数里创建副本
  • [ ] 数组参数写了长度却不用额外参数传void print(int arr[5]) → 编译器忽略 5,arr 不知道自己的长度 → ✅ void print(int arr[], int len)

📝 练习

基础题

1. 选择题

(1)C++ 中默认的参数传递方式是?
A. 引用传递   B. 值传递   C. 指针传递   D. 地址传递

(2)要让函数能够修改传入的实参,应该使用?
A. 值传递   B. 引用传递(&)  C. void 返回   D. 全局变量

(3)以下关于 const 引用的说法,错误的是?
A. 避免复制大对象   B. 函数内不能修改参数   C. 可以绑定字面量   D. 函数内可以修改参数

2. 填空题

(1)值传递时,形参是实参的_____;引用传递时,形参是实参的_____。

(2)void swap(int &a, int &b)& 的作用是让 a 和 b 成为_____传递参数。

(3)数组作为函数参数时,实际上传递的是数组_____元素的_____。

3. 判断题

(1)值传递函数内部对参数的修改一定不会影响实参。( )

(2)const int &x 可以绑定字面量,如 func(5)。( )

4. 读代码写结果

cpp
void mystery(int &a, int b) {
    a = a + b;
    b = a - b;
}
int main() {
    int x = 5, y = 3;
    mystery(x, y);
    cout << x << " " << y;
}

输出:_____ _____

提高题

5. 编程题 — 多个返回值

函数只能 return 一个值。但如果你需要"返回"两个值怎么办?用引用传递!写一个函数 void minmax(int a, int b, int &mn, int &mx),把较小值存入 mn,较大值存入 mx。在 main() 里测试。

输入示例a=15, b=8
输出示例min=8, max=15

6. 编程题 — 数组统计

写一个函数 void stats(const int arr[], int n, int &sum, double &avg),计算数组的总和与平均值。注意:用 const 保护数组,用引用"返回"两个计算结果。

测试数据{85, 92, 78, 90, 88}
预期输出sum=433, avg=86.6

7. 编程题 — 字符串大写转换

写一个函数 void toUpper(string &s),用引用传递把字符串中所有的小写字母转换为大写字母。在 main() 里输入一个字符串,输出转换后的结果。

(提示:遍历字符串每个字符,用 toupper(c) 函数,需要 #include <cctype>

挑战题

8. 编程题 — 数组去重(原地修改)

写一个函数 int unique(int arr[], int n),把有序数组中重复的元素去掉(原地修改),返回去重后的新长度。

示例

输入:arr = {1, 1, 2, 3, 3, 3, 4, 5, 5}, n = 9
输出:新长度 = 5, arr 前 5 位变为 {1, 2, 3, 4, 5}

要求:不用额外数组,直接原地修改,时间复杂度 O(n)。

(提示:用快慢指针——一个指针遍历,一个指针记录"不重复元素应该放的位置")

9. 编程题 — 数组区间反转

写一个函数 void reverseRange(int arr[], int L, int R),用引用思维(数组本身就传地址)将数组下标 [L, R] 区间内的元素反转。在 main() 里输入数组和区间,输出反转后的完整数组。

示例arr = {1, 2, 3, 4, 5, 6, 7}, L=2, R=5{1, 2, 6, 5, 4, 3, 7}


🧠 本章小结

                ┌──────────────┬──────────────────┬─────────────────────┐
                │   值传递      │   引用传递 (&)    │   const 引用 (&)     │
     ┌──────────┼──────────────┼──────────────────┼─────────────────────┤
     │ 本质     │ 复制一份副本  │ 原变量的别名       │ 原变量的只读别名     │
     │ 内存     │ 新开空间      │ 共用原地址         │ 共用原地址           │
     │ 修改影响 │ ❌ 不影响原值 │ ✅ 直接修改原值    │ ❌ 编译器禁止修改    │
     │ 传字面量 │ ✅ 可以       │ ❌ 不行            │ ✅ 可以              │
     │ 适用场景 │ 小数据类型   │ 需要修改原变量      │ 大对象只读           │
     └──────────┴──────────────┴──────────────────┴─────────────────────┘

数组参数:自动退化为指针,传递的是首地址 → 函数内修改直接影响原数组
swap 的正确写法:void swap(int &a, int &b)  ← 必须用引用!

📝 配套练习

共5题。值vs引用的核心差别需要通过"改不掉"和"改得掉"的对比来理解。Ch24题量精简。。★核心(课堂必做) ◆拓展(课后练习)

级别题号链接覆盖知识点
★ 核心luogu-P5735https://hydro.ac/p/luogu-P5735值传递、只读不写
◆ 拓展J0101https://hydro.ac/d/srqc/p/J0101值传递+bool返回、设计决策
◆ 拓展J0102https://hydro.ac/d/srqc/p/J0102值传递+bool返回、区间
◆ 拓展luogu-P5743https://hydro.ac/p/luogu-P5743值传递、递归计算
◆ 拓展luogu-P5744https://hydro.ac/p/luogu-P5744结构体+函数、传结构体

💡 练习建议:先完成 1 道★核心题,确保掌握本章基本方法;再完成 4 道◆拓展题,覆盖不同变式和细节。


配套练习

共5题。值vs引用的核心差别需要通过"改不掉"和"改得掉"的对比来理解。Ch24题量精简。★核心(课堂必做) · ◆拓展(课后练习)

级别题号链接覆盖知识点
★ 核心luogu-P5735https://hydro.ac/p/luogu-P5735值传递、只读不写
◆ 拓展J0101https://hydro.ac/d/srqc/p/J0101值传递+bool返回、设计决策
◆ 拓展J0102https://hydro.ac/d/srqc/p/J0102值传递+bool返回、区间
◆ 拓展luogu-P5743https://hydro.ac/p/luogu-P5743值传递、递归计算
◆ 拓展luogu-P5744https://hydro.ac/p/luogu-P5744结构体+函数、传结构体

练习建议:先在课堂完成 1 道★核心题,掌握本章基本方法;课后完成 4 道◆拓展题,覆盖不同变式和细节。

自查清单

  • [ ] 我能在脑子里画出值传递和引用传递的内存模型(副本 vs 别名)
  • [ ] 写 swap 函数时我会本能地加上 &
  • [ ] 我知道 const & 的三重好处:不复制、只读、能接受字面量
  • [ ] 我能判断一个场景该用哪种传递方式
  • [ ] 我理解数组作为参数时"不复制、传地址"的特性
  • [ ] 调用引用传递函数时,我不会傻傻地在实参前加 &

🚀 下章预告:参数搞懂了——但变量自己呢?为什么 if 里声明的变量出了大括号就消失?全局变量方便为什么老师说它是"万恶之源"?第 25 章,我们来画一张"变量地图"——作用域与生命周期。