c++ devived object model
單一虛函數繼承
class A
{
public:
virtual int foo( ) { return val ; }
virtual int funA( ) {}
private:
int val ;
char bit1 ;
} ;
class B : public A
{
public:
virtual int foo( ) { return bit2; }
virtual int funB( ) {}
private:
char bit2 ;
};
一個class只要有一個虛函數,那麽每一個class object被安插上一個由編譯器內部產生的指針,指向該表格(virtual table)。
virtual table 的第一項是表示class的類型。
因為,基類指針的特殊性,它可以指向基類對象,也可以指向派生類對象。故:ptr->foo( ) ;這種調用,我們需要知道ptr所指對象的真實類型。(就算不知道ptr所指對象的類型,也可以正確調用fun函數,但是由於fun函數有編譯器插入的this指針,this指針要與ptr指向的對象地址正確對應,以正確訪問對象中的成員變量,但ptr中卻沒有這樣的信息)
virtual table 之後的表格是class中的每個虛函數地址。
一個class只會有一個virtual table。派生類的virtual table是在基類的virtual table上增加,修改的。
派生類中的虛函數會改寫(overriding)與基類中同名且參數相同的虛函數,把virtual table表中相應的基類虛函數地址改寫為相應派生類虛函數的地址。
以上工作都是由編譯器完成的。執行期要做的就是在特定的virtual table表項中激活相應的虛函數,然後根據virtual table首項的類型信息,正確執行此虛函數。
例如:ptr->foo( ) ;
一般而言,我並不知道ptr所指對象的真正類型。然而我知道。經由ptr可以存取到該對象的virtual table。
雖然我不知道哪一個foo( )實體會被調用,但我知道每一個foo( )函數的地址都放在虛表的第二項。
故:根據以上信息,編譯器可以將該調用轉化為:
( *ptr->vptr[1] )( ptr ) ;
【註意:】基類指針雖然可以指向派生類,但是它實際上指向的是派生類中的基類部分。(這就不違反指針的特性了,指針類型與其指向範圍是一致的)故基類指針不能訪問派生類的成員。(但基類指針可以通過訪問派生類的虛函數,間接操作派生類成員)
上面的內存圖驗證了這個例子,就是指的是指向派生類的指針,指針指向base class中沒有被devived class繼承的虛函數,或者指向devived class中重寫base class中的虛函數,
多重繼承
class A { public: A() {} virtual ~A() {} virtual int foo( ) { return val ; } virtual int funA( ) {} private: int val ; char bit1 ; } ; class B : { public: B() {} virtual ~B() {} virtual int foo( ) { return bit2; } virtual int funB( ) {} private: char bit2 ; }; class Derived : public A, public B { public: Derived() {} virtual ~Derived() {} virtual int foo( ) { return bit3; } virtual int funDerived( ) {} private: char bit3 ; };
註意:在多重繼承下,若有n個基類,則派生類中有n個virtual table.
針對每一個virtual table,派生類對象中有對應的vptr。這些vptrs將在構造函數中被設立初值。
派生類的虛函數會覆蓋(改寫)其每個基類virtualtable中相應的虛函數索引值。
多重繼承最左端的基類,在派生類中作為主要實體,其virtualtable為主要表格,其它基類的virtual table為次要表格。
當你將Derived對象地址指定給一個Base1指針或Derived指針時,被處理的virtualtable是主要表格vptr_Base1。
故主要表格要包含Derived的所有虛函數(包括繼承得來的虛函數)。所以其中有Base1、Base2、Derived中的虛函數。
而其它次要表格中的項目數不變,只是有些虛函數的索引值被重寫。
涉及多重繼承的指針的轉換
多重繼承的問題主要發生於:派生類對象和其第二或後繼的基類對象之間的轉換。
【對一個多重派生對象,將其地址指定給“最左端(也就是第一個)”base class的指針,情況將和單一繼承時相同,因為二者都指向相同的起始地址。需付出的成本只有地址的指定操作而已。至於第二個或後繼的base class的地址指定操作,則需要將地址修改:加上(或減去)介於中間的base class subobject(s)大小】
例如:
Derived dObj ;
A* pA = &dObj ;
只需要簡單地拷貝地址就行了。
而:
Derived* pd ;
B* pB = pd ;
需要這樣的內部轉化:
//虛擬C++碼
pB = pd ? (B*)( (char*)pd + sizeof(A) ) : 0 ;
含虛繼承的多重繼承
class A { } ; class B : public virtual A { } ; class C : public virtual A { } ; class D : public B, public C { };
註意】class A { };實際上並不是空的,它有一個隱晦的1字節,那是被編譯器安插進去的一個char。這是為了使得class A的對象得以在內存中配置獨一無二的地址。
當語言支持虛基類時,就會導致一些額外負擔。在派生類中會有一個額外指針,指針指向一個相關表格,表格中存放的或者是虛基類對象,或者是其偏移量。
若虛基類為空,則表格中存放的是虛基類對象(即一個安插字節)
VC++編譯器的優化:
VC++特別對 空virtual baseclass做了處理。空虛基類的派生類中只有一個4字節的指針。沒有安插char,也沒有字節填充。
(因為安插char的目的是為了空類實例化的對象在內存中有地址,而現在其派生類對象中已經有個指針了,其可以取地址,故不需要安插char了)
註意:如果虛基類中有數據成員,則兩種編譯器(“有特殊處理者”和“無特殊處理者”)就會產生完全相同的對象布局。
2、虛基類有數據成員
例:
class A { public: … private: int x, y ; } ; class B : public virtual A { public: … private: int valB ; } ; class C : public virtual A { public: … private: int valC ; } ; class D : public B, public C { public: … private: int valD ; };
最終的派生類對象底部是共享虛基類的部分,派生類class B、class C部分的指針指向的虛函數表的前面增加了一項:offset(從對象的開頭算起,到共享虛基類部分的字節數)
這樣可以快速地訪問到共享虛基類的成員。
問:
①為什麽虛繼承不像一般繼承那樣,把基類成員放在頂部,把新增派生類成員放在尾部?
我想是:因為在最後的多重繼承時,要求最終的派生類對象中只有一份基類成員,故最終派生類會從其各個父類中提取它們各自的派生數據成員。若按一般繼承那樣的對象模型,很容易找到父類中的派生類成員,但難以確定邊界,故難以提取數據。為了提取數據方便,把基類成員放在尾部。
②為什麽虛繼承的派生類中有兩個虛指針?
我想是:因為要實現多態。當基類指針指向派生類中的基類部分時,必須要有指向虛表的指針,才能實現多態。
註意:class D對象模型與一般多重繼承的派生類對象的布局相似,多重繼承最左邊的基類部分的虛表是“主要表格”。故class B、class C、class A部分的虛表各不相同。它們都是為了實現多態。
【編程風格】一般而言,virtual base class最有效的一種運用形式是:一個抽象的virtual base class,沒有任何數據成員。
c++ devived object model