【原創】Performanced C++ 經驗規則 第一條:你不知道的建構函式(上)

來源:互聯網
上載者:User

Performanced C++ 經驗規則

前言:Performanced C++,意為“高效能C++“編程,是筆者和所在團隊多年C++編程總結的經驗規則,按條款方式講述(參考了《Effective C++》的方式),希望能對初入C++的程式員提供協助,少走彎路,站在前人的肩膀上,看得更高走的更遠。我們也同樣是腳踩許許多多大牛的經典著作,還有無數默默付出的程式員的辛勞,以及自己許許多多慘痛的編程體驗,才有了這些“規則”。

icc(JoneZhang,張峻崇)原創內容,難免有錯誤,歡迎拍磚指正,共同進步。轉載請註明出處,保留追究一切責任的權利。

============================================================================================

第一條:你不知道的建構函式(上)

首先來看,我們“知道”的建構函式,C++建構函式究竟做了哪些事情?

1、建立一個類的對象時,編譯器為對象分配記憶體空間,然後調用該類的建構函式;

2、建構函式的目的,是完成對象非靜態成員的初始化工作(靜態成員如何初始化?記住以下要點:在類外進行、預設值為0、在程式開始時、在主函數之前、單線程方式、主線程完成),記住:C++類非靜態成員是沒有預設值的(可對比Java)。

3、如果建構函式有初始化列表,則先按照成員聲明順序(非初始化列表中的順序)執行初始化列表中的內容,然後再進入建構函式體。這裡又有疑問了,如果類本身沒有非虛擬基類,應顯式地調用直接基類的某個建構函式,否則,將會自動其直接基類的預設建構函式(如果此時直接基類沒有預設建構函式,得到編譯錯誤);如果類本身有虛擬基類,也應顯式地調用虛擬基類的某個建構函式,否則,將會自動調用虛擬基類的預設建構函式;如果成員有其它類的對象,則應顯式地調用成員所屬類的相應建構函式,否則對於沒有在初始化列表中出現的類成員,也會自動調用其預設的建構函式

注意上述調用順序,編程時應按照“先祖再客最後自己”的原則進行,即,首先完成自身包含的“祖先對象”的初始化,之後,完成自身包含的成員是其它類型(客人)的初始化,最後才是自身非類類型成員的初始化工作。

再注意,上面多次提到了術語“預設建構函式”,預設建構函式是指:無參建構函式或每個參數均有預設值的建構函式。若且唯若,一個類沒有聲明任何建構函式時,可認為編譯器會自動為該類建立一個預設建構函式(無參的,注意“可認為”,即實際情況並非如此,編譯器並不一定總是會自動建立預設建構函式,除非必要,這涉及到更深的彙編層面。當然,在寫代碼的時候,這個“可認為”是正確的)。

這一小部分內容可能資訊量過大,讓我們看一段代碼以加深理解。 

 1 #include <iostream> 2 using namespace std; 3  4 class Base 5 { 6 private: 7         int _x; 8 public: 9         Base(int x) : _x(x) { cout << "Base(x) _x=" << _x << endl; }10         Base() {}11 };12 13 class DerivedA :virtual  public Base14 {15         int _y;16 public:17         DerivedA(int x = 0, int y = 1) : Base(x), _y(y)18         { cout << "DerivedA(x,y) _y=" << _y << endl; }19 };20 21 class DerivedB :virtual  public Base22 {23         int _z;24 public:25         DerivedB(int x = 0, int z = 2) : Base(x), _z(z)26         { cout << "DerivedB(x,z) _z=" << _z << endl; }27 };28 29 class Other30 {31         int _o;32 public:33         Other() : _o(3) { cout << "Other() _o=" << _o << endl; }34 };35 36 class DerivedFinal : public DerivedB, public DerivedA37 {38         int _xyz;39         Other _other;40 public:41         DerivedFinal(int x = 10, int y = 20, int z = 30, int o = 50) : DerivedA(x,y), DerivedB(x,z), Base(x), _xyz(x * y * z)42         { cout << "DerivedFinal(x,y,z,o) _xyz=" << _xyz << endl; }43 };44 45 int main(int argc, char** argv)46 {47         DerivedFinal df;48         return 0;49 }

 

輸出結果(Ubuntu 12.04 + gcc 4.6.3):

Base(x) _x=10DerivedB(x,z) _z=30DerivedA(x,y) _y=20Other() _o=3DerivedFinal(x,y,z,o) _xyz=6000

和你心中的答案是否一致呢?

一切從DerivedFinal的調用順序說起,首先,這是虛繼承,故虛基類Base的建構函式將首先被調用,儘管它在DerivedFinal建構函式的初始化列表順序中排在後面的位置(再次記住,調用順序與初始化列表中的順序無關),接下來是DerivedB(x,z),因為它先被繼承;之後是DerivedA(x,z),再之後,DerivedFinal自身非類類型成員_xyz被初始化,最後是Other(),other成員並沒有出現在DerivedFinal的初始化列表中,所以它的預設建構函式將被自動調用。另外,如果不是虛繼承,調用間接基類Base的建構函式將是非法的,但此處是虛繼承,必須這樣做。

接下來繼續討論,上面提到,編譯器不一定總是會產生預設建構函式,雖然在編寫代碼時,你“可以這麼認為”,這聽起來太玄乎了,那麼,到底什麼時候,編譯器才會真正在你沒有定義任何建構函式時,為你產生一個預設建構函式呢?有以下三種情況,編譯器一定會產生預設建構函式:

(1)該類、該類的基類或該類中定義的類類型成員對象中,有虛函數存在。

發生這種情況時,由於必須要完成對象的虛表初始化工作(關於虛函數的原理,筆者建議參考陳皓的《C++虛函數表解析》),所以編譯器在沒有任何建構函式的時候,會產生一個預設建構函式來完成這部分工作;然而,如果已經有任何建構函式,編譯器則把初始化虛表這部分工作“合成”到你已定義的建構函式之中(用心良苦)。

讓我們稍稍進入彙編領域(筆者強烈建議,要精通C/C++,一定的彙編和反組譯碼能力是必須的,能精通更好)看一下,一個有虛函數的類,建構函式的x86反組譯碼代碼:

class VirtualTest{public:    virtual void foo(int x) { cout << x << endl; }};int main(int argc, char** argv){    VirtualTest vt;00401340 lea ecx, [ebp-4]  ;擷取對象首地址00401344 call @ILT+15(VitrualTest::VirtualTest) (0048A500);調用建構函式,由於該類沒有定義任何建構函式又包含虛函數,編譯器產生了一個預設建構函式並調用        return 0;}//下面是預設建構函式反組譯碼004013D0 55               push        ebp 004013D1 8B EC            mov         ebp,esp004013D3 51               push        ecx;頭三句,初始化函數調用過程,詳見彙編知識004013D4 89 4D FC         mov         dword ptr [ebp-4],ecx;擷取對象首地址,即this指標004013D7 8B 45 FC         mov         eax,dword ptr [this];取出this指標,這個地址將會作為指標儲存到虛表首地址004013DA C7 00 60 68 40 00 mov         dword ptr [eax],offset VirtualTest::`vftable' (0042201c);取虛表首地址,儲存到虛表指標中(即對象頭4位元組)004013E0 8B 45 FC         mov         eax,dword ptr [this];再次取出this指標地址,返回函數調用,即得到對象004013E3 8B E5            mov         esp,ebp004013E5 5D               pop         ebp 004013E6 C3               ret

由該彙編代碼還可以看出,虛表指標初始化,在建構函式初始化列表之後,進入建構函式體代碼之前。

(2)該類、該類的基類中所定義的類類型成員對象中,帶有建構函式。

發生這種情況時,由於需要顯式地調用這些類類型成員的建構函式,編譯器在沒有任何建構函式的時候,也會產生一個預設建構函式來完成這個過程;同樣,如果你已經定義一個建構函式但沒有對這些類類型成員顯式調用建構函式,編譯器則把這部分工作“合成"到你定義的建構函式中(調用它們的預設建構函式,再次用心良苦)。

(3)該類擁有虛基類。

發生這種情況,需要維護“獨此一份"的虛基類繼承而來的對象,所以也需要通過建構函式完成。方式同(1)(2)。

除上述3種情況外,“可認為在沒有任何建構函式時候,編譯器產生一個預設建構函式”是不對的,因為這樣的預設建構函式是“無用”的,編譯器也就不會再用心良苦去做沒用的工作。這部分涉及彙編較多,如果想詳細瞭解,建議閱讀錢林松所著的《C++反組譯碼與逆向分析技術揭秘》,機械工業出版社,2012.5。

這裡只要記住結論就可以了。

 

終於講述完了,進入建構函式體之前的奧秘,你是否覺得不過癮呢?不著急,下一篇將講述C++進入建構函式體之後,那些你不知道的內容。

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.