C++析構、拷貝、賦值、移動拷貝函數的幾個知識點(不全)

衆所周知,C++的類若是沒有默認構造函數,會自動生成一個。程序員

同理,若是沒有複製構造函數即A::A(const A&){}這個函數 ,則系統也會自動生成一個,但這個自動生成的複製構造函數不必定知足咱們的要求。析構函數也會自動生成(若是沒定義的話)。函數

好比下面的例子:this

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 };

 

其中咱們定義了默認構造函數、另外一個重載版本的構造函數。可是咱們沒有定義複製構造函數,因此係統自動幫咱們生成了一個,做用大體能夠理解爲下面的函數:spa

A(const A& another){
    a=another.a;
    b=another.b;
}

咱們也沒有定義析構函數,系統也自動生成了一個,相似下面:指針

~ A(){
  delete a;
  delete b;
}

而咱們注意到類A中有一個指針成員,那麼在默認的拷貝構造函數中就會簡單的複製指針到另外一個A變量。如A x1(x2); x1和code

x2的指針成員a是同樣的。這根本不是咱們的本意,咱們的本意是在上面代碼第5行:每一個A變量的成員a應該初始化爲一個新建int變量的地址,而不一樣A變量之間的成員a應該是不一樣的。blog

因此可能出現的問題就是:若是x2空間被釋放了,x1的成員a也就無效了,其指向的值是未定義的。。get

或者可能有另外一個函數這樣定義,更好理解:編譯器

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也被自動析構釋放。固然這個例子是創建在已經寫好複製構造函數和析構函數的前提下,不然這個函數中 程序員應該本身寫好對應的功能。

相關文章
相關標籤/搜索