c++复习笔记——右值引用(概念,使用场景),移动拷贝构造函数,赋值拷贝构造函数。
Posted 努力学习的少年
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++复习笔记——右值引用(概念,使用场景),移动拷贝构造函数,赋值拷贝构造函数。相关的知识,希望对你有一定的参考价值。
- 💂 个人主页:努力学习的少年
- 🤟 版权: 本文由【努力学习的少年】原创、在CSDN首发、需要转载请联系博主
- 💬 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦
目录
一. 左值和右值的概念
🚀 1. 左值概念
- 左值是一个数据的表达式(如变量名或引用的指针),我们可以获取到它的地址,正常情况下是可以能够对它赋值,定义const修饰后的左值,不能给它赋值,但是可以取出它的地址。
- 左值可以出现在赋值符号( " = " )的左边,也可以出现在赋值符号(" = “ )的右边。
- 左值具有持久的状态。
🚀 2. 右值概念
- 右值也是一个数据表达式,右值是字面常量或者是求值过程种创建的临时对象,右值的生命周期是短暂的,如:字面常量,表达式返回值,函数返回值(不是左值引用的返回值),临时变量,匿名对象等等,
- 右值不能出现在赋值符号的左边,右值也不能取出地址,更不能对它赋值。
🚀 3. 左值引用
- 左值引用是对左值的一种引用,相当于给左值取别名。
- 普通的左值引用不能引用右值,但是const的左值引用可以引用右值。
- 引用方法: 类型+&,例如: int& pa=a;pa引用a变量。
🚀 4.右值引用
- 右值引用是给右值取别名,所有的右值引用是不能引用左值。
- 右值是不能取出地址的,但是当右值去别名后,这个右值会被存到特定的位置,且可以取到该值的地址,也就是说右值引用值是一个左值。
- 右值引用会开辟一块空间去存右值,其中普通的右值引用是可以被修改这块空间的,const的右值引用时不可以被修改的。
- 标准库中的move函数可以将一个左值强制转换为右值
二. 左值引用的使用场景和缺陷
自定义的string类,下面的使用的string类可能会使用我们自定义类,做参考。
🚀 1. 左值引用的使用场景
- 左值引用作为函数的参数,能够减少拷贝。
如下:func1函数是传值参数,s1在func1函数内拷贝构造出s;func2函数是传引用参数,s是s1的别名,两个指向的是同一个对象。
- 左值引用可以引用函数返回值,也可以减少拷贝构造。
当一个函数的返回值出了函数作用域不会被销毁,那么该返回值是引用的方法传出去,不需要拷贝构造出新的对象。
🚀 2. 左值引用的缺陷
如果一个函数的返回值出了该函数作用域后就会被销毁掉,这个返回值的生命周期就结束了,因此就不能使用传引用返回,所以只能通过传值返回,而传值返回至少需要进行一次拷贝构造(如果编译器比较旧,就需要进行两次的拷贝构造),在旧的编译器下,如果一个函数是返回值给另一个函数并创建出新对象的时候,中间过程中需要先拷贝构造出一个临时对象,然后再通过这个临时对象在另一个函数内拷贝构造出一个新对象,然后这个临时对象就会被销毁掉。但是有的编译器为了提高拷贝效率,就将调用两次拷贝构造函数优化只调用一次拷贝构造函数。
但是如果是将返回值赋值给另一个函数体内已存在值的时候,它会调用一次拷贝构造函数构造出一个临时对象,然后调用赋值重载函数将临时对象赋值给s,由于这两个过程调用的函数是是不一样的,所以编译器就不会发生优化。
虽然编译器会将两次拷贝构造函数优化成一次拷贝构造函数,但是如果是返回对象是一个string等容器类型的对象,那么会发生一次深拷贝的问题。深拷贝的代价是十分大的。如下,str对象拷贝构造出一个s的对象,深拷贝的过程:
- 拷贝构造出s对象
- 在堆区拷贝构造出字符串2,然后将字符2的地址赋值给s对象中的指针。
- 销毁to_string函数栈帧,释放字符串1和str对象。
如上,str拷贝构造对象时,除了拷贝构造出一个string对象外, 还需要在堆区拷贝一个字符串赋值给string对象上的指针,这就是深拷贝问题。
三. 右值引用和移动拷贝构造函数
为了解决深拷贝的问题,我们可以在我们自己实现的sjp::string中增加一个移动构造函数,移动构造函数本质是将参数右值中的资源给窃取过来,占为己有,这样就不用做深拷贝,所以他叫移动构造,就是窃取别人的资源来构造自己,然后右值直接释放掉。这样就不会去在堆区上拷贝构造新的资源。
🚀 1. 移动拷贝构造函数
移动拷贝构造函数跟构造函数一样,参数需要是一个本身类型的对象,但移动拷贝构造函数的参数是一个该类型的右值引用。移动拷贝函数创建出一个新对象,都将新对象中的值都设置为0,接下来与传进来的右值对象进行资源交换。
- 根据函数匹配规则,如果调用拷贝构造对象的时候传的是左值,编译器会自动匹配到拷贝构造函数,如果传的是右值,那么就会匹配到移动拷贝函数。
- 使用移动拷贝构造函数后,源对象指向资源就被交换出去,这些资源的所有权都归属到了新对象,因此,如果源对象是一个长期存在的对象的时候,需要谨慎使用移动拷贝构造函数。调用移动拷贝构造函数创建出s3,s1的资源被转移到了s3,s1中没有指向任何资源,所以就不能通过s1去寻找之前的资源。
当我们理解了移动拷贝构造函数后,我们再来理解为什么移动拷贝构造函数可以解决传值返回可以解决深拷贝的问题。
当函数的返回值进行返回的时候,它会调用拷贝出一个临时对象,此时返回值就会去拷贝出一个临时对象,由于返回值是一个将亡值,属于一个右值,因此就会调用移动拷贝构造函数拷贝出临时对象,然后临时对象去拷贝出新对象,由于临时对象也属于一个右值,所以回去调用移动拷贝构造函数去拷贝出新对象。由于两个过程都是调用移动拷贝构造函数,则编译器会直接优化成只调用一次移动拷贝构造函数。
- 为什么移动拷贝构造函数可以解决深拷贝的问题呢?
如下,str对象中包含一个指针指向堆上一个字符串,当我们调用移动拷贝构造函数创建出s的时候,可以将str对象中的资源和s的对象资源进行交换,所以指向字符串的指针就给了s,因此s对象就可以通过指针使用该字符串,这就不需要去堆上再创建一块新的字符串给s对象。
🚀 2. 移动赋值
如果string类中只有一个移动拷贝构造函数,那么函数返回值构造临新对象的时候,那么只需要调用一次移动拷贝构造函数将资源转移给新对象。如果是将返回值赋值给一个已存在的值时,那么就会调用一次移动拷贝构造函数和一次赋值重载函数,赋值重载函数也是进行深拷贝的。如下:
因此,为了解决赋值重载的深拷贝的问题,我们还需要再实现一个移动赋值重载函数,移动赋值重载函数跟拷贝构造函数一样,都是解决深拷贝的问题,都是进行转移资源。
移动赋值重载函数跟赋值重载函数的定义是类似的,只是移动赋值重载函数的参数是右值引用,是为了让右值能够调用该函数,如下:
当有了移动赋值重载函数后,临时对象就会去调用移动赋值函数,直接转移资源给s。
四. 右值引用与STL容器
🚀 1. 移动拷贝构造函数和赋值重载函数
在c++标准库,很多容器为了解决深拷贝的问题,都会定义一个移动拷贝构造函数和一个赋值重载函数。 如下:
string类的移动拷贝构造函数和赋值重载函数:
list类的移动拷贝构造函数和一个赋值重载函数:
🚀 2.push_back和insert
在c++11标准库中,很多容器的push_back或者insert接口都至少会重载两个函数,其中一个函数的参数是左值引用,另一个函数的参数是右值引用。如下vector的接口:
在上面种的,参数是const 左值引用的insert,它既可以接收左值,也可以接受右值,那么为什么还需要多定义一个参数是右值引用的push_back函数重载?
如果调用vector中insert时,传的参数是右值,那么就会编译器就会匹配到右值引用参数的insert的重载函数,因为insert函数内部会对该参数值进行赋值重载到vector内,如果是右值,那么就会调用移动拷贝构造函数,所以可以避免深拷贝的出现。
如下:
- 调用inser接口,如果传的是左值,编译器匹配左值引用的赋值重载函数,x赋值给*pos时会调用string类赋值重载函数,进行深拷贝,如下:
- 如果传的是右值,编译器匹配右值引用的赋值重载函数,x赋值给*pos时会调用string类的移动赋值重载函数,进行资源转换。
因此,我们在调用insert和push_back时,如果涉及深拷贝的问题,尽量传右值(匿名对象),这样可以减少深拷贝的问题。
🚀 3. 完美转发
但是在上面调用参数是右值引用的insert接口中存在一个问题,如果右值引用x去接受一个右值,那么这个x就会退化成一个左值,所以*pos=x;这个过程是调用的时候是会直接调用赋值重载函数,不是移动拷贝构造函数。因此,为了保持x是一个右值,那么我们可以做这样一个动作:
- std::forward<T>(x):它可以在传参的时候保持x原生属性,也就是可以保持x的右值属性,因此,这样可以保证*pos=x去调用移动赋值重载函数。
这种调用std::forward<T>(x)保持原生属性的过程就叫做完美转发。
以上是关于c++复习笔记——右值引用(概念,使用场景),移动拷贝构造函数,赋值拷贝构造函数。的主要内容,如果未能解决你的问题,请参考以下文章