3.5.1 对象引用
引用是 C++ 中的一种复合类型。引用必须用来指代一个对象。例如:
int a = 42;int& b = a; // b 是一个整数类型的引用,它指代了对象 a一个对象的引用可以当做这个对象来使用,例如:
int a = 42;int& b = a;
b = 43; // 通过对 b 赋值,修改了 a 的值引用必须要指代一个对象,不能默认初始化,也不能重新绑定到另一个对象。
int a = 42;int& b; // 错误,引用必须指代一个对象int& c = a; // 正确,c 绑定到 aint d = 43;c = d; // 这是将 d 的值赋给 a,而不是将 c 绑定到 d除了一些特例外,引用总是当做指代的对象来使用(对于读取值或者写入值,一定是当做指代的对象来使用)。并且,在初始化之后,不能更改这种指代。
用一个对象初始化引用,也称为将引用绑定到对象。
例如:
int a = 1; // a 初始化为不确定值int& b = a; // 将 b 绑定到 a
// 以 b 初始化 c,b 此时只有指代 a 的功能// 因此,c 也绑定到 aint& c = b;
c = 1; // 将 1 赋值给 c,也即是赋值给 a将引用绑定到对象时,并不依赖对象的值,因此也可以使用不确定值对象来初始化引用。
int a; // a 初始化为不确定值int& b = a; // 以具有不确定值的 a 初始化引用
b = 1; // 将 1 赋值给 a引用不是一个对象,具体来说,引用类型不需要有对齐和大小、不需要占用内存,也不能声明引用的引用。(但是,根据需要,编译器可以生成使用存储来实现的引用。)
在前面的声明一节中提到,变量不一定是对象。这里我们可以给出更准确的说法:对象,以及绑定到对象的引用,统称为变量。
引用类型的声明与初始化
Section titled “引用类型的声明与初始化”引用类型有两种,左值引用类型和右值引用类型。
左值引用类型的形式是
type_name &右值引用类型的形式是
type_name &&其中,type_name 是引用所指代的目标的类型。注意,&& 是一个整体,不是两个 & 符号,不可以拆开。
顾名思义,左值引用必须以左值表达式初始化,例如:
int a = 1, b = 2;int& ref = a;int& ref2 = 1; // 错误,表达式 1 不是左值int& ref3 = a + b; // 错误,表达式 a + b 不是左值int& ref4 = true ? a : b; // 正确,这个条件表达式是左值,ref4 绑定到 a左值引用绑定到左值表达式,因此也能如左值表达式一般被赋值。
int a;int& b = (a = 2); // 正确,b 绑定到 a
b = 1也可以使用 auto & 来自动推导左值引用类型:
int a = 1;auto& ref = a; // ref 是 int& 类型函数中的左值引用
Section titled “函数中的左值引用”可以在函数参数中使用左值引用,来修改作为参数传入的对象:
void set_to_42(int& a) { a = 42;}
int b = 1;set_to_42(b); // b 的值变为 42可以在返回值中使用左值引用,例如:
int& max(int& a, int& b) { if (a > b) { return a; } else { return b; }}
int a = 1, b = 2;
auto &ref = max(a, b); // ref 绑定到 a 或 b 中较大的那个ref = 3; // 修改了 b 的值
max(a, b) = 4; // 由于 max 返回的是引用,因此可以直接修改返回值,b 的值变为 4这里可以发现,当函数返回一个左值引用时,可以直接对返回值进行赋值操作。结合在值类别中的讲解,这种函数的调用是左值表达式。我们可以补充前面值类别的定义:
下列表达式是左值表达式:
- 返回值类型是引用类型的函数调用
下列表达式是右值表达式:
- 返回值类型是非引用类型的函数调用
右值引用必须以右值初始化,例如:
int a = 1, b = 2;
int&& ref = 1; // 正确,1 是右值int&& ref2 = a; // 错误,a 是左值int&& ref3 = a + b; // 正确,a + b 是右值int&& ref4 = true ? a : b; // 错误,这个条件表达式是左值观察下面的代码:
int&& ref = 1;ref = 2; // 为什么?这不是字面量吗?你可能会奇怪,为什么右值引用绑定到字面量之后,这个右值引用还能修改?右值引用不是右值吗?
首先,右值引用的名字本身(也即这里的ref),是一个标识符组成的基础表达式,当然是左值表达式。
此外,ref 所绑定到的对象,实际上是一个从 1 构造而来的临时对象。为了将一个右值表达式绑定给右值引用,会将右值表达式转换为一个与引用类型匹配的将亡值表达式。
将亡值表达式代指这个临时的对象,这个临时的对象的生命周期会延长到绑定到它的引用的生命期结束。举例而言:
int&& ref = true; // 用 true 初始化一个 int 类型的临时对象,将这个临时对象绑定到 refref = 2; // 修改了这个临时对象的值,而非字面量 true 的值函数中的右值引用
Section titled “函数中的右值引用”类似于左值引用,右值引用也可以用在函数参数中,例如:
int a = 1;void set_a_to(int&& value) { a = value;}这个 set_a_to 函数接受一个右值引用,可以接受右值表达式作为参数,例如:
set_a_to(1);set_a_to(a + 1); // a + 1 是纯右值表达式当然,这个函数不接受左值表达式作为参数,例如:
set_a_to(a); // 错误,a 是左值表达式函数返回值中当然也可以使用右值引用,其作用和左值引用相似,但是二者的差异涉及重载的知识,在本章后面的部分会介绍相关内容,这里暂且放下。
常量左值引用
Section titled “常量左值引用”常量左值引用是左值引用的一种特殊形式,它不仅可以绑定到左值对象,也可以绑定到右值对象,例如:
const int& ref = 1; // 正确
int a = 1;const int& ref2 = a; // 正确类似于右值引用,常量左值引用绑定到右值时,是绑定到右值表达式转换为将亡值表达式时产生的临时对象,例如:
const int& ref = true; // 用 true 初始化一个 int 类型的临时对象,将这个临时对象绑定到 ref也可以使用 const auto& 来自动推导常量左值引用类型:
const auto& ref = 1; // ref 是 const int& 类型const auto& ref2 = a; // ref2 也是 const int& 类型藉由常量左值引用,我们可以设计一个接受左值表达式也接受右值表达式的函数:
int a;
int& set_a_to(const int& value) { a = value; return a;}
set_a_to(1); // 1 是右值,但是可以传入set_a_to(a); // a 是左值,也可以传入这个 set_a_to 函数当然也可以使用 int 类型作为参数,但当类型更为复杂,构造参数的成本也不得不被考虑时,常量左值引用的作用就会凸显出来。
常量左值引用的语法结构
Section titled “常量左值引用的语法结构”前面我们介绍了 const int& 这种形式的常量左值引用。考虑引用的语法形式 type_name &,所以这里引用指代的类型是 const int 而不是 int。换句话说,const 限定的是 int 类型,而非 int & 这个引用类型。
在const限定一章中,我们提到限定符的顺序是可以交换的,const int 和 int const 是等价的。因此,const int& 和 int const& 是等价的。
但需要注意,C++ 规定,引用类型本身是不能被限定的,因此 int& const 是错误的写法。
此外,使用别名处理时,添加在引用类型上的限定符会被忽略,例如:
using int_ref = int&;using const_int_ref = const int_ref; // const_int_ref 是 int& 类型,而非 const int&
int a = 1;const int_ref ref = a; // ref 是 int& 类型当使用引用作为函数返回值时,可能会导致引用绑定的对象生命期结束却仍然可用,这种引用称为悬垂引用。
考虑下面的例子:
int&& max(int&& a, int&& b) { if (a > b) { return a; } else { return b; }}
max(1, 2) = 3; // 错误,试图使用悬垂引用在这个例子中,max 函数的两个参数绑定到了从字面量 1 和 2 构造的临时对象。这两个临时对象的生命期在 max 函数返回后结束,但是 ref 仍然绑定到这两个临时对象中的较大者。
这种情况下,ref 就是一个悬垂引用。在第二次调用 max 函数时,需要使用 ref 绑定的对象的值来比较大小,但是这个对象的生命期已经结束,因此这个操作是错误的。
上面的例子用常量左值引用写时,会更难以被发现:
const int& max(const int& a, const int& b) { if (a > b) { return a; } else { return b; }}
const int& ref = max(1, 2); // ref 是悬垂引用,由于不能写入,因此显得很隐晦const int& ref2 = max(ref, 3); // 错误,试图使用悬垂引用