早上看到的文章,現在也轉載出來,以下轉載自:
http://blog.csdn.net/dongfengsun/archive/2007/02/05/1502916.aspx
原文地址:http://www.cppblog.com/fwxjj/archive/2007/01/25/17996.html
多態性 (polymorphism) 是物件導向編程的基本特徵之一。而在C++ 中,多態性通過虛函數 (virtual function) 來實現。我們來看一段簡單的代碼:
- #include <iostream>
- using namespace std;
- class Base{
- int a;
- public:
- virtual void fun1() {cout<<"Base::fun1()"<<endl;}
- virtual void fun2() {cout<<"Base::fun2()"<<endl;}
- virtual void fun3() {cout<<"Base::fun3()"<<endl;}
- };
- class A:public Base{
- int a;
- public:
- void fun1() {cout<<"A::fun1()"<<endl;}
- void fun2() {cout<<"A::fun2()"<<endl;}
- };
- void foo (Base& obj){
- obj.fun1();
- obj.fun2();
- obj.fun3();
- }
- int main(){
- Base b;
- A a;
- foo(b);
- foo(a);
- }
運行結果為:
Base::fun1()
Base::fun2()
Base::fun3()
A::fun1()
A::fun2()
Base::fun3()
僅通過基類的介面,程式調用了正確的函數,它就好像知道我們輸入的對象的類型一樣!
那麼,編譯器是如何知道正確代碼的位置的呢?
其實,編譯器在編譯時間並不知道要調用的函數體的正確位置,但它插入了一段能找到正確的函數體的代碼。這稱之為 晚捆綁(late binding) 或 運行時捆綁(runtime binding) 技術。
通過virtual 關鍵字建立虛函數能引發晚捆綁,編譯器在幕後完成了實現晚捆綁的必要機制。它對每個包含虛函數的類建立一個表(稱為VTABLE),用於放置虛函數的地址。在每個包含虛函數的類中,編譯器秘密地放置了一個稱之為vpointer(縮寫為VPTR)的指標,指向這個對象的VTABLE。所以無論這個對象包含一個或是多少虛函數,編譯器都只放置一個VPTR即可。VPTR由編譯器在建構函式中秘密地插入的代碼來完成初始化,指向相應的VTABLE,這樣對象就“知道”自己是什麼類型了。
VPTR都在對象的相同位置,常常是對象的開頭。這樣,編譯器可以容易地找到對象的VTABLE並擷取函數體的地址。
如果我們用sizeof查看前面Base類的長度,我們就會發現,它的長度不僅僅是一個int的長度,而是增加了剛好是一個void指標的長度(在我的機器裡面,一個int佔4個位元組,一個void指標佔4個位元組,這樣正好類Base的長度為8個位元組)。
每當建立一個包含虛函數的類或從包含虛函數的類派生一個類時,編譯器就為這個類建立一個唯一的VTABLE。在VTABLE中,放置了這個類中或是它的基類中所有虛函數的地址,這些虛函數的順序都是一樣的,所以通過位移量可以容易地找到所需的函數體的地址。假如在衍生類別中沒有對在基類中的某個虛函數進行重寫(overriding),那麼還使用基類的這個虛函數的地址(正如上面的程式結果所示)。
至今為止,一切順利。下面,我們的實驗開始了。
就目前得知的,我們可以試探著通過自己的代碼來調用虛函數,也就是說我們要找尋一下編譯器秘密地插入的那段能找到正確函數體的代碼的足跡。
如果我們有一個Base指標作為介面,它一定指向一個Base或由Base派生的對象(譬如:A,或者是其它什麼)。這無關緊要,因為VPTR的位置都一樣,一般都在對象的開頭。如果是這樣的話,那麼包含有虛函數的對象的指標,例如Base指標,指向的位置恰恰是另一個指標——VPTR。VPTR指向的VTABLE其實就是一個函數指標的數組,現在VPTR正指向它的第一個元素,那是一個函數指標。如果VPTR向後位移一個Void指標長度的話,那麼它應該指向了VTABLE中的第二個函數指標了。
這看來就像是一個指標連成的鏈,我們得從當前指標擷取它指向的下一個指標,這樣我們才能“順藤摸瓜”。那麼,我來介紹一個函數:
- void *getp (void* p){
- return (void*)*(unsigned long*)p;
- }
我們不考慮它漂亮與否,我們只是實驗。getp() 可以從當前指標擷取它指向的下一個指標。如果我們能找到函數體的地址,用什麼來儲存它呢?我想應該用一個函數指標:
- typedef void (*fun)();
它與Base中的三個虛函數相似,為了簡單我們不要任何輸入和返回,我們只要知道它實際上被執行了即可。
然後,我們負責“摸瓜”的函數登場了:
- fun getfun (Base* obj, unsigned long off){
- void *vptr = getp(obj);
- unsigned char *p = (unsigned char *)vptr;
- p += sizeof(void*) * off;
- return (fun)getp(p);
- }
第一個參數是Base指標,我們可以輸入Base或是Base派生對象的指標。第二個參數是VTABLE位移量,位移量如果是0那麼對應fun1(),
如果是1對應fun2()。getfun()
返回的是fun類型函數指標,我們上面定義的那個。可以看到,函數首先就對Base指標調用了一次getp(),這樣得到了vptr這個指標,然後用一個
unsigned char指標運算位移量,得到的結果再次輸入getp(),這次得到的就應該是正確的函數體的位置了。
那麼它到底能不能正確工作呢?我們修改main() 來測試一下:
- int main(){
- Base *p = new A;
- fun f = getfun(p, 0);
- (*f)();
- f = getfun(p, 1);
- (*f)();
- f = getfun(p, 2);
- (*f)();
- delete p;
- }
激動人心的時刻到來了,讓我們運行它!
運行結果為:
A::fun1()
A::fun2()
Base::fun3()
至此,我們真的成功了。通過我們的方法,我們擷取了對象的VPTR,在它的體外執行了它的虛函數。