zoukankan      html  css  js  c++  java
  • 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++当成右值。    

      

      

  • 相关阅读:
    266. Palindrome Permutation
    265. Paint House II
    264. Ugly Number II
    263. Ugly Number
    261. Graph Valid Tree
    260. Single Number III
    259. 3Sum Smaller
    pthon 批量压缩当前目录,子目录下图片
    不小心打开了N多的压缩文件窗口,一句命令就搞定!
    # Writing your-first Django-app-part 4-simple-form
  • 原文地址:https://www.cnblogs.com/MyOnlyBook/p/9465592.html
Copyright © 2011-2022 走看看