參考c++什么時候會生成默認構造函數 以及《深度探索C++對象模型》
在需要的時候,編譯器會在用戶沒有定義的情況下,默認創建拷貝構造函數A::A()、復制構造函數、賦值運算符重載函數、析構函數A::~A()(非virtual)。
編譯器一定為類生成構造函數了嗎?只有一個空的類定義的話,我們可以肯定的說——沒有。對編譯器這樣的做法,我們不必感到驚訝。試想一個空的類——沒有數據成員,沒有成員函數,即使生成了構造函數又能做什么呢?即便是生成了,也只是一個空構造函數而已。
代碼如下:
A(){}
它什么也做不了,也什么都不必做。更“悲劇”,它的出現不僅沒有任何積極意義,還會為編譯器和程序運行增加完全不必要的函數調用負擔。
若A為:
//例1
class A
{
public:
int var;
void fun(){}
};
//即便如此,結果還是和上邊的一樣,不生成構造函數!
//因為沒有任何理由對var初始化,況且編譯器也不知道用什么值給它初始化。
- 編譯器生成默認構造函數的四個正當理由:
1、類A內數據成員是對象B,并且該對象B的類提供了一個默認構造函數。
若把int改為類B,如果B沒有定義構造函數(和這里的A一個樣子),那么編譯器仍然沒有理由生成構造函數——為B初始化什么呢?反之,B一旦定義了默認構造函數B::B(),即便它是空的,編譯器就不得不為A創建默認構造函數了(這里不考慮編譯器的深度優化)。因為A并不知道B的構造函數是什么樣子。
2、類的基類提供了默認的構造函數。
現在,我們回到例子1,這里我們不修改var的類型,而是讓A繼承于另一個類C。代碼如下:
class A:public C
我們都知道,在C++構造函數初始化語法中,構造函數會先初始化基類C,再初始化自身的數據成員或者對象。因此,這里的問題和對象成員var類似。如果基類C沒有提供任何構造函數,那么編譯器仍然不提供A的默認構造函數。如果C提供了默認構造函數,結果和前邊類似。
結果不出所料,編譯器為A生成了構造函數,并且調用了基類C定義的默認構造函數。同樣,若C沒有提供默認默認構造函數,而提供了其他構造函數,編譯是無法通過的。
3、類內定義了虛函數
我們再次回到例子1,這次我們修改成員函數fun。代碼如下:
virtual void fun(){}
我們把類A的成員函數fun修改為虛函數,再次看看是否產生了默認構造函數。
這次編譯器“毫不客氣”的為A生成了默認構造函數,雖然它沒有調用任何其他的構造函數!這是什么原因呢?原來,C++為了實現多態機制,需要為類維護一個虛函數表(vftable),而每個該類的對象都保存一個指向該虛函數表的一個指針(一般保存在對象最開始的四個四節處,多態機制的實現這里暫不介紹)。編譯器為A生成構造函數,其實不為別的,就為了保證它定義的對象都要正常初始化這個虛函數表的指針(vfptr)!
好了,因此我們得出編譯器生成默認構造函數的第三個正當理由——類內定義了虛函數。這里可能還涉及一個更復雜點的情況:類內本身沒有定義虛函數,但是繼承了基類的虛函數。其實按照上述的原則,我們可以推理如下:基類既然定義了虛函數,那么基類本身就需要生成默認構造函數初始化它本身的虛函數表指針。而基類一旦產生了默認構造函數,派生類就需要產生默認構造函數調用它。同時,如果讀者對多態機制了解清除的話,派生類在生成的默認構造函數內還會初始化一次這個虛函數表指針的。
4、類使用了虛繼承。
最后,我們再次回到例子1,這次仍然讓A繼承于C,但是這次C是一個空類——什么都沒有,也不會自動生成默認構造函數。但是A繼承C的方式要變化一下。代碼如下:
class A:public virtual C
A虛繼承于C,這次又有什么不同呢?
這次編譯器也生成了A的構造函數,并且初始化過程和虛函數時有點類似。細心觀察下發現,這次構造函數也初始化了一張表——vbtable。了解虛繼承機制的讀者應該不會陌生,這張表叫虛基類表,它記錄了類繼承的所有的虛基類子對象在本類定義的對象內的偏移位置(至于虛繼承機制的實現,我們以后詳細探討)。為了保證虛繼承機制的正確工作,對象必須在初始化階段維護一個指向該表的一個指針,稱為虛表指針(vbptr)。編譯器因為它提供A的默認構造函數的理由和虛函數時類似。
這樣,我們得出編譯器生成默認構造函數的第四個正當理由——類使用了虛繼承。
到這里,我們把編譯器為類生成默認構造函數的正當理由闡述完畢,相信大家應該對構造函數的生成時機有了一個大致的認識。這四種“正當理由”其實是編譯器不得不為類生成默認構造函數的理由,《Inside The C++ Object Model》里稱這種理由為nontrival的(候sir翻譯的很別扭,所以怎么翻譯隨你啦)。除了這四種情況外,編譯器稱為trival的,也就是沒有必要為類生成默認構造函數。這里討論的構造函數生成準則的內容是寫進C++Standard的,如此看來標準就是“貼合正常思維”的一套準則(簡單YY一下),其實本就是這樣,編譯器不應該為了一致化做一些沒有必要的工作。
通過對默認構造函數的討論,相信大家對復制構造函數、賦值運算符重載函數、析構函數的生成時機應該可以自動擴展了。沒錯,它們遵循著一個最根本的原則:只有編譯器不得不為這個類生成函數的時候(nontrival),編譯器才會真正的生成它。
因此,正如標題所說,我們不要被C++語法中所描述的那些條條框框所“蒙騙”了。的確,相信這些生成規則不會對我們的編程帶來多大的影響(不會產生錯誤),但是只有了解它們的背后操作,我們才知道編譯器究竟為我們做了什么,我們才知道如何使用C++才能讓它變得更有效率——比如消除不必要的構造和虛擬機制等(如果可以的話)。相信本文對C++自動生成的內容的描述讓不少人認清對象構造函數產生的前因后果,希望本文對你有所幫助。