拷贝构造函数:cop constructor
拷贝赋值运算符:copy-assignment operator
移动构造函数:move constructor
移动赋值运算符:move-assignment operator
析构函数:destructor
13.1拷贝、赋值与销毁
13.1.1拷贝构造函数
拷贝构造函数:第一个参数是自身类型的引用,额外参数都有默认值
class Provider { public: Provider(const Provider&); public: string pwd; }; Provider::Provider(const Provider & provider): pwd(provider.pwd) { }
如果定义了自己的构造函数,便没有了合成的默认构造函数,如果想继续保持有默认的构造函数,需要定义为ClassName()=default;如果类成员中的类没有默认构造函数,则
如果定义了自己的拷贝构造函数,仍然有合成的默认构造函数,如果想删除默认的拷贝构造函数,需要定义为ClassName(const ClassName&)=delete;
拷贝构造函数使用的情况:
- 使用=定义变量时:string str=”string”;
- 将对象做为实参,传递到非应用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类的成员
总结起来就是:当对象需要复制到一个待初始化的内存空间中时,会使用相应的拷贝构造函数。
有时候还会先发生类型转换创建对象,这个转换的过程调用了[转换]构造函数,然后再使用拷贝构造函数,通常编译器会优化为直接使用[转换]构造函数构造对象,也就是说string str=”string”;会被优化为string str(“string”);。
13.1.2拷贝赋值运算符
运算操作本质上是函数操作,重载运算符就是对函数进行重载,函数的名称就是operator=、operator-、operator+、……,函数有一个返回类型和参数列表。
对于拷贝赋值运算符,必须定义为成员函数,左侧的运算对象绑定到函数的隐式this参数,右侧对象为显式参数。一般来讲,赋值运算符需要返回一个左侧对象的引用(应该是为了可以写成连等式)。
ClassName& operator=( const ClassName& object) { //这个顺序可以防止自赋值时,如果首先delete buff出错的问题 auto newBuff = new Buff(object.buff);//临时复制右侧内存 delete buff;//删除本内存 buff = newBuff;//使用右侧内存 return *this; }
如果未定义自己的拷贝赋值运算符,编译器会生成一个默认的合成拷贝赋值运算符
13.1.3析构函数
析构函数是类的一个成员函数,名字由波浪号连接类名构成,没有返回值,没有参数。
例如
~ClassName();
调用析构函数的情况:
- 变量离开作用域
- 对象被销毁时,作为对象的成员会调用自己的析构函数
- 容器被销毁时,其元素调用自己的析构函数
- delete作用于动态分配的对象时
- 临时对象在创建他的完整表达式结束时被销毁
注意:析构函数本身不销毁成员,成员是在析构函数体之后,调用自身析构函数完成销毁。
当一个类未定义自己的析构函数时,编译器会为他定义一个合成析构函数
13.1.5使用default、delete和private控制拷贝
- 使用=default可以显式的让编译器生成默认合成构造函数、拷贝构造函数、拷贝赋值运算符、析构函数。
- 使用=delete可以显式的让编译器删除默认合成拷贝构造函数、拷贝赋值运算符(以及所有的其他任何自定义函数)。删除的函数虽然声明了,但是不能以任何方式使用它们。
- 删除了析构函数的类型,只能通过new来初始化,但是无法销毁,无法使用delete。
删除的函数出现的情形:
- 自定的删除
- 有成员中的析构函数是删除的,或不可访问(private),则此类的合成析构函数也是删除的
- 有成员中的析构函数是删除的,或不可访问(private),则此类的合成拷贝构造函数也是删除的
- 有成员中的析构函数是删除的,或不可访问(private),则此类的合成构造函数也是删除的
- 有成员中的拷贝构造函数是删除的,或不可访问(private),则此类的合成拷贝构造函数也是删除的
- 有成员中拷贝赋值运算符是删除的,或不可访问(private),或者是const、引用成员,则此类的合成拷贝赋值运算符构造函数也是删除的
- 有成员为引用,没有类内初始化,或者是const,则默认构造函数为删除的
总结:类内成员不可以默认构造、赋值、复制、销毁,则此类对应的函数是删除的。
- 使用private可以达到删除拷贝控制成员函数的目的,但是友元和成员函数可以进行访问。为了控制,可以只声明不定义,这样友元和成员函数也无法访问。(旧标准的做法)
13.6对象移动
在很多情况下,对象需要被拷贝,比如创建临时对象、函数的返回等,在拷贝后,对象将会立刻销毁,对象移动是为了实现将这个对象直接使用,而不是先拷贝后销毁,提高程序效率。
在使用对象移动机制前,需要再次明确两个概念:
- 左值:当一个表达式被当做左值时,使用的是其内存,一般是会被写入。
- 右值:当一个表达式被当做右值时,只用的是其内存中存储的数据,一般是会被读取。
- 左值引用:须将一个左值赋予左值引用的表达式。
- 右值引用:须将一个右值赋予右值引用的表达式,这个表达式本来是应该立即销毁的临时对象。
- 变量是左值,因此不可以将一个变量赋值给右值引用,即使是右值引用的变量,也是左值,况且,变量也不是一个临时对象。
- 右值引用变量不能够被左值赋值,为了将左值的内存位置绑定到右值引用变量,需要使用一个在<utility>中的函数std::move(…)函数,使用时意味着承诺我们将不再使用这个左值的值,但是我们仍然可以对其进行赋值和销毁操作。
13.6.2移动构造函数与移动赋值运算符
移动构造函数类似于拷贝构造函数,但是其参数是自身的右值引用:
假设ClassName类中有一个动态内存string* name,拷贝构造函数和赋值运算符都需要重新分配这块内存,而对于临时对象,如果我们定义了移动构造函数和移动运算符,则可以避免重新分配内存。
ClassName(ClassName&& object) noexcept //不抛出异常,声明和定义中都得指定 :name(object.name) { object.name = nullptr; //临时对象会被释放,如果不置空,对应的内存被销毁,白用了 } ClassName& operator=(ClassName&& object) noexcept //不抛出异常,声明和定义中都得指定 { if (this != &object) //判断自赋值的情况 { free();//释放等号左侧对象内存 doSomething(); } return *this; }
为什么指明没有异常?
标准库可以对其操作中的异常提供保障,如vector的push_back,当使用拷贝构造函数时,如果出错只需要将vector分配的新内存收回,旧元素并没有改变。如果使用移动构造函数,如果出错,则新内存与旧内存就无法保证完整性了,所以非明确安全,标准库将不适用移动构造函数。
- 如果定义了自己的拷贝、赋值、析构函数,则编译器不会合成移动构造函数、移动赋值运算符了。
- 如果定义了自己的移动构造函数或者移动赋值运算符,则合成拷贝构造函数、拷贝赋值运算符定义为删除的。
- 类成员的移动操作定义为删除的不可访问的,则其移动操作也是删除的。
- 类成员是const或者引用,则移动操作也是删除的。
拷贝并交换技术
ClassName& operator=(ClassName object) noexcept { swap(*this, object); return *this; }
这里定义的赋值运算符,参数为非引用类型。因此,参数传递中需要进行拷贝,根据实参的类型,要么使用拷贝构造函数,要么使用移动构造函数。这一个赋值运算符就实现了拷贝赋值运算符和移动赋值运算符的功能。
并且,由于拷贝,自动处理了自赋值情况,函数内部天然就是异常安全的。
交换操作
如果一个对象定义了自己的swap操作,对于交换的操作(比如容器中的排序),算法将使用类自定义的版本。
class ClassName { public: friend void swap(ClassName&, ClassName&); private: std::string *name = nullptr; }; void swap(ClassName& obj1, ClassName& obj2) { std::string* temp = obj1.name; obj1.name = obj2.name; obj2.name = temp; std::cout << "ClassName" << std::endl; } class Foo { public: friend void swap(Foo&, Foo&); private: ClassName className; }; void swap(Foo& f1, Foo& f2) { //即使std::swap在此处是可用的,下边的代码也会使用ClassName自定义的swap //1. 模板函数与非模板函数提供同样好的匹配时,选择非模板函数 //2. 明明空间中名字隐藏规则有一个重要的例外,函数参数有类类型对象时,除了在常规作用于查找外,还会在类所属的命名空间 using std::swap; swap(f1.className, f2.className); }
通常情况下,拷贝并交换技术中使用swap将会十分方便
右值引用和成员函数
定义成员函数的时候,如果有能够接收左值引用和右值引用的两个版本的函数,将会提供更好的操作效率:
void push_back(const ClassName&); void push_back(ClassName&&);
比如对于vector<string>的push_back函数
string s = "str"; vec.push_back(s);//调用左值引用的版本 vec.push_back("str");//调用右值引用的版本
对于成员函数,还可以指定其this的左值/右值属性
这里的&& & const符号限定的是隐含的this参数
class Foo { public: //赋值运算符的左侧,必须是左值 Foo & operator=(const Foo&)& { return *this; } //排序的对象必须是右值 Foo sorted() && { } //排序的对象必须是左值 Foo sorted() const& { } };
13.1.4三五法则(The rule of three/five)
类的拷贝操作由三个函数构成:拷贝构造函数、拷贝赋值运算符和析构函数。新标准下还有移动构造函数和移动赋值运算符。
三法则:需要析构函数的类也需要拷贝和赋值操作
五法则:为了减少拷贝开销,定义移动操作也是必要的。
零法则:有拷贝、移动操作的类应该只处理自己类的资源,其他类(没有需要处理的资源)不必定义这些函数。
因为需要析构函数,所以类中存在动态内存分配的成员。假如只定义析构函数,不定义拷贝、赋值操作,当此对象销毁后被默认拷贝、赋值出来的其他对象就会出现严重错误。
13.2拷贝控制和资源管理
13.2.1类值行为
class HasPtr { public: HasPtr(const string &s = string()) :ps(new string(s)), i(0) { } HasPtr(const HasPtr& p) :ps(new string(*p.ps)), i(p.i) { } HasPtr& operator=(const HasPtr& p) { //拷贝底层string auto newP = new string(*p.ps); delete ps;//删除旧的string ps = newP; i = p.i; return *this; } ~HasPtr() { delete ps; } private: string *ps; int i; };
13.2.2类指针行为
class HasPtr { public: HasPtr(const string &s = string()) :ps(new string(s)), i(0),user(new int(1)) { } HasPtr(const HasPtr& p) :ps(p.ps), i(p.i) { ++*user; } HasPtr& operator=(const HasPtr& p) { //拷贝底层string ps = p.ps; i = p.i; ++*p.user;//递增被复制对象的引用计数 if (--*user == 0)//递减被覆盖对象的引用计数 { delete ps; delete user; } return *this; } ~HasPtr() { if (--user == 0) { delete ps; delete user; } } private: string *ps; int i; int* user; };