章节目录

左值引用

本节阅读量:

在C++中,引用是现有对象的别名。定义引用后,对引用的任何操作都将应用于被引用的对象。

这意味着我们可以使用引用来读取或修改被引用的对象。尽管一开始引用可能看起来很傻、无用或冗余,但在C++中引用无处不在(我们将在几节课中看到这方面的例子)。

您还可以创建对函数的引用,尽管这样做的频率较低。

现代C++包含两种类型的引用:左值引用和右值引用。在本章中,我们将讨论左值引用。


左值引用类型

左值引用(通常只是称为引用,因为在C++11之前只有一种类型的引用)充当现有左值(例如变量)的别名。

要声明左值引用类型,我们在类型声明中使用与号(&):

1
2
3
int      // 普通的int类型
int&     // int类型对象的左值引用
double&  // double类型对象的左值引用

左值引用变量

对于左值引用类型,我们可以做的事情之一是创建左值引用变量。

要创建左值引用变量,我们只需定义具有左值引用类型的变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <iostream>

int main()
{
    int x { 5 };    // x 是普通的int类型变量
    int& ref { x }; // ref 是int类型的左值引用变量,充当变量x的别名

    std::cout << x << '\n';  // 打印 x (5)
    std::cout << ref << '\n'; // 打印 x 通过 ref (5)

    return 0;
}

在上面的示例中,类型int&将ref变量定义为对int的左值引用,然后使用左值表达式x对其进行初始化。此后,ref和x可以同义使用。因此,该程序打印:

1
2
5
5

从编译器的角度来看,与号是“附加”到类型名(int& ref)还是变量名(int &ref)并不重要,您选择哪一个是风格问题。现代C++程序员倾向于将与号附加到类型上,因为它清楚地表明引用是类型信息的一部分,而不是变量标识符的一部分。


通过左值引用修改值

在上面的例子中,展示了可以使用引用来读取被引用对象的值。还可以使用引用来修改被引用对象的值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

int main()
{
    int x { 5 }; // 普通int变量
    int& ref { x }; // ref 现在是 x 的别名

    std::cout << x << ref << '\n'; // 打印 55

    x = 6; // x 现在值是 6

    std::cout << x << ref << '\n'; // 打印 66

    ref = 7; // 对引用的对象(x)现在值是 7

    std::cout << x << ref << '\n'; // 打印 77

    return 0;
}

此代码打印:

1
2
3
55
66
77

在上面的示例中,ref是x的别名,因此我们可以通过x或ref更改x的值。


左值引用的初始化

与常量很相似,所有引用都必须初始化。

1
2
3
4
5
6
7
8
9
int main()
{
    int& invalidRef;   // error: 引用必须被初始化

    int x { 5 };
    int& ref { x }; // okay: 有初始化值

    return 0;
}

当使用对象(或函数)初始化引用时,我们说它绑定到该对象(或函数)。绑定这种引用的过程称为引用绑定。被引用的对象(或函数)有时称为被引用对象。

左值引用必须绑定到可修改的左值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int main()
{
    int x { 5 };
    int& ref { x }; // 有效: 左值引用绑定到可修改的左值

    const int y { 5 };
    int& invalidRef { y };  // 无效: 不能绑定到不可修改的左值
    int& invalidRef2 { 0 }; // 无效: 不能绑定到右值

    return 0;
}

左值引用不能绑定到不可修改的左值或右值(否则,您可以通过引用更改这些值,这将违反它们的常量属性)。由于这个原因,左值引用有时被称为非常量的左值引用(有时缩写为非常量引用)。

在大多数情况下,引用的类型必须与被引用的类型匹配(当讨论类继承时,将讨论该规则的一些例外):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int main()
{
    int x { 5 };
    int& ref { x }; // okay: 类型匹配

    double y { 6.0 };
    int& invalidRef { y }; // 无效; int 引用不能绑定到 double变量
    double& invalidRef2 { x }; // 无效: double 引用不能绑定到 int变量

    return 0;
}

不允许对void进行左值引用。


引用无法重置(更改为引用其他对象)

一旦初始化,C++中的引用就不能重置,这意味着不能将其更改为引用另一个对象。

新的C++程序员通常试图通过使用赋值来为引用提供另一个要引用的变量来重置引用。这将通过编译并运行——但不会像预期的那样运行。考虑以下程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>

int main()
{
    int x { 5 };
    int y { 6 };

    int& ref { x }; // ref 是 x 的别名
    
    ref = y; // 将 6 (y的值) 设置给 x (ref所引用的对象)
    // 上面这一行不会将ref的引用重置为y!

    std::cout << x << '\n'; // 你可能认为这里打印 5

    return 0;
}

也许令人惊讶的是,这打印了:

1
6

在表达式中计算引用时,它解析为它引用的对象。因此 ref = y 不会将ref更改为引用y。相反,因为ref是x的别名,表达式的计算就像 x = y 一样——并且由于y的计算结果为值6,所以x被赋值6。


左值引用的作用域

引用变量遵循与普通变量相同的作用域:

1
2
3
4
5
6
7
8
9
#include <iostream>

int main()
{
    int x { 5 }; // 普通变量
    int& ref { x }; // 变量x的引用

     return 0;
} // x 和 ref 在这里失效

引用和被引用对象具有独立的生命周期

除了一个例外(我们将在下一课中讨论),引用和被引用对象的生命周期是独立的。换句话说,以下两项都是正确的:

  1. 引用可以在其引用的对象之前销毁。
  2. 被引用的对象可以在引用之前销毁。

当引用在被引用对象之前被销毁时,被引用对象不会受到影响。下面的程序演示了这一点:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>

int main()
{
    int x { 5 };

    {
        int& ref { x };   // ref 是 x 的引用
        std::cout << ref << '\n'; // 打印 ref 的值 (5)
    } // ref 在这里被销毁 -- x 不受影响

    std::cout << x << '\n'; // 打印 x 的值 (5)

    return 0;
} // x 在这里被销毁

以上打印内容:

1
2
5
5

当ref被销毁时,变量x继续正常存在,不感知对它的引用已被破坏。


悬空引用

当被引用的对象在引用之前被销毁时,引用将继续引用不再存在的对象。这样的引用称为悬空引用。访问悬空引用会导致未定义的行为。

悬空引用是很容易避免的,我们将在后续展示这样一种情况——按引用返回和按地址返回。


引用不是对象

也许令人惊讶的是,引用不是C++中的对象。引用不需要存在或占用存储。如果可能,编译器将通过用被引用项替换引用的所有出现的地方来优化引用。然而,这并不总是可能的,在这种情况下,引用可能需要存储空间。

这也意味着术语“引用变量”有点用词不当,因为变量是有名称的对象,而引用不是对象。

因为引用不是对象,所以不能在需要对象的任何地方使用它们(例如,不能引用引用,因为左值引用必须引用可识别的对象)。在需要引用作为对象或可以重置的引用的情况下,std::std::reference_wrapper(后续介绍)提供了一个解决方案。


12.1 值类别(左值 lvalue 和右值 rvalue)

上一节

12.3 const的左值引用

下一节