参数与实参
函数的参数(形参)是它定义中那些有名字的变量;实参是你调用它时实际交给它的值。上一页展示了如何定义和调用函数,这一页讲的是这些值究竟是怎么进入函数的。C++ 提供了好几种方式,而你的选择会同时影响正确性和速度。
C++ 的默认方式是值传递:函数收到的是一份副本。
在 addTen 内部,n 是一个用 score 初始化的独立变量。给 n 重新赋值只会影响这份副本,所以回到 main 中,score 毫发无损。这种方式安全、可预测,函数不会意外破坏你的数据,这正是它成为默认方式的原因。
引用传递:让函数修改调用者的变量
有时你确实希望函数修改调用者的变量。在参数类型后面加一个 &,它就变成了引用:它是原变量的别名,而不是副本:
和第一个例子相比,唯一的区别就是这个 &,但现在 n 和 score 是同一个对象。这是“返回”多个值或就地更新某个东西的标准做法。一个经典用法是交换两个变量:
如果没有 &,swapValues 交换的只是两份副本,main 根本看不到任何变化。这是一个非常常见的新手 bug。
const 引用:低成本的只读访问
值传递会复制实参。对 int 来说这毫无代价,但每次调用都复制一个大的 string 或 vector,就是实实在在的浪费。解决办法是 const 引用(const T&):你既得到了引用的速度(不复制),又有编译器保证函数不会修改实参。
一条好用的经验法则:小的内置类型(int、double、char、bool、指针)按值传递;只需要读取的大对象按 const 引用传递。普通的非 const T& 只留给你确实打算修改调用者对象的情况。
一个不易察觉的坑:普通的 int& n 不能绑定到临时值或字面量。如果第一个例子中的参数是 int&,addTen(5) 就会编译失败,因为 5 不是一个可以取别名的变量。而 const int& 可以绑定到 5,这也是 const 引用被如此广泛使用的又一个原因。
默认实参
你可以给参数一个备用值,让调用者可以省略它。如果缺少这个实参,就使用默认值:
有两条规则常常让人出错。第一,默认值必须放在末尾:一旦某个参数有了默认值,它后面的每个参数也都必须有。你不能写 void f(int a = 1, int b),因为没有办法在跳过 a 的同时提供 b。第二,当函数在头文件中声明、在别处定义时,默认值只写在声明中,不要在定义中重复,重复写会导致编译错误。
传递数组和 vector
原始数组在传递时会退化为指针,所以函数会丢失它的大小信息,你几乎总是要把长度一起传进去:
因为数组变成了指针,在 sum 内部用 sizeof(arr) 得到的是指针的大小,而不是数组的大小,这是一个臭名昭著的 bug。在现代 C++ 中,优先使用按 const 引用传递的 std::vector(或 C++20 的 std::span),它自己带着大小信息:
注意这里的 const&:去掉它,每次调用都会复制整个 vector。对于只有四个元素的 vector 这无伤大雅,但对于一百万个元素,它就是一个悄无声息的性能黑洞。
指针参数
你也可以传递指针(T*)。和引用一样,这能让函数访问调用者的数据,但指针可以重新指向别处,也可以为空。所以当“没有值”是一种合理的情况时,指针就是正确的工具:
调用者传入 &value 来共享它的地址,函数通过 *out 写入结果。与引用的关键区别在于:指针可能是 nullptr,所以接受指针的函数应该在解引用之前先检查。跳过这个检查去解引用空指针是未定义行为,通常会导致崩溃。如果“没有值”永远没有意义,引用会更简洁,因为它从一开始就不可能为空。
下一步:引用
参数是引用大显身手的地方,但引用本身也是一个独立的特性:你可以为任何变量创建别名,而不只是在函数签名中。下一页深入讲解引用本身是如何工作的:怎样声明引用,为什么必须立即初始化,左值引用与 const 引用的区别,以及引用可能变成悬空引用的那些隐蔽情况。
常见问题
C++ 中值传递和引用传递有什么区别?
值传递会把实参复制到参数中,所以函数内部的修改不会影响调用者。引用传递(int&)让参数成为调用者变量的别名,所以修改在函数外部也看得到。用 void f(int x) 复制,用 void f(int& x) 修改原来的变量。
C++ 中什么时候应该使用 const 引用参数?
当你想读取一个大对象,既不想复制它,又不想让函数修改它时,就用 const T&,例如 void print(const string& s)。它兼具引用传递的速度和值传递的安全。对于 int 或 char 这样的小类型,普通的值传递同样快。
C++ 中的默认实参是什么?
默认实参让参数在调用者省略它时取一个备用值,例如 void greet(string name = "there")。带默认值的参数必须位于参数列表的末尾(最右边);如果声明和定义是分开的,只在声明中指定默认值,不要在定义中再写。