【C++】C++11新特性—右值引用,来看看怎么个事儿

06-02 1736阅读
【C++】C++11新特性—右值引用,来看看怎么个事儿 🚀个人主页:@小羊 🚀所属专栏:C++ 很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~

【C++】C++11新特性—右值引用,来看看怎么个事儿

目录

  • 前言
  • 一、左值引用和右值引用
  • 二、右值引用和移动语义
    • 2.1 移动构造
    • 2.2 移动赋值
    • 2.3 STL容器插入接口
    • 2.4 左值右值相互转换
    • 2.5 完美转发
    • 三、类的新功能
      • 3.1 新默认成员函数
      • 3.2 新关键字

        前言

        传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。


        一、左值引用和右值引用

        引用简单来说就是给对象取别名,我们刚开始接触C++的时候就学过,这里又区分出左值引用和右值引用,它们有什么不同?要想探讨这个问题,首先应该了解清楚具体什么是左值什么是右值。

        • 左值: 一个表示数据的表达式,一般情况可以赋值(如果被const修饰就不能修改),左值可以出现在“=”的左边或右边,最关键的特性是左值可以取地址
        • 右值: 一个表示数据的表达式,一般不能修改,通常是字面常量、临时对象、匿名对象等,右值在“=”的右边,不能在左边,右边不能取地址
          int main()
          {
          	//以下的a、p、b、*p、s[0]都是左值
          	int a = 1;
          	int* p = &a;
          	const int b = a;
          	*p = 10;
          	string s("abcdef");
          	s[0];
          	//以下都是右值
          	10;
          	x + y;
          	fmin(x, y);//函数返回值
          	string("1234");
          	return 0;
          }
          

          引用都是给对象取别名,左值引用就是给左值取别名,右值引用就是给右值取别名。

          那左值引用能不能给右值取别名,右值引用能不能给左值取别名呢?

          如果左值引用不能给右值取别名,那C++11出来之前右值是不是都不能取别名?猜测一下也知道大概率不是的。

          左值引用一般是不能给右值取别名的,但是可以用const修饰就行了。因为前面也说了右值一般都是字面常量、临时对象、匿名对象等,而这些值都具有常性,如果不用const修饰就存在权限放大的问题。所以早期右值引用没出来之前右值也可以通过左值引用给取别名。

          const int& r1 = 10;
          const int& r2 = x + y;
          const int& r3 = fmin(x, y);
          const string& r4 = string("abcdef");
          

          例如下面的场景:

          int main()
          {
          	vector v;
          	string s("1111");
          	v.push_back(s);
          	v.push_back(string("2222"));
          	v.push_back("3333");
          	
          	return 0;
          }
          

          前面我们模拟实现List的push_back:void push(const T& x),加上const修饰另一个目的也是为了既能接收左值又能接收右值,这样我们既可以插入一个有名对象,又能插入匿名对象了。

          同样的右值引用也一般不能给左值取别名,但是可以通过move(左值)的方式来给左值取别名。move()可以看作像是强制类型转换,所以也不会改变操作对象本身的属性。

          int main()
          {
          	int a = 1;
          	int* p = &a;
          	const int b = a;
          	*p = 10;
          	string s("abcdef");
          	s[0];
          	int&& r1 = move(a);
          	int*&& r2 = move(p);
          	const int&& r3 = move(b);
          	string&& r4 = move(s);
          	string&& r5 = (string&&)s;
          	return 0;
          }
          

          右值不能取地址,但是给右值取别名后,右值会被存储到特定位置,且可以取到该位置的地址,可以修改,如果不想被修改可以用const修饰。


          二、右值引用和移动语义

          引用的意义是减少拷贝。 在右值引用出现之前,左值引用还不太全面,有些传返回值的场景只能传值返回,不能传引用返回。比如传局部对象:

          yjz::string to_string(int value)
          {
          	bool flag = true;
          	if (value  0)
          	{
          		int x = value % 10;
          		value /= 10;
          		str += ('0' + x);
          	}
          	if (flag == false)
          	{
          		str += '-';
          	}
          	std::reverse(str.begin(), str.end());
          	return str;
          }
          

          这里的str是一个局部对象,出了作用域就销毁,传引用会造成野引用,所以只能传值,返回值传值会先拷贝构造一个临时对象,再用临时对象拷贝构造目标对象。


          2.1 移动构造

          右值可分为纯右值和消亡值,纯右值比如字面常量,消亡值比如临时对象。临时对象用完就要消亡,再对它拷贝构造显得有点多余,既然它的结局已经注定了还不如把它的东西直接拿过来,这里就引出了移动构造,所以移动构造直接将构造的对象和被构造的对象数据交换(掠夺)一下就行。

          移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

          //拷贝构造
          string(const string& str)
          {
          	_str = new char[str._capacity + 1];//多开一个存'\0'
          	strcpy(_str, str._str);
          	_size = str._size;
          	_capacity = str._capacity;
          }
          //移动构造
          string(string&& str)
          {
          	swap(str);
          }
          

          虽然即使没有移动构造,只有上面的拷贝构造也能因为有const修饰而接收左值和右值,但是有了移动构造编译器会走最匹配的。

          1、string类只有拷贝构造,没有移动构造

          【C++】C++11新特性—右值引用,来看看怎么个事儿

          2、string类有拷贝构造,也有移动构造

          【C++】C++11新特性—右值引用,来看看怎么个事儿

          虽然str是一个左值,但是它出了作用域就消亡,和临时对象的结局是一样的,所以可以把str作为一个右值来走移动构造,这里是隐式的将strmove为右值。返回局部的大对象,调用移动构造的代价非常低,很实用。

          也不是说所有的局部对象传值返回都要走移动构造,只有需要深拷贝的对象移动构造才有意义,像日期类这种对象拷贝构造和移动构造没有区别。

          上面我们提到了像VS2022这种比较激进的编译器优化比较夸张,它一步到位优化为直接构造,这里str就像ret1的左值引用一样。

          【C++】C++11新特性—右值引用,来看看怎么个事儿

          那既然编译器都优化的这么好了,那移动构造还有意义吗?并且它是直接构造,而走移动构造的话是构造+移动构造。既然右值引用现在被广泛使用了,就说明移动构造还是有重要意义的。

          1. 移动构造代价很小
          2. 不是所有的编译器都像VS2022这样做极致的优化
          3. 有其他场景下优化不了

          2.2 移动赋值

          除了移动构造,还有移动赋值,本质还是一样的。下面我们来看下有移动赋值和没有移动赋值有什么区别。

          //赋值重载
          string& operator=(const string& str)
          {
          	//防止自己给自己赋值
          	if (this != &str)
          	{
          		delete[] _str;
          		_str = new char[str._capacity + 1];
          		strcpy(_str, str._str);
          		_size = str._size;
          		_capacity = str._capacity;
          	}
          	return *this;
          }
          //移动赋值
          string& operator=(string&& str)
          {
          	swap(str);
          	return *this;
          }
          

          【C++】C++11新特性—右值引用,来看看怎么个事儿

          有调用赋值重载的情况时编译器不能像之前一样优化为直接构造,因为这里调用to_string前ret1是已经存在的对象,编译器就没办法优化了。

          这里还是把左值str隐式作为右值调用了移动赋值,因为虽然str是左值,但它是局部对象,终归是为ret1服务的,出了作用域就消亡,和临时对象的意义差不多。

          所以不管是移动拷贝还是移动赋值,都是有意义的,编辑器的极致优化也处理不了所有情况,相比之下这里编译器的极致优化反倒显得意义不大,因为即使多了移动拷贝和移动赋值这一步骤,它们的消耗也是非常小的。

          不管是移动构造还是移动赋值,处理的都是传值返回的问题。


          2.3 STL容器插入接口

          右值引用解决的不只是传值返回的问题,还有一些容器插入接口的问题。

          【C++】C++11新特性—右值引用,来看看怎么个事儿

          int main()
          {
          	std::list lt;
          	yjz::string s1("111111");
          	lt.push_back(s1);
          	lt.push_back(yjz::string("222222"));
          	lt.push_back("333333");
          	lt.push_back(move(s1));
          	return 0;
          }
          

          有了右值引用,我们就可以很方便的插入一些匿名对象,这样写不仅简单还会少一次拷贝构造。所以以后我们可以插入匿名对象,少了一次拷贝构造,消耗更低一些。

          这里插入匿名对象时还有一个奇怪的现象:

          【C++】C++11新特性—右值引用,来看看怎么个事儿

          其中红色箭头是实际执行路径。首先插入了一个string类型的一个匿名对象,push_back调到了右值引用的函数没问题,但下一步调用insert函数时为什么调到了左值引用的函数呢?

          探讨这个问题前我们先来看这个:

          yjz::string&& r1 = yjz::string("11111111");
          

          这是一个右值引用没错,但右值是yjz::string("11111111"),而r1却是一个左值,所以右值引用本身是一个左值。

          虽然右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。 例如:不能取字面量10的地址,但是r1引用后,可以对r1取地址,也可以修改r1。如果不想r1被修改,可以用const int&& r1去引用。

          其实右值引用本身是左值也不奇怪,如果右值引用本身是右值,右值一般不能修改,那还怎么通过移动语义来掠夺资源呢。

          再回到上面的问题,虽然匿名对象push_back时调到了右值引用的接口,但是接口中的x却是一个左值,所以接下来调用insert时就调到了左值引用的接口。

          x本身一个左值,其引用的对象是一个右值,在调用insert时我们期望调用右值引用的接口,是参数匹配的问题,所以可以考虑用move进行类似强转的操作。

          void push_back(T&& x)
          {
          	insert(end(), move(x));
          }
          

          这样就完了吗?还没完,在insert函数内部也存在着相同的问题。

          【C++】C++11新特性—右值引用,来看看怎么个事儿

          这样就完了吗?还没完,这里move(x)传过去是一个右值,所以Node的构造函数也需要有一个右值引用为接口的版本。

          【C++】C++11新特性—右值引用,来看看怎么个事儿

          这样就完了吗?还没完,这里的x也是一个左值,所以下面初始化_data时也需要move强转一下。

          【C++】C++11新特性—右值引用,来看看怎么个事儿

          这样就完了吗?是的,这次真的完了。

          上面的层层转换过程少一步都不行,这类似一个属性退化的问题。


          2.4 左值右值相互转换

          x是一个左值,如果我们需要它是一个右值也只是一句代码的事,同样右值如果我们需要它是一个左值也可以强转得到。

          【C++】C++11新特性—右值引用,来看看怎么个事儿

          通过上面的一些实例可以看出,左值和右值可以相互转换,其实说到底左值和右值在底层没什么区别,其能不能取地址也只是语法层面上的约束,当然现阶段的我们还不适合过多关注底层,因为底层和语法层在某些地方是相悖的,这不利于我们小萌新学习,我们学习主要还是以语法层为主的。


          2.5 完美转发

          上面看到C++11后STL容器插入接口基本都对左值和右值做了对应的函数,那以后类似这样的场景我们都要写两个甚至更多的版本吗?为了方便C++11又引入了万能引用:

          template
          void func(T&& x)
          {
          	//...
          }
          

          在函数模版中,这里的T&& x不再是前面我们见到的右值引用,而是万能引用。它不是具体的左值引用或右值引用,而是根据传过去的参数自动推导引用类型。传左值就是左值引用,传右值就是右值引用。

          在各种场景下它帮助我们实例化出下面四种函数:

          void func(int& x);        //左值
          void func(const int& x);  //const 左值
          void func(int&& x);       //右值
          void func(const int&& x); //const 右值
          

          为什么那些容器接口没有使用万能引用呢?

          1. 那些函数是在类模版中的,类模版实例化后函数中的参数就是一个确定的类型,除非再套一层模版
          2. 历史遗留的原因,因为前面已经有左值引用的版本了

          模板的万能引用只是提供了既能接收左值又能接收右值的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值。

          void func(int& x) { cout  cout  cout  cout 
          	func(t);
          }
          int main()
          {
          	PerfectForward(10);        //右值
          	int a = 1;
          	PerfectForward(a);         //左值
          	PerfectForward(move(a));   //右值
          	const int b = 2;
          	PerfectForward(b);         //const 左值
          	PerfectForward(move(b));   //const 右值
          	return 0;
          }
          
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

相关阅读

目录[+]

取消
微信二维码
微信二维码
支付宝二维码