第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 constructor
inline Bar::Bar() {
// C++偽代碼
foo.Foo::Foo();
}
注意,被合成的default constructor只滿足編譯器的需要,而不是程序的需要,為了讓這個程序片段能夠正確執行,字符指針str也需要被初始化.假設程序員經由下面的default constructor提供了str的初始化操作:
// 程序員定義的default constructor
Bar::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 constructor
Snow_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都派生自Widget
void 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的默認值
這兩個都是錯誤的.