意义:可以避免无谓的复制,提高程序的性能。
左值:表达式结束后依然存在的持久化对象
右值:表达式结束后不再存在的临时对象
所有的具名变量和对象都是左值,而右值不具名。
区分左值和右值的快捷方法:
看能不能对表达式取地址,如果能则是左值,否则就是右值。
右值分为纯右值和将亡值。
纯右值是C++98中的右值概念,如非引用函数返回的临时变量;
一些运算表达式,如4+6产生的临时变量;不和对象关联的字面量值,
如10,‘s’,true,“hello”等这些不能被取地址的值。
将亡值:c++11中新增的和右值引用相关的表达式,这样的表达式通常是将要移动的对象
T&&函数返回值,std::move()函数的返回值等。
将亡值和纯右值统一看成右值,不影响使用。
c++98中引用很常见,就是给变量取一个别名,在c++11中,因为增加了右值引用的概念,
所以c++98中的引用都称为左值引用。
int a = 10;
int &refA = a;
int &b = 1; //编译错误,1是右值,不能使用左值引用
c++11中的右值引用使用&&符号,如
int &&a = 1;
int b = 1;
int &&c = b; //编译错误,不能将左值赋值给一个右值引用
class A
{
public:
int a;
};
A getTemp()
{
return A();
}
A &&a = getTemp(); //getTemp()返回值是右值(临时变量)
getTemp()返回的右值本来在表达式语句结束后,其生命也就该终结了(临时变量),而通过
右值引用,该右值又获得了新生,其生命周期与右值引用类型变量a的生命一样,只要a活着,该
右值临时变量将会一直存活下去,实际上就是给临时变量去了一个名字。
a的类型为右值引用类型(int &&),如果从左值和右值的角度区分它,它实际上是一个左值。
因为可以对它取地址,而且它还有名字,是一个已经命名的右值。
所以,左值引用只能绑定左值,右值引用只能绑定右值。常量左值引用是个特例,它可以算一个
万能的引用类型,可以绑定非常量左值,常量左值,右值,而且在绑定右值的时候,可以像右值引用一样
将右值的生命期延长,缺点是只能读不能改。例子如下:
const int &a = 1; //常量左值引用绑定右值,不会报错
class A
{
public:
int a;
};
A getTemp()
{
return A();
}
const A &a =getTemp(); //不会报错,而A& a会报错
实际上,我们在很多情况下都使用了常量左值引用这个功能,例子如下:
class Copyable
{
public:
Copyable() {}
Copyable(const Copyable &o)
{
std::cout << "Copied" << std::endl;
}
};
Copyable ReturnRvalue()
{
return Copyable(); //返回一个临时对象
}
void AcceptVal(Copyable a)
{
}
void AcceptRef(const Copyable &a)
{
}
int main()
{
std::cout<<"pass by value"<<std::endl;
AcceptVal(ReturnRvalue()); //应该调用2次拷贝构造函数
std::cout<<"pass by reference"<<std::endl;
AcceptRef(ReturnRvalue()); //应该只调用一次拷贝构造函数
}
上述例子运行之后,结果和预想的不一样。AcceptVal(ReturnRvalue())需要调用两次拷贝构造函数,一次在ReturnRvalue()函数中,构造一个Copyable()对象,返回的时候会调用拷贝构造函数生成一个临时对象。在调用AcceptVal()时,会将这个对象拷贝给函数的局部对象a,一共调用了两次拷贝构造函数。而AcceptRef()的不同之处在于形参是常量左值引用,它能接收一个右值,而不需要拷贝。
实际的结果是,不管哪种方式,一次拷贝构造函数都没有调用。
这是因为编译器开启了返回值优化(RVO/NRVO,RVO,Return Value Optimization返回值优化;NRVO,Nameed Return Valude Optimization)。编译器发现ReturnRvalue内部生成了一个对象,返回之后还需要生成一个临时对象调用拷贝构造函数,很麻烦,所以直接优化成一个对象,避免拷贝,而这个临时变量又被赋值给了函数的形参,还是没必要,这3个变量都用一个变量代替了,不需要调用拷贝构造函数。
为了能够更好的观测结果,可以在编译的时候加上-fno-elide-constructors选项(关闭返回值优化),此时结果和预想的一样。上述的例子是想说明常量左值可以绑定一个右值,可以减少一次拷贝(使用非常量左值引用会使失败,因为ReturnRvalue()返回的是临时对象(右值))。
//g++ test.cpp -o test -fno-elide-constructors
总结:T是一个具体类型
(1)左值引用,使用T&,只能绑定左值
(2)右值引用,使用T&&,只能绑定右值
(3)常量左值,使用conts T&,可以绑定左值和右值。
(4)已命名的右值引用,编译器会认为是左值。
(5)编译器有返回值优化功能,但不可过于依赖。