對於C++預設建構函式,我曾經有兩點誤解: 類如果沒有定義任何的建構函式,那麼編譯器(一定會!)將為類定義一個合成的預設建構函式。 合成預設建構函式會初始化類中所有的資料成員。
第一個誤解來自於我學習C++的第一本書 《C++ Primer》,在書中392頁:“只有當一個類沒有定義建構函式時,編譯器才會自動產生一個預設建構函式”。
實際上這句話也沒有說錯,它說明了預設建構函式定義的必要非充分條件,然而卻給當時初學C++的我造成了一定的誤解。
第二個誤解依舊來自於Primer中的一句話:“合成的預設建構函式使用與變數初始化相同的規則來初始化成員。具有類類型的成員通過運行各自的預設建構函式來進行初始化”。然而這也是我理解的片面,因為Primer也說到了:“如果類包含內建或複合類型的成員,則該類不應該依賴於合成的預設建構函式”,言下之意就是合成的預設建構函式並不會初始化內建或複合類型的成員。
總結了我有這些誤解的原因,第一是初學時知識體系沒形成,對Primer中所說的內容沒有真正的理解,第二就是Primer在某種程度上的確不是C++初學者能看懂的書,或許看時覺得懂了,卻是遺漏了很多知識。也說明了Primer 是座寶庫,常常回顧將會有新的感悟。
讓我對上面兩個觀點產生疑惑,是在看《Effective C++》時,條款05《瞭解C++預設編寫並調用哪些函數》中說到“….惟有當這些函數被需要(被調用),它們才會被編譯器建立出來。” (“這些函數“指的是編譯器版本的複製建構函式、賦值操作符和解構函式,還包括了預設建構函式。)也就是說,預設建構函式“被需要”的時候編譯器才會幫我們合成,那什麼情況才是預設建構函式”被需要“呢。這個問題《Effective C++》並沒有給出答案,直到看了《深度探索C++物件模型》,才明白了編譯器何時才會幫我們合成一個預設建構函式。
我寫這篇文章的目的是給和我有同樣誤解或疑惑的C++初學者看的,如果你對合成預設建構函式已有充分的認識,請忽略本文的內容。
本文 什麼是預設建構函式。
預設建構函式是可以不用實參進行調用的建構函式,它包括了以下兩種情況: 沒有帶明顯形參的建構函式。 提供了預設實參的建構函式。
類設計者可以自己寫一個預設建構函式。編譯器幫我們寫的預設建構函式,稱為“合成的預設建構函式”。強調“沒有帶明顯形參”的原因是,編譯器總是會為我們的建構函式形參表插入一個隱含的this指標,所以”本質上”是沒有不帶形參的建構函式的,只有不帶明顯形參的建構函式,它就是預設建構函式。 預設建構函式什麼時候被調用。
如果定義一個對象時沒有提供初始化式,就使用預設建構函式。例如:
class A{public: A(bool _isTrue= true, int _num=10){ isTrue = isTrue; num = _num; }; //預設建構函式 bool isTrue; int num;};int main(){ A a; //調用類A的預設建構函式}
理解“被需要”這三個字
前面提到在《Effective C++》中指出惟有預設建構函式”被需要“的時候編譯器才會合成預設建構函式。關鍵字眼是”被需要“。被誰需要?做什麼事情。像下面這段代碼,預設建構函式”被需要“了嗎。
class A{public: bool isTrue; int num;};int main(){ A a; if (a.isTrue) cout << a.num; return 0;}
類只含有內建類型或複合類型的成員時,編譯器是不會為類合成預設建構函式的,這種類並不符合”被需要“的條件,甚至當類滿足“被需要”條件,編譯器合成了預設建構函式時,類中內建類型與複合類型資料成員依然不會在預設建構函式中進行初始化。
Primer中也有提到:“如果類包含內建或複合類型的成員,則該類不應該依賴於合成的預設建構函式“。
上面代碼中,預設建構函式”被需要“是對程式來說的,程式需要isTrue被初始化以便可以進行條件判斷,需要num被初始化以便可以輸出。然而這種需要並不會促使編譯器合成預設建構函式。惟有被編譯器所需要時,編譯器才會合成預設建構函式。那怎樣的類才是編譯器需要合成預設建構函式的呢。
總結: 合成預設建構函式總是不會初始化類的內建類型及複合類型的資料成員。 分清楚預設建構函式被程式需要與被編譯器需要,只有被編譯器需要的預設建構函式,編譯器才會合成它。 何時預設建構函式才會被編譯器需要。
以下四種情況的類,編譯器總是需要預設建構函式完成某些工作:
1. 含有類對象資料成員,該類物件類型有預設建構函式。
如果一個類沒有任何建構函式,但是它含有一個類對象資料成員,且該類物件類型有預設建構函式,那麼編譯器就會為該類合成一個預設建構函式,不過這個合成操作只有在建構函式真正需要被調用的時候才會發生。舉個例子,編譯器將為類B合成一個預設建構函式:
class A{public: A(bool _isTrue=true, int _num = 0){ isTrue = _isTrue; num = _num; }; //預設建構函式 bool isTrue; int num;};class B{public: A a;//類A含有預設建構函式 int b; //...};int main(){ B b; //編譯至此時,編譯器將為B合成預設建構函式 return 0;}
被合成的預設建構函式做了什麼事情。大概如下面這樣:
B::B(){ a.A::A();}
被合成的預設建構函式內只含必要的代碼,它完成了對資料成員a的初始化,但不產生任何代碼來初始化B::b。正如上面所說,初始化類的內建類型或複合類型成員是程式的責任而不是編譯器的責任。為了滿足程式的需要,我們一般會自己寫建構函式來對B::b進行初始化,像這樣:
B::B(){ a.A::A(); //編譯器插入的代碼 b = 0; //顯示定義的代碼}
如果類中有多種類對象成員,則編譯器按照這些類對象成員聲明的順序,在建構函式按順序插入調用各個類預設建構函式的代碼。
2.基類帶有預設建構函式的衍生類別。 當一個類派生自一個含有預設建構函式的基類時,該類也符合編譯器需要合成預設建構函式的條件。編譯器合成的預設建構函式將根據基類聲明順序調用上層的基類預設建構函式。同樣的道理,如果設計者定義了多個建構函式,編譯器將不會重新定義一個合成預設建構函式,而是把合成預設建構函式的內容插入到每一個建構函式中去。
3、含有虛函數的類
類帶有虛函數可以分為兩種情況: 類本身定義了自己的虛函數 類從繼承體系中繼承了虛函數(成員函數一旦被聲明為虛函數,繼承不會改變虛函數的”虛性質“)。
這兩種情況都使一個類成為帶有虛函數的類。這樣的類也滿足編譯器需要合成預設建構函式的類,原因是含有虛函數的類對象都含有一個虛表指標vptr,編譯器需要對vptr設定初值以滿足虛函數機制的正確運行,編譯器會把這個設定初值的操作放在預設建構函式中。如果設計者沒有定義任何一個預設建構函式,則編譯器會合成一個預設建構函式完成上述操作,否則,編譯器將在每一個建構函式中插入代碼來完成相同的事情。
4、帶有虛基類的類
虛基類的概念是存在於類與類之間的,是一種相對的概念。例如類A虛繼承於類X,則對於A來說,類X是類A的虛基類,而不能說類X就是一個虛基類。虛基類是為瞭解決多重繼承下確保子類對象中每個父類只含有一個副本的問題,比如菱形繼承。
於是,類A對象中含有一份類X對象,類C中也含有一份類X對象。 代碼如下:
class X { public: int i; };class A : public virtual X{ public:int j; };class B : public virtual X{ public:double d; };class C : public A, public B{ public: int k; };void function(A *pa){ pa->i = 1000;}int main(){ A *a= new A(); C *c= new C(); function(a); //關注重點在這裡 function(c); //關注重點在這裡 return 0;}
函數function參數pa的真正類型是可以改變的,既可以把A對象指標賦值給pa,也可以把對象指標賦值給pa,在編譯階段並無法確定pa儲存的i是屬於A還是C的虛基類對象。為瞭解決這問題,編譯器將產生一個指向虛基類X的指標,使得程式得以在運行期確定經由pa而存取的X::i的實際儲存位置。這個指標的安插,編譯器將會在合成預設建構函式中完成,同樣的,如果設計者已經寫了多個建構函式,那麼編譯器不會重新寫預設建構函式,而是把虛基類指標的安插代碼插入已有的建構函式中。
總結:
只有在編譯器需要預設建構函式來完成編譯任務的時候,編譯器才會為沒有任何建構函式的類合成一個預設建構函式,或者是把這些操作插入到已有的建構函式中去。
編譯器需要預設建構函式的四種情況,總結起來就是:
a) 調用對象成員或基類的預設建構函式。
b) 為對象初始化虛表指標與虛基類指標。
轉自: http://www.cnblogs.com/QG-whz/p/4676481.html