Learning C++ No.29 【右值引用实战】

引言:

北京时间:2023/6/7/9:39,上午有课,且今天是周三,承接之前博客,今天我又去帮我舍友签到早八,但愿这次不会被发现吧!嘻嘻嘻!并且刚刚发文有关对C++11相关知识,由于所剩时间不多,这里我们就简单的为下篇博客,当然也就是该篇博客打一打铺垫,哦!对了,今天是高考哦!对于2022年的今天,哈哈哈,不多做赘述,往事不堪回首,把握今朝最重要,当然承接上篇博客,该篇博客最重要的知识就是有关右值引用的知识,当然不是右值引用的语法,最为重要的是对右值引用真实编码场景的分析和模仿,当然还有为什么要使用右值引用等相关知识,这里不多做细讲,反正右值引用非常重要就对头啦!

什么是右值引用

简简单单,想要学习什么是右值引用,最好的方法,还是如我们以前所说,要么通过图示来学习,要么通过对比或者模仿的方式来学习,当然,此时我们选择对比的方式,把问题归结到什么是左值引用,所以什么是左值引用?使用左值引用的目的又是什么呢?想要搞懂这些问题,如下所述:

什么是左值
当然,在我们想要搞懂什么是左值引用之前,首先肯定要明白什么是左值,谈到左值,大家可能会十分的陌生,但是,还是同理,如果使用另一个名称去和它进行对比,那么理解左值不过是砍瓜切菜那么简单,如下图所示:就是一些列的左值
Learning C++ No.29 【右值引用实战】_第1张图片
当然,如上图所示,变量 p/a/b 表示的就是左值 ,此时对左值我们就有了一个最简单的认识,左值就是在赋值符号左边的变量,当然,此时只是简单认识,具体认识和理解需要等我们将右值的定义给理解一下,明白了什么是左值之后,此时理所当然,就应该学习什么是左值引用,当然,本质上左值引用的概念在我们之前C++入门学习类和对象时,在学习引用这个知识点的时候,我们就已经充分了解过了,所以左值引用本质就是我们之前学习的引用,当然字面意思理解,也就是对左值取别名,以下就是一系列对左值取别名代码,也就是我们之前学习的有关引用的语法使用,如下:
Learning C++ No.29 【右值引用实战】_第2张图片

正式学习右值引用

在上述知识的铺垫下,此时我们知道了什么是左值,什么是左值引用,左值引用的本质就是我们之前在类和对象中学习的有关引用的知识,所以此时明白,左值引用在C++编码中是至关重要的,在合适的位置使用,它就可以最简单粗暴的方式提高代码效率,如:在返回值处使用引用,在函数参数中使用引用,在模板参数中使用引用等方面,引用都可以很好的减少数据频繁的拷贝,从而提高代码效率,所以同理,先不管什么是左值引用,什么是右值引用,使用引用的本质目的,都只是为了提高代码效率而已;正式了解右值引用相关知识,明白上述知识,知道左值引用的学习是在吃回锅肉,但右值引用相关知识,我们却实实在在的是第一次了解,同理,第一点,明白什么是右值,如下图所示:
Learning C++ No.29 【右值引用实战】_第3张图片
明白,右值也是一个数据的表达式,如:字面常量,表达式返回值,函数返回值等…,总而言之,右值是一个可以生成临时对象的表达式或者是一个不可以被修改的值 ,具体如何辨别右值和左值,等我们将右值引用讲解完,再做进一步区分,明白什么是右值之后,顺理成章,此时学习右值引用相关知识,虽然本质上理解,右值引用就是对右值取别名,但和左值引用在语法使用方面还是有一定区别,如下图所示:
Learning C++ No.29 【右值引用实战】_第4张图片
如上图所示,此时右值引用和左值引用在语法使用上并不相同,使用左值引用我们只需要在类型之后,对象之前添加一个取地址符号就行,而右值引用,我们则需要添加两个取地址符号,首先这就是右值引用和左值引用最大的一个区别,并不是说对右值使用引用符号(&)那么就是右值引用,对左值使用,则就是左值引用,当然这样设计最大的好处就是能让我们很快的分清,那个变量是左值引用变量,那个变量是右值引用变量,从而区分具体代码的编写和使用,明白了这些之后,下述就进行对什么是左值,什么是右值进行特别区分

综上所述: 左值是在内存中有实际地址,能随时随地获取到其地址并且允许直接赋值和修改的变量和对象,而右值则是一个可以产生临时对象的表达式,在内存中没有实际的地址,不能直接获取到地址,也不支持赋值和修改,总的来说,通过一个值是否支持取地址操作,我们就可以判断一个值是左值,还是右值

左值引用和右值引用分析

当我们了解了什么是右值和什么是右值引用之后,此时想必大家都疑问,平时在编写代码的时候,好像也没注意,反正无论是左值还是右值,我们都可以使用左值引用表示,那么这是个什么情况呢?此时想要彻底明白如何在编码时,区分左值引用接收左值,还是左值引用接收右值,亦或者是右值引用接收左值,还是右值引用接收右值,如下分析所示:

1.左值引用是否可以给左值取别名
当然由于这个是吃回锅饭,这里的答案肯定是可以,因为平时我们用引用用的最多的就是左值引用给左值取别名,如下图所示:
在这里插入图片描述
2.左值引用是否可以给右值取别名
相信刚接触这个问题,你肯定是犯迷糊,所以到底行还是不行呢?让我们通过代码说话,如下图所示:
在这里插入图片描述
很显然,编译器不允许我们编写这样的代码,那么原因是什么呢?其实本质就是我们之前在学习引用时,谈到的有关权限放大问题,因为如上代码中,无论是a+b,还是常量10,它们本质都具有常属性,如果此时支持这种写法,让ref1和ref2成为了这些具有常属性数据的别名,那么就会导致修改ref1或者修改ref2,就可以将具有常属性的值进行修改,但,在C++语法中规定,具有常属性的值是一定不允许被修改的,所以导致冲突,编译器不允许该写法,所以左值引用不允许给右值取别名 ,但,同理,如果加上const,那么左值引用就允许给右值取别名,如下代码所示:
在这里插入图片描述
所以总的来说,左值引用并不允许给右值取别名,但是const左值引用允许给右值取别名

3.右值引用是否可以给右值取别名
这个位置闭着眼睛我都知道可以,不然右值怎么取引用,哈哈哈,你说有没有道理,具体不多说,如下代码所示:
在这里插入图片描述
当然值得一提的是,此时不仅仅是x+y表示的是一个右值,连x和y进行运算完之后得到的值本质也是一个右值,因为运算完之后得到的是一个临时变量,虽然该临时变量在内存中有地址,但是由于它是由x+y这个右值进行初始化或者说我们并拿不到这个地址,所以此时该临时变量还是一个右值(关键

4.右值引用是否可以给左值取别名
哈哈哈,简简单单,类比左值引用给右值引用取别名,此时我首先明白,取肯定是可以取,但是肯定没有那么简单,哈哈哈!同理,本质还是左值和右值的区别,右值是一个没有名称,不可寻址的对象,而左值是一个实实在在的地址,如果此时右值可以成为左值的别名,那么就会导致右值可以被取地址,但是右值和左值最大的区分就是右值不允许被取地址,那么此时就会导致冲突,所以编译器肯定是不支持右值引用给左值取别名的,这里不多做演示;同理,此时编译器肯定有一定的解决方法,从而让右值引用可以给左值取别名,那就是使用move接口,在我看来,move接口的本质就是进行资源的转换,也就是对使用move接口的左值进行资源转移,从而让我们不能再直接获取到对应左值的地址,当然也就是将左值间接转换成了一个右值(重点),具体使用如下图所示:
在这里插入图片描述

右值引用真实使用场景

搞定了上述什么是右值引用和右值引用的具体使用方式,此时我们就正式走进代码,看看右值引用具体在那些方面可以大放异彩,可以让我们的代码在无形中效率得到提高,首先同理左值引用,右值引用也可以作为输出型参数使用,如下代码所示:
Learning C++ No.29 【右值引用实战】_第5张图片
此时发现,由于函数参数一个是左值引用,一个是右值引用,所以此时两个函数可以构成函数重载,并且,我们不仅可以使用左值传参,也可以直接使用右值进行传参,所以右值引用在编码过程中带来的第一个好处就是让我们可以直接使用右值进行传参,当然在没有右值引用之前,我们使用const左值也能完成,但是两者的区别非常大,具体等我们将右值引用具体的使用场景分析完毕,我们再进行详细讲解,下述我们就再来看看右值引用在其它方面有什么显著作用,如下:

1.移动拷贝

文章讲解到这里,才真正算右值引用登堂入室的一个环节,当然也就是该篇博客的重点,具体下述内容,我们都将讲述有关右值引用在完美转发、移动拷贝和移动赋值方面的知识,当然这些知识也就是我们使用右值引用实现C++高效代码的关键,所以此时我们就正式来看看什么是移动拷贝,如下代码所示:

Learning C++ No.29 【右值引用实战】_第6张图片
上图就是有关使用右值引用实现移动拷贝的经典代码,并且从代码中,我们就可以看出,移动拷贝和深拷贝本质的区别就是是否需要再次开空间,那么此时我相信大家肯定有一个疑问,那就是为什么左值必须要执行深拷贝,而右值却可以不需要执行深拷贝,而是执行移动拷贝呢?并且移动拷贝具体是什么?接下来就让我来为大家解惑,如下:

什么是移动拷贝
首先想要搞懂移动拷贝,那么最重要的一点就是搞懂,为什么可以进行移动拷贝,想要搞懂为什么可以进行移动拷贝,本质还是需要明白与右值相关的知识,此时我们就知道,当一个右值是自定义类型,那么该右值也被称为将亡值,当一个右值是内置类型,该右值也被称为纯右值,为什么自定义类型的右值被称为将亡值呢?原因就在于该右值是一个临时变量,我们无法通过名称获取该变量的地址,并且它的所有资源都是临时性的,简单理解也就是即将被析构的资源,明白了这点之后,一切都迎刃而解,编译器本着效率优先原则,它就允许我们在使用右值的时候,通过右值引用符号为标示(&&),进行移动拷贝,因为如果当一个值为右值,且为将亡值,本来就需要被析构的临时变量,让它去执行拷贝构造,构造出一块新空间,并且在构造完新空间之后,它自己又被释放掉,那就等于是在脱裤子放屁,多此一举 ,虽然不是不行,但是本着效率优先原则,编译器就支持了移动拷贝的语法(前提被拷贝对象是一个将亡值,也就是自定义类型的右值)
所以此时我们就明白了什么是移动拷贝,移动拷贝就是将被拷贝对象中已分配的内存和内存中存储的数据等资源转移给目标对象,如下图所示:
Learning C++ No.29 【右值引用实战】_第7张图片
所以从上图可以看出,如果我们将右值(将亡值)识别成执行移动拷贝,那么该程序就可以减少一次深拷贝,一次析构,从而提高代码执行效率,优化资源管理方式

总: 之所以左值不可以执行移动拷贝,而右值可以执行移动拷贝,就是因为左值是一个有对应名称、地址、并且我们可以直接获取到对应地址的变量或者对象,所以该变量或者对象可以在程序中的任何函数中被使用,如果我们将它进行资源转移,那么就会导致使用该变量或者对象的代码块出现错误或者异常(本质就是两个指针指向了同一块空间,析构时导致该空间析构了两次,导致其中一个指针出现野指针),所以左值坚决不允许使用移动拷贝, 而右值可以使用移动拷贝的原因,还是同理,且右值通常表示的就是将要被销毁或者不再使用的临时对象,我们不能获取到对应的地址,所以可以直接进行移动拷贝,当然也就是资源转移

注意: 明白了上述总括的知识,和上述有关右值引用对左值取别名的知识,此时我们知道,move虽然可以让右值引用对左值取别名,但本质上还是将对应的左值对象变成了一个右值,进而才支持右值引用对左值取别名的操作,所以明白,使用move将一个左值变成右值,是存在风险的,一不小心就会出现问题,在使用move时一定要保证该左值在别的代码块中没有被使用,否则就不允许其调用move接口,反正move接口需要谨慎使用

移动拷贝具体使用场景
想要看看移动拷贝具体的使用场景,首先我们要回顾一下有关使用左值引用的知识,比如:使用左值引用的前提是被返回对象出了函数作用域不会随着栈帧的销毁而销毁,明白这点,此时我们就知道,如果返回对象是一个局部对象,那么此时我们就不可以使用引用返回,就只能通过创建临时变量传值返回,这样就会导致效率较低,因为需要多执行一次拷贝构造和析构,但如果此时我们能将这种场景进行优化,优化成直接调用右值引用的构造函数,执行移动拷贝,那么就可以大大提高代码执行效率和优化资源管理方式,具体如下图所示:

无右值引用之前,C++98,传值返回具象图:
Learning C++ No.29 【右值引用实战】_第8张图片
从图中可以看出,如果没有右值引用类型的构造函数,那么就只能走拷贝构造,虽然在编译器优化之后,两次拷贝构造被优化成了一次,但是如果对应局部对象在堆区上的数据非常多,或者说该对象是一个嵌套类型,那么就会导致执行一次拷贝构造的成本变得非常大,所以在C++11中,为了解决这个问题,就提出了右值引用,进而推出了移动构造的想法,具体如下图所示:
Learning C++ No.29 【右值引用实战】_第9张图片
在将返回值str识别成一个右值的情况下,可以看出,此时可以将传值返回给优化到极致,直接变成调用一次移动拷贝就能完成,可以想象,效率得到了极大的提升,并且因为此时str本身就是作为一个返回值,也就是即将被销毁的值,将它从左值识别成右值本质上并不会带来任何的负面效果(类似于就是调用了move接口),所以这种编码方式是极其完美的,直接将代码执行效率和资源管理方式拉满

并且明白,在C++11中STL的所有容器,都增加了移动构造语法,本质也就是右值引用的拷贝构造函数,如下图所示:
Learning C++ No.29 【右值引用实战】_第10张图片
发现s1的资源,因为在初始化s3对象时,编译器识别到s1是一个右值,所以直接执行移动拷贝,导致s1的资源直接被转移到了s3对象中

最终明白,STL中的容器不仅仅是只有拷贝构造函数增加了右值引用调用移动拷贝的写法,在其它许多的接口中,都有涉及到与右值引用调用移动拷贝,例如插入接口,如下:
在这里插入图片描述
可以发现,在插入数据时,当编译器识别到该值是一个右值,那么它就一定会去调用与之最匹配的右值插入接口,从而调用移动构造的写法,而不再像是左值写法,先去调用拷贝构造初始化该结点,然后再将该结点插入到对应的容器中,而是直接将对应的右值进行移动构造,也就是资源转换,然后直接插入对应的容器中,如下图所示:
在这里插入图片描述
注意:移动构造不敢不清楚,上述不仅有代码,还有讲解,本质就是一个swap,对指针进行交换而已

2.完美转发

搞定了上述有关移动构造的知识,此时我们就来学习一下右值引用的另一个大知识点,完美转发,同理,在搞懂什么是完美转发之前,我们首先要明白,什么是万能引用,如下图所示:
Learning C++ No.29 【右值引用实战】_第11张图片
从图中,我们可以看出,由于此时我们使用了模板参数,所以此时 PerfectForward() 接口不仅可以接收左值,而且也可以接收右值,此时该接口就被我们称之为万能引用接口,搞懂了什么是万能引用,接下来我们就可以正式进入完美转发的学习,如下:

什么是完美转发
想要搞懂什么是完美转发,首先我们应该搞懂为什么要有完美转发,只要懂了为什么有,那么完美转发是什么自然不攻自破,并且想要搞懂为什么要有完美转发,那么同理,要搞懂其它相关知识,所以此时我们依旧先探讨一下,有关左值和右值的转变,如下图所示:
Learning C++ No.29 【右值引用实战】_第12张图片
如图,此时我们发现,当一个右值作为参数进行传参时,相应函数接口在接收该值时,会将该值识别成左值,具体原理类似于我们使用右值去调用移动构造再去初始化对应的左值,同理被初始化的变量就类似于接收右值的那个参数,本质上都是一个可以直接获取到相应地址的左值,明白了该点之后,搞懂为什么要有完美转发,等于喝汤吃菜,如下:

明白上述知识之后,此时就可以抽象类比一下,假如此时你调用一个插入数据的接口,但是该接口并不是完整代码实现,而是使用自己对应的参数去复用另一个接口,那么此时根据上述原理,无论是右值还是左值,只要进行了参数传递,那么就都会变成一个左值,此时就可以想象到,如果你的参数从右值变成了左值,那么在之后的函数调用中,无论是拷贝构造还是其它接口,都只能被识别成左值,导致最终只能调用相关左值引用实现的函数接口,无法达到预期目标(想要通过右值引用去调用移动构造),如下图所示:
Learning C++ No.29 【右值引用实战】_第13张图片
上述代码中唯一值得 注意 的是,我们此时string类是作为一个模板类别调用,所以在初始化list结点的时候,本质是在初始化string,所以具体执行拷贝构造还是移动构造本质是还是由string的拷贝构造函数是左值引用还是右值引用决定

使用完美转发解决上述问题
所以当我们碰到了上述问题,也就是一个右值被提前转换为了左值,此时的解决方法就是使用完美转发,本质就是让我们的右值一直保持右值属性,不被替换成左值,关键字:forward,具体如下图所示:
Learning C++ No.29 【右值引用实战】_第14张图片

上述就是有关完美转相关的知识,具体就这样啦!

总结: 为了可以在晚上9点之前更新这篇博客,现在肚子空空,现在唯一的想法就是赶紧码完这最后几个字,然后出门去觅食,哈哈哈,总的来说右值引用、移动构造和完美转发这些知识,So,So,哈哈哈,反正我还没找到我写完博客觉得很难懂的知识点,哈哈哈,估计这就是博客的魅力吧!没有写博客搞不定的东西,啦啦啦啦!剩下有关右值引用的知识,下篇博客见,See you!

你可能感兴趣的:(C++学习,c++,开发语言)