標籤:
第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的建構操作(第二章)