C++在單繼承、多繼承、虛繼承時,構造函數、複製構造函數、賦值操作符、析構函數的執行順序和執行內容

一、本文目的與說明

    1. 本文目的:理清在各種繼承時,構造函數、複製構造函數、賦值操作符、析構函數的執行順序執行內容

    2. 說明:雖然複製構造函數屬於構造函數的一種,有共同的地方,但是也具有一定的特殊性,所以在總結它的性質時將它單獨列出來了。

    3. 單繼承、多繼承、虛繼承,既然都屬於繼承,那麼雖然有一定的區別,但還是相同點比較多。如果放在一塊講,但爲了將內容製作成遞進的,就分開了,對相同點進行重複,(大量的複製粘貼哈),但在不同點進行了標註。
        注意:三塊內容是逐步遞進的 
                     如果你懂虛函數,那麼單繼承和多繼承那塊你就可以不看; 
                     如果你懂多繼承,那單繼承你就不要看了,至於虛繼承就等你懂虛繼承再回來看吧; 
                     如果你只懂單繼承,那你就只看單繼承就好。

二、基本知識

    1. 對於一個空類,例如:

  1. class EmptyClass{};  

  雖然你沒有聲明任何函數,但是編譯器會自動爲你提供上面這四個方法。

  1. class EmptyClass {  
  2. public:  
  3.     EmptyClass();                        //  默認構造函數  
  4.     EmptyClass(const EmptyClass &rhs);    //  複製構造函數  
  5.     ~EmptyClass();                       // 析構函數  
  6.     EmptyClass& operator=(const EmptyClass &rhs);    //  賦值運算符  
  7. }  

對於這四個方法的任何一個,你的類如果沒有聲明,那麼編譯器就會自動爲你對應的提供一個默認的。(在《C++ primer》中,這個編譯器自動提供的版本叫做“合成的***”,例如合成的複製構造函數)當然如果你顯式聲明瞭,編譯器就不會再提供相應的方法。

    2. 合成的默認構造函數執行內容:如果有父類,就先調用父類的默認構造函數。

    2. 合成的複製構造函數執行內容:使用參數中的對象,構造出一個新的對象。

    3. 合成的賦值操作符執行內容:使用參數中的對象,使用參數對象的非static成員 依次對 目標對象的成員賦值。注意:在賦值操作符執行之前,目標對象已經存在。

    4. 在繼承體系中,要將基類(或稱爲父類)的析構函數,聲明爲virtual方法(即虛函數)。

    5. 子類中包含父類的成員。即子類有兩個部分組成,父類部分和子類自己定義的部分。

    6. 如果在子類中顯式調用父類的構造函數,只能在構造函數的初始化列表中調用,並且只能調用其直接父類的。

    7. 在多重繼承時,按照基類繼承列表中聲明的順序初始化父類。

    8. 在虛繼承中,虛基類的初始化 早於 非虛基類,並且子類來初始化虛基類(注意:虛基類不一定是子類的直接父類)。

 

三、單繼承

核心:在構造子類之前一定要執行父類的一個構造函數。

1.構造函數(不包括複製構造函數)。

        順序:①直接父類;②自己  
            注意:若直接父類還有父類,那麼“直接父類的父類”會在“直接父類” 之前 構造。 可以理解爲這是一個遞歸的過程,知道出現一個沒有父類的類才停止。

    2.1 如果沒有顯式定義構造函數,則“合成的默認構造函數”會自動調用直接父類的“默認構造函數”,然後調用編譯器爲自己自動生成的“合成的默認構造函數”。
    2.2 如果顯式定義了自己的構造函數 
        2.2.1 如果沒有顯式調用直接父類的任意一個構造函數,那麼和“合成的默認構造函數”一樣,會先自動調用直接父類的 默認構造函數,然後調用自己的構造函數。 
        2.2.2 如果顯式調用直接父類的任意一個構造函數,那麼會先調用直接父類相應的構造函數,然後調用自己的構造函數。

2. 複製構造函數

           順序:①直接父類;②自己 
               注意:和構造函數一樣,若直接父類還有父類,那麼“直接父類的父類”會在“直接父類” 之前 構造。 可以理解爲這是一個遞歸的過程,知道出現一個沒有父類的類才停止。

    2.1 如果 沒有顯式定義複製構造函數,則“合成的複製構造函數”會自動調用直接父類的“複製構造函數”,然後調用編譯器爲自己自動生成的“合成的複製構造函數”(注意:不是默認構造函數) 
    2.2 如果顯式定義了自己的複製構造函數 (和構造函數類似) 
        2.2.1 如果沒有顯式調用父類的任意一個構造函數,那麼會先調用直接父類的 默認構造函數(注意:不是 複製構造函數)。
        2.2.2 如果顯式調用直接父類的任意一個構造函數,那麼會先調用直接父類相應的構造函數。

3.賦值操作符重載

    3.1 如果沒有顯式定義,會自動調用直接父類賦值操作符。(注意:不是 默認構造函數)  
    3.2 如果顯式定義了,就只執行自己定義的版本,不再自動調用直接父類的賦值操作符,只執行自己的賦值操作符。
            注意:如有需要對父類子部分進行賦值,應該在自己編寫的代碼中,顯式調用父類的賦值操作符。 
4. 析構函數  
    與構造函數 順序相反。

四、多繼承

繼承的差別就是:需要考慮到多個直接父類。其它的都相同

1.構造函數(不包括複製構造函數)。

               順序:①所有直接父類;(按照基類繼承列表中聲明的順序)②自己  
            注意:若直接父類還有父類,那麼“直接父類的父類”會在“直接父類” 之前 構造。 可以理解爲這是一個遞歸的過程,知道出現一個沒有父類的類才停止。

    2.1 如果 沒有 顯式定義構造函數,則“合成的默認構造函數”會自動依次調用所有直接父類的“默認構造函數”,然後調用編譯器爲自己自動生成的“合成的默認構造函數”。
    2.2 如果顯式定義了自己的構造函數 
        2.2.1 如果沒有顯式調用父類的任意一個構造函數,那麼和“合成的默認構造函數”一樣,會自動依次調用所有直接父類的 默認構造函數,然後調用自己的構造函數。
        2.2.2 如果顯式調用了父類的任意一個構造函數,那麼按照基類列表的順序,對於每一個父類依次判斷:若顯式調用了構造函數,那麼會調用該父類相應的構造函數;如果沒有顯式調用,就調用默認構造函數。最後調用自己的構造函數。

2. 複製構造函數

               順序:①所有直接父類;(按照基類繼承列表中聲明的順序)②自己  
            注意:和構造函數一樣,若直接父類還有父類,那麼“直接父類的父類”會在“直接父類” 之前 構造。 可以理解爲這是一個遞歸的過程,知道出現一個沒有父類的類才停止。

    2.1 如果 沒有顯式定義複製構造函數,則“合成的複製構造函數”會自動依次調用所有直接父類的“複製構造函數”,然後調用編譯器爲自己自動生成的“合成的複製構造函數”(注意:不是默認構造函數)
    2.2 如果顯式定義了自己的複製構造函數 (和構造函數類似) 
        2.2.1 如果沒有顯式調用父類的任意一個構造函數,那麼會先自動依次調用直接父類的 默認構造函數(注意:不是 複製構造函數)。
        2.2.2 如果顯式調用直接父類的任意一個構造函數,那麼按照基類列表的順序,對於每一個父類依次判斷:若顯式調用了構造函數,那麼會調用該父類相應的構造函數;如果沒有顯式調用,就調用默認構造函數。最後調用自己的複製構造函數。

3.賦值操作符重載

    3.1 如果沒有顯式定義,會自動依次調用直接父類賦值操作符。(注意:不是 默認構造函數)
    3.2 如果顯式定義了,就只執行自己定義的版本,不再自動調用直接父類的賦值操作符,只執行自己的賦值操作符。 
            注意:如有需要對父類子部分進行賦值,應該在自己編寫的代碼中,顯式調用所有直接父類的賦值操作符。
4. 析構函數  
    與 構造函數 順序相反。

五、虛繼承

繼承的差別就是:要考慮到虛基類,其它的都相同。(虛基類的初始化要早於非虛基類,並且只能由子類對其進行初始化)

1.構造函數(不包括複製構造函數)。

                                順序:①所有虛基類(按照基類繼承列表中聲明的順序進行查找);②所有直接父類;(按照基類繼承列表中聲明的順序)③自己 
            注意:若虛基類或者直接父類還有父類,那麼“直接父類的父類”會在“直接父類” 之前 構造,“虛基類的父類”也會在“虛基類”之前構造。 可以理解爲這是一個遞歸的過程,知道出現一個沒有父類的類才停止。

    2.1 如果 沒有 顯式定義構造函數,則“合成的默認構造函數”會先依次調用所有虛基類的默認構造函數,然後再自動依次調用所有直接父類的“默認構造函數”,最後調用編譯器爲自己自動生成的“合成的默認構造函數”。
    2.2 如果顯式定義了自己的構造函數         2.2.1 如果沒有顯式調用父類的任意一個構造函數,那麼和“合成的默認構造函數”一樣,會先依次調用所有虛基類的默認構造函數,然後再自動依次調用所有直接父類的 默認構造函數,最後調用自己的構造函數。 
        2.2.2 如果顯式調用了父類的任意一個構造函數,那麼按照基類列表的順序,先初始化所有虛基類,再初始化所有直接父類。對於每一個父類依次判斷:若顯式調用了構造函數,那麼會調用該父類相應的構造函數;如果沒有顯式調用,就調用默認構造函數。最後調用自己的構造函數。

2. 複製構造函數

                               順序:①所有虛基類(按照基類繼承列表中聲明的順序進行查找);②所有直接父類;(按照基類繼承列表中聲明的順序)③自己 
            注意:和構造函數一樣,若虛基類或者直接父類還有父類,那麼“直接父類的父類”會在“直接父類” 之前 構造,“虛基類的父類”也會在“虛基類”之前構造。 可以理解爲這是一個遞歸的過程,知道出現一個沒有父類的類才停止。

    2.1 如果 沒有顯式定義複製構造函數,則“合成的複製構造函數”會自動依次調用所有直接父類的“複製構造函數”,然後調用編譯器爲自己自動生成的“合成的複製構造函數”(注意:不是默認構造函數)
    2.2 如果顯式定義了自己的複製構造函數 (和構造函數類似) 
        2.2.1 如果沒有顯式調用父類的任意一個構造函數,那麼會先依次調用所有虛基類的默認構造函數,然後再依次調用所有直接父類的 默認構造函數(注意:不是 複製構造函數)。 
        2.2.2 如果顯式調用直接父類的任意一個構造函數,那麼按照基類列表的順序,先初始化所有虛基類,再初始化所有直接父類。對於每一個父類依次判斷:若顯式調用了構造函數,那麼會調用該父類相應的構造函數;如果沒有顯式調用,就調用默認構造函數。

3.賦值操作符重載

    3.1 如果沒有顯式定義,會自動依次調用所有虛基類所有直接父類賦值操作符。(注意:不是 默認構造函數)
             3.2 如果顯式定義了,就只執行自己定義的版本,不再自動調用直接父類的賦值操作符,只執行自己的賦值操作符。 
            注意:如有需要對父類子部分進行賦值,應該在自己編寫的代碼中,顯式調用所有虛基類所有直接父類的賦值操作符。
4. 析構函數  
    與 構造函數 順序相反。

六、總結:

1. 整體順序:虛基類  -->  直接父類  -->自己

2. 在任何顯式定義的構造函數中,如果沒有顯式調用父類的構造函數,那麼就會調用父類的默認構造函數。

3. 合成的複製構造函數合成的賦值操作符,(當沒有顯式定義時,編譯器自動提供),會自動調用的是虛基類直接父類的複製構造函數和賦值操作符,而不是默認構造函數;

4. 自己顯式定義的複製構造函數,除非在初始化列表中顯示調用,否則只會調用虛基類和父類的默認構造函數。

5. 自己顯式定義的賦值操作符,除非顯式調用,否則只執行自己的代碼。

6. 析構函數的執行順序與 構造函數 相反。

七、例子程序

話說只有自己寫一個程序,然後研究運行結果,纔會掌握的更好。所以下面就是個例子程序了。可以根據需要,註釋掉某個類的相應函數,觀察結果。

1. 該例子的繼承層次圖爲:(M和N是虛基類)

2. 代碼如下:

  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class A   
  5. {  
  6.     public:  
  7.         A() { cout<<"int A::A()"<<endl; }  
  8.         A(A &a) { cout<<"int A::A(A &a)"<<endl;}  
  9.         A& operator=(A& a)   
  10.         {  
  11.             cout<<"int A::operator=(A &a)"<<endl;  
  12.             return a;  
  13.         }  
  14.         virtual ~A() {cout<<"int A::~A()"<<endl;}  
  15. };  
  16.   
  17. class M :public A   
  18. {  
  19.     public:  
  20.         M() {cout<<"int M::M()"<<endl;}  
  21.         M(M &a) {cout<<"int M::M(M &a)"<<endl;}  
  22.         M& operator=(M& m)   
  23.         {  
  24.             cout<<"int M::operator=(M &a)"<<endl;  
  25.             return m;  
  26.         }  
  27.         virtual ~M() {cout<<"int M::~M()"<<endl;}  
  28. };  
  29.   
  30. class B:virtual public M   
  31. {  
  32.     public:  
  33.         B() {cout<<"int B::B()"<<endl;}  
  34.         B(B &a) {cout<<"int B::B(B &a)"<<endl;}  
  35.         B& operator=(B& b)   
  36.         {  
  37.             cout<<"int B::operator=(B &a)"<<endl;  
  38.             return b;  
  39.         }  
  40.         virtual ~B() {cout<<"int B::~B()"<<endl;}  
  41. };  
  42.   
  43. class N :public A   
  44. {  
  45.     public:  
  46.         N() {cout<<"int N::N()"<<endl;}  
  47.         N(N &a) {cout<<"int N::N(N &a)"<<endl;}  
  48.         N& operator=(N& n)   
  49.         {  
  50.             cout<<"int N::operator=(N &a)"<<endl;  
  51.             return n;  
  52.         }  
  53.         virtual ~N() {cout<<"int N::~N()"<<endl;}  
  54. };  
  55.   
  56. class C:virtual public N   
  57. {  
  58.     public:  
  59.         C() {cout<<"int C::C()"<<endl;}  
  60.         C(C &a) {cout<<"int C::C(C &a)"<<endl;}  
  61.         C& operator=(C& c)   
  62.         {  
  63.             cout<<"int C::operator=(C &a)"<<endl;  
  64.             return c;  
  65.         }  
  66.         virtual ~C() {cout<<"int C::~C()"<<endl;}  
  67. };  
  68.   
  69. class E:virtual public M  
  70. {  
  71.     public:  
  72.         E() {cout<<"int E::E()"<<endl;}  
  73.         E(E &a) {cout<<"int E::E(E &a)"<<endl;}  
  74.         E& operator=(E& e)   
  75.         {  
  76.             cout<<"int E::operator=(E &a)"<<endl;  
  77.             return e;  
  78.         }  
  79.         virtual ~E() {cout<<"int E::~E()"<<endl;}  
  80. };  
  81.   
  82. class D:public B, public C, public E   
  83. {  
  84.     public:  
  85.         D() { cout<<"int D::D()"<<endl;}  
  86.         D(D &a) {cout<<"int D::D(D &a)"<<endl;}  
  87.         D& operator=(D& d)   
  88.         {  
  89.             cout<<"int D::operator=(D &a)"<<endl;  
  90.             return d;  
  91.         }  
  92.         virtual ~D() {cout<<"int D::~D()"<<endl;}  
  93. };  
  94.   
  95.   
  96. int main(int argc, char **argv)   
  97. {  
  98.     cout<<"-------構造函數-------"<<endl;  
  99.     D d;  
  100.     cout<<"-------複製構造函數-------"<<endl;  
  101.     D d1(d);  
  102.     cout<<"-------賦值操作符-------"<<endl;  
  103.     d = d1;  
  104.     cout<<"-------析構函數-------"<<endl;  
  105.   
  106.     return 0;  
  107. }  

 

3. 運行結果與分析

       分析:M和N是虛基類,但是A不是虛基類。B和E共享一個M,但是M和N都會含有類A的部分,因爲A不是虛基類,所以M和N不共享A。下面的註釋部分爲添加的分析。

  1. -------構造函數-------  
  2. int A::A()  
  3. int M::M()//構造虛基類M時,要先構造其父類A  
  4. int A::A()  
  5. int N::N()//和M一樣,構造虛基類N時,也要先構造其父類A  
  6. int B::B()//構造完虛基類,開始構造直接父類,按照聲明順序爲B、C、E  
  7. int C::C()  
  8. int E::E()  
  9. int D::D()//最後構造自己  
  10. -------複製構造函數-------  
  11. int A::A()  
  12. int M::M()  
  13. int A::A()  
  14. int N::N()  
  15. int B::B()  
  16. int C::C()  
  17. int E::E()  
  18. int D::D(D &a)//因爲D中定義了複製構造函數,並且沒有顯式調用父類的構造函數,所以所有的“虛基類”和“直接父類”都調用默認構造函數  
  19. -------賦值操作符-------  
  20. int D::operator=(D &a) //因爲顯式調用了賦值操作符,那麼就只調用自己的代碼,不會隱式調用其它的函數  
  21. -------析構函數-------  
  22. int D::~D()  
  23. int E::~E()  
  24. int C::~C()  
  25. int B::~B()  
  26. int N::~N()  
  27. int A::~A()  
  28. int M::~M()  
  29. int A::~A()//因爲main函數中定義了兩個D對象,所以main函數結束時要進行析構兩個D對象。析構的順序與 構造函數相反。  
  30. int D::~D()  
  31. int E::~E()  
  32. int C::~C()  
  33. int B::~B()  
  34. int N::~N()  
  35. int A::~A()  
  36. int M::~M()  
  37. int A::~A()  
  38.   
  39. Press any key to continue.  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章