多型性 (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;}//在A的物件中,函式存放的地址順序:A::fun1() ,A::fun2(),A.Base::fun3()。
};
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個位元組)。
//這是由於virtual 關鍵字建立虛擬函式能引發晚捆綁;如果沒有virtual函式,不會分配此指標的空間。
每當建立一個包含虛擬函式的類或從包含虛擬函式的類派生一個類時,編譯器就為這個類建立一個唯一的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);
//如果VPTR向後偏移一個Void指標長度的話,那麼它應該指向了VTABLE中的第二個函式指標了。
(*f)();
f = getfun(p, 1);
(*f)();
f = getfun(p, 2);
(*f)();
//f = getfun(p,3);//沒有結果
//(*f)();
//f = getfun(p, 4);// //沒有結果
//(*f)();
A aa;
aa.Base::fun1();
aa.Base::fun2();
//在派生類中,依然有基類Virtual函式的存在。但是目前沒找到方法用指標呼叫。
delete p;
}
激動人心的時刻到來了,讓我們執行它!
執行結果為:
A::fun1()
A::fun2()
Base::fun3()
至此,我們真的成功了。通過我們的方法,我們獲取了物件的VPTR,在它的體外執行了它的虛擬函式。
源文件 <http://www.cppblog.com/fwxjj/archive/2007/01/25/17996.html>
探尋vtable例項:
#include <iostream> using namespace std; typedef void (*fun)(void); #if 1 #define VIRTUAL virtual #else #define VIRTUAL #endif class Base { public: VIRTUAL void fun1(void) {cout<<"Base::fun1()"<<endl;} VIRTUAL void fun2(void) {cout<<"Base::fun2()"<<endl;} VIRTUAL void fun3(void) {cout<<"Base::fun3()"<<endl;} public: int a; }; class A:public Base { public: int a; void fun1() {cout<<"A::fun1()"<<endl;} void fun2() {cout<<"A::fun2()"<<endl;} //在A的物件中,函式存放的地址順序:A::fun1() ,A::fun2(),A.Base::fun3()。 }; void foo (Base& obj) { obj.fun1(); obj.fun2(); obj.fun3(); } void showsizes() { Base b; A a; printf("sizeof(class Base) = %d\n", sizeof(class Base)); printf("sizeof(class A) = %d\n", sizeof(class A)); printf("sizeof(int) = %d\n", sizeof(int)); } void *getp (void* p) { return (void*)*(unsigned long*)p; } 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); } //下面這個函式主要說明VTABLE的存在以及其中的內容 void show_a_VTABLE(void) { printf("下面這段程式碼主要說明VTABLE的存在以及其中的函式排布\n"); Base *p = new A; fun f = getfun(p, 0); (*f)(); f = getfun(p, 1); (*f)(); f = getfun(p, 2); (*f)(); //f = getfun(p, 3); //VTABLE裡面只有3個函式, 因此這裡出現Segmentation fault //(*f)(); } //下面這段程式碼主要說明VTABLE是一個Class所有的例項共享的,即所有的例項的VTABLE都是同一個。而每個Class各自維護一個VTABLE void show_3_VTABLE_addr(void) { Base *p = new A; void *ptr = NULL; printf("下面這段程式碼主要說明VTABLE是一個Class所有的例項共享的,即所有的例項的VTABLE都是同一個。而每個Class各自維護一個VTABLE\n"); fun f = getfun(p, 0); ptr = (void*) f; printf("show a addr of p->fun0() is %x\n", ptr); A aa; f = getfun(&aa, 0); ptr = (void*) f; printf("show aa addr of p->fun0() is %x\n", ptr); Base b; f = getfun(&b, 0); ptr = (void*) f; printf("show b addr of p->fun0() is %x\n", ptr); printf("A class的不同例項的第一個成員函式地址相同,但不同於B Class的一個例項第一個成員函式地址\n"); delete p; } //下面的程式碼反映了一個類裡面的所有能呼叫的方法 void show_all_funcs(void) { A aa; printf("下面的程式碼反映了一個類裡面的所有能呼叫的方法\n"); printf("使用指標引用class裡面的函式的結果:\n"); fun f = getfun(&aa, 0); (*f)(); f = getfun(&aa, 1); (*f)(); f = getfun(&aa, 2); (*f)(); printf("直接使用class裡面的函式的結果:\n"); aa.fun1(); aa.fun2(); aa.fun3(); printf("使用子類中來自於基類的函式:\n"); aa.Base::fun1(); aa.Base::fun2();//在派生類中,依然有基類Virtual函式的存在。但是目前沒找到方法用指標呼叫。 } int main() { printf("DATE:"__DATE__" TIME:" __TIME__ "\n"); show_all_funcs(); show_a_VTABLE(); show_3_VTABLE_addr(); return 0; } /* [root@localhost test]# g++ test.cpp ;./a.out DATE:May 30 2016 TIME:02:29:14 下面的程式碼反映了一個類裡面的所有能呼叫的方法 使用指標引用class裡面的函式的結果: A::fun1() A::fun2() Base::fun3() 直接使用class裡面的函式的結果: A::fun1() A::fun2() Base::fun3() 使用子類中來自於基類的函式: Base::fun1() Base::fun2() 下面這段程式碼主要說明VTABLE的存在以及其中的函式排布 A::fun1() A::fun2() Base::fun3() 下面這段程式碼主要說明VTABLE是一個Class所有的例項共享的,即所有的例項的VTABLE都是同一個。而每個Class各自維護一個VTABLE show a addr of p->fun0() is 8048ba2 show aa addr of p->fun0() is 8048ba2 show b addr of p->fun0() is 8048c26 A class的不同例項的第一個成員函式地址相同,但不同於B Class的一個例項第一個成員函式地址 */
一個類的某個方法如果不定義為vitual,它就不會存在於方法列表中。
/*
#if 0
#define VIRTUAL virtual
#else
#define VIRTUAL
#endif
[root@localhost test]# g++ test.cpp ;./a.out
DATE:May 30 2016 TIME:02:31:17
下面的程式碼反映了一個類裡面的所有能呼叫的方法
使用指標引用class裡面的函式的結果:
Segmentation fault
*/
一個類的方法只有定義為vitual,才會存在於方法列表中,並且第一個宣告為vitual的函式在offset為0的位置。依次類推。
/*
class Base
{
public:
void fun1(void) {cout<<"Base::fun1()"<<endl;}
virtual void fun2(void) {cout<<"Base::fun2()"<<endl;}
void fun3(void) {cout<<"Base::fun3()"<<endl;}
public:
int a;
};
[root@localhost test]# g++ test.cpp ;./a.out
DATE:May 30 2016 TIME:02:35:55
下面的程式碼反映了一個類裡面的所有能呼叫的方法
使用指標引用class裡面的函式的結果:
A::fun2()
Segmentation fault
*/
如果基類沒有vitual函式,將不會建立虛擬函式表。
/*
class Base
{
public:
void fun1(void) {cout<<"Base::fun1()"<<endl;}
void fun2(void) {cout<<"Base::fun2()"<<endl;}
void fun3(void) {cout<<"Base::fun3()"<<endl;}
public:
int a;
};
sizeof(class Base) = 4
*/
不管基類的方法是不是vitural虛擬函式,子類都回將基類的函式繼承下來。子類的頭四個位元組將會儲存基類的資訊。
/*
class Base
{
public:
void fun1(void) {cout<<"Base::fun1()"<<endl;}
void fun2(void) {cout<<"Base::fun2()"<<endl;}
void fun3(void) {cout<<"Base::fun3()"<<endl;}
public:
int a;
};
void showaaa(void)
{
A aaa;
aaa.a = 101;
unsigned int addr;
int *paaa_a = NULL;
addr = (unsigned int) &aaa;
addr+=4;
paaa_a = (int *)addr;
printf("aaa.a = 101, *paaa_a = %d\n", *paaa_a);//aaa.a = 101, *paaa_a = 101
aaa.fun1();
aaa.fun2();
aaa.fun3();
aaa.Base::fun1();
aaa.Base::fun2();
aaa.Base::fun3();
}
sizeof(class Base) = 4
sizeof(class A) = 8
sizeof(int) = 4
aaa.a = 101, *paaa_a = 101
A::fun1()
A::fun2()
Base::fun3()
Base::fun1()
Base::fun2()
Base::fun3()
*/