4.“食谱进阶”:参数传递
“食谱进阶”:参数传递 (值传递 vs. 引用传递)
本章导读 (The Hook)
在上一章,我们学会了如何向函数传递“原料”(参数)。我们当时使用的,是 C++中最基本、最安全的参数传递方式——值传递 (Pass by Value)。
它的工作模式可以比喻成制作复印件:当我们将一个变量(比如 myScore)作为实参传递给函数时,系统并不会把 myScore 这个“原件”直接交给函数。相反,它会复制一份 myScore 的值,然后把这份“复印件”交给函数。函数在内部对这份复印件做的任何修改(比如打分、涂改),都完全不会影响到 main 函数中的那个“原件”。
这种方式非常安全,能有效防止函数意外地修改外部数据。但它也有两个明显的局限性:
- 无法实现“修改原件”的需求:如果我就是希望函数能帮我修改某个变量的值,值传递就无能为力了。
- “复印”大型数据时开销大:如果传递的不是一个简单的
int,而是一个包含了成千上万个元素的巨大数组或对象,每次调用函数都完整地复制一份,会造成不必要的性能浪费。
为了解决这些问题,C++ 提供了另一种更强大、更高效的参数传递方式——引用传递 (Pass by Reference)。本章,我们将深入这两种传递方式的内部机理,理解它们的区别,并学会何时使用它们。
专业术语速查表 (Glossary)
点击展开/折叠本章术语表
值传递 (Pass by Value)
- 核心原理:将实参的值复制一份,传递给函数的形参。形参是实参的一个独立副本。
- 特点:函数内部对形参的修改,不会影响到函数外部的实参。安全,但有复制开销。
引用传递 (Pass by Reference)
- 核心原理:不复制实参,而是将实参的别名 (alias) 传递给函数的形参。形参和实参本质上是同一个内存地址上的同一个变量。
- 特点:函数内部对形参的修改,会直接影响到函数外部的实参。高效,无复制开销,但需谨慎使用。
引用 (Reference)
- 通俗比喻:给一个变量起的“小名”或“外号”。比如,
int &b = a;就相当于给变量a起了个小名叫b。你对b操作,就等于对a操作。 - 解释:C++ 中的一种复合类型,它为已存在的变量提供一个别名。引用在声明时必须被初始化,并且一旦绑定到一个变量,就不能再更改为另一个变量的引用。它通过在类型名后加上
&符号来声明。
- 通俗比喻:给一个变量起的“小名”或“外号”。比如,
副作用 (Side Effect)
- 解释:一个函数除了返回一个值之外,还修改了函数外部的状态(例如,修改了全局变量或通过引用传递的参数)。通过引用传递来修改实参就是一种典型的副作用。
const关键字- 通俗比喻:给变量或参数贴上“只读,不许修改”的标签。
- 解释:一个类型限定符,用于指定一个变量的值不能被改变。当与引用结合使用时(如
const string &s),它可以保证函数不会通过引用去修改外部的实参。
核心概念讲解 (The Core Concept)
1. 值传递 (Pass by Value) 的回顾与剖析
我们之前所有的函数参数都是值传递。
- 代码示例:尝试在函数内修改变量
#include <iostream> using namespace std; // 函数 tryToModify 接收一个 int 类型的参数 value // 这里的 value 是 main 函数中 number 的一个“复印件” void tryToModify(int value) { cout << "函数内部:进入函数时,value 的值是 " << value << endl; value = 100; // 修改这个复印件的值 cout << "函数内部:修改后,value 的值是 " << value << endl; } int main() { int number = 10; cout << "main 函数:调用函数前,number 的值是 " << number << endl; tryToModify(number); // 将 number 的“值”复制一份传给函数 cout << "main 函数:调用函数后,number 的值是 " << number << endl; return 0; } - 运行结果:
main 函数:调用函数前,number 的值是 10 函数内部:进入函数时,value 的值是 10 函数内部:修改后,value 的值是 100 main 函数:调用函数后,number 的值是 10 - 结论:
tryToModify函数对value的修改,丝毫没有影响到main函数中的number。它们是两个独立的变量,存在于不同的内存空间。
2. 引用传递 (Pass by Reference) 的引入与实践
要使用引用传递,我们只需要在函数定义的参数类型后面加上一个 & 符号。
- 代码示例:通过引用修改变量
#include <iostream> using namespace std; // 注意这里的 int& value // value 不再是 number 的复印件,而是 number 的一个“别名” void modifyByReference(int& value) { cout << "函数内部:进入函数时,value (即 number) 的值是 " << value << endl; value = 100; // 修改别名 value,就等于修改了原件 number cout << "函数内部:修改后,value (即 number) 的值是 " << value << endl; } int main() { int number = 10; cout << "main 函数:调用函数前,number 的值是 " << number << endl; modifyByReference(number); // 传递的是 number 的“引用” cout << "main 函数:调用函数后,number 的值是 " << number << endl; return 0; } - 运行结果:
main 函数:调用函数前,number 的值是 10 函数内部:进入函数时,value (即 number) 的值是 10 函数内部:修改后,value (即 number) 的值是 100 main 函数:调用函数后,number 的值是 100 - 结论:
modifyByReference函数成功地通过别名value修改了main函数中number的值。
一个经典的例子就是编写一个 swap 函数来交换两个变量的值。
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 5, y = 10;
cout << "交换前: x = " << x << ", y = " << y << endl;
swap(x, y);
cout << "交换后: x = " << x << ", y = " << y << endl;
return 0;
}3. “常量引用”:兼得安全与高效
我们已经知道,引用传递可以避免复制大型对象带来的开销。但如果我们只是想“读取”这个大型对象,而不希望函数修改它,该怎么办呢?
答案是使用 常量引用 (const reference)。
语法:
const 数据类型& 变量名代码示例:高效且安全地打印
string#include <iostream> #include <string> using namespace std; // 使用 const string& 作为参数 // 1. & 符号避免了 string 的复制,提高了效率 // 2. const 关键字告诉编译器:这个函数“保证”不会修改 s void printString(const string& s) { cout << "字符串内容: " << s << endl; // s = "试图修改"; // 如果你取消这行代码的注释,程序将无法编译! } int main() { string myLongText = "这是一段非常非常长的文本..."; printString(myLongText); // 调用函数后,我们完全放心 myLongText 的内容没有被篡改 return 0; }
最佳实践:当你传递一个大型对象(如 string、或我们未来会学的 vector、class 对象)给函数,并且你不打算在函数内修改它时,永远优先使用常量引用。
何时使用哪种传递方式?(选择的艺术)
| 传递方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
值传递void func(int x); | 安全:不会修改实参。 | 有复制开销:对于大型对象效率低。 | 传递内置基本类型 (int, double, char, bool 等) 时。当不希望函数修改实参时。 |
引用传递void func(int& x); | 高效:无复制开销。 可以修改实参。 | 不安全:可能会无意中修改实参。 | 当你明确需要函数去修改一个或多个实参的值时(例如 swap 函数)。 |
常量引用传递void func(const int& x); | 高效:无复制开销。 安全:无法修改实参。 | 无明显缺点。 | 传递大型对象 (string, vector, 自定义类等),并且只读不写时。这是 C++ 中非常重要的性能优化和编码规范。 |
“防坑”指南与常见错误 (The Pitfalls)
引用必须被初始化
引用在本质上是一个别名,它必须在被创建的时候就明确它是“谁的”别名。
int a = 10;
int& ref_a = a; // 正确:ref_a 在声明时就被初始化为 a 的引用
int& ref_b; // 错误!引用必须在声明时初始化不要返回局部变量的引用
这是一个非常危险且隐蔽的错误!
// 致命的错误代码
int& createNumber() {
int num = 10; // num 是一个局部变量
return num; // 返回局部变量的引用
} // 当函数结束时,num 占用的内存被系统回收了!
int main() {
int& myRef = createNumber();
// 此时 myRef 引用了一块已经被释放的、无效的内存区域
// 对 myRef 的任何操作都可能导致程序崩溃或未知行为
cout << myRef << endl;
return 0;
}法则:永远不要返回一个在函数结束时就不复存在的局部变量的引用或指针。
本章小结 (The Summary)
我们深入探讨了函数参数传递的两种核心机制。
- 值传递 (Pass by Value) 传递的是副本,函数内无法修改原件,适用于基本数据类型。
- 引用传递 (Pass by Reference) 传递的是别名,函数内可以直接修改原件,适用于需要修改实参的场景。
- 常量引用传递 (Pass by const Reference) 既能像引用传递一样高效(避免复制),又能像值传递一样安全(防止修改),是传递大型只读对象的最佳选择。
- 我们必须根据函数的目的(是否需要修改实参)和参数的大小来明智地选择参数传递方式。
理解引用是理解 C++ 内存模型的关键一步。它让我们得以一窥 C++ 中一个更底层、更强大但也更危险的概念。在下一章,我们将直面 C++ 的“终极挑战”,也是它强大性能的根源——【挑战】“门牌号”:指针入门。
