有兩個常見的誤解:
1.任何類如果沒有定義默認構造函數,就會被合成出一個來。
2.編譯器合成的默認構造函數會顯式地設定類內每一個數據成員的默認值。
對於第一個誤解,並不是任何類在沒有顯式定義默認構造函數時都會被編譯器合成一個默認構造函數。
在以下4種情況下,編譯器才會合成默認構造函數,以滿足編譯器自己的需求(並不是為了滿足程序的需求)。
一、父類有默認構造函數(default constructor)
如果一個沒有任何構造函數的類派生自 “一個有默認構造函數的” 父類,那麼這個派生類的默認構造函數被認為是 ”有用的(被編譯器所需求)“,因此需要被合成出來。它會調用父類的默認構造函數。
如果派生類含有多個構造函數,但其中不含默認構造函數,編譯器並不會為他合成新的默認構造函數,而是會擴展每一個構造函數,將所有需要調用的默認構造函數的代碼安插進去。
二、類中帶有類類型成員
如果類A中帶有類類型成員,並且這個類類型是有默認構造函數的,那麼類A的默認構造函數被認為是 ”有用的“,需要合成。
//X,Y,Z都帶有顯式默認構造函數 class X { public: X(); }; class Y { public: Y(); }; class Z { public: Z(); }; class A { public: X x; //三個類類型 Y y; Z z; A(int a); //帶有一個構造函數 private: int val; }; //程序員對A的構造函數的實現(你所看到的) A::A(int a) { val = a; } //編譯器擴展合成後(編譯器認為應該這樣) A::A(int a) { //按聲明順序安插代碼,調用構造 x.X::X(); y.Y::Y(); z.Z::Z(); //顯式的用戶代碼 val = a; }
三、帶有虛函數的類
在編譯期間會發生兩個擴展行動:
1.編譯器會產生一個虛表(存放著類內虛函數的地址)。
2.在每一個類對象中,會有一個額外的虛表指針被編譯器合成出來,用來指向相關虛表。
四、帶有虛基類的類
虛基類的實現必須滿足虛基類在其 ”每一個派生子類的對象中的位置“ 能夠於執行期准備妥當。
class A { public: int a; }; class X :public virtual A { public: int x; }; class Y :public virtual A { public: int y; }; class Z :public X, public Y { public: int z; }; //編譯時無法確定p->A::a的位置 void fun(X *p){ p->a = 1; } //編譯時無法確定p的真正類型(基類指針可以指向派生類對象,此時真正類型為派生類類型) int main() { fun(new X); //真正類型為X fun(new Z); //真正類型為Z return 0; }
編譯器無法確定fun()中 ”經由p存取的A::a “的實際偏移位置,因為p的真正類型是可變的,如在main()中既可以是X類型,也可以是Z類型。編譯器必須改變存取操作的代碼,使A::a延遲到執行時才確定下來。
fun()可以被編譯器改寫成如下:
//編譯器轉變操作,其中vcbA是編譯器產生的指針,指向虛基類A void fun(X *p){ p->vcbA->a = 1; } //vcbA是在類對象構造期間被完成的
除此四種情況之外,如果類沒有聲明任何構造函數,他們就會有一個隱式而”無用“的默認構造函數,他們實際上並不會被合成構造出來。
對於第二個誤解,在合成的默認構造函數中,只有基類子對象和類類型對象會被初始化,而其他所有的非靜態成員(如整數,指針,數組等),都不會初始化,對他們進行初始化的應該是程序員,而非編譯器。
注意:值類型的默認值並不是默認構造的初始化。