拷贝控制1(拷贝赋值与销毁)
Posted ygeloutingyu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了拷贝控制1(拷贝赋值与销毁)相关的知识,希望对你有一定的参考价值。
拷贝控制操作即对象的拷贝,移动,赋值和销毁。一个类通过拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符和析构函数来完成这些工作。拷贝和移动构造函数定义了当用相同类型的另一个对象初始化本对象时做什么。拷贝和移动运算符定义了将一个对象赋予同类型的另一个对象时做什么。析构函数定义了当此类型对象销毁时做什么。
如果一个类没有定义这些拷贝控制成员,编译器会自动为它定义缺失的操作。不过,对于一些特殊的类来说,这会引起很大的麻烦
拷贝、赋值与销毁:
拷贝构造函数:
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函
拷贝构造函数的第一个参数几乎总是一个 const 的引用。因为拷贝构造函需要数被用来初始化非引用类类型参数。如果其参数不是引用类型,则永远不会成功——为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们又需要调用拷贝构造函数,如此无限循环。
拷贝构造函数在有些情况下会被隐式的使用,因此拷贝构造函数通常不应该是 explicit 的
与合成默认构造函数不同,即使我们定义了其它构造函数(没有定义拷贝构造函数),编译器也会为我们合成一个拷贝构造函数
对于某些类来说,合成拷贝构造函数用来阻止我们拷贝该类类型的对象。对于这些特殊的类(如IO类等),编译器会默认合成拷贝构造函数并将其定义为 delete 的成员
而一般情况,合成的拷贝构造函数会将其参数逐个拷贝到我们正在创建的对象中。编译器从给定的对象中将每个非 static 成员拷贝到正在创建的对象中。
每个成员的类型决定了它如何拷贝:对类类型的成员,会使用拷贝构造函数来拷贝;内置类型的成员则直接拷贝。虽然我们不能直接拷贝一个数组,但合成拷贝构造函数会逐元素地拷贝一个数组的成员。如果数组元素是类类型,则使用元素的拷贝构造函数来进行拷贝。
拷贝初始化:
1 #include <iostream> 2 using namespace std; 3 4 int main(void){ 5 string a(10, \'a\');//直接初始化 6 string b(a);//直接初始化 7 string c = a;//拷贝初始化 8 string d = "fk";//拷贝初始化 9 string e = string("fl");//拷贝初始化 10 11 return 0; 12 }
直接初始化和拷贝初始化的差异:当使用直接初始化时,我们实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数。当我们使用拷贝初始化时,我们要求编译器将运算符右侧的对象拷贝到正在创建的对象中,如果需要的话还需要进行类型转换。即,通常使用了 = 运算符初始化的就是拷贝初始化,反之则是直接初始化
注意:只有用 = 运算符定义变量时才是拷贝初始化,而对于已经存在的变量使用 = 则是拷贝赋值运算。
拷贝构造初始化发生的情况:
- 使用赋值运算符(=)定义变量
- 将对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
- 某些类类型还会对它们所分配的对象使用拷贝初始化。如:我们初始化标准库容器或是调用其 insert 或 push 成员时,容器会对其元素进行拷贝初始化。与之相对的,用 emlace 成员创建的元素都进行直接初始化
拷贝初始化通常使用拷贝构造函数来完成,但当你定义了移动构造函数,下列情况将调用移动构造函数:
- 从一个返回类型为非引用类型的函数返回一个对象
拷贝初始化的限制:
如果我们使用的初始化值要求通过一个 explicit 的构造函数来进行类型转换,那么使用拷贝初始化还是直接初始化就不是无关紧要的了:
1 #include <iostream> 2 #include <vector> 3 using namespace std; 4 5 int main(void){ 6 vector<int> v1(10);//正确,直接初始化 7 // vector<int> v2 = 10;//错误,接受大小参数的构造函数是 explicit 8 9 void f(vector<int>);//f的形参进行拷贝初始化 10 // f(10);//错误,不能用一个explicit的构造函数拷贝一个实参 11 f(vector<int>(10));//正确,从一个int直接构造一个临时vector 12 13 return 0; 14 }
注意:当传递一个实参或从函数返回一个值时,如果发生类型转换,我们就不能隐式使用一个 explicit 构造函数。如果我们希望使用 explicit 构造函数,就必须显示的使用
当我们的类中存在指针的时候,对于拷贝构造函数应该要注意深拷贝和浅拷贝的问题
编译器可以绕过拷贝构造函数:
编译器的思想是能不用临时对象就不用临时对象。在拷贝初始化过程中,编译器可以 (但不是必须) 跳过拷贝 / 移动构造函数,直接创建对象。因此对于下面这些拷贝初始化,都不会生成临时对象再进行拷贝或移动到目标对象,而是直接通过函数匹配调用相应的构造函数:
1 #include <iostream> 2 using namespace std; 3 4 class gel{ 5 private: 6 int x, y, z; 7 8 public: 9 gel(int a, int b, int c) : x(a), y(b), z(c) {cout << "111" << endl;} 10 gel(int a) : x(a), y(0), z(0) {cout << "222" << endl;} 11 gel(const gel& it) : x(it.x), y(it.y), z(it.z){cout << "333" << endl;} 12 ~gel(){cout << "444" << endl;} 13 14 }; 15 16 int main(void){ 17 // 编译器的思想是能不用临时对象就不用临时对象。因此对于下面这些拷贝初始化, 18 // 都不会生成临时对象再进行拷贝或移动到目标对象,而是直接通过函数匹配调用相应的构造函数。 19 gel a = 1;//拷贝初始化 20 //略过了拷贝构造函数,直接调用的gel(int a)构造函数,相当于gel a(1); 21 22 cout << "====" << endl; 23 gel b = gel(1, 2, 3);//相当于gel b(a, 2, 3) 24 cout << "----" << endl; 25 26 // 输出: 27 // 222 28 // ==== 29 // 111 30 // ---- 31 // 444 32 // 444 33 34 return 0; 35 }
注意:即便编译器略过了拷贝 / 移动构造函数,但在这个程序点上,拷贝 / 移动构造函数必须是存在且可访问的(如,不能是 private 的)。
拷贝赋值运算符:
重载赋值运算符:
如果我们未定义自己的拷贝赋值运算符,编译器会自动生成一个合成拷贝赋值运算符。
赋值运算符重载时必须被定义为成员函数
为了与内置类型保持一致,赋值运算符通常应该返回一个指向其左侧对象的引用(不然将不能使用连等运算)。而且,标准库通常要求保持在容器中的类型要具有赋值运算符,且其返回值是左侧运算对象的引用。
我们应该要特别注意深拷贝和浅拷贝问题
关于重载 = :http://www.cnblogs.com/geloutingyu/p/8283810.html
合成拷贝赋值运算符:
类似于拷贝构造函数,对于某些类,合成拷贝赋值运算符用来禁止该类型对象的赋值:如果类的某个成员的拷贝赋值运算符是删除的或不可访问的,或是类有一个 const 的或引用的成员,则类的合成拷贝赋值运算符被定义为删除的
如果并非出于此目的,它会将右侧运算对象的每个非 static 成员赋予左侧运算对象的对应成员。对于数组类型的成员,逐个赋值数组元素。合成拷贝赋值运算符返回一个指向其左侧运算对象的引用
析构函数:
析构函数执行与构造函数相反的操作:释放对象使用的资源,并销毁对象的非 static 数据成员
析构函数是类的一个成员函数,名字由波浪号接类名构成。它没有返回值,也不接受参数——因此析构函数不能被重载,对于一个给定的类只会有唯一一个析构函数
析构函数完成的工作:
在一个析构函数中,首先执行函数体,然后按初始化顺序的逆序销毁成员。
在对象最后一次使用之后,析构函数的函数体可执行类设计者希望执行的任何收尾工作。通常,析构函数释放对象在生存期分配的所有资源。
在一个构造函数中,不存在类似构造函数中初始化列表的东西来控制成员如何销毁,析构部分是隐式的。成员销毁时发生什么完全依赖于成员的类型。销毁类类型的成员需要执行成员自己的析构函数。内置类型没有析构函数,因此销毁内置类型成员什么也不需要做
注意:隐式销毁一个内置指针类型的成员不会 delete 它所指向的对象,因此我们需要在析构函数中显式地 delete 内置指针成员
与普通指针不同,智能指针是类类型,具有析构函数,因此智能指针成员在析构阶段会自动销毁
1 #include <iostream> 2 #include <memory> 3 using namespace std; 4 5 class gel{ 6 public: 7 gel(int a = 0, int *b = new int, shared_ptr<int> c = make_shared<int>()) : 8 x(a), y(b), z(c) {} 9 ~gel(){ 10 //析构函数首先执行函数体,然后隐式地销毁成员 11 delete y; 12 //隐式销毁一个内置指针类型的成员不会delete其所指向的对象 13 //因此我们需要显式的用delete释放其所指向的内存,隐式销毁阶段时再自动销毁指针变量y本身 14 //x只是一个内置类型成员,会被自动隐式销毁 15 //z是一个智能指针成员,在销毁时会调用自己的析构函数,其所指对象在引用计数为0时自动释放 16 } 17 18 private: 19 int x; 20 int *y; 21 shared_ptr<int> z; 22 23 }; 24 25 int main(void){ 26 gel *a(new gel); 27 delete a; 28 29 return 0; 30 }
什么时候会调用析构函数:
无论何时一个对象被销毁,就会自动调用其析构函数:
变量离开其作用域时被销毁
当一个对象被销毁时,其成员被销毁
容器被销毁时其成员被销毁
对于动态分配的对象,当指向它的指针应用 delete 运算符时被销毁
对于临时对象,当创建它的完整表达式结束时被销毁
注意:当指向一个对象的引用或指针离开作用域时,析构函数不会执行:
1 #include <iostream> 2 #include <memory> 3 using namespace std; 4 5 class gel{ 6 friend ostream& operator<<(ostream&, const gel&); 7 8 private: 9 int x, y; 10 11 public: 12 gel(int a, int b) : x(a), y(a) {} 13 gel(int a) : gel(a, 0) {} 14 gel() : gel(0, 0) {} 15 ~gel() { 16 cout << "~gel" << endl; 17 } 18 19 }; 20 21 ostream& operator<<(ostream &os, const gel &it){ 22 cout << it.x << " " << it.y; 23 return os; 24 } 25 26 void gg(gel *it){//it是指针变量,离开函数gg的作用域时不会调用析构函数 27 28 } 29 30 void yy(gel &it){//it是一个引用,离开函数yy的作用域时不会调用析构函数 31 32 } 33 34 int main(void){ 35 gel a(1); 36 gg(&a); 37 cout << "===" << endl; 38 39 gel *b = new gel(2);//b是指针变量,在离开main作用域时不会调用析构函数 40 gg(b); 41 cout << "---" << endl; 42 43 yy(a); 44 cout << "///" << endl; 45 46 47 // 输出: 48 // === 49 // --- 50 // /// 51 // ~gel 52 53 //很显然只有变量a在离开main函数的作用域时调用了析构函数 54 55 return 0; 56 }
合成析构函数:
当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数。
对于某些类,合成析构函数被用来阻止该类型的对象被销毁。如果类的某个成员的析构函数是删除的或不可访问的 (如,是 private 的),则该类的合成析构函数被定义为删除的。
如果不是这种情况,合成析构函数的函数体就为空
注意:析构函数体本身不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的。整个对象的销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。
三/五法则:
需要自定义析构函数的类也需要自定义拷贝和赋值操作:
很显然,需要自定义析构函数意味着我们的类中有用 new 创建的内置指针成员,那么如果我们使用合成拷贝构造函数以及合成拷贝赋值运算符而不自定义拷贝和赋值操作的话,进行拷贝和赋值操作都将是浅拷贝,这可能导致多个对象中的指针成员都指向相同的内存地址。
需要自定义拷贝操作的类也需要自定义赋值操作,反之亦然:
如:考虑一个类为每个对象分配一个独有的、唯一的序号。这个类需要一个拷贝构造函数为每个新创建的对象生成一个新的、独一无二的序号。除此之外,这个拷贝构造函数从给定对象拷贝所有其它数据成员。为了达到我们刚才所说的目标,我们还需要将拷贝赋值运算符定义为 delete 的,用来避免将序号赋予目的对象。显然,这个类不需要自定义析构函数
使用 = default:
我们可以将拷贝控制成员定义为 = default 来显示的要求编译器生成合成版本。当我们在类内使用 = default 修饰成员的声明时,合成的函数将隐式地声明为内联的。如果我们不希望如此,应该对成员的类外定义使用 = default
阻止拷贝:
定义删除的函数:
在函数的参数列表后面加上 = delete 来指出我们希望将它定义为 删除的:
1 #include <iostream> 2 using namespace std; 3 4 struct NoCopy{ 5 NoCopy() = default;//使用合成默认构造函数 6 NoCopy(const NoCopy&) = delete;//阻止拷贝 7 NoCopy& operator=(const NoCopy&) = delete;//阻止赋值 8 ~NoCopy() = default;//使用合成析构函数 9 10 //其它成员 11 }; 12 13 void f(int) = delete;//非成员函数也可以被定义成delete的 14 15 int main(void){ 16 17 }
注意·:删除函数不能以任何方式被使用
与 = default 不同,= delete 必须出现在函数第一次声明的时候。我们可以这样来理解,一个默认的成员只影响这个成员而生成的代码,因此 = default 直到编译器生成代码的时候才需要。而另一方面,编译器一开始就需要知道一个函数是删除的,以便禁止试图使用它的操作
与 = default 的另一个不同之处是,我们可以对任何函数 (包括非成员函数) 指定 = delete。我们只能对编译器可以合成的默认构造函数或拷贝控制成员使用 = default
删除函数的主要用途是禁止拷贝控制成员,也可以用来引导函数匹配过程
析构函数通常不会被定义成 delete 的:
如果析构函数被删除,就无法销毁此类型的对象。对于一个删除了析构函数的类型,编译器将不允许定义该类型的变量或创建给类型的临时对象。而且,如果一个类有某个成员的类型删除了析构函数,我们也不能定义该类的变量或临时对象。因为如果一个成员的析构函数是删除的,则该成员无法被销毁。而如果一个成员无法被销毁,则对象整体也就无法被销毁了
对于删除了析构函数的类型,虽然我们不能定义这种类型的变量或成员,但可以动态分配这种类型的对象。但是,不能释放这样的对象:
1 #include <iostream> 2 using namespace std; 3 4 class gel{ 5 private: 6 int x; 7 8 public: 9 gel(int a) : x(a) {} 10 gel() = default; 11 ~gel() = delete; 12 }; 13 14 int main(void){ 15 // gel a;//错误,不能创建该类变量 16 // gel(1);//错误,不能创建该类临时对象 17 18 gel *b = new gel();//正确,可以分配该类型的对象 19 delete b;//错误,gel的析构函数是删除的 20 21 return 0; 22 }
合成的拷贝控制成员可以是删除的:
如果类的某个成员的析构函数是删除的或不可访问的 (如,private 的),则类的合成析构函数被定义为删除的
如果类的某个成员的拷贝构造函数是删除的或不可访问的,或是某个成员的析构函数是删除的或不可访问的,则类的合成拷贝构造函数被定义为删除的
如果类的某个成员的拷贝赋值运算符是删除的或不可访问的,或是类有一个 const 的或引用的成员,则类的合成拷贝赋值运算符被定义为删除的
如果类的某个成员的析构函数是删除的或不可访问的,或是类有一个引用的成员,它没有类内初始化器,或是类有一个 const 成员,它没有类内初始化器且未显示定义默认构造函数,则该类的默认构造函数被定义为删除的。
本质上,这些规则的含义是:如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的
private 拷贝控制:
在 c++11 之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为 private 来阻止拷贝。同时为了避免友元和成员函数中的拷贝操作我们应该只声明而不定义。试图访问一个未定义的成员将导致一个链接时错误。试图拷贝对象的用户代码将在编译阶段被标记为错误,成员函数或友元函数中的拷贝操作将导致链接时错误。
注意:用 private 拷贝控制通常报错会非常难读,因此,希望阻止拷贝的类应该使用 = delete 来声明对应的拷贝控制函数而不是将它们声明成 private 的
以上是关于拷贝控制1(拷贝赋值与销毁)的主要内容,如果未能解决你的问题,请参考以下文章