多态性----vptr----vtable
多态性 (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()
*/