c++中的右值引用的简单理解

以下的内容是我看了网上的博文后自己的总结,如果时间充裕的话,建议直接看原文

简介:

  c++中的右值引用十分不好理解,但是当你大概知道右值引用解决了c++中的什么问题后,会感觉右值引用还是很有用的。平时我们在c++中使用的引用为了和c++11引入的右值引用区分,一般把它称之为左值引用。左值引用很好理解,就是多个变量名绑定到了同一块内存上,操作这几个不同的变量名相当于操作同一块内存。但是右值引用就让人有点摸不着头脑了,一是右值(可以简单地认为就是字面常量或者临时变量)根本就没有变量名,这怎么引用?二是左值引用已经解决了按值传参的缺陷,右值引用又是干嘛的?正是由于这些问题让人感觉难以理解,我们需要分解问题,一步步去理解它。先复习一下什么是左值和右值。知道了左值右值的概念后,直接讲右值引用还是会感觉有点晦涩难懂,所以先引入一个move语义的概念,然后给出一个场景,再去看右值引用存在的意义。

 

左值和右值的区别:

  区分左值和右值最简单的办法就是看一个内存空间是否可以使用&符号取得它的地址。能取到的就是左值,取不到的就是右值。比如

  

 

什么是move语义:

  假设我们有一个这样的class:它里面包含着一个 int *m_memory 的成员变量,指向一块从堆上分配来的内存,并且假设这块内存有点大,复制一份这块内存中的字节需要一定的时间。

  我们一般写一个重载了赋值运算符构造函数会这么写(记得深拷贝):

    BaseClass :: operator=(const BaseClass &other)

    {

      释放该对象m_memory指向的内存

      复制一份other中的m_memory的字节,并使该对象m_memory指向这块内存

    }

  然后拷贝构造函数也是一样的操作。

  当我们在其他地方使用到BaseClass的时候,可能会出现这样的场景:

    BaseClass func ();     //假设定义了一个返回BaseClass对象的函数

    BaseClass b;

    b = func();

  最后一行会发生如下操作:

    <1> 调用b的赋值构造函数,释放b.m_memory和b中的其他资源, 把func()函数返回的临时对象中的m_memory和其他资源都复制一份到b中。

    <2> 释放临时对象中的m_memory和其他资源

  上面展示的就是常规的使用变量接受函数返回值的流程,并且这个流程没有问题,可以正常运行。但是可以发现,在上面的代码中,首先是b.m_memory被释放,b.m_memory重新malloc一块一样大的内存,然后把func()函数返回临时变量中的m_memory的字节拷贝一份给b.m_memory, 最后释放临时变量中的m_memory。如果我们换一种思路,把临时变量中的m_memory和b.m_memory指针交换一下,交换后的b.m_memory不用释放的同时省去了重新申请内存的操作。然后由临时对象释放交换后自身的m_memory。也就是把赋值构造函数和拷贝构造函数实现成这样:  

    BaseClass :: operator=(BaseClass &other)

    {

      swap(this.m_memory, other.m_memory);

    }

  这就是所谓的移动语义,move语义。

  但是把赋值构造函数实现成这样有一个问题:

    如果我们把赋值构造函数实现成这样,那下面的代码中的情况1怎么办?本来只是想赋值的,结果做了交换:

      BaseClass b1;

      BaseClass b2;

      b2 = b1;      // 情况1

      BaseClass b3;

      b3 = func();  //情况2

  所以现在的问题就在于,我们想情况调用赋值构造函数。当我们传入的是左值时,我们希望调用拷贝内存这一版的赋值构造函数。当我们传入的是临时变量是,我们希望调用的是有move语义的赋值构造函数,就比如情况2中的代码。

 

右值引用:

  这时就可以引入右值引用的概念了,我们可以重载两个赋值构造函数(拷贝构造函数一样)

    BaseClass& operator=(const BaseClass& other);       //内部实现为内存拷贝的拷贝构造函数

    BaseClass& operator=(BaseClass&& other);         //带有右值拷贝,内部实现为移动语义的拷贝构造函数。因为要交换,所以形参不能是const

  右值引用的概念:如果BaseClass是一个类型,那么BaseClass&&就成为右值引用。c++在语法级别做了适应。就以拷贝构造函数为例,如果发现传入的参数是左值,则会调用常规的拷贝构造函数,如果发现传入的是临时变量,则会调用形参为右值引用的那个拷贝构造函数。如果是这样的话,上面的情况1和情况2中的代码就有救了,在c++11中,情况1会自动调用常规拷贝构造函数,而情况2因为传入的是临时变量,则会调用移动拷贝构造函数。

  如果m_memory比较大,而且拷贝构造在代码中使用很频繁(比如在STL的容器以及各种对应算法中,拷贝构造相当频繁),移动拷贝构造函数可以极大地提高代码的性能。这时,c++11引入右值引用的意义也出来了,就是为了方便实现移动语义,提升c++的性能。

 

左值使用移动语义:

  按照上面的逻辑,只有右值通过右值引用才可以使用移动语义。但是在一些情况下,左值也想使用移动语义怎么办,比如以下场景:

    template<class T>

    void swap(T& a, T&b)

    {

      T tmp(a);

      a = b;

      b = a;

    }

  swap中全部都是左值,所以无法像右值那样通过右值引用使用移动语义。但是如果这样的函数可以使用移动语义,岂止美滋滋?为了实现左值也能够使用移动语义,c++11引入了std::move函数。这个函数的作用就是把一个左值变成一个右值再返回来。所以swap函数可以改成这样:

    template<class T>

    void swap(T& a, T&b)

    {

      T tmp(std::move(a));

      a = std::move(b);

      b = std::move(tmp);

    }

  这时需要特别注意一个问题,以移动拷贝构造函数为例,对象使用move之后,那么这个对象就不应该再被使用了。因为对象传进去的时候本身就不是const,很有可能被具有移动语义的函数给改变了,比如情况1。后续的代码继续使用调用move之后的对象,一不小心就会写出很让人头大的bug。这也是为什么默认只有右值可以使用移动语义,因为右值一般使用了之后就会马上被释放掉。上面的swap代码中这几个变量在调用move后已经确认过不会再被后续的代码做复杂的使用,所以才可以放心地使用移动语义。

  c++对于刚才说的这一点也有一些安全措施,比如下面的代码中,会直接调用BaseClass的常规拷贝构造函数:

    void func(BaseClass&& x)

    {

      BaseClass y = x;

    }

  虽然形参是右值引用类型,但是c++认为,x在func的整个作用域内都是可见的,所以会把x当做左值来使用。在右值引用这一块,可以简单地认为,有名字的会被c++当成左值,没名字的会被c++当成右值。    

 

  

 

  

 

 

 

 

 

 

 

 

 

 

 

posted @ 2018-08-12 23:56  Frogjie  阅读(1031)  评论(0编辑  收藏  举报