• C++11:移动语义(Move Semantic)


    2020-07-14更新:参考了其他博客,对内容进行补全。


     

    浅拷贝、深拷贝

    • 浅拷贝(shallow copy):按位拷贝对象,创建的新对象有着原始对象属性值的一份精确拷贝(但不包括指针指向的内存)。

    • 深拷贝:拷贝所有的属性(包括属性指向的动态分配的内存)。换句话说,当对象和它所引用的对象一起拷贝时即发生深拷贝。

     class Vector{
         int num;
         int* a;
     public:
         void ShallowCopy(Vector& v);
         void DeepCopy(Vector& v);
     };
     //浅拷贝
     void Vector::ShallowCopy(Vector& v){
         this.num = v.num;
         this.a = v.a;//拷贝后对象和原对象的指针指向相同对象
     }
     //深拷贝
     void Vector::DeepCopy(Vector& v){
         this.num = v.num;
         this.a = new int[num];
         for(int i=0;i<num;++i){a[i]=v.a[i]}
     }

    可以看到,深拷贝的开销往往比浅拷贝大(除非没有指向动态分配内存的属性),所以我们就倾向尽可能使用浅拷贝。

    但是浅拷贝有一个问题:当有指向动态分配内存的属性时,会造成多个对象共用这块动态分配内存,从而可能导致冲突。一个可行的办法是:每次做浅拷贝后,必须保证原始对象不再访问这块内存(即转移所有权给新对象),这样就保证这块内存永远只被一个对象使用。

    那有什么对象在被拷贝后可以保证不再访问这块内存呢?答案是临时对象。

    要解决这个问题,我们先来认识左右值。

    左值和右值

    C++98左右值的概念:

    • 左值(lvalue) :表达式结束后依然存在的持久对象。

    • 右值(rvalue) :表达式结束后就不再存在的临时对象。

    之所以取名左值右值,是因为在等式左边的值往往是持久存在的左值类型,在等式右边的表达式值往往是临时对象。字符串字面量是唯一不可算入右值的字面量,因为其代表字符数组,实际存储在静态数据区,这里的静态数据区是相对于堆栈等动态数据区而言,存放全局变量和静态变量的内存区。

    C++11左右值被重新定义,使用下面两种独立的性质来区别类别:

    1. 拥有身份:指代某个非临时对象。

    2. 可被移动:可被右值引用类型匹配。

    每个C++表达式只属于三种基本值类别中的一种:左值 (lvalue)、纯右值 (prvalue)、将亡值 (xvalue)

    • 拥有身份且不可被移动的表达式被称作 左值 (lvalue) 表达式,指持久存在的对象或类型为左值引用类型的返还值。

    • 拥有身份且可被移动的表达式被称作 将亡值 (xvalue) 表达式,一般是指类型为右值引用类型的返还值。

    • 不拥有身份且可被移动的表达式被称作 纯右值 (prvalue) 表达式,也就是指纯粹的临时值(即使指代的对象是持久存在的)。

    • 不拥有身份且不可被移动的表达式无法使用。

    可归纳:

    • 左值(lvalue) 指持久存在(有变量名)的对象或返还值类型为左值引用的返还值,是不可移动的。

    • 右值(rvalue) 包含了 将亡值、纯右值,是可移动(可被右值引用类型匹配)的值。

    如此分类是因为移动语义的出现,需要对类别重新规范说明。例如不能简单定义说右值就是临时值(因为也可能是std::move过的对象,该代指对象并不一定是临时值)。

    右值引用

    声明:左值引用声明符号为&,右值引用声明符号为&&。

     //C++11中通过在某个类型后放置一个符号&&来声明一个右值引用,用于引用一个右值(即临时量)
     //声明
     int&& a = 1;
     void Func(T&& rhs);

    C++11引入右值引用,目的之一是为了支持移动操作。使用右值引用的思想,即通过移动语义实现浅拷贝,就解决了临时对象的问题,减少了原本使用深拷贝的开销。

    移动语义

    在对两个类型进行数据交换时,我们有时实际想要的是让A所拥有的资源转让给B,即转让资源所有权,而不是发生对象拷贝。

    因此,移动语义的引入可以在进行大规模数据复制的时候,将动态申请的内存空间的所有权直接转让出去。

    注意:使用移动语义意味着

    • 原对象不再被使用,如果使用会造成不可预知的后果。

    • 所有权转移,资源的所有权被转移至新的对象。

    移动语义通过移动构造函数移动赋值操作符实现,其特点如下:

    • 参数的符号必须为右值引用符号,即为&&。

    • 参数不可以是常量,因为函数内需要修改参数的值

    • 参数的成员转移后需要修改(如改为nullptr),避免临时对象的析构函数将资源释放掉。

    实例:

     template <typename Object>
     class Vector
     {
     public:
         //移动构造函数
         Vector(Vector&& rhs) noexcept : theSize{ rhs.theSize }, theCapacity{ rhs.theCapacity }, objects{ rhs.objects }
         {
             rhs.theSize = 0;
             rhs.theCapacity = 0;
             rhs.objects = nullptr;
         }
         //移动赋值函数 
         Vector& operator= (Vector&& rhs) noexcept
         {
             std::swap(theSize, std::move(rhs.theSize));
             std::swap(theCapacity, std::move(rhs.theCapacity));
             std::swap(objects, std::move(rhs.objects));
             return *this;
         }
     private:
         int theSize;
         int theCapacity;
         Object* objects;
     }

    标准库函数std::move

    定义在头文件utility中。用于把任何左值(或右值)转换成右值,简单来说,它使一个值易于“移动”。但不会真正移动数据。

     //std::move的函数原型定义
     template <typename T>
     typename remove_reference<T>::type&& move(T&& t)
     {
         return static_cast<typename remove_reference<T>::type&&>(t);//强制转换类型为右值引用
     }

    下面以实现swap例程为例:

     //通过3次复制的实现
     void swap( vector<string> &x, vector<string> &y)
     {
         vector<string> tmp = x;
         x = y;
         y = tmp;
     }

    这种实现会调用vector的拷贝赋值运算符进行拷贝,显然只适合小规模数据的交换,如果数据量稍大一点拷贝开销也十分巨大。

     //通过3次移动的实现
     void swap( vector<string> &x, vector<string> &y)
     {
         vector<string> tmp = std::move(x);
         x = std::move(y);
         y = std::move(tmp);
     }

    这种实现方式将x,y,tmp通过std::move转换成右值后,再调用vector的移动赋值运算符进行移动,大数据情况下减少很大部分拷贝开销。

     参考博客

    [1]作者:KillerAery 出处:http://www.cnblogs.com/KillerAery/

    [2]https://wendeng.github.io/2019/05/14/c++%E5%9F%BA%E7%A1%80/c++11std-move%E4%BD%BF%E7%94%A8%E4%B8%8E%E5%8E%9F%E7%90%86/

  • 相关阅读:
    转载-如何高效的学习技术
    Lc176-第二高的薪水
    Lc4-寻找两个有序数组的中位数
    Lc175-组合两个表
    Lc3-无重复字符的最长子串
    Lc2-俩数相加
    Lc1- 两数之和
    jpa-子查詢
    20191225页面样式
    leetcode二刷结束
  • 原文地址:https://www.cnblogs.com/HDDDDDD/p/12966344.html
Copyright © 2020-2023  润新知