R值参考
我们需要了解l值和r值,然后告诉你不要太担心它们。这是C ++ 11之前的建议。但是理解C ++ 11中的移动语义需要重新审视该主题。所以我们现在就这样做。
L值和r值
尽管名称中包含“值”一词,但l值和r值实际上不是值的属性,而是表达式的属性。
C ++中的每个表达式都有两个属性:一个类型(用于类型检查)和一个值类别(用于某些类型的语法检查,例如是否可以将表达式的结果赋值)。在C ++ 03及更早版本中,l值和r值是唯一可用的两个值类别。
这些表达式是l值和r值的实际定义令人惊讶地复杂,因此我们将简单地对主题进行简化,这对于我们的目的来说基本上是足够的。
将l值(也称为定位器值)视为函数或对象(或计算为函数或对象的表达式)是最简单的。所有l值都分配了内存地址。
当最初定义l值时,它们被定义为“适合在赋值表达式的左侧的值”。但是,稍后,const关键字被添加到语言中,并且l值被分成两个子类:可修改的l值(non-const)和不可修改的l值(const)。
想到一个r值是最简单的作为“一切不是l值”。这尤其包括文字(例如5),临时值(例如x + 1)和匿名对象(例如Fraction(5,2))。r值通常是针对它们的值进行评估,具有表达式范围(它们在它们所在的表达式的末尾处死亡),并且不能分配给它们。这种非赋值规则是有意义的,因为赋值会对对象应用副作用。由于r值具有表达式范围,如果我们要为r值赋值,那么在我们有机会在下一个表达式中使用指定值之前,r值将超出范围(这使得赋值无用)或者我们必须使用在表达式中多次应用副作用的变量(现在你应该知道它会导致未定义的行为!)。
为了支持移动语义,C ++ 11引入了3个新的值类别:pr值,x值和gl值。我们将在很大程度上忽略这些,因为理解它们对于有效地学习或使用移动语义是没有必要的。如果您有兴趣,cppreference.com有大量的表达式列表,这些表达式符合各种值类别的要求,以及有关它们的更多详细信息。
L值参考
在C ++ 11之前,C ++中只存在一种类型的引用,因此它被称为“引用”。但是,在C ++ 11中,它有时被称为l值引用。L值引用只能用可修改的l值初始化。
L值参考 | 可以初始化 | 可以修改 |
---|---|---|
可修改的l值 | 是 | 是 |
不可修改的l值 | 没有 | 没有 |
R值 | 没有 | 没有 |
L值参考 | 可以初始化 | 可以修改 |
---|---|---|
L值引用const | 是 | 没有 |
不可修改的l值 | 是 | 没有 |
R值 | 是 | 没有 |
对const对象的L值引用特别有用,因为它们允许我们将任何类型的参数(l值或r值)传递给函数,而无需复制参数。
R值参考
C ++ 11添加了一种称为r值引用的新类型引用。r值引用是一个用r值(仅)初始化的引用。使用单个&符创建l值引用时,使用双符号创建r值引用:
int x = 5;
int &lref = x; // l值引用用l值x初始化
int &&rref = 5; // 用r值5初始化的r值参考
无法使用l值初始化R值引用。
R值参考 | 可以初始化 | 可以修改 |
---|---|---|
可修改的l值 | 没有 | 没有 |
不可修改的l值 | 没有 | 没有 |
R值 | 是 | 是 |
对const的R值引用 | 可以初始化 | 可以修改 |
---|---|---|
可修改的l值 | 没有 | 没有 |
不可修改的l值 | 没有 | 没有 |
R值 | 是 | 没有 |
R值引用有两个有用的属性。首先,r值引用将它们初始化的对象的生命周期延长到r值引用的生命周期(对const对象的l值引用也可以这样做)。其次,非常量r值引用允许您修改r值!
我们来看看一些例子:
#include <iostream>
class Fraction
{
private:
int m_numerator;
int m_denominator;
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()
{
Fraction &&rref = Fraction(3, 5); // r值引用临时分数
std::cout << rref << '\n';
return 0;
} //rref(和临时分数)超出了范围
该程序打印:
3/5
作为一个匿名对象,Fraction(3,5)通常会在定义它的表达式的末尾超出范围。但是,由于我们用它初始化一个r值引用,它的持续时间会延长到块的结尾。然后我们可以使用该r值引用来打印Fraction的值。
现在让我们来看一个不太直观的例子:
#include <iostream>
int main()
{
int &&rref = 5; // 因为我们用文字初始化r值引用,所以在这里创建一个值为5的临时值
rref = 10;
std::cout << rref;
return 0;
}
该程序打印:
10
虽然使用文字值初始化r值引用然后能够更改该值可能看起来很奇怪,但在使用文字初始化r值时,会从文字构造临时值,以便引用引用临时值对象,而不是文字值。
R值参考不经常以上述任何一种方式使用。
R值引用作为函数参数
R值引用更常用作函数参数。当您希望对l值和r值参数具有不同的行为时,这对于函数重载最有用。
void fun(const int &lref) // l-value arguments will select this function
{
std::cout << "l-value reference to const\n";
}
void fun(int &&rref) // r-value argument will select this function
{
std::cout << "r-value reference\n";
}
int main()
{
int x = 5;
fun(x); // l-value argument calls l-value version of function
fun(5); // r-value argument calls r-value version of function
return 0;
}
这打印:
l-value reference to const
r-value reference
如您所见,当传递l值时,重载函数已解析为具有l值引用的版本。当传递r值时,重载函数解析为具有r值引用的版本(这被认为是比对const的l值引用更好的匹配)。
你为什么要这样做?我们将在下一课中更详细地讨论这个问题。不用说,它是移动语义的重要组成部分。
返回r值引用
你应该几乎永远不会返回一个r值引用,因为你几乎永远不会返回一个l值引用。在大多数情况下,当引用的对象超出函数末尾的范围时,您最终将返回挂起引用。
Quiz Time:
1)说明以下哪个字母语句不能编译:
int main()
{
int x;
// l值参考
int &ref1 = x; // A
int &ref2 = 5; // B
const int &ref3 = x; // C
const int &ref4 = 5; // D
//r值参考
int &&ref5 = x; // E
int &&ref6 = 5; // F
const int &&ref7 = x; // G
const int &&ref8 = 5; // H
return 0;
}
解决方案
B,E和G不会编译。