左值和右值
左值和右值
左值:能用在赋值语句等号左侧的东西, 它能够代表一个地址
右值:不能作为左值的值就是右值,右值不能出现在赋值语句中等号的左侧
结论:
C++中的一条表达式,要么就是右值,要么就是左值,不可能两者都不是。
左值有的时候能够被当作右值使用
i = i + 1
,i是个左值,不是个右值,虽然它出现在了等号右边
i用在等号右边的时候,被当作一个具体的值,我们说i有一种右值属性(不是右值)
i出现在等号左边的时候,用的是i这个变量代表的内存中的地址,这就被称为左值属性。
因此一个左值,同时具有左值属性和右值属性
用到左值的运算符有哪些?
1.赋值运算符
1 | int a; |
整个赋值语句的结果仍然是左值 (a = 5) = 8;
2.取地址&
1 | int a = 5; |
3.容器的下标[]都需要左值,
1 | string a = "fengyun" |
4.迭代器的–,++需要左值
1 | vector<int>::iterator iter; |
一般而言,如果运算符不能对字面量值进行操作的,这个运算符就需要用左值。
左值:代表的是一个地址,所以左值表达式的求值结果,就是得到一个对象,就得有地址。
但是求值结果为对象的表达式不代表一定是左值,需要具体分析。
引用分类
- 左值引用:即绑定到左值。
- const引用:常量引用,也是左值引用。不希望改变值得对象
- 右值引用:它是一个引用,引用的对象侧重于临时变量,生命周期短。
int &&refrightvalue = 3;//绑定到了一个常数
refrightvalue = 5;
左值引用&
即绑定到一个左值上。
引用不存在“空引用”这个说法,所以左值引用初始化的时候必须绑定到一个左值上。
1 | int a = 1; |
右值引用&&
即绑定到一个右值。必须绑定到右值的引用。
C++11右值引用是希望我们用右值引用绑定一些即将销毁的或者是一些临时对象上。
因此我们可以把右值引用理解成一个对象的名字。
能绑定到左值上的引用,一般都不能绑定到右值。
1 | string t1("fengyun"); |
右值引用的引入目的
- c++11引入
&&
代表一种新数据类型,引入新数据类型肯定有目的。 - 提高程序运行效率。 把拷贝对象变成移动对象来提高程序运行效率。(假设对象A不再使用,我可以直接把对象A开辟的内存块直接转给对象B,将A的指针将置为nullptr,但内存没有回收,B也无需new一段内存,这块内存是转换了一个“主人”)
- 移动对象如何发生? 移动赋值运算符和移动拷贝构造函数需要参数类型是右值引用,右值引用也就是为了提高系统运行速率。
总结
返回左值引用的函数,连同赋值,下标,解引用和前置递增递减运算符(–i), 都是返回左值表达式的例子;我们可以将一个左值引用绑定到这样的例子上。
返回非引用类型的函数,连同算术,关系,位以及后置递增运算符(i–),都生成右值,不能将一个左值引用绑定到这类表达式。
但是我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式
1 | int i = 100; |
重点强调:
- 虽然r1是右值引用(绑定到了右值),但是要把r1看成一个变量,r1本身是一个左值,在=号左边呆着。
1 | int && r1 = 100; |
- 所有变量,看成左值。因为他们是有地址的,而且用右值引用也绑定不上。
1 | int &&r3 = r1; |
任何函数里边的形参都是左值。void f(int &&w); w是右值引用,但w本身是左值。
临时对象都是右值。
std::move
C++11标准库里的新函数。
虽然move的中文意思是“移动”,但是这个函数本身没有做任何移动的操作。
move只有一个能力:把一个左值强制转换为一个右值。右值引用就可以绑定到原本是左值的变量上。
1 | int i = 10; |
一个有趣现象:
1 | string st = "fengyun"; |
这是因为``std::move(st)是一个右值引用,
string st2 = std::move(st); `自动触发了string的移动构造函数,将st的内容转移到了st2中。而不是std::move转移的。
1 | string st = "fengyun"; |
一般而言,我们std::move(st)
后,我们写代码的不应该再使用st了。(这是系统不希望我们这么使用st,非要继续用st系统也拿我们没办法)。
临时变量
1 | int &&r1 = i++;//先生成一个临时变量_tempi,记录i的值(_tempi = i)的值用于使用的目的,再给i+1; |
另外一些临时对象(临时变量)是因为我们代码书写问题而产生的。
产生临时对象的情况与解决
1.以传值的方式给函数传递参数
1 | int CTempValue:: Add (CTempValue tobj); |
2.类型转换生成的临时对象/隐式类型转换以保证
1 | CTempValue sum; |
优化:
1 | CTempValue sum = 1000;//避免不必要的拷贝构造函数 |
3.函数返回对象的时候。
对象移动
- A移动到B后,那么A对象我们就不能再使用了
- 移动:并不是把内存中的数据从一个地址移到另一个地址。是“所有者”变更。
拷贝构造函数:Test::Test(const Test& tmpTest){......}
const左值引用
移动构造函数:Test::Test(const Test &&tmpTest){......}
右值引用
移动构造函数和移动赋值运算符应该完成的功能
(1)完成必要的内存移动,斩断原对象和内存的关系。
(2)确保移动后源对象处于一种“即便被销毁也没有什么问题”的一种状态。B <- A
1 |
|
注意我们定义了移动构造函数,那么A a = getA();
先调用构造函数然后调用移动构造函数,析构函数,如下
1 | A() :m_pb(new B()) |
如果未定义移动构造函数,那么A a = getA();
先调用构造函数,再调用拷贝构造函数,析构函数:如下:
1 | A() :m_pb(new B()) |
如果定义了移动构造函数,且A &&a = getA()
从getA()返回的临时对象被a接管了。1个构造函数,1个移动构造函数,1个析构函数
main函数中
1 | A a = getA(); |
运行结果:
1 | A() :m_pb(new B()) |
合成的移动操作
某些条件下,编译器能合成移动构造函数,移动赋值运算符
- 但是如果有自己的拷贝构造函数,自己的拷贝赋值运算符,或者自己的析构,那么编译器就不会为它合成移动构造函数和移动赋值运算符
所以有一些类是没有移动构造函数和移动赋值运算符。 - 如果我们没有自己的移动构造函数和移动赋值运算符,那么系统会调用我们自己写的拷贝构造函数和拷贝赋值运算符来代替。
- 只有一个类没定义任何自己版本的拷贝构造成员,且类的每个非静态成员都可以移动时,编译器才会为该类合成移动构造函数或者移动赋值运算符
什么叫成员可以移动?
- 内置类型是可以移动的
- 类类型的成员,则这个类要有对应的移动操作相关的函数,就可以移动。