一、本文目的與說明
1. 本文目的:理清在各種繼承時,構造函數、複製構造函數、賦值操作符、析構函數的執行順序和執行內容。
2. 說明:雖然複製構造函數屬於構造函數的一種,有共同的地方,但是也具有一定的特殊性,所以在總結它的性質時將它單獨列出來了。
3. 單繼承、多繼承、虛繼承,既然都屬於繼承,那麼雖然有一定的區別,但還是相同點比較多。如果放在一塊講,但爲了將內容製作成遞進的,就分開了,對相同點進行重複,(大量的複製粘貼哈),但在不同點進行了標註。
注意:三塊內容是逐步遞進的
如果你懂虛函數,那麼單繼承和多繼承那塊你就可以不看;
如果你懂多繼承,那單繼承你就不要看了,至於虛繼承就等你懂虛繼承再回來看吧;
如果你只懂單繼承,那你就只看單繼承就好。
二、基本知識
1. 對於一個空類,例如:
- class EmptyClass{};
雖然你沒有聲明任何函數,但是編譯器會自動爲你提供上面這四個方法。
- class EmptyClass {
- public:
- EmptyClass(); // 默認構造函數
- EmptyClass(const EmptyClass &rhs); // 複製構造函數
- ~EmptyClass(); // 析構函數
- EmptyClass& operator=(const EmptyClass &rhs); // 賦值運算符
- }
對於這四個方法的任何一個,你的類如果沒有聲明,那麼編譯器就會自動爲你對應的提供一個默認的。(在《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. 代碼如下:
- #include <iostream>
- using namespace std;
- class A
- {
- public:
- A() { cout<<"int A::A()"<<endl; }
- A(A &a) { cout<<"int A::A(A &a)"<<endl;}
- A& operator=(A& a)
- {
- cout<<"int A::operator=(A &a)"<<endl;
- return a;
- }
- virtual ~A() {cout<<"int A::~A()"<<endl;}
- };
- class M :public A
- {
- public:
- M() {cout<<"int M::M()"<<endl;}
- M(M &a) {cout<<"int M::M(M &a)"<<endl;}
- M& operator=(M& m)
- {
- cout<<"int M::operator=(M &a)"<<endl;
- return m;
- }
- virtual ~M() {cout<<"int M::~M()"<<endl;}
- };
- class B:virtual public M
- {
- public:
- B() {cout<<"int B::B()"<<endl;}
- B(B &a) {cout<<"int B::B(B &a)"<<endl;}
- B& operator=(B& b)
- {
- cout<<"int B::operator=(B &a)"<<endl;
- return b;
- }
- virtual ~B() {cout<<"int B::~B()"<<endl;}
- };
- class N :public A
- {
- public:
- N() {cout<<"int N::N()"<<endl;}
- N(N &a) {cout<<"int N::N(N &a)"<<endl;}
- N& operator=(N& n)
- {
- cout<<"int N::operator=(N &a)"<<endl;
- return n;
- }
- virtual ~N() {cout<<"int N::~N()"<<endl;}
- };
- class C:virtual public N
- {
- public:
- C() {cout<<"int C::C()"<<endl;}
- C(C &a) {cout<<"int C::C(C &a)"<<endl;}
- C& operator=(C& c)
- {
- cout<<"int C::operator=(C &a)"<<endl;
- return c;
- }
- virtual ~C() {cout<<"int C::~C()"<<endl;}
- };
- class E:virtual public M
- {
- public:
- E() {cout<<"int E::E()"<<endl;}
- E(E &a) {cout<<"int E::E(E &a)"<<endl;}
- E& operator=(E& e)
- {
- cout<<"int E::operator=(E &a)"<<endl;
- return e;
- }
- virtual ~E() {cout<<"int E::~E()"<<endl;}
- };
- class D:public B, public C, public E
- {
- public:
- D() { cout<<"int D::D()"<<endl;}
- D(D &a) {cout<<"int D::D(D &a)"<<endl;}
- D& operator=(D& d)
- {
- cout<<"int D::operator=(D &a)"<<endl;
- return d;
- }
- virtual ~D() {cout<<"int D::~D()"<<endl;}
- };
- int main(int argc, char **argv)
- {
- cout<<"-------構造函數-------"<<endl;
- D d;
- cout<<"-------複製構造函數-------"<<endl;
- D d1(d);
- cout<<"-------賦值操作符-------"<<endl;
- d = d1;
- cout<<"-------析構函數-------"<<endl;
- return 0;
- }
3. 運行結果與分析
分析:M和N是虛基類,但是A不是虛基類。B和E共享一個M,但是M和N都會含有類A的部分,因爲A不是虛基類,所以M和N不共享A。下面的註釋部分爲添加的分析。
- -------構造函數-------
- int A::A()
- int M::M()//構造虛基類M時,要先構造其父類A
- int A::A()
- int N::N()//和M一樣,構造虛基類N時,也要先構造其父類A
- int B::B()//構造完虛基類,開始構造直接父類,按照聲明順序爲B、C、E
- int C::C()
- int E::E()
- int D::D()//最後構造自己
- -------複製構造函數-------
- int A::A()
- int M::M()
- int A::A()
- int N::N()
- int B::B()
- int C::C()
- int E::E()
- int D::D(D &a)//因爲D中定義了複製構造函數,並且沒有顯式調用父類的構造函數,所以所有的“虛基類”和“直接父類”都調用默認構造函數
- -------賦值操作符-------
- int D::operator=(D &a) //因爲顯式調用了賦值操作符,那麼就只調用自己的代碼,不會隱式調用其它的函數
- -------析構函數-------
- int D::~D()
- int E::~E()
- int C::~C()
- int B::~B()
- int N::~N()
- int A::~A()
- int M::~M()
- int A::~A()//因爲main函數中定義了兩個D對象,所以main函數結束時要進行析構兩個D對象。析構的順序與 構造函數相反。
- int D::~D()
- int E::~E()
- int C::~C()
- int B::~B()
- int N::~N()
- int A::~A()
- int M::~M()
- int A::~A()
- Press any key to continue.