虛函數 原理

轉載:http://blog.chinaunix.net/uid-23735893-id-2976429.html

虛(virtual)函數的一般實現模型是:每一個類(class)有一個虛表(virtual table),內含該class之中有作用的虛(virtual)函數的地址,然後每個對象有一個vptr,指向虛表(virtual table)的所在。

請允許我援引自深度探索c++對象模型一書上的一個例子:

class Point { 
public: 
   virtual ~Point();  

   virtual Point& mult( float ) = 0; 

   float x() const { return _x; }     //非虛函數,不作存儲 
   virtual float y() const { return 0; }   
   virtual float z() const { return 0; }   
   // ...

protected: 
   Point( float x = 0.0 ); 
   float _x; 
};

1、在Point的對象pt中,有兩個東西,一個是數據成員_x,一個是_vptr_Point。其中_vptr_Point指向着virtual table point,而virtual table(虛表)point中存儲着以下東西:

  • virtual ~Point()被賦值slot 1,
  • mult() 將被賦值slot 2.
  • y() is 將被賦值slot 3
  • z() 將被賦值slot 4.

class Point2d : public Point { 
public: 
   Point2d( float x = 0.0, float y = 0.0 )   
      : Point( x ), _y( y ) {} 
   ~Point2d();   //1

   //改寫base class virtual functions 
   Point2d& mult( float );  //2 
   float y() const { return _y; }  //3

protected: 
   float _y; 
};

2、在 Point2d的對象pt2d中,有三個東西,首先是繼承自基類pt對象的數據成員_x,然後是pt2d對象本身的數據成員_y,最後是 _vptr_Point。其中_vptr_Point指向着virtual table point2d。由於Point2d繼承自Point,所以在virtual table point2d中存儲着:改寫了的其中的~Point2d()、Point2d& mult( float )、float y() const,以及未被改寫的Point::z()函數。

class Point3d: public Point2d { 
public: 
   Point3d( float x = 0.0, 
            float y = 0.0, float z = 0.0 ) 
      : Point2d( x, y ), _z( z ) {} 
   ~Point3d();

   // overridden base class virtual functions 
   Point3d& mult( float ); 
   float z() const { return _z; }

   // ... other operations ... 
protected: 
   float _z; 
};

3、在 Point3d的對象pt3d中,則有四個東西,一個是_x,一個是_vptr_Point,一個是_y,一個是_z。其中_vptr_Point指向着 virtual table point3d。由於point3d繼承自point2d,所以在virtual table point3d中存儲着:已經改寫了的point3d的~Point3d(),point3d::mult()的函數地址,和z()函數的地址,以及未被改寫的point2d的y()函數地址。

ok,上述1、2、3所有情況的詳情,請參考下圖。

(圖:virtual table(虛表)的佈局:單一繼承情況)

本文,日後可能會酌情考慮增補有關內容。ok,更多,可參考深度探索c++對象模型一書第四章。 
最近幾章難度都比較小,是考慮到狂想曲有深有淺的原則,後續章節會逐步恢復到相應難度。

第四節、虛函數的佈局與彙編層面的考察

      ivan、老夢的兩篇文章繼續對虛函數進行了一番深入,我看他們已經寫得很好了,我就不饒舌了。ok,請看:1、VC虛函數佈局引發的問題,2、從彙編層面深度剖析C++虛函數、http://blog.csdn.net/linyt/archive/2011/04/20/6336762.aspx

第五節、虛函數表的詳解

本節全部內容來自淄博的共享,非常感謝。

一般繼承(無虛函數覆蓋) 
下面,再讓我們來看看繼承時的虛函數表是什麼樣的。假設有如下所示的一個繼承關係:

請注意,在這個繼承關係中,子類沒有重載任何父類的函數。那麼,在派生類的實例中,

對於實例:Derive d; 的虛函數表如下:

我們可以看到下面幾點: 
1)虛函數按照其聲明順序放於表中。 
2)父類的虛函數在子類的虛函數前面。 
我相信聰明的你一定可以參考前面的那個程序,來編寫一段程序來驗證。

一般繼承(有虛函數覆蓋) 
覆蓋父類的虛函數是很顯然的事情,不然,虛函數就變得毫無意義。 
下面,我們來看一下,如果子類中有虛函數重載了父類的虛函數,會是一個什麼樣子?假設,我們有下面這樣的一個繼承關係。

爲了讓大家看到被繼承過後的效果,在這個類的設計中,我只覆蓋了父類的一個函數:f()  。 
那麼,對於派生類的實例,其虛函數表會是下面的一個樣子: 
 
我們從表中可以看到下面幾點, 
1)覆蓋的f()函數被放到了虛表中原來父類虛函數的位置。 
2)沒有被覆蓋的函數依舊。 
這樣,我們就可以看到對於下面這樣的程序, 
Base *b = new Derive();

b->f();

由b所指的內存中的虛函數表的f()的位置已經被Derive::f()函數地址所取代, 
於是在實際調用發生時,是Derive::f()被調用了。這就實現了多態。

多重繼承(無虛函數覆蓋)

下面,再讓我們來看看多重繼承中的情況,假設有下面這樣一個類的繼承關係(注意:子類並沒有覆蓋父類的函數):

對於子類實例中的虛函數表,是下面這個樣子:

我們可以看到: 
1) 每個父類都有自己的虛表。 
2) 子類的成員函數被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的)

這樣做就是爲了解決不同的父類類型的指針指向同一個子類實例,而能夠調用到實際的函數。

多重繼承(有虛函數覆蓋) 
下面我們再來看看,如果發生虛函數覆蓋的情況。 
下圖中,我們在子類中覆蓋了父類的f()函數。

下面是對於子類實例中的虛函數表的圖:

我們可以看見,三個父類虛函數表中的f()的位置被替換成了子類的函數指針。 
這樣,我們就可以任一靜態類型的父類來指向子類,並調用子類的f()了。如:

Derive d; 
Base1 *b1 = &d; 
Base2 *b2 = &d; 
Base3 *b3 = &d; 
b1->f(); //Derive::f() 
b2->f(); //Derive::f() 
b3->f(); //Derive::f() 
b1->g(); //Base1::g() 
b2->g(); //Base2::g() 
b3->g(); //Base3::g()

安全性 
每次寫C++的文章,總免不了要批判一下C++。 
這篇文章也不例外。通過上面的講述,相信我們對虛函數表有一個比較細緻的瞭解了。 
水可載舟,亦可覆舟。下面,讓我們來看看我們可以用虛函數表來乾點什麼壞事吧。

一、通過父類型的指針訪問子類自己的虛函數 
我們知道,子類沒有重載父類的虛函數是一件毫無意義的事情。因爲多態也是要基於函數重載的。 
雖然在上面的圖中我們可以看到Base1的虛表中有Derive的虛函數,但我們根本不可能使用下面的語句來調用子類的自有虛函數:

Base1 *b1 = new Derive(); 
b1->g1(); //編譯出錯

任何妄圖使用父類指針想調用子類中的未覆蓋父類的成員函數的行爲都會被編譯器視爲非法,即基類指針不能調用子類自己定義的成員函數。所以,這樣的程序根本無法編譯通過。 
但在運行時,我們可以通過指針的方式訪問虛函數表來達到違反C++語義的行爲。 
(關於這方面的嘗試,通過閱讀後面附錄的代碼,相信你可以做到這一點)

二、訪問non-public的虛函數 
另外,如果父類的虛函數是private或是protected的,但這些非public的虛函數同樣會存在於虛函數表中, 
所以,我們同樣可以使用訪問虛函數表的方式來訪問這些non-public的虛函數,這是很容易做到的。 
如:

class Base { 
private:  
virtual void f() { cout << "Base::f" << endl; }  
};

class Derive : public Base{  
}; 
typedef void(*Fun)(void); 
void main() { 
Derive d; 
Fun pFun = (Fun)*((int*)*(int*)(&d)+0); 
pFun();  
}

對上面粗體部分的解釋(@a && x):

1. (int*)(&d)取vptr地址,該地址存儲的是指向vtbl的指針 
2. (int*)*(int*)(&d)取vtbl地址,該地址存儲的是虛函數表數組 
3. (Fun)*((int*)*(int*)(&d) +0),取vtbl數組的第一個元素,即Base中第一個虛函數f的地址 
4. (Fun)*((int*)*(int*)(&d) +1),取vtbl數組的第二個元素(這第4點,如下圖所示)。

下圖也能很清晰的說明一些東西(@5):

ok,再來看一個問題,如果一個子類重載的虛擬函數爲privete,那麼通過父類的指針可以訪問到它嗎?

#include <IOSTREAM>   
class B   
{     
public:     
    virtual void fun()       
    {      
        std::cout << "base fun called";      
    };     
};  

class D : public B    
{     
private:   
    virtual void fun()       
    {      
        std::cout << "driver fun called";     
    };     
};  

int main(int argc, char* argv[])   
{        
    B* p = new D();     
    p->fun();     
    return 0;     
}  
運行時會輸出 driver fun called

從這個實驗,可以更深入的瞭解虛擬函數編譯時的一些特徵: 
在編譯虛擬函數調用的時候,例如p->fun(); 只是按其靜態類型來處理的, 在這裏p的類型就是B,不會考慮其實際指向的類型(動態類型)。 
    也就是說,碰到p->fun();編譯器就當作調用B的fun來進行相應的檢查和處理。 
因爲在B裏fun是public的,所以這裏在“訪問控制檢查”這一關就完全可以通過了。 
然後就會轉換成(*p->vptr[1])(p)這樣的方式處理, p實際指向的動態類型是D, 
    所以p作爲參數傳給fun後(類的非靜態成員函數都會編譯加一個指針參數,指向調用該函數的對象,我們平常用的this就是該指針的值), 實際運行時p->vptr[1]則獲取到的是D::fun()的地址,也就調用了該函數, 這也就是動態運行的機理。

爲了進一步的實驗,可以將B裏的fun改爲private的,D裏的改爲public的,則編譯就會出錯。 
C++的注意條款中有一條" 絕不重新定義繼承而來的缺省參數值" 
(Effective C++ Item37, never redefine a function's inherited default parameter value) 也是同樣的道理。

可以再做個實驗 
class B   
{     
public:   
    virtual void fun(int i = 1)       
    {      
        std::cout << "base fun called, " << i;      
    };     
};  

class D : public B    
{     
private:     
    virtual void fun(int i = 2)       
    {      
        std::cout << "driver fun called, " << i;      
    };     
}; 

則運行會輸出driver fun called, 1

關於這一點,Effective上講的很清楚“virtual 函數系動態綁定, 而缺省參數卻是靜態綁定”, 
也就是說在編譯的時候已經按照p的靜態類型處理其默認參數了,轉換成了(*p->vptr[1])(p, 1)這樣的方式。

補遺

   一個類如果有虛函數,不管是幾個虛函數,都會爲這個類聲明一個虛函數表,這個虛表是一個含有虛函數的類的,不是說是類對象的。一個含有虛函數的類,不管有多少個數據成員,每個對象實例都有一個虛指針,在內存中,存放每個類對象的內存區,在內存區的頭部都是先存放這個指針變量的(準確的說,應該是:視編譯器具體情況而定),從第n(n視實際情況而定)個字節纔是這個對象自己的東西。

下面再說下通過基類指針,調用虛函數所發生的一切: 
One *p; 
p->disp();

1、上來要取得類的虛表的指針,就是要得到,虛表的地址。存放類對象的內存區的前四個字節其實就是用來存放虛表的地址的。 
2、得到虛表的地址後,從虛表那知道你調用的那個函數的入口地址。根據虛表提供的你要找的函數的地址。並調用函數;你要知道,那個虛表是一個存放指針變量的數組,並不是說,那個虛表中就是存放的虛函數的實體。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章