深度探索c++對象模型之虛繼承的對象構造

      鄭重聲明:以下文字“借鑑”自侯捷老師的譯作《深度探索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。


發佈了53 篇原創文章 · 獲贊 113 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章