鄭重聲明:以下文字“借鑑”自侯捷老師的譯作《深度探索C++對象模型》部分內容【5.2節,大概在211頁左右】,寫在這裏,算是加深自己對此書內容的記憶,因爲鄙人水平太淺,難免有理解錯誤的地方,如果有朋友看出來,還請費神指出,鄙人不勝感激!
讓我們先來看這樣一個繼承體系,首先聲明一個Point類作爲基類,然後再聲明兩個Point3d和Vertex,它們倆都虛擬繼承自Point類,接着再申請一個Vertex3d類,它繼承自Point3d和Vertex類,最後再申請一個PVertex類,它繼承自Vertex3d類,它們的繼承的關係圖如下:
代碼如下:
#include <string>
#include <iostream>
using namespace std;
class Point{
public:
Point(int x = 0, int y = 0):_x(x),_y(y){
cout << "This is Point construct!" << endl;
};
protected:
int _x, _y;
};
class Point3d :virtual public Point{
public:
Point3d(int z=0) :_z(z){
cout << "This is Point3d construct!" << endl;
};
protected:
int _z;
};
class Vertex :virtual public Point{
public:
Vertex():_pv(NULL){
cout << "this is Vertex construct!" << endl;
};
protected:
Vertex *_pv;
};
class Vertex3d : public Vertex,public Point3d{
public:
Vertex3d(){
cout << "This is Vertex3d construct" << endl;
};
};
class PVertex :public Vertex3d{
public:
PVertex(){
cout << "This is PVertex construct" << endl;
}
};
int main()
{
PVertex pv;
return 0;
}
讓我們按下F5執行,會看到執行效果如圖:
根據這個圖片我們可以看出,對於虛繼承體系來說,任何一個對象的構造執行順序總是從深到淺、從遠到近,既最先構造的總是最底層的基類。
這樣看起來似乎並沒有什麼值得奇怪的地方,但是讓我們想想,如果我們把主函數main中的代碼改成【Point3d p3d;】呢?相信按下F5後,會輸出兩句執行結果:【This is Point construct!】和【This is Point3d construct!】,這是因爲在構造Point3d對象時,會先執行Point的構造器。看到這裏,細心的朋友或許會生出疑問了:不是說在構造PVertex對象時,執行完Point的構造器,還會執行Vertex、Point3d等等類的構造器嗎,而在這些類的構造中,不還是要重新執行Point類的構造器嗎?如果真是這樣,那Point構造器應該被執行很多次啊,那爲什麼我們只看到一句【This is Point construct!】呢??
嗯,按道理來說,是應該如此,但這種“講道理”實在是太浪費效率了:因爲對於那些多層繼承來說,這種構造方式無疑是一種冗餘大災難!所以,C++的先驅們,就設計出一種黑魔法,讓編譯器可以不講道理式的高效率構造對象,而這種黑魔法,我稱之爲“隱參選構法”。顧名思義,就是在執行一個派生類對象的構造時,會有條件的選擇要不要執行基類的構造器。具體做法就是,給繼承體系中所有的構造器傳入一個隱形參數,根據這個參數的具體數值來確定要不要調用基類的構造器。再具體一點的說,根據書中的描述,這個隱形參數的名字叫做【_most_derived】,是緊跟隱形this指針參數的第二個參數,它有兩個值,一個爲true,一個爲false,當參數爲true時,那麼它就調用最基類的Point構造器,否則就不調用。
那麼什麼時候值纔是true呢?答案是當我們聲明一個完整的派生類對象時,比如我們例子中的代碼【PVertex pv;】,因爲pv是一個完整派生類對象,所以編譯器在調用它的構造器時,會傳入一個true值【1】,所以代碼【PVertex pv;】會被編譯器擴展成大概這樣的形式【PVertex pv(&pv,1);】。那麼什麼時候傳入的值是false呢?答案是在任何一個派生類對象的構造器中,遞歸調用上一層類的構造器時。比如我們的PVertex類,在編譯器給它合成的構造器裏面,一定會調用它的繼承類Vertex3d類的構造器,而在調用後者時,一定會傳入一個false值【0】,以此類推,Vertex3d在調用Vertex和Point3d時,也會傳入false值;
說了這麼多,讓我們來看一下編譯器給PVertex類合成的構造器內部代碼大概是什麼樣子的【以下僞代碼只是鄙人推測,未必如實!】:
PVertex* PVertex::PVertex(PVertex *this, bool _most_derived, int x, int y, int z)
{
//如果是聲明一個完整類對象,那麼默認傳入一個true值,所
//以在我們的例子中表達式結果爲真,Point構造器得以執行。
if (_most_derived != false)
this->Point::Point(x, y);
//只要是在派生類的構造器中遞歸調用上一層類的構造器,傳
//入的值一定是false,所以Vertex3d構造器中的Point構造器
//不會得到執行!以此類推,在Vertex3d中調用Vertex和
//Point3d時,Point構造器也不會執行
this->Vertex3d::Vertex3d(false, x, y, z);
//接着我們需要設置一下虛表指針等
this->_vptr_PVertex = _vtbl_PVertex;
this->vptr_PVertex_Point = _vtbl_PVertex_Point;
/*****************************************
這裏可以用來安插一些用戶自己寫的構造器代碼
******************************************/
//最後返回this指針
return this;
}
在現代編譯器中,會把每一個construct一分爲二,一種針對完整的繼承對象,另一種針對子對象【subobject】;完整版無條件調用virtual base construct,設置所有的vptrs,而“subobject”則不調用virtual base construct,也可能不設置vptrs。無疑這是一種提高對象構造效率的方法,畢竟在構造器裏面省卻了條件分支判斷了,代價則是編譯後代碼的臃腫。
講完了constructor,接下來再講一講destructor的工作原理,我們就只說一下它的工作步驟好了,如果有讀者感興趣,請自行閱讀《深度探索C++對象模型》一書第五章最後一節的內容,大約在235頁:
1):destructor函數本身首先被執行。
2):如果該類擁有成員類對象,而這些成員類對象又擁有destructor,那麼它們會以聲明次序的相反順序逐個被調用。
3):如果object內帶一個vptr,則就在現在被重新設定,指向適當的基類的虛函數表。
4):如果該類有上一層的nonvirtual base class,並且這些基類們擁有destructor,那麼就按照它們的聲明次序的相反順序調用。
5):如果有任何的virtual base class擁有destructor,而當前討論的這個類是最尾端【most derived】的類,那麼它們會以其原來的構造順序相反的順序被調用。
類似constructor,目前對於destructor的實現策略也是維護兩份destructor實體:
1):一個complete object【完全對象】實體,它總是設定好vptrs,並調用virtual base class的destructor。
2):一個base class object實體,除非在destructor調用一個virtual function,否則它絕對不會調用virtual function或設定vptrs。