引用

引用

什么是引用?

对象的别名(替代名称)。

引用常用于按引用传递。

void swap(int& i, int& j)
{
  int tmp = i;
  i = j;
  j = tmp;
}

int main()
{
  int x, y;
  // ...
  swap(x,y);
  // ...
}

在这里,`i` 和 `j` 分别是 main 函数中 `x` 和 `y` 的别名。换句话说,`i` **就是** `x`——不是指向 `x` 的指针,也不是 `x` 的副本,而是 `x` 本身。你对 `i` 进行的任何操作都会作用于 `x`,反之亦然。这包括取其地址。`&i` 和 `&x` 的值是相同的。

作为程序员,你应该这样理解引用。现在,为了避免混淆,我将从另一个角度解释引用的实现方式。在底层,对对象 `x` 的引用 `i` 通常是对象 `x` 的机器地址。但是当程序员写 `i++` 时,编译器会生成递增 `x` 的代码。特别是,编译器用来查找 `x` 的地址位没有改变。C 程序员会认为这就像使用了 C 风格的按指针传递,语法变体是 (1) 将 `&` 从调用者移到被调用者,(2) 消除 `*`。换句话说,C 程序员会认为 `i` 是 `(*p)` 的宏,其中 `p` 是指向 `x` 的指针(例如,编译器自动解引用底层指针;`i++` 变为 `(*p)++`;`i = 7` 自动变为 `*p = 7`)。

重要提示:尽管在底层汇编语言中,引用通常是使用地址实现的,但请不要将引用视为一个长相奇特的指向对象的指针。引用**就是**对象,只是换了一个名字。它既不是指向对象的指针,也不是对象的副本。它**就是**对象。C++ 中没有任何语法允许你操作引用本身,使其与它所引用的对象分离。

对引用赋值会发生什么?

你改变了被引用对象(被引用对象是引用所指的对象)的状态。

记住:引用**就是**被引用对象,所以改变引用会改变被引用对象的状态。用编译器作者的行话来说,引用是一个“左值”(可以出现在赋值运算符左侧的东西)。

返回引用会发生什么?

函数调用可以出现在赋值运算符的左侧。

这种能力一开始可能看起来很奇怪。例如,没有人会认为表达式 `f() = 7` 有意义。然而,如果 `a` 是 `Array` 类的对象,大多数人会认为 `a[i] = 7` 有意义,即使 `a[i]` 实际上只是一个伪装的函数调用(它调用 `Array::operator[](int)`,这是 `Array` 类的下标运算符)。

class Array {
public:
  int size() const;
  float& operator[] (int index);
  // ...
};

int main()
{
  Array a;
  for (int i = 0; i < a.size(); ++i)
    a[i] = 7;    // This line invokes Array::operator[](int)
  // ...
}

`object.method1().method2()` 是什么意思?

它将这些方法调用串联起来,这就是为什么这被称为方法链

首先执行的是 `object.method1()`。这会返回某个对象,它可能是对 `object` 的引用(即 `method1()` 可能以 `return *this;` 结束),也可能是其他对象。我们将返回的对象称为 `objectB`。然后 `objectB` 成为 `method2()` 的 `this` 对象。

方法链最常见的用途是在 `iostream` 库中。例如,`cout << x << y` 之所以能工作,是因为 `cout << x` 是一个返回 `cout` 的函数。

方法链一个不那么常见但仍然很巧妙的用法是命名参数惯用法

如何重新绑定引用以使其引用不同的对象?

没有办法。

你无法将引用与其所指对象分离。

与指针不同,一旦引用绑定到对象,它就**无法**“重新绑定”到另一个对象。引用不是一个独立的对象。它没有自己的身份。获取引用的地址会得到其所指对象的地址。记住:引用**就是**其所指对象。

从这个意义上说,引用类似于 `const` 指针,例如 `int* const p`(而不是 指向 `const` 的指针,例如 `const int* p`)。但是请不要将引用与指针混淆;从程序员的角度来看,它们非常不同。

C++ 为什么既有指针又有引用?

C++ 从 C 继承了指针,因此无法在不引起严重兼容性问题的情况下移除它们。引用在很多方面都很有用,但它们被引入 C++ 的直接原因是支持运算符重载。例如

    void f1(const complex* x, const complex* y) // without references
    {
        complex z = *x+*y;  // ugly
        // ...
    }

    void f2(const complex& x, const complex& y) // with references
    {
        complex z = x+y;    // better
        // ...
    }   

更一般地说,如果你想同时拥有指针的功能和引用的功能,你需要两种不同的类型(如 C++ 中)或对单个类型进行两组不同的操作。例如,对于单个类型,你需要一个操作来赋值给所引用的对象,以及一个操作来赋值给引用/指针。这可以使用单独的运算符来完成(如 Simula 中)。例如

    Ref<My_type> r :- new My_type;
    r := 7;         // assign to object
    r :- new My_type;   // assign to reference

或者,你可以依赖类型检查(重载)。例如

    Ref<My_type> r = new My_type;
    r = 7;          // assign to object
    r = new My_type;    // assign to reference

我什么时候应该使用引用,什么时候应该使用指针?

能用引用时就用引用,不得不时才用指针。

只要你不需要“重新绑定”,通常都会优先选择引用而不是指针。这通常意味着引用在类的 `public` 接口中最有用。引用通常出现在对象的“表面”,而指针出现在“内部”。

上述规则的例外情况是当函数的参数或返回值需要一个“哨兵”引用时——一个不引用对象的引用。这通常最好通过返回/接收指针,并将 `nullptr` 值赋予这种特殊意义来完成(引用必须始终是对象的别名,而不是解引用后的空指针)。

注意:老一辈的 C 程序员有时不喜欢引用,因为它们提供了在调用者代码中不明确的引用语义。然而,在一些 C++ 经验之后,人们很快就会意识到这是一种信息隐藏的形式,它是一种优势而不是负担。例如,程序员应该用问题语言而不是机器语言来编写代码。

引用必须引用一个对象,而不是一个解引用后的空指针,这意味着什么?

这意味着这是非法的

T* p = nullptr;
T& r = *p;  // illegal

注意:请**不要**发邮件给我们说上述代码在您特定版本的特定编译器上有效。它仍然是非法的。C++ 语言,如 C++ 标准所定义,规定它是非法的;这使得它非法。C++ 标准不要求对这个特定的错误进行诊断,这意味着您特定的编译器没有义务注意到 `p` 是 `nullptr` 或给出错误消息,但它仍然是非法的。C++ 语言也不要求编译器生成会在运行时崩溃的代码。事实上,您特定版本的特定编译器可能会或可能不会生成您认为有意义的代码,如果您这样做。但这正是重点:由于编译器不被要求生成有意义的代码,您不知道编译器会做什么。所以请不要发邮件给我们说您特定的编译器生成了好的代码;我们不关心。它仍然是非法的。有关更多信息,请参阅 C++ 标准,例如 C++ 2014 标准的 8.3.2 [dcl.ref] p5 节。

举例说明,**不**作为限制,一些编译器会优化 `nullptr` 测试,因为它们“知道”所有引用都指向真实对象——引用永远不会(合法地)是解引用后的 `nullptr`。这可能导致编译器优化掉以下测试

// ...the above code...
T* p2 = &r;
if (p2 == nullptr) {
  // ...
}

如上所述,这只是一个示例,说明您的编译器可能会根据语言规则(引用必须引用有效对象)执行的一种操作。不要将您的思维局限于上述示例;本 FAQ 的信息是,如果您违反规则,编译器不要求执行有意义的操作。所以不要违反规则。

病人:“医生,医生,我用勺子戳眼睛时,眼睛疼。”

医生:“那你就别戳它了。”

什么是对象的句柄?它是指针吗?它是引用吗?它是指向指针的指针吗?它是什么?

术语句柄用于表示任何能让你访问另一个对象的技术——一个广义的伪指针。这个术语是(故意)模糊和不明确的。

在某些情况下,模糊性实际上是一种优势。例如,在早期设计阶段,你可能还没有准备好确定句柄的具体表示。你可能不确定是想使用简单指针、引用、指向指针的指针、指向指针的引用、数组中的整数索引、字符串(或其他键)可以通过哈希表(或其他数据结构)查找、数据库键,还是其他技术。如果你只是知道你需要某种东西来唯一标识和访问一个对象,你就可以把这个东西叫做句柄。

因此,如果你的最终目标是让一段代码能够唯一识别/查找某个 `Fred` 类的特定对象,你需要将一个 `Fred` 句柄传递给那段代码。该句柄可能是一个字符串,可以用作某个知名查找表中的键(例如,`std::map``std::map` 中的键),或者它可能是一个整数,作为某个知名数组的索引(例如,`Fred* array = new Fred[maxNumFreds]`),或者它可能是一个简单的 `Fred*`,或者它可能是其他东西。

新手通常会从指针的角度思考,但实际上使用裸指针存在风险。例如,如果 `Fred` 对象需要移动怎么办?我们如何知道何时可以安全地 `delete` `Fred` 对象?如果 `Fred` 对象需要(暂时)序列化到磁盘怎么办?等等。大多数时候,我们会添加更多的间接层来管理这些情况。例如,句柄可能是 `Fred**`,其中指向的 `Fred*` 指针保证永不移动,但当 `Fred` 对象需要移动时,你只需更新指向的 `Fred*` 指针。或者你将句柄设为一个整数,然后让 `Fred` 对象(或指向 `Fred` 对象的指针)在表/数组/其他地方查找。或者其他什么。

重点是,当我们尚不清楚具体要做什么时,我们就使用“句柄”这个词。

我们使用“句柄”一词的另一个时机是,当我们想模糊地说明我们已经做了什么时(有时也用“魔术饼干”这个词,例如,“该软件传递了一个魔术饼干,用于唯一标识和定位相应的 `Fred` 对象”)。我们(有时)想模糊地说明我们已经做了什么的原因是为了在句柄的具体细节/表示发生变化时最大限度地减少连锁反应。例如,如果/当有人将句柄从用于查找表的字符串更改为在数组中查找的整数时,我们不希望去更新成千上万行代码。

为了在句柄的细节/表示发生变化时进一步简化维护(或通常使代码更易于阅读/编写),我们通常将句柄封装在一个类中。这个类通常会重载运算符 `operator->` 和 `operator*`(因为句柄行为类似于指针,所以它最好看起来也像指针)。

我应该使用按值调用还是按引用调用?

(注意:此 FAQ 需要针对 C++11 进行更新。)

这取决于你想要达到的目标。

  • 如果你想改变传递的对象,请按引用调用或使用指针;例如,`void f(X&);` 或 `void f(X*);`。
  • 如果你不想改变传递的对象并且它很大,请按 const 引用调用;例如,`void f(const X&);`。
  • 否则,按值调用;例如 `void f(X);`。

“大”是什么意思? 任何大于几个字的东西。

你为什么想改变一个参数? 好吧,我们经常不得不这样做,但我们通常还有另一种选择:产生一个新值。考虑一下

    void incr1(int& x); // increment
    int incr2(int x);   // increment

    int v = 2;
    incr1(v);   // v becomes 3
    v = incr2(v);   // v becomes 4

对于读者来说,`incr2()` 可能更容易理解。也就是说,`incr1()` 更容易导致错误。因此,只要创建和复制新值的成本不高,我们应该更喜欢返回新值的风格,而不是修改现有值的风格。

如果你确实想改变参数,是应该使用指针还是引用? 如果传递“非对象”(例如,空指针)是可接受的,那么使用指针是有意义的。一种风格是在你想修改对象时使用指针,因为在某些情况下,这使得更容易发现修改的可能性。

另请注意,成员函数的调用本质上是对对象的按引用调用,因此我们通常在想要修改对象的值/状态时使用成员函数。

为什么 `this` 不是引用?

因为 `this` 在引用加入之前就已引入 C++(实际上是 C with Classes)。此外,Stroustrup 选择 `this` 遵循 Simula 的用法,而不是(后来的)Smalltalk 中 `self` 的用法。