引用某样东西的两种方式
你已经认识了指针:存放地址的变量,让你能访问住在那里的对象。引用是 C++ 提供的另一种间接操作已有对象的工具。两者重叠的地方很多,初学者常常分不清该选哪个,所以本页把它们放在一起逐项对比。
简单说:引用是别名。int& r = x; 执行之后,r 就是 x:同一个对象,不同的名字。指针是一个独立的对象,只不过它恰好存着另一个对象的地址。其他所有差异都源于这一点。
引用就是别名
引用在创建的那一刻就必须绑定到一个对象上,从此以后,对引用的每次使用都作用在原对象上。
注意,使用时既没有用来解引用的 *,也没有用来“取地址”的 &:你读写 alias 的方式和普通的 int 完全一样。int& alias 中的 & 是类型的一部分,不是取地址运算符。
它们的不同之处
下面这些行为差异,正是两种工具同时存在的全部理由。这张表值得记住。
// reference pointer
// must be initialized? yes no (but should be)
// can be null? no yes (nullptr)
// can be reseated? no yes
// pointer arithmetic? no yes
// syntax to use it just the name *p or p->member
// taking address &ref == &original &p is the pointer's own address
其中有两点最容易让人犯错。第一,引用永远不能重新绑定:给它赋值是把一个值拷贝进它所引用的对象,而不是让引用指向新的东西。
相比之下,指针随时都可以改为指向别处:
第二,引用永远不可能合法地为空,而指针可以。这意味着“没有值”可以用指针表示,却无法用引用表示,这个特性你会经常用到。
在函数参数中如何选择
这种选择最常出现在函数参数上。当函数需要读取或修改调用者的对象时,两者都能用,但传达的意图不同。
引用版本(addTax(cart))不可能用“什么都没有”来调用,所以函数内部永远不用检查空值,对象一定存在。指针版本(applyDiscount(&cart))则在调用处通过 & 表明参数可能会被修改,还允许调用者传入 nullptr 来表示“不适用”。选那个保证与你的函数相符的。
对于大类型的只读参数,惯用的选择是 const T&:它避免了拷贝,同时承诺不做修改。按值传递和按引用传递的更多内容,请看函数参数一节。
一条简单的经验法则
拿不准的时候,默认用引用,只有在需要引用做不到的能力时才换成指针:
- 用引用:当对象一定存在,而且身份永远不变时。这是函数参数和别名的常见情况。
- 用指针:当下面任意一条成立时:
- “什么都没有”是合法状态(可选参数、可能找不到匹配项的搜索):指针可以是
nullptr。 - 你需要随时间指向不同的对象:指针可以重新指向。
- 你在管理之后要
delete的堆内存(或者更好的做法,用智能指针),或者在用指针运算遍历数组。
- “什么都没有”是合法状态(可选参数、可能找不到匹配项的搜索):指针可以是
如果以上都不适用,引用就是更简洁、更安全的选择,因为编译器会替你保证“始终有效,绝不重新绑定”。
要避开的常见错误
- 以为
ref = other会重新绑定。 它其实是把一个值赋给所引用的对象。引用一旦绑定就终身不变;需要重新指向时,用指针。 - 返回局部变量的引用(或指针)。
int& f() { int x = 5; return x; }返回的是一个悬空引用:f返回时x就销毁了,使用这个结果是未定义行为。指针也有同样的陷阱(return &x;)。 - 伪造一个“空引用”。 在
p是nullptr时写int& r = *p;,解引用的那一刻就是未定义行为,而不是一个安全的“空”引用。请改用指针或std::optional来表达可选性。 - 出于习惯就用指针。 如果参数一定存在、你也永远不会重新指向它,引用就能免去一整类空值检查和崩溃。用不到的能力,就别为它付出代价。
下一节:动态内存
到目前为止,你引用或指向的每个对象都是在栈上自动创建的。下一页动态内存会讲解 new 和 delete:如何在运行时向操作系统申请内存,为什么拥有这块内存的是指针(而不是引用),以及忘记释放它为什么会导致内存泄漏。
常见问题
C++ 中引用和指针有什么区别?
引用是一个已有对象的别名:它必须初始化,永远不能为空,之后也不能再改为引用另一个对象。指针是一个存放地址的独立变量:它可以为空,可以重新指向别处,还支持指针运算。引用使用 & 语法,指针使用 */->。
C++ 中什么时候该用指针而不是引用?
当“什么都没有”是一种合法状态时(可选参数、未找到的结果),当你需要随时间让它指向不同对象时,或者当你持有之后要 delete 的堆内存时,用指针。当那个东西一定存在、身份也从不改变时,用引用,大多数函数参数都属于这种情况。
C++ 中的引用可以为空吗?
不可以。合法的引用总是引用一个真实的对象,所以你从不需要检查它是否为空。如果你通过解引用一个空指针来创建引用(int& r = *p;,其中 p 为空),得到的是未定义行为,而不是一个空引用。需要表达“可能什么都没有”时,请用指针或 std::optional。