Performanced C++ 經驗規則(1):你不知道的構造函數(上)

jopen 8年前發布 | 10K 次閱讀 C/C++開發

Performanced C++ 經驗規則

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

第一條:你不知道的構造函數(上)

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

1、創建一個類的對象時,編譯器為對象分配內存空間,然后調用該類的構造函數;

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

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

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

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

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

#include <iostream>
using namespace std;

class Base
{
private:
        int _x;
public:
        Base(int x) : _x(x) { cout << "Base(x) _x=" << _x << endl; }
        Base() {}
};

class DerivedA :virtual  public Base
{
        int _y;
public:
        DerivedA(int x = 0, int y = 1) : Base(x), _y(y)
        { cout << "DerivedA(x,y) _y=" << _y << endl; }
};

class DerivedB :virtual  public Base
{
        int _z;
public:
        DerivedB(int x = 0, int z = 2) : Base(x), _z(z)
        { cout << "DerivedB(x,z) _z=" << _z << endl; }
};

class Other
{
        int _o;
public:
        Other() : _o(3) { cout << "Other() _o=" << _o << endl; }
};

class DerivedFinal : public DerivedB, public DerivedA
{
        int _xyz;
        Other _other;
public:
        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)
        { cout << "DerivedFinal(x,y,z,o) _xyz=" << _xyz << endl; }
};

int main(int argc, char** argv)
{
        DerivedFinal df;
        return 0;
}

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

Base(x) _x=10
DerivedB(x,z) _z=30
DerivedA(x,y) _y=20
Other() _o=3
DerivedFinal(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;
lea ecx, [ebp-4]  ;獲取對象首地址
call @ILT+15(VitrualTest::VirtualTest) (0048A500)
;調用構造函數,由于該類沒有定義任何構造函數又包含虛函數,編譯器產生了一個默認構造函數并調用

    return 0;
}

//下面是默認構造函數反匯編

004013D0 55               push        ebp 

004013D1 8B EC            mov         ebp,esp

004013D3 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,ebp

004013E5 5D               pop         ebp 

004013E6 C3               ret

由該匯編代碼還可以看出,虛表指針初始化,在構造函數初始化列表之后,進入構造函數體代碼之前。

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

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

(3)該類擁有虛基類。

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

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

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

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

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!