通过定义的五种特殊的成员函数,控制类的对象在拷贝。赋值、移动或销毁时做什么。包括:拷贝构造函数、拷贝赋值运算符、拷贝赋值运算符、移动赋值运算符以及析构函数。

13.1 拷贝、赋值、销毁

13.6 对象移动

在旧标准的标准库中,容器中所保存的类必须是可以拷贝的,新标准中,可以在容器中保存不可拷贝的类型,只要他们能够被移动就可以。

13.6.1 右值引用

通过&&来获得右值引用,意为绑定到右值的引用。
重要性质:只能绑定到一个将要销毁的对象。
所引用的对象将要被销毁,该对象没有其他用户
– 左值引用:返回左值引用的函数,赋值、下标、解引用、前置递增/递减运算符。
– 右值引用:返回非引用的函数,算数、关系、位以及后置递增/递减运算符。
区分的方法:一个区分左值与右值的便捷方法是:看能不能对表达式取地址,如果能,则为左值,否则为右值。右值引用比较

move函数(utility头文件中)

1
2
3
int &&rr3=std::move(rr1);  //相当于告诉编译器,我们希望像处理右值一样处理左值rr1。
//调用move后,除了赋值和销毁rr1外不会再使用它
//使用move直接用std::move

移动构造函数和移动赋值运算符

目的:让类支持移动操作。
移动构造函数:第一个参数是一个该类类型的右值引用,任何其他额外的参数都要有默认实参。不需要分配新内存,直接接管原来的内存。不会跑异常,不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept。(原因是标准库很多对于存在异常的处理方式,例如vector)
移动赋值运算符:首先要处理自赋值的情况。(为什么检查自赋值?因为右值可能是move调用返回的结果,不能在使用右侧运算符对象之前释放左侧运算对象的资源。)
如果类有拷贝构造函数和拷贝赋值运算符,没有定义移动操作时,编译器不会为其分配默认的移动操作,而是会采用对应的拷贝进行代替。如果没有定义任何的拷贝操作,才会为其合成移动构造函数或移动赋值运算符。
移动操作不会被隐式的定义为删除的操作,定义为删除元素的原则:

  • 有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或有类成员未定义自己的拷贝构造函数且编译器不能为其合成默认移动构造函数
  • 有类成员的移动构造函数或移动赋值运算符被定义为删除的或不可访问的
  • 类的析构函数被定义为删除的
  • 有类成员是const的或是引用

三/五法则
通常,拥有一个资源的类,必须定义拷贝构造函数、拷贝赋值运算符、析构函数才能工作,而由于大量的拷贝会影响资源的额外开销,定义移动构造函数和移动赋值运算符可以避免此类问题。

13.6.1 右值引用和成员函数

区分移动和拷贝的重载函数通常有一个版本接受const T&,另一个版本接受一个T&&。很多时候它们看起来调用的是一个函数,通常它们内部的构造也很接近,有一点差别。我们可以根据实参的类型进行判断,我们用的是左值和右值区分调用的版本。
有的时候我们想要避免对右值进行赋值(在旧的标准中是有可能发生的,并且新的标准为了向后兼容没有禁止),可以在参数列表后放置一个 引用限定符。还可以通过使用&或者&&指出指向一个左值或者一个右值。eg:

1
2
3
4
class Foo{
public:
Foo &operator=(const Foo&) &; //只能向可修改的左值赋值。
}

引用限定符可以区分重载版本,如同const可以用来区分一个成员函数的重载版本。一般当我们定义两个或两个以上的具有相同名字和相同参数列表的成员函数,就必须对所有函数加上引用限定符,或者都不加。

1
2
3
4
5
6
7
8
class Foo{
public:
Foo sorted() &&;
Foo sorted() const; //错误,需要加上引用限定符
/////////////////
Foo sorted(Comp*);
Foo sorted(Comp*) const; //正确,两个版本都没有引用限定符
}