C++物件模型——Default Constructor的建構操作(第二章)

來源:互聯網
上載者:User

標籤:

第2章    建構函式語意學 (The Semantics of Constructor)    關於C++,最常聽到的一個抱怨就是,編譯器背著程式員做了太多事情.Conversion運算子就是最常被引用的一個例子.
2.1    Default Constructor的建構操作    C++ Annotated Reference Manual (ARM)指出"default constructors ...在需要的時候被編譯器產生出來".關鍵字眼是"在需要的時候".被誰需要?做什麼事情?看看下面這段程式碼:
class Foo {public:    int val;    Foo *pnext;};void foo_bar() {    Foo bar;    if (bar.val || bar.pnext)        // ... do something}
    在這個例子中,正確的程式語意是要求Foo有一個default constructor,可以將它的兩個members初始化為0,上面這段代碼不符合ARM所述的"在需要的時候".其間的差別在於一個是程式的需要,一個是編譯器的需要.上述代碼不會合成出一個default constructor.
    那麼 什麼時候才會合成出一個default constructor呢?當編譯器需要它的時候!此外,被合成出來的constructor只執行編譯器所需的行動,也就是說,即使有需要為class Foo合成一個default constructor,那個constructor也不會將兩個data members val和pnext初始化為0.為了讓上一段代碼正確執行,class Foo的設計者必須提供一個顯式的default constructor,將兩個members適當地初始化.
    C++ Standard已經修改了ARM的說法,雖然其行為事實上仍然相同.C++ Standard指出"對於class X,如果沒有任何user-declared constructor,那麼會有一個default constructor被隱式聲明出來....一個被隱式聲明的default constructor將是一個trivial constructor..."
"帶有 Default constructor"的Member Class Object    如果一個class沒有任何constructor,但它內含一個member object,而後者有default constructor,那麼這個class的implicit default constructor就是"nontrivial",編譯器需要為此class合成出一個default constructor,不過這個合成操作只有在constructor真正需要被調用時才會發生.
    於是出現一個有趣的問題: 在C++各個不同的編譯模組中,編譯器如何避免合成出多個default constructor(譬如說一個是為A.C檔合成,另一個為B.C檔合成)?解決的辦法是把合成的default constructor,copy constructor,destructor,assignment copy operator都以inline方式完成.一個inline函數有靜態連結(static linkage),不會被檔案以外者看到.如果函數太複雜,不適合做成inline,就會合成出一個explicit non-inline static實體.
    例如,下面的程式片段中,編譯器為class Bar合成一個default constructor:
class Foo {public:    Foo();    Foo(int)    ...};class Bar {public:    Foo foo;    char *str;};void foo_bar() {    Bar bar;    //Bar::foo必須在此處初始化                //Bar::foo是一個member object.而其class Foo擁有defautl constructor    if (str) {        ...    }};
    被合成的Bar default constructor內含必要的代碼,能夠調用class Foo的default constructor來處理member object Bar::foo,但它並不產生任何代碼來初始化Bar::str.將Bar::foo初始化是編譯器的責任,將Bar::str初始化是程式員的責任,被合成的default constructor可能如下:
// Bar的default constructor可能被這樣合成// 被member foo調用class Foo的default constructorinline Bar::Bar() {    // C++虛擬碼    foo.Foo::Foo();}

    注意,被合成的default constructor只滿足編譯器的需要,而不是程式的需要,為了讓這個程式片段能夠正確執行,字元指標str也需要被初始化.假設程式員經由下面的default constructor提供了str的初始化操作:
// 程式員定義的default constructorBar::Bar() { str = 0; }
    現在程式的需求獲得滿足了,但是編譯器還需要初始化member object foo.由於default constructor已經被顯式定義,編譯器沒有辦法合成第二個.
    編譯器採取行動:如果 class A內含一個或一個以上的member class objects,那麼 class A的每一個constructor必須調用每一個member classes的default constructor,編譯器會擴張已存在的constructors,在其中安插一些碼,使得user code在被執行之前,先調用必要的default constructors.沿續前一個例子,擴張後的constructors可能像這樣:
// 擴張後的default constructor// C++虛擬碼Bar::Bar() {    foo.Foo::Foo();    // 附加上complier code    str = 0;        // 顯式user code}
    如果有多個class member objects都要求constructor初始化操作,將如何呢? C++語言要求以"member objects在class中的聲明次序"來調用各個constructors.這一點由編譯器完成,它為每一個constructor安插程式碼,以"member聲明次序"調用每一個member所關聯的default constructor.這些碼被安插在顯式user code之前.如果有如下所示三個classes:
class Dopey {public:    Dopey();};class Sneezy {public:    Sneezy(int);    Sneezy();};class Bashful {public:    Bashful();};    以及一個 class Snow_White:class Snow_White {public:    Dopey dopey;    Sneezy sneezy;    Bashful bashful;private:    int number;};
    如果Snow_White沒有定義default constructor,就會有一個nontrivial constructor被合成出來,依次序調用Dopey、Sneezy、Bashful的default constructor。然而如果Snow_White定義下面這樣的default constructor:
// 程式員所寫的default constructorSnow_White::Snow_White() : Sneezy(1024) {    member = 2048;}    它會被擴張為:// 編譯器擴張後的default constructor// C++虛擬碼Snow_White::Snow_White() : Sneezy(1024) {    // 插入member class object    // 調用其constructor    dopey.Dopey::Dopey();    sneezy.Sneezy::Sneezy(1024);    bashful.Bashful::Bashful();    // 顯式user mode    member = 2048;}
    順序依次為類的成員對象,類的建構函式。
"帶有Default Constructor"的Base Class    如果一個沒有任何的constructors的class派生自一個"帶有default constructor"的base class ,那麼這個derived class 的default constructor會被視為nontrivial,並因此需要被合成出來,它將調用上一層的base classes的default constructor(根據它們的聲明次序),對一個後繼派生的 class 而言,這個合成的constructor和一個"被明確提供的default constructor"沒有差異。
    如果設計者提供多個constructor,但其中都沒有default constructor呢?編譯器會擴張現有的每一個constructor,將"用以調用所有必要的default constructors"的程式碼加進去,它不會合成一個新的default constructor,這是因為其他"由user所提供的constructor"存在的緣故。如果同時存在"帶有default constructors"的member class objects,那些default constructor也會被調用在——在所有base class constructor都被調用之後。可以看出, 構造是從類層次的最根處開始的,在每一層,首先調用基類建構函式,然後調用成員對象的建構函式
"帶有一個Virtual Function"的Class    另有兩種情況,也需要合成出default constructor:
    1.    class 聲明(或繼承)一個virtual function
    2.    class 派生自一個繼承串鏈,其中有一個或更多的virtual base classes.
    不管哪一種情況,由於缺乏由user聲明的constructors,編譯器會詳細記錄合成一個default constructor的必須資訊,以下面這個程式片段為例:
class Widget {public:    virtual void flip() = 0;    // ...};void flip(const Widget &widget) {    widget.flip();}// 假設Bell和Whistle都派生自Widgetvoid foo() {    Bell b;    Whistle w;    flip(b);    flip(w);}
    下面兩個擴張會在編譯期間發生:
    1.    一個virtual function table(在cfront中被稱為vtbl)會被編譯器產生出來,內放 class 的 virtual functions 地址。
    2.    在每一個 class object中,一個額外的pointer member(也就是vptr)會被編譯器合成出來,內含相關的 class vtbl的地址。
    此外,widget.flip()的虛擬引發操作(virtual invocation)會被重新改寫,以使用widget的vptr和vtbl中的flip()條目:
// widget.flip()的虛擬引發操作(virtual invocation)的轉變(*widget.vptr[1])(&widget)
    其中:
    1表示flip()在virtual table中的固定索引
    &widget代表要交給"被調用的某個flip()函數實體"的this指標。
    為了讓這機制發揮功效, 編譯器必須為每一個Widget(或其衍生類別的)object的vptr設定初始值,放置適當的virtual table地址。對於 class 所定義的每一個constructor,編譯器會安插一些代碼做這樣的事情(看5.2節)。對於那些未聲明任何 constructor的classes,編譯器會為它們合成一個default constructor,以便正確地初始化每一個 class object的vptr.
"帶有一個Virtual Base Class"的Class    Virtual base class 的實現法在不同的編譯器之間有極大的差異,然而, 每一種實現法的共通點在於必須使virtual base class 在其每一個derived class object 中的位置,能夠於執行期準備妥當,例如下面這段代碼中:
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;};// 無法在編譯時間期決定(resolve) pa->X::i的位置void foo(const A *pa) {    pa->i = 1024;}main(){    foo(new A);    foo(new C);    // ...}
    編譯器無法確定foo()中"經由pa而存取的X::i"的實際位移位置,因為pa的真正類型可以改變。 編譯器必須改變"執行存取操作"的那些碼,使X::i可以延遲至執行期才決定。原先cfront的做法是靠"在derived class object的每一個virtual base classes中安插一個指標"完成。所有"經由reference或pointer來存取一個virtual base class"的操作都可以通過相關指標完成。foo可以被改寫如下,以符合這樣的實現策略:
// 可能的編譯器轉換操作void foo(const A *pa) {    pa->_vbcX->i = 1024;}
    其中,_vbcX表示編譯器所產生的指標,指向virtual base class X.
    _vbcX(或編譯器所做出的某個東西)是在 class object 建構期間被完成的。對於 class 所定義的每一個constructor,編譯器會安插那些"允許每一個virtual base class的執行期存取操作"的碼。如果 class 沒有聲明任何constructor,編譯器必須為它合成一個default constructor.
總結    有四種情況,會導致"編譯器必須為未聲明constructor的classes合成一個default constructor‘",C++ Stardard把那些合成物稱為implicit nontrivial default constructor。被合成出來的constructor只能滿足編譯器(而非程式)的需要。它之所以能夠完成任務,是藉著"調用member object或者base class的default constructor"或者"為每一個object初始化其virtual function機制或virtual base class機制"而完成。至於沒有存在那四種情況而又沒有聲明任何constructor的classes,它們擁有的是implicit trivial default constructor,它們實際上並不會被合成出來.
    在合成的default constructor中,只有base class subjects和member class objects會被初始化,所有其它的nonstatic data member,如整數,整數指標,整數資料等都不會被初始化,這些初始化操作對程式而言或許需要,但對編譯器則並非必要.如果程式需要一個"把某指標設為0"的default constructor,那麼提供它的應該是程式員.
    C++新手一般有兩個常見的誤解:
    1.    任何class如果沒有定義default constructor,就會被合成出來
    2.    編譯器合成出來的default constructor會明確設定"class 內有每一個data member的預設值"
    這兩個都是錯誤的.


著作權聲明:本文為博主原創文章,未經博主允許不得轉載。

C++物件模型——Default Constructor的建構操作(第二章)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.