1、拷贝控制操作(copy control)
- 一个类通过定义五种特殊的成员函数来控制对象拷贝、移动、赋值和销毁:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数。
- 拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。
- 拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。
- 析构函数定义了当此类型对象销毁时做什么。
2、拷贝、赋值和销毁
拷贝构造函数
- 如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
class Foo {
public:
Foo(); //默认构造函数
Foo(const Foo&); //拷贝构造函数
//...
};
- 如果我们没有为一个类定义拷贝构造函数,编译器会为我们定义一个。编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中。
Sales_data::Sales_data(const Sales_data &orig) :
bookNo(orig.bookNo),
units_sold(orig.units_sold),
revenue(orig.revenue)
{}
- 直接初始化与拷贝初始化之间的差异:调用不同的构造函数。
拷贝赋值运算符
- 拷贝赋值运算符接受一个与其所在类相同类型的参数,返回一个指向其左侧运算对象的引用。
- 如果我们没有为一个类定义拷贝赋值运算符,编译器会为我们定义一个。
Foo& operator=(const Foo&); //赋值运算符
Sales_data& Sales_data::operator=(const Sales_data &rhs) {
bookNo = rhs.bookNo;
units_sold = rhs.units_sold;
revenue = rhs.revenue;
return *this; //返回一个此对象的引用
}
- 如果将一个对象赋予它自生,赋值运算符必须能正常工作。
- 大多数赋值运算符组合了析构函数和拷贝构造函数的工作。
析构函数
- 析构函数释放对象使用的资源,并销毁对象的非static数据成员。
class Foo {
public:
~Foo(); //析构函数
};
- 在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。
三/五法则
- 如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数。
- 如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符。反之亦然。然而,无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要 析构函数。
使用=default
- 我们可以通过将拷贝控制成员定义为=default来显式地要求编译器生成合成的版本。
阻止拷贝
- =delete(delete function):虽然声明了它们,但不能以任何方式使用它们。
- 当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。
3、对象移动
- 右值引用就是必须绑定到右值的引用。我们通过&&来获得右值引用。
- 右值引用有一个重要的性质–只能绑定到一个将要销毁的对象。因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。
- 一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。
int i = 42;
int &&r = i * 42; //将r绑定到乘法结果上
- 左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
- 由于右值引用只能绑定到临时对象,我们得知:
1)所引用的对象将要被销毁
2)该对象没有其它用户
- 这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。
变量是左值
- 变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。
int &&rr1 = 42; //正确:字面常量是右值
int &&rr2 = rr1; //错误:表达式rr1是左值
标准库move函数
- 虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显式地将一个左值转换为对应的右值引用类型。
int &&rr3 = std::move(rr1); //ok
- move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。我们必须意识到,调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。
移动构造函数与移动赋值运算符
StrVec::StrVec(StrVec &&s) noexcept //移动构造函数不应抛出任何异常
: elements(s.element), first_free(s.first_free), cap(s.cap) {
s.element = s.first_free = s.cap = nullptr; //令s进入这样的状态,对其进行析构函数是安全的
}
StrVec &StrVec::operator=(StrVec &&rhs) noexcept {
if (this != &rhs) {
free();
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
- 在移动操作后,以后源对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设。
- 只有当一个类没有定义任何版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。
- 定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地被定义为删除的。
- 移动右值,拷贝左值,但如果没有移动构造函数,右值也被拷贝。