標籤:特性 off bit mod return 插入 父類 取數 rtu
單一虛函數繼承
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