13-拷贝控制
13.1 拷贝,赋值与销毁
以上这些操作,必须明白定义与不定义会对类的操作产生何种影响,变编译器定义的合成版本未必符合类设计的初衷。
13.1.1 拷贝构造函数
如果一个构造函数的第一个参数是同类型的引用,且任何其他参数都有默认值,则此构造函数为拷贝构造函数。
必须是引用类型参数,因为在调用非引用参数的函数时,会拷贝实参,而拷贝实参又需要调用拷贝构造函数,那么会无休止的调用下去。
Foo(const Foo&);
合成拷贝构造函数会将其参数的成员(非static)逐个拷贝到正在创建的对象中。
直接初始化:使用普通的函数匹配,选择参数最匹配的构造函数。
拷贝初始化:将对象或者可以转换为相同类型的对象拷贝到正在创建的对象中。
1,使用=运算符定义变量
2,将对象作为实参传递给一个非引用类型
3,从非引用类型返回类型的函数里返回一个对象
4,使用初始值列表初始化一个数组中的元素或一个聚类中的成员
13.1.2 拷贝赋值运算符
Foo& operator=(const Foo&);
如果运算符是一个成员函数,其左侧对象就绑定到隐式的this参数。
合成拷贝赋值运算符:将右侧运算对象的每个成员(非static)赋予左侧运算对象的对应成员
13.1.3 析构函数
~Foo();
在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照在类中出现的顺序进行的;而在析构函数中,首先执行函数体,然后销毁成员,成员按初始化顺序逆序销毁。
当一个对象被销毁时会自动调用其析构函数。当指向一个对象的引用会指针离开作用域是,析构函数不会执行。
合成析构函数:空的函数体
13.1.4 三/五法则
如果一个类需要自定义析构函数(一般是销毁动态分配的内存),则可能也需要拷贝构造函数和拷贝赋值运算符。
需要拷贝操作的类也需要赋值操作,反之亦然,但未必需要析构函数。
13.1.5 使用=default
使用=default修饰拷贝控制成员,编译器将生成相应成员的合成版本。=default在类内则隐式的声明为内联。
Foo& operator=(const Foo&) = default;
Foo(const Foo&) =default;
只能用来修饰具有合成版本的成员函数。
13.1.6 阻止拷贝
将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝,即,虽然声明了,但是不可以使用它们。
Foo(const Foo&) = delete;//阻止拷贝
Foo& operator=(const Foo&) = delete;//阻止赋值
=delete必须出现在第一次声明的时候;可以对任何函数指定=delete
对于删除了析构函数的类型,不可以定义此类型的变量或成员,但可以动态分配这种类型,但是无法释放它们。
合成的拷贝控制成员可能是删除的
如果类有数据成员不能被默认构造,拷贝,赋值,复制或销毁(delete或者private修饰),则此类对应的合成成员函数被定义为删除的。(引用成员或无法默认构造的const成员)
通过声明但不定义private的拷贝构造函数或拷贝赋值运算符,试图拷贝或赋值的操作在编译阶段被标记为错误;成员函数或友元函数中的拷贝或赋值会导致链接错误。(旧的方法)
13.2 拷贝控制和资源管理
管理类外的类必须定义拷贝控制成员。
13.2.1 行为像值的类
对于类管理的资源,每个对象都有一份拷贝。
赋值操作会销毁左侧运算对象的资源,之后从右侧运算对象拷贝数据,必须确保自赋值是异常安全的(可先将右侧运算对象的数据拷贝到零时对象中)。
13.2.2 行为像指针的类
多个此类的对象共享同一份数据(使用智能指针或引用计数管理)
13.3 交换操作
void swap(Foo &lhs, Foo &rhs){
using std::swap;//若没有类型自定义的swap版本,则调用std的版本
swap(lhs.h, rhs.h);//类型自定义的版本
}
拷贝并交换技术
Foo& Foo::operator=(Foo rhs){
swap(*this, rhs);
return *this;
}
参数并不是引用,在函数执行完后会释放。可自动处理自赋值的情况,且是异常安全的。
13.6 对象移动
当一个对象拷贝后就立即销毁,此时使用移动操作可以提高性能。
标准库容器,string,shared_ptr类支持移动和拷贝,IO类和unique_ptr类只支持移动。
13.6.1 右值引用
即,必须绑定到右值的引用。只能绑定到将要销毁的对象,故可以将右值引用的资源移动到另一个对象中。
一个左值表达式表示一个对象的身份,而右值表达式表示的是对象的值。
int &&r = i*42;
返回左值引用的函数,连同赋值,下标,解引用和前置递增递减运算符,都返回左值;
返回非引用类型的函数,连同算数,关系,位以及后置递增递减运算符,都生成右值,可以用const的左值引用和右值引用绑定。
左值有持久的状态,右值要么是字面值常量,要么是表达式求值过程中创建的临时变量。
右值意味着:该对象将被销毁;该对象没有其他用户。故使用右值引用的代码可以自由的接管引用的对象的资源。
int &&rr1 = 42;
int &&rr2 = rr1;//错误:rr1是右值引用类型的变量,是左值
#include <utility>
int &&rr3 =std::move(rr1);//move函数获得绑定到左值上的右值引用。
move意味着希望像处理右值一样处理一个左值,即,除了对rr1赋值和销毁外,代码不会再使用它。
13.6.2 移动构造函数和移动赋值运算符
从给定对象“窃取”而不是拷贝资源。
移动拷贝构造函数第一个参数是该类类型的右值引用,其他的参数必须都具有默认实参。
StrVec::StrVec(StrVec &&s) noexcept:e(s.e), f(s.f){s.e = s.f = nullptr;}
1,移动操作不应该抛出异常,noexcept通知标准库移动操作是安全的的,无需标准库做额外的操作
2,成员初始化器中接管s中的资源
3,函数体中使对s进行析构是安全的
noexcept在声明和定义中都需要指定。
StrVec &StrVec::operator=(StrVec &&rhs) noexcept{}
在移动操作之后,移后源对象必须保持有效的,可析构的状态,但用户不可对其值进行任何假设。
当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会合成移动构造函数或移动赋值运算符。
可以移动:内置类型,类类型定义了相应的移动操作。
移动操作不会隐式的定义为删除的函数,但显式定义=default的移动操作,且编译器不能移动所有成员,则编译器将移动操作定义为删除的函数。
如果类定义了一个移动构造函数或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的。
如果一个类既有移动构造函数,又有拷贝构造函数,则使用普通的函数匹配规则来选择调用。如果没有移动构造函数或移动赋值运算符,右值会被拷贝。
移动迭代器
移动迭代器的解引用运算符生成一个右值引用,make_move_iterator(origin_iterator);函数将普通的迭代器转换为一个移动迭代器,会调用相应的西东构造函数或移动赋值运算符操作。
13.6.3 右值引用和成员函数
成员函数提供移动版本:
void push_back(const X&);//拷贝
void push_back(X&&);//移动
引用限定符
Foo &operator=(const Foo&) &;//只能向可修改的左值赋值
引用限定符可以是&和&&,分别指出this可以指向一个左值或右值,只能用于非static的成员函数,且必须同时出现在声明和定义中。
一个函数可以同时用const和引用限定,但引用限定符必须跟随在const之后。
引用限定符可以区分重载版本,并且可以和const综合起来区分。
当定义多个具有相同名字和相同参数列表的成员函数时,必须所有函数都加上引用限定符或都不加。