⭐c++11引入右值引用,并且提供了move函数,用来获得绑定到左值上的右值引用,此函数定义在头文件utility中。
什么是左值和右值?
-
C/C++语言中可以放在赋值符号左边的变量,即具有对应的可以由用户访问的存储单元,并且能够由用户去改变其值的量。
- 左值表示存储在计算机内存的对象,而不是常量或计算的结果。或者说左值是代表一个内存地址值 ,并且通过这个内存地址,就可以对内存进行读并且写(主要是能写)操作;这也就是为什么左值可以被赋值的原因了。
- 相对应的还有右值:当一个符号或者常量放在操作符右边的时候,计算机就读取他们的“右值”,也就是其代表的真实值 。
-
简单来说就是,左值相当于地址值,右值相当于数据值。右值指的是引用了一个存储在某个内存地址里的数据。
⭐左值: 指的是表达式结束后依然存在的持久化对象。
⭐右值: 指的是表达式结束时就不再存在的临时对象。
左值右值总结:(T是一个具体类型)
(1)左值引用,使用T&,只能绑定左值
(2)右值引用,使用T&&,只能绑定右值
(3)常量左值,使用conts T&,可以绑定左值和右值。
(4)已命名的右值引用,编译器会认为是左值。
(5)编译器有返回值优化功能,但不可过于依赖。
已命名的右值引用,编译器会认为是个左值
左值引用使用&符号,如
int a = 10;
int &refA = a;
int &b = 1; //编译错误,1是右值,不能使用左值引用
右值引用使用&&符号,如
int &&a = 1;
int b = 1;
int &&c = b; //编译错误,不能将左值赋值给一个右值引用
class A
{
public:
int a;
};
A getTemp()
{
return A();
}
A &&a = getTemp(); //getTemp()返回值是右值(临时变量)
a的类型为右值引用类型(int &&),如果从左值和右值的角度区分它,它实际上是一个左值。
因为可以对它取地址,而且它还有名字,是一个已经命名的右值。
- 所以,左值引用只能绑定左值,右值引用只能绑定右值。常量左值引用是个特例,它可以算一个
- 万能的引用类型,可以绑定非常量左值,常量左值,右值,而且在绑定右值的时候,可以像右值引用一样
- 将右值的生命期延长,缺点是只 能读不能改
左值和右值是相对于赋值表达式而言的。左值是能出现在赋值表达式左边的表达式。左值表达式可以分为可读写的左值和只读左值。右值是可以出现在赋值表达式右边的表达式,他可以是不占据内存空间的临时量或字面量,可以是不具有写入权的空间实体
左值和右值判断:
1)可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。
2)有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。
int i = 10;
10 = i;
错误,10为右值,不能当左值用
int j = 20;
j = i;
i和j都是左值,但是i可以当右值用
int i=42;
int &r=i; //正确,r引用i
int &&rr=i //错误,不能将一个右值引用绑定到一个左值上
int &r2=i*42; //错误,i*42是一个右值
const int &r3=i*42; //正确,我们可以将一个const的引用绑定到一个右值上
int &&r2=i*42; //正确,将rr2绑定到乘法结果上
以上面定义的变量 i、j 为例,i 和 j是变量名,且通过 &a 和 &b 可以获得他们的存储地址,因此 a 和 b 都是左值;反之,字面量10、20,它们既没有名称,也无法获取其存储地址(字面量通常存储在寄存器中,或者和代码存储在一起),因此10、20 都是右值。
int i = 10;
int &a = i;
int &b = 10;//错误
const int &c = 10;//正确
常规的我们使用的一个 &表示左值引用 ,普通左值引用只能接收左值,不能接收右值,但是常量左值引用既可以接收左值也可以接收右值。
int j = 10;
int &&d = 10;
int &&e = j;//错误
支持移动操作,c++新标准引入了一种新的引用类型—右值引用。所谓右值引用就是必须绑定到右值的引用。我们通过&& 而不是&来获得右值引用。如我们将要看到的,右值引用有一个重要的性质—只能绑定到一个将要销毁的对象。 因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。
&&表示右值引用 ,常规右值引用可以用来接收右值,但是不能用来接收左值
- 1.左值持久,右值短暂
左值有持久的状态,而右值要么是字面值常量,要么是表达式求值过程中创建的临时对象。
由于右值引用只能绑定到临时对象,我们得知- 1 所引用的对象将要被销毁
- 2 该对象没有其他用户
这两个特征意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。
- 2.变量是左值
变量可以看作只有一个运算对象而没有运算符的表达式,虽然我们很少这样看待变量。类似于其他任何表达式,变量表达式也有左值/右值属性。变量表达式都是左值,带来的结果就是,我们不能将一个右值引用绑定到一个右值引用类型的变量上。
int &&rr1 =42; //正确,字面值常量是右值
int &&r2 =rr1; //错误,表达式rr1是左值!
-
注意: 变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不可以。
-
因此,可以给一个这样的结论可以取地址的,有名字的,非临时的就是左值;不能取地址的,没有名字的,临时的就是右值。
-
可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值。
-
⭐从本质上理解,创建和销毁由编译器幕后控制,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象)。
move函数
- 虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显式地将一个左值转换为对应的右值引用类型。我们可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件utility中。
int &&rr3 =std::move(rr1); //OK
-
move调用告诉编译器:我们有一个左值,但我们希望像右值一样处理它。我们必须认识到,调用move就意味着承诺:除了对rr1赋值或者销毁之外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。
-
注意:
- 1.我们可以销毁一个移后源对象,也可以赋予它新值,但是不能使用一个移后源对象的值。
- 2.对于move的使用应该是std:move而不是move。这样做可以避免潜在的名字冲突。
⭐std::move()将一个左值转换成一个右值,强制使用移动拷贝和赋值函数,这个函数本身并没有对这个左值什么特殊操作。
⭐std::forward()和universal references通用引用共同实现完美转发。
移动构造函数
移动构造函数类似于拷贝构造函数,第一参数是该类类型的一个引用,但这个引用参数在移动构造函数中是一个右值引用。
与拷贝构造函数不同,移动构造函数不分配新内存,它接管内存并把对象中的指针都置为nullptr。最终,移后源对象会被销毁,意味着将在其上运行析构函数。
vector<int> v1={1,2,3,4,5};
vector<int> v2=move(v1);
cout<<"v1.size:"<<v1.size()<<endl;
cout<<"v2.size:"<<v2.size()<<endl;
//执行结果为:
v1.size:0
v2.size:5
⭐用empalce_back()替换push_back()增加性能。