众所周知,C 的类如果没有默认构造函数,会自动生成一个。
同理,如果没有复制构造函数即A::A(const A&){}这个函数 ,则系统也会自动生成一个,但这个自动生成的复制构造函数不一定满足我们的要求。析构函数也会自动生成(如果没定义的话)。
比如下面的例子:
1 class A{ 2 public: 3 int* a; 4 int b; 5 A()=default; 6 A(int x):a(new int(10)){b=x;} 7 ~A(){delete a;cout<<"我删除a了!"<<endl;} 8 };
其中我们定义了默认构造函数、另一个重载版本的构造函数。但是我们没有定义复制构造函数,所以系统自动帮我们生成了一个,作用大致可以理解为下面的函数:
A(const A& another){ a=another.a; b=another.b; }
我们也没有定义析构函数,系统也自动生成了一个,类似下面:
~ A(){ delete a; delete b; }
而我们注意到类A中有一个指针成员,那么在默认的拷贝构造函数中就会简单的复制指针到另一个A变量。如A x1(x2); x1和
x2的指针成员a是一样的。这根本不是我们的本意,我们的本意是在上面代码第5行:每个A变量的成员a应该初始化为一个新建int变量的地址,而不同A变量之间的成员a应该是不同的。
所以可能出现的问题就是:如果x2空间被释放了,x1的成员a也就无效了,其指向的值是未定义的。。
或者可能有另一个函数这样定义,更好理解:
void f(A temp) { //....... }
那么调用f(x1)的时候,会先调用拷贝构造函数,复制一个x1的副本作为形参。然后这个副本temp就拥有了和x1一样的成员a,当退出函数f的时候,temp的成员a被析构释放,这导致x1的成员a也变成了野指针。
我们自己定义好正确的拷贝构造函数即可解决上面的问题。
所以,遇到类的成员有非普通类型的时候(如指针),就一定要自己写拷贝构造函数、重载赋值符、移动构造函数、重载移动赋值符、析构函数。
注意:如果只定义了移动构造函数 or 重载移动赋值符,那么编译器是不会自动帮你生成拷贝构造函数和重载赋值符的,而是会默认定义为删除的(=delete;)。
下面看下各种构造函数和拷贝函数,加深下印象。
要注意的是如果我们有A x1;
A x2=x1;和 A x3;x3=x1;是不一样的阿!
前者是声明时就初始化,属于拷贝初始化。调用的是拷贝构造函数( A& (const A& another){ } )
后者是先声明,默认初始化。然后赋值。先调用默认构造函数(A( ) { }),再调用重载赋值符,即( A& operator=(const A& a) )
1 class A 2 { 3 private: 4 int x; 5 public: 6 A(){cout<<"A()"<<endl;} //默认构造函数 7 A(int&& a){x=a;cout<<"A(int&& a)"<<endl;} //重载的构造函数 8 A(A&& a){cout<<"A(A&& a)"<<endl;x=a.x;} //移动拷贝函数 9 A(A& a){cout<<"A(A& a)"<<endl;x=a.x;} //拷贝构造函数 10 A& operator=(const A&& a){if(this!=&a){x=a.x;}cout<<"A& operator=(A&&)"<<endl;} //移动赋值符 11 A& operator=(const A& a){if(this!=&a){x=a.x;}cout<<"A& operator=(A&)"<<endl;} //拷贝赋值符 12 ~A(){cout<<"删除了!"<<endl;} //析构函数 13 }; 14 int main() 15 { 16 17 int a=1; 18 A x1; 19 A x2(4); 20 A x3=move(x2); 21 A x4=x3; 22 x1=move(x2); 23 x2=x3; 24 getchar(); 25 return 0; 26 }
输出:
另外一个知识点,好像之前看剑指offer也看过来着,当时印象不深:
编写类的赋值运算符重载时,几个要求:
1.自赋值能正常运行不报错。
2.赋值运算符一般都集合了复制构造函数和析构函数二者的功能。
3.不要先删数据,再拷贝新数据到this的空间!这样容易删完了自己的,但拷贝又异常失败了,那该实例原来的数据就没得了
例子:
1 A& operator=(const A& x){ 2 if(this!=&x){ 3 A temp(x); 4 a=temp.a; 5 b=temp.b; 6 //......// 7 } 8 return *this; 9 }
这样的目的是避免在函数中new空间时抛异常,会导致之前实例的数据变化。上面代码中的临时变量A如果申请失败,函数直接退出,不会影响原先该实例的数据。
先建立一个临时变量,然后依次赋值成员变量的值到this的成员,最后返回当前实例的引用,这样函数退出时temp也被自动析构释放。当然这个例子是建立在已经写好复制构造函数和析构函数的前提下,否则这个函数中 程序员应该自己写好对应的功能。