C 析构、拷贝、赋值、移动拷贝函数的几个知识点(不全)

时间:2022-11-04 18:50:55

众所周知,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 }

 

输出: 

C  析构、拷贝、赋值、移动拷贝函数的几个知识点(不全)

 

 

 

 

另外一个知识点,好像之前看剑指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也被自动析构释放。当然这个例子是建立在已经写好复制构造函数和析构函数的前提下,否则这个函数中 程序员应该自己写好对应的功能。