第十三章-第六小节-对象移动
前言
最近在看C++ Primer的时候,对于对象移动一直不太懂,所以在查找各种资料,仔细研究代码后,打算写篇博客记录下来,果然还是不要得过且过,看见不懂的就查,弄懂为止最好了。
对象移动
很多时候都会发生对象拷贝,但是拷贝有个问题,对于有些仅仅做完拷贝就销毁的情况,其实没必要,更好的做法是进行移动元素;在新标准中,可以用容器保存不可拷贝的类型,前提是他能被移动即可;
右值引用
-
标准库容器、string和shared_str类既支持移动也支持拷贝,IO类和 unique _xstr 类可以移动但不能拷贝;
-
右值引用只能绑定到一个将要销毁的对象,通过 && 来获得右值引用;
-
右值引用:所引用的对象将要被销毁;对象没有其他用户;这些意味着右值引用的代码可以自由的接管所引用的对象的资源;
-
变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,但是常量可以,即使变量是右值引用类型也不可以;
例子:
int && rr1 = 42; //正确:字面意思是右值 int && rr2 = rr1; //错误:表达式rr1是左值
-
调用了move函数就意味着承诺,不能对移动后对源对象的值做任何的假设,但是你可以销毁,也可以赋予它新值,就是不能使用一个移后源对象的值;
-
使用move的代码应该使用std:move而不是简单的move,这样做可以避免潜在的名字冲突;
例子:
int && rr3 = std::move(rr1);
移动构造函数和移动赋值运算符
-
和拷贝构造函数不同,移动构造函数不分配任何内存,在完成资源移动后,移动构造函数还需要确保移后源对象是否可以销毁掉,其必须不再指向被移动的资源,因为其资源的所有权已经归属到新创建的对象(如果对象中存在成员指针,则需要将其置为空);
例子:
Derived::Derived(Derived && d) noexcept: tag(d.tag){ d.tag = ""; }
其中tag是Derived的成员属性;
-
noexcept 是我们承诺一个函数不抛出异常的一种方法,在一个构造函数中,noexcept出现在参数列表和初始化列表开始的冒号之间(在类的声明和定义中都需要指定noexcept);
(使用方法如上)
-
不能在使用右侧运算对象的资源之前就释放左侧运算对象的资源;在移动操作之后,移后源对象必须保持有效的、可析构的状态,但是客户不能对其进行任何假设;
例子:
Derived& operator=(const Derived& other) { if (this != &other) { name = other.name; tag = other.tag; } return *this; }
注意:函数参数是引用类型的
-
和拷贝构造操作不同,如果一个类定义了自己的拷贝构造函数,拷贝赋值函数或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符,则其就会使用拷贝操作替代移动操作;
只有当一个类没有定义任何自己版本的拷贝控制成员,而且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数和移动赋值运算符;
-
和拷贝操作不同,移动操作永远不会隐式定义为删除的函数;
合成的移动操作定义为删除的函数遵循定义删除的合成拷贝类似原则:
-
(和拷贝构造函数不同)移动构造函数被定义为删除的函数的条件:有类成员定义了自己的拷贝构造函数并且未定义移动构造函数,或者又是有类成员未定义自己的拷贝构造函数并且编译器不能为其合成移动构造函数;
-
如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的;
-
(和拷贝构造函数类似)如果类的析构函数被定义为删除的或是不可访问的,则类的移动构造函数被定义为删除的;
-
(和拷贝赋值运算符类似)如果类成员是const的或者是引用,则类的移动赋值运算符被定义为删除的;
下面这种情况是正确的,没有定义为删除的情况:
class X { public: int i; // 内置类型可以移动 string s; // string定义了自己的移动操作 }; class HasX { public: X ele; // X有何合成的移动操作 }; X x; X x2 = std::move(x); // 使用合成的移动构造函数 HasX hx; HasX hx2 = std::move(hx); //使用合成的移动构造函数
但是在列举的四种情况就是错的,移动操作则会被定义为删除的。
注意:定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,否则,这些成员默认地被定义为删除的。
-
-
如果一个类既有移动构造函数,也有拷贝构造函数,编译器则会使用普通的函数匹配规则来确定会使用哪个构造函数,赋值也是类似;
-
如果一个类没有移动构造函数,那么函数匹配原则则会保证该类型的对象会被拷贝,即使试图通过调用move来移动也是如此;
例子:
class Foo { public: Foo() = default; Foo(const Foo&); //拷贝构造函数 //并未定义移动构造函数 }; Foo f1; Foo f2(f1); //拷贝构造函数 Foo f3(std::move(f1)); //拷贝构造函数,因为未定义移动构造函数
注意:用拷贝操作代替移动操作是安全的,一般情况下,前者都会满足后者的要求;
-
拷贝并交换赋值运算符和移动操作
同时实现移动构造函数和赋值运算符:
class Person { public: // 添加的移动构造函数 Person(Person && p) noexcept : name(p.name) { p.name = ""; } // 赋值运算符既是移动赋值运算符,也是拷贝赋值运算符 Person& operator=(Person p) { swap(*this, p); return *this; } private: string name; };
赋值运算符会交换两个对象的指针以及成员,在swap之后,p的指针会指向this,而this则会指向p的内存,当p离开其作用域之后,其的成员也会被销毁,自然两者就会同时实现喽~;
![](https://i.loli.net/2018/11/17/5beff4d639cbb.jpg)
关注公众号:数据结构与算法那些事儿,每天一篇数据结构与算法