標籤:his 整數 預設 物件模型 沒有 無法 function point 函數
我們通常會說當生命一個 class 時,如果我們不為該 class 指定一個 constructor,那麼編譯器會替我們實現一個 connstructor,那麼這種說法一定對嗎?
事實上,這是不對的。這個不是我說的,是深入理解C++物件模型說的,原話是:
C++新手一般有兩個常見的誤解:
- 任何 class 如果沒有定義 default constructor,就會被合成出一個來。
- 編譯器合成出來的 default constructor 會明確設定 "class 內每一個 data member 的預設值。
首先解釋第一點,只有四種情況才會導致 ”編譯器必須為未聲明 constructor 之 classes 合成一個 default constructor“。被合成出來的 constructor 只能滿足編譯器(而非程式)的需要。它們之所以能夠完成任務,是藉著 “調用 member object 或base class 的 default constructor” 或是 “為每一個 object 初始化其virtual function 機制或 virtual base class 機制”而完成。至於沒有存在那四種情況,而又沒有聲明任何 constructor 的 classes,我們說它擁有的是 implicit trivial(淺薄而無能,沒啥用的),它們實際上並不會被合成出來。
下面分別討論上述四種合成 constructor 的情況:
情況一:一個 class 內含一個 member object,且其 member object 擁有 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 擁有default constructor,符合本小節主題 if( str ) { } ... }
此時 class Bar 內含一個 member object Foo,並且 Foo 有 default constructor Foo(),那麼編譯器就會為 Bar 合成一個 default constructor,看起來像這樣:
//Bar 的 default constructor 可能會這樣合成 //被 member foo 調用 class Foo 的 default constructor inline Bar::Bar(){ //C++偽碼 foo.Foo::Foo(); }
再一次請你注意,被合成的 default constructor 只滿足編譯器的需要,而不是程式的需要,為了讓這個程式片段能夠正確執行,字元指標 str 也需要初始化,那麼程式員可能會這麼做:
Bar::Bar() { str = 0; } //程式員定義的 default constructor
此時編譯器還需要初始化 member object foo,但是由於 default constructor 已經被程式員明確定義出來了,編譯器沒辦法合成第二個。編譯器採取的行動是:“如果 class A 內含一個或一個以上的 member class object,那麼 class A 的每一個 constructor 必須調用每一個 member classes 的 default constructor”,編譯器會向使用者程式員的 constructors 前面插入必要的 default constructor。插入後可能像這樣:
//插入後的 default constructor //C++偽碼 Bar::Bar(){ foo.Foo::Foo(); //插入的 compiler code str = 0; //explicit user code }
如果有多個 class member objects 都要求初始化操作,將如何做呢?C++會按照 “member objects 在 class 中的聲明次序“ 來調用各個 consructors。這一點由編譯器完成。並且如果某個 member object 的 default constructor 被程式員定義過了,它照樣會被按順序調用。如:
//程式員對 Snow_White 類所寫的 default constructor,顯示初始化一個 member object Snow_White::Snow_White() : sneezy( 1024){ mumble = 2048 }
它會被擴張為:
Snow_White::Snow_White() : sneezy( 1024) { //插入 member class onject //調用其 constructor dopey.Dopey::Dopey(); sneezy.Sneezy::Sneezy(1024); bashfun.Bashfun::Bashful(); //explicit user code mumble = 2048; } 情況二:帶有 “default constructor” 的 base class 類似的道理,如果一個沒有任何 constructors 的 class 派生自一個 ”帶有 default constructor“ 的 base class,那麼這個 derived class 的 default constructor 會被是為 nontrivial,並且因此需要被合成出來,它將調用上一層 base classes 的 default constructor(根據它們的聲明次序)。對一個後繼派生的 class 而言,這個合成的 constructor 和一個 ”被明確提供的 default constructor” 其實沒什麼區別。如果設計者提供多個 constructors(比如帶 int 參數的 constructor),但其中都沒有 default constructor 呢? 編譯器會擴張現有的每一個 constructors,在前面插入 base class 的 default constructor。如果有成員 member object,同樣按順序調用。情況三:帶有一個 “virtual Function” 的 class
如果一個 class 聲明或繼承一個 virtual function,並且缺乏由使用者聲明的 constructors,編譯器會詳細記錄合成一個 default constructor 的必要資訊。如:
class Widget{ public: virtual void filp() = 0; ... } void filp( const Widget& widget ) { widget.filp(); } //假設 Bell 和 Whistle 都派生自 Widget void foo(){ Bell b; Whistle w; filp( b ); filp( w ); }
下面兩個擴張步驟在編譯期間發生:
- 一個 virtual function table (在 cfront 中被稱為 vtbl)會被編譯器產生出來,內放 class 的 virtual functions 地址。
- 在每一個 class onject 中,一個額外的 pointer member(也就是 vptr)會被編譯器合成出來,內含相關的 class vtbl 的地址。
此外,widget.filp() 的虛擬引發操作(virtual invocation)會被重新改寫(實際上就是虛函數調用傳入的 this 會變化),以使用 widget 的 vptr 和 vtbl 中的 filp() 條目:
//widget.filp() 的虛擬引發操作(virtual invocation)的轉變 // (* widget.vptr[ 1 ] )( &widget )
其中:
- 1 表示 filp() 在 virtual table 中的固定索引
- &widget 代表要交給 “被調用的某個 filp() 函數實體” 的 this 指標
為了讓這個機制發揮功效,編譯器必須為每一個 Widget(或其衍生類別之)object 的 vptr 設定初始值,放置適當的 virtual table 地址,對於 class 所定義的每一個 constructor,編譯器會插入類似前文所說的偽碼來做這樣的事情。對於那些什麼都沒有聲明的 classes,編譯器則就像本文主題,為它們合成一個 default constructor,以便正確的初始化每一個 class object 的 vptr。(你可以理解,vptr 必須賦初值,就像我們平時用的指標,不用是賦NULL一樣)。情況四:帶有一個 ”virtual base class“ 的 classvirtual 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, pulic 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 可以延遲至執行期才決定下來。做法通常是在 “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 構造期間完成的。對於 class 所定義的每一個 constructor,編譯器會安插哪些 ”允許每一個 virtual base class 的執行期存取操作“ 的碼。如果 class 沒有聲明任何 constructors,編譯器必須為它合成一個 default constructor。(最後這點我的理解是,像這種多態指標不能固定下來,我們需要給它指一個方向,不能讓它亂指,所以編譯器會合成default constructor)。好了,前面解釋了唯有的四種編譯器會為未聲明 constructor 的 classes 合成一個 default constructor 的情況。現在解釋一下這句:
- 編譯器合成出來的 default constructor 會明確設定 "class 內每一個 data member 的預設值
在合成的 default constructor 中,只有 base class subobject 和 member objects 會被初始化。所有其他的 nonstatic data member,如整數、整數指標、整數數組等等都不會被初始化,這些初始化操作對程式而言或許又需要,但對編譯器則並非必要。如果一個程式需要 "把某指標設為 0” 的 default constructor,那麼提供它的人應該是程式員。
c++ 2.1 編譯器何時建立預設建構函式