1. 程式人生 > >深度探索c++物件模型之vptr初始化語意學

深度探索c++物件模型之vptr初始化語意學

      接上文,還是這個圖,還是這個繼承關係:

   

其中它們的構造順序是,是從內到外、從根源到末端。所以對一個PVertex物件來說,它的構造順序是:1、Point   2、Point3d  3、Vertex  4、Vertex3d  5、PVertex,然後書中說到【大意】:在以上的每一個類中,都定義兩個虛擬函式,一個是fun【我自己取的名】,一個是size,其中它倆的關係是,在size中呼叫fun;然後再在每一個類的構造器中,呼叫一下size。問:這樣的話,在以上每一個類的構造器中呼叫的size裡面,會呼叫PVertex中的fun虛擬函式【因為完整類物件就是PVertex,既語句“PVertex pv;”】,還是呼叫當前構造類中的fun虛擬函式?如果讀者朋友看到這裡不明白我問的是什麼,那麼我換一種方式提問:比如一句【PVertex pv;】語句,最先執行的是Point類的構造器,那麼在執行Point構造器時,會呼叫size函式,在這個size函式裡面,究竟是呼叫的是PVertex::fun虛擬函式,還是Point::fun虛擬函式?

      書中首先給出了答案:在執行Point構造器時,裡面呼叫的fun函式應該是Point中的fun函式,而不應該是PVertex::fun。因為“經由構造中的物件來呼叫一個virtual base function,其函式實體應該是在此class中有作用的那個”,“當base class constructor執行時,derived實體還沒有被構造出來。”在PVertex constructor執行完畢之前,pv並不是一個完整的物件,在執行Point constructor完之後,只有Point類物件被構造出來。

      然後,作者提供的解決之道是什麼呢,就是在執行類的constructor時,限制一組virtual function候選名單。文中這樣說道:“想一想,什麼是決定一個class的virtual function名單的關鍵?答案是virtual table。virtual table如何被處理?答案是通過vptr。所以,為了控制一個class中有所作用的函式,編譯系統只要簡單的控制住vptr的初始化和設定操作即可。當然,設定vptr是編譯器的責任,任何程式設計師都不必操心此事。”。

      但vptr在什麼時候被設定呢,答案是它當前隸屬的class的constructor呼叫完成之後,但是要在任何使用者提供的程式碼和“member initialization list中所列的members初始化操作”之前。這樣每一個類的構造器都一直等到它的基類構造器執行完畢之後,再去設定自己的vptr,那麼每次它都能夠呼叫正確的virtual function實體。

      讓每一個base class constructor設定好物件的vptr,使它指向相關聯的virtual table地址,構造中的物件就可以正確的變成“構造過程中所幻化出來的每一個class”物件。比如,一個PVertex會先形成一個Point物件,接著是Point3d、Vertex、Vertex3d,最後才變成一個PVertex完整物件,在此過程中它會呼叫每一個base class constructor。“個體”的連續概括了“系統”,僅此而已。

     在此過程中,關於vptr的設定,總結為3點:

1):在派生類物件構造器中,所有的虛基類及上一層基類的構造器都會被呼叫;

2):在上述工作完成之後,該類的vptr才會被初始化,指向相關的virtual table;

3):如果有“members initialization list”或者使用者自己定義的程式碼,那麼它們都將放置在vptr設定工作完成以後【如果兩者都存在,那麼“members initialization list”先於使用者自定義程式碼】,以免發生錯誤的 virtual function呼叫。

      比如,如果我們定義的PVertex constructor如下:

PVertex::Pvertex(int x, int y, int z)
{
	cout << "PVertex class size is" << size() << endl;
}

那麼它會在編譯器內部被擴充套件成如下形式:
//注意,以下是C++偽碼
PVertex::Pvertex(PVertex *this,bool _most_deriver,int x, int y, int z)
{
	//條件選擇式決定要不要呼叫base class constructor
	if (_most_derived != false) this->Point::Point(x, y);

	//呼叫上一層的基類
	this->Vertex3d::Vertex3d(x, y, z);

	//就在這裡!設定vptr!!
	this->_vptr_PVertex = _vtbl_PVertex;
	this->_vptr_Point_PVertex = _vtbl_Point_PVertex;

	//下面才是“members initialization list”和使用者自定義程式碼
	cout << "PVertex class size is" 
		//以下是經由虛擬機制呼叫size虛擬函式
		<< (*this->_vptr_PVertex[3].faddr)(this)
		<< endl;
}

      在兩種情況下,vptr必須被設定好。首先是一個完整類的物件構造完成之前,其次是一個subobject class constructor中要呼叫一個虛擬函式。現在的編譯器,有的將constructor分裂為兩個版本,一個是類完整版,一個是subobject版本,在subobject版本中,vptr的設定可以省略。

      但文中最後也說道:儘管vptr的設定是在使用者程式碼或members initialization list之前,但在語意上來說,在constructor中使用虛擬函式依然有不安全的可能性,因為也許函式還依賴未被正確初始化的members,所以作者並不推薦文中做法。