在第 12 章中,我们引入了值类别
如果你对左值与右值已经模糊,现在正是复习的好时机,因为本章将频繁使用它们。
左值引用回顾
在 C++11 之前,C++ 只有一种引用,因此直接称为“引用”。但从 C++11 起,它被称为左值引用。左值引用只能以可修改的左值初始化。
左值引用 | 可初始化的类型 | 能否修改 |
---|---|---|
可修改左值 | 可以 | 可以 |
不可修改左值 | 不可以 | 不可以 |
右值 | 不可以 | 不可以 |
const
左值引用既可绑定到可修改左值,也可绑定到不可修改左值和右值;但通过这些引用无法修改原值。
const 左值引用 | 可初始化的类型 | 能否修改 |
---|---|---|
可修改左值 | 可以 | 不可以 |
不可修改左值 | 可以 | 不可以 |
右值 | 可以 | 不可以 |
因此,const
左值引用尤为实用:在无需拷贝实参的前提下,可把任何类型(左值或右值)的实参传递给函数。
右值引用
C++11 新增了一种引用类型,称为右值引用。右值引用只能以右值初始化。与左值引用(单个 &
)不同,右值引用使用双 &&
:
int x{ 5 };
int& lref{ x }; // 左值引用,以左值 x 初始化
int&& rref{ 5 }; // 右值引用,以右值 5 初始化
右值引用不能以左值初始化。
右值引用 | 可初始化的类型 | 能否修改 |
---|---|---|
可修改左值 | 不可以 | 不可以 |
不可修改左值 | 不可以 | 不可以 |
右值 | 可以 | 可以 |
const
右值引用只能绑定到右值,且不可修改其绑定对象。
const 右值引用 | 可初始化的类型 | 能否修改 |
---|---|---|
可修改左值 | 不可以 | 不可以 |
不可修改左值 | 不可以 | 不可以 |
右值 | 可以 | 不可以 |
右值引用具备两个实用特性:
- 延长绑定对象的生命周期,直至该引用自身消亡(
const
左值引用亦可做到)。 - 非
const
右值引用允许修改绑定的右值!
示例一:
#include <iostream>
class Fraction
{
private:
int m_numerator{ 0 };
int m_denominator{ 1 };
public:
Fraction(int numerator = 0, int denominator = 1)
: m_numerator{ numerator }, m_denominator{ denominator }
{
}
friend std::ostream& operator<<(std::ostream& out, const Fraction& f1)
{
out << f1.m_numerator << '/' << f1.m_denominator;
return out;
}
};
int main()
{
auto&& rref{ Fraction{ 3, 5 } }; // 右值引用绑定到临时 Fraction
// operator<< 的形参 f1 引用该临时对象,无需拷贝
std::cout << rref << '\n';
return 0;
} // rref(及其绑定的临时 Fraction)在此离开作用域
输出:
3/5
Fraction(3, 5)
作为匿名对象,本应在其定义所在表达式结束时销毁;但因其被用于初始化右值引用,其生命周期被延长至块末,故可安全通过 rref 打印其值。
示例二(稍反直觉):
#include <iostream>
int main()
{
int&& rref{ 5 }; // 用字面量 5 初始化,编译器会为之创建临时 int
rref = 10;
std::cout << rref << '\n';
return 0;
}
输出:
10
虽然用字面量初始化右值引用并修改其值看似怪异,但编译器会为字面量 5 创建临时对象,右值引用实际绑定的是该临时对象,而非字面量本身。
上述用法在实践中并不常见。
右值引用作为函数形参
右值引用更常用于函数形参的重载,以区分左值与右值实参的不同处理。
#include <iostream>
void fun(const int& lref) // 左值实参选中此版本
{
std::cout << "l-value reference to const: " << lref << '\n';
}
void fun(int&& rref) // 右值实参选中此版本
{
std::cout << "r-value reference: " << rref << '\n';
}
int main()
{
int x{ 5 };
fun(x); // 左值实参 → 调左值版本
fun(5); // 右值实参 → 调右值版本
return 0;
}
输出:
l-value reference to const: 5
r-value reference: 5
可见,传入左值时重载决议选择左值引用版本;传入右值时选择右值引用版本(相比 const
左值引用,右值引用被视为更佳匹配)。
为何需要这样做?下一节将详细讨论;简言之,这是实现移动语义的关键一环。
右值引用变量本身是左值
考虑以下片段:
int&& ref{ 5 };
fun(ref);
你以为会调用 fun(int&&)
吗?答案可能出乎意料:实际调用的是 fun(const int&)
。
尽管变量 ref
的类型是 int&&
,但在表达式中使用时,所有具名变量都是左值。对象类型与其值类别相互独立。
你已知道字面量 5
是类型为 int
的右值,int x
是类型为 int
的左值;同理,int&& ref
是类型为 int&&
的左值。
因此 fun(ref)
不仅调用 fun(const int&)
,而且根本不匹配 fun(int&&)
,因为右值引用不能绑定到左值。
返回右值引用
你几乎永远不应返回右值引用,原因与不应返回左值引用相同。绝大多数情况下,当函数结束时,被引用对象将离开作用域,导致返回悬空引用。
小测验
问题 1
指出下列带字母的语句中哪些无法通过编译:
int main()
{
int x{};
// 左值引用
int& ref1{ x }; // A
int& ref2{ 5 }; // B
const int& ref3{ x }; // C
const int& ref4{ 5 }; // D
// 右值引用
int&& ref5{ x }; // E
int&& ref6{ 5 }; // F
const int&& ref7{ x }; // G
const int&& ref8{ 5 }; // H
return 0;
}