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/

 

 

 

posted @ 2020-05-26 16:24  HighDefinition  阅读(201)  评论(0编辑  收藏  举报