12-3 左值引用
在 C++ 中,引用是现有对象的别名。一旦定义了引用,对该引用执行的任何操作都会应用于被引用的对象。这意味着我们可以使用引用来读取或修改被引用的对象。
虽然引用乍一看似乎很愚蠢、无用或多余,但在 C++ 中到处都使用了引用(我们将在几节课中看到一些例子)。
关键见解:
引用本质上与被引用的对象相同。
你也可以创建对函数的引用,尽管这种情况不太常见。
现代 C++ 包含两种类型的引用:左值引用和右值引用。本章我们将讨论左值引用。
相关内容:
由于本课我们将讨论左值和右值,如果您需要复习这些术语,请在继续学习之前回顾12.2 节——值类别(左值和右值) 。右值引用在关于移动语义的章节(第 22 章)中介绍。
左值引用类型
左值引用(通常简称为“引用”,因为在 C++11 之前只有一种类型的引用)充当现有左值(例如变量)的别名。
就像对象的类型决定了它可以存储什么类型的值一样,引用的类型决定了它可以引用什么类型的对象。左值引用类型可以通过在类型说明符中使用单个 & 符号来标识:
// regular types
int // a normal int type (not an reference)
int& // an lvalue reference to an int object
double& // an lvalue reference to a double object
const int& // an lvalue reference to a const int object
例如,int&是指向int对象的左值引用的类型,也是const int&指向const int&对象的左值引用的类型。
指定引用的类型(例如int&)称为引用类型。可以被引用的类型(例如int)称为被引用类型。
命名法:
左值引用有两种类型:
- 指向非常量值的左值引用通常简称为“左值引用”,但也可以称为指向非常量的左值引用或非常量左值引用(因为它没有使用const关键字定义)。
- 指向常量的左值引用通常被称为指向常量的左值引用或常量左值引用。
在本课中,我们将重点讨论非常量左值引用,在下一课中讨论常量左值引用(12.4 -- 左值引用 const)。
左值参考变量
我们可以使用左值引用类型执行的操作之一是创建左值引用变量。左值引用变量是一个指向左值(通常是另一个变量)的引用变量。
要创建一个左值引用变量,我们只需定义一个具有左值引用类型的变量即可:
#include <iostream>
int main()
{
int x { 5 }; // x is a normal integer variable
int& ref { x }; // ref is an lvalue reference variable that can now be used as an alias for variable x
std::cout << x << '\n'; // print the value of x (5)
std::cout << ref << '\n'; // print the value of x via ref (5)
return 0;
}
在上面的例子中,类型int&定义ref为指向 int 的左值引用,然后我们使用左值表达式对其进行初始化x。之后,ref和x 可以互换使用。因此,该程序会输出:

从编译器的角度来看,& 符号是附加在类型名(int& ref)还是变量名(int &ref)上并不重要,选择哪一个只是编程风格的一个问题。现代 C++ 程序员倾向于将 & 符号附加在类型上,因为这样可以更清晰地表明该引用是类型信息的一部分,而不是标识符。
最佳实践:
定义引用时,将 & 符号放在类型旁边(而不是引用变量的名称旁边)。
适合高级读者:
对于已经熟悉指针的人来说,这里的 & 符号并不表示“地址”,而是表示“左值引用”。
通过非常量左值引用修改值
在上面的例子中,我们展示了如何使用引用来读取被引用对象的值。我们还可以使用非常量引用来修改被引用对象的值:
#include <iostream>
int main()
{
int x { 5 }; // normal integer variable
int& ref { x }; // ref is now an alias for variable x
std::cout << x << ref << '\n'; // print 55
x = 6; // x now has value 6
std::cout << x << ref << '\n'; // prints 66
ref = 7; // the object being referenced (x) now has value 7
std::cout << x << ref << '\n'; // prints 77
return 0;
}

在上面的例子中,ref是x的别名,所以我们可以通过x或``ref来更改x`的值。
引用初始化
与常量类似,所有引用都必须初始化。引用的初始化采用一种称为引用初始化的初始化方式。
int main()
{
int& invalidRef; // error: references must be initialized
int x { 5 };
int& ref { x }; // okay: reference to int is bound to int variable
return 0;
}

当一个引用被初始化为某个对象(或函数)时,我们称它绑定到了该对象(或函数)。这种绑定过程称为引用绑定。被引用的对象(或函数)有时被称为被引用项。
非常量左值引用只能绑定到可修改的左值。
int main()
{
int x { 5 };
int& ref { x }; // okay: non-const lvalue reference bound to a modifiable lvalue
const int y { 5 };
int& invalidRef { y }; // invalid: non-const lvalue reference can't bind to a non-modifiable lvalue
int& invalidRef2 { 0 }; // invalid: non-const lvalue reference can't bind to an rvalue
return 0;
}


关键见解:
如果非常量左值引用可以绑定到不可修改的(常量)左值或右值,那么就可以通过引用来改变这些值,这将违反它们的常量性。
不允许使用左值引用void(这样做有什么意义呢?)。
即使引用的类型(例如int&)与被绑定对象的类型(例如int)不完全匹配,这里也不会进行任何转换(甚至不会进行简单的转换)——类型差异会在引用初始化过程中进行处理。
引用(通常)只会绑定到与其引用类型匹配的对象。
在大多数情况下,引用只会绑定到类型与被引用类型匹配的对象(这条规则也有一些例外,我们将在讨论继承时再做介绍)。
如果尝试将引用绑定到与其引用类型不匹配的对象,编译器将尝试隐式地将该对象转换为引用类型,然后将引用绑定到该类型。
关键见解:
由于转换的结果是一个右值,而非常量左值引用不能绑定到右值,因此尝试将非常量左值引用绑定到与其引用类型不匹配的对象将导致编译错误。
int main()
{
int x { 5 };
int& ref { x }; // okay: referenced type (int) matches type of initializer
double d { 6.0 };
int& invalidRef { d }; // invalid: conversion of double to int is narrowing conversion, disallowed by list initialization
double& invalidRef2 { x }; // invalid: non-const lvalue reference can't bind to rvalue (result of converting x to double)
return 0;
}
引用不能重新定位(不能更改为指向另一个对象)
C++ 中的引用一旦初始化就不能重新定位,这意味着它不能被更改为引用另一个对象。
新手 C++ 程序员经常尝试通过赋值语句重新定义引用,将另一个变量赋给原引用。这样做虽然可以编译运行,但实际效果却不如预期。请看以下程序:
#include <iostream>
int main()
{
int x { 5 };
int y { 6 };
int& ref { x }; // ref is now an alias for x
ref = y; // assigns 6 (the value of y) to x (the object being referenced by ref)
// The above line does NOT change ref into a reference to variable y!
std::cout << x << '\n'; // user is expecting this to print 5
return 0;
}
或许令人惊讶的是,这幅画作

当表达式中对引用进行求值时,它会解析为它所引用的对象。因此ref = y不能改变ref 为现在引用 y, 想法的 因为ref 是 x 的别名, 只有被写成x = y时表达式才会求值---由于 y 的值为 6 , 所以 x 也为 6;
引用的作用域和持续时间
引用变量遵循与普通变量相同的作用域和持续时间规则:
#include <iostream>
int main()
{
int x { 5 }; // normal integer
int& ref { x }; // reference to variable value
return 0;
} // x and ref die here
引用和引用对象具有独立的生命周期。
除了一个例外(我们将在下一课中讲解)之外,引用的生命周期与其被引用对象的生命周期是相互独立的。换句话说,以下两个命题都成立:
引用可以在它所引用的对象之前被销毁。
被引用的对象可能在引用失效之前就被销毁。
当引用在被引用对象之前被销毁时,被引用对象不会受到影响。以下程序演示了这一点:
#include <iostream>
int main()
{
int x { 5 };
{
int& ref { x }; // ref is a reference to x
std::cout << ref << '\n'; // prints value of ref (5)
} // ref is destroyed here -- x is unaware of this
std::cout << x << '\n'; // prints value of x (5)
return 0;
} // x destroyed here
以上将会打印出:

当ref变量死亡时,x它仍然像往常一样继续运行,完全不知道对它的引用已被销毁。
悬空引用
当被引用的对象在指向它的引用被销毁之前就被销毁时,该引用就指向了一个已经不存在的对象。这样的引用被称为悬空引用。访问悬空引用会导致未定义行为。
悬空引用很容易避免,但我们将在第12.12 课“按引用返回和按地址返回”中展示一个在实践中可能出现这种情况的例子。
引用不是对象
或许令人惊讶的是,在 C++ 中,引用并非对象。引用并非必须存在或占用存储空间。如果可能,编译器会通过将所有引用替换为被引用对象来优化掉引用。然而,这并非总是可行,在这种情况下,引用可能需要存储空间。
这也意味着“引用变量”这个术语有点名不副实,因为变量是具有名称的对象,而引用不是对象。
由于引用并非对象,因此不能在任何需要对象的地方使用引用(例如,不能引用引用本身,因为左值引用必须指向一个可识别的对象)。在需要引用对象或可重新定位的引用时(我们将在第23.3 课——聚合std::reference_wrapper中讲解),可以使用以下方法。
顺便提一下……
考虑以下变量:int var{}; int& ref1{ var }; // an lvalue reference bound to var int& ref2{ ref1 }; // an lvalue reference bound to var因为ref2(一个引用)是用ref1(另一个引用)初始化的,你可能会认为它ref2是对另一个引用的引用。但事实并非如此。
因为ref1是对 var 的引用,所以当它在表达式(例如初始化器)中使用时,ref1其值为var。因此,ref2它只是一个普通的左值引用(如其类型所示int&),绑定到var。
对引用的引用(对一个引用)的引用int将有语法int&&——但由于 C++ 不支持对引用的引用,因此该语法在 C++11 中被重新用于表示右值引用(我们将在第22.2 课——右值引用中介绍)。
作者注
如果此时引用看起来有点没用,别担心。引用经常被使用,我们将在第12.5 课——按左值引用传递和第 12.6 课——按常量左值引用传递中介绍其中一个主要原因。
测验时间
问题 1
请自行确定以下程序打印哪些值(不要编译该程序)。
#include <iostream>
int main()
{
int x{ 1 };
int& ref{ x };
std::cout << x << ref << '\n';
int y{ 2 };
ref = y;
y = 3;
std::cout << x << ref << '\n';
x = 4;
std::cout << x << ref << '\n';
return 0;
}
解决方案

因为ref绑定到 x,并且 ref 是 x 的同义词,所以它们总是会打印相同的值。该行 ref = y 将 y(2) 的值赋给 ref,它不会改变对 x 的引用(不会使得ref引用改变为 y)。下一行 y = 3 只改变了 y 。

浙公网安备 33010602011771号