1. 程式人生 > >c++ devived object model

c++ devived object model

virt mod 新增 urn 特性 size 被調用 基類 最終

單一虛函數繼承

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