大家都知道c++的虚函数有个虚表,那这个需要具体在哪呢,在程序的代码段还是数据段的?一个类有成员变量,成员变量在代码段内是怎么分布的呢?
如何根据一个对象指针调用某个虚函数?如何根据一个对象的指针直接修改成员变量?如果生成多个对象,那是不是会为每一个对象都生成一份虚表?
ok,问了这么多,咱们来一一解答吧。虚表其实在数据段。对于一个具体类型来说,它的行为是确定的,即对于同一个类型的所有实例,它们的虚函数表的内容相同,在编译时可以确定。因此,如果把虚函数表的全部内容附着在对象实例上,这样的对象模型显然是浪费内存的。因此,在对象实例起始处,放的是一个指针,指向其虚函数表,因此每个对象的虚函数表在实例中仅占一个指针(4 bytes / 32位系统)空间。如果一个类型有多个实例,它们指向的是同一份虚函数表(典型情况是位于进程空间的 .rdata section)。虚函数表指针的初始化由编译器生成的各种构造函数负责。如果一个函数不包含虚函数,则对象实例中不包含虚函数表指针。虚函数表可以认为是由函数指针组成的数组,数组元素由该类型的所有虚函数的地址组成,用 NULL 表示结尾(取决于编译器对模型的实现)。如果该类型实现了自己的虚函数,它将覆盖从父类继承下来的元素。编译器知道表中每个元素对应是那个虚函数,因此调用时取出元素,通过 call 指令实现调用。观察 VC debug 版本的汇编代码,虚函数表的内容被编译到只读的 section(和其他常量字符串一起),每个元素代表的是函数的地址(这些元素是代码段起始部的一组 jump 语句的地址,类似中断向量表,用于跳到真正的函数体)。由于虚函数表位于只读 section 中,所以其元素(函数指针)是不能直接改写的。
看下面一个简单的例子,来看看虚函数:
class Base
{
public:
virtual void func1(){}
};
class Derive: public Base
{
public:
virtual void func1(){}
virtual void func2(){}
};
int main()
{
Base base;
Derive derive1;
Derive derive2;
derive1.func1();
}
在vc 2010下debug一下,可以看到一个Base对象和两个Derive对象的虚函数情况:
可以看到,derive1和derive2对应的虚表地址是一样的,都是0x00415844,说明相同类型的实例确实只有一份虚表,而不同类型的虚表当然是不一样的,
上图Base对象和Derive对象对应的虚表地址就不一样。
那如何获得虚表的地址呢,能直接通过对象的地址调用虚函数吗?(细节可以看看陈浩的博客:http://blog.csdn.net/haoel/article/details/1948051)
下面再给给例子:
class Base
{
public:
virtual void func1()
{
cout<<"Base::func1"<<endl;
}
};
class Derive: public Base
{
public:
virtual void func1()
{
cout<<"Derive::func1"<<endl;
}
virtual void func2()
{
cout<<"Derive::func2"<<endl;
}
};
typedef void(*PFunc)(void);
int main()
{
Derive *derive=new Derive;
PFunc ptr1=(PFunc)*(int*)(*(int*)derive);
ptr1();
}
输出为:
可以看到,上面成功调用了子类的虚函数。上面的指针转换是啥意思呢,其实很好懂,先看一下debug时的这个信息:
可以看到,new出来的子类对象指针其实是指向虚表的,我们通过子类对象的地址就能直接找到虚表。*(int*)derive 的意思是取得虚表地址,
(指针的值是个地址,这个地址指向虚表),然后*(int*)(*(int*)derive) 是指把虚表地址转换成int* ,然后取虚表里的第一个值(数组首地址代表数组第一个元素,有印象没?)
ok,这个时候已经去到虚表里虚函数的地址了,然后再转换成对应的函数指针(PFunc)*(int*)(*(int*)derive) ,最后再调用一把这个函数ptr1();
如果虚表里有多个函数呢,我该怎么调用呢,只需要在上面转换时加上便宜即可,比如,如果虚表里有两个函数,第二个函数的地址是:
PFunc ptr1=(PFunc)*(int*)(*(int*)derive+1);
如何通过对象指针获得成员变量呢,看看下面的代码:
class A
{
int value;
public:
A(int n = 0) : value(n) {}
virtual int GetValue() { return value; }
};
int main() {
A a;
*((int *)&a+1) = 5;
cout<<a.GetValue()<<endl;
return 0;
}
我故意将函数弄成虚函数,看看有虚函数时的成员是如何布局的:
可以看到成员变量在对象地址上加一个便宜即可,因为虚函数的指针占了前四个字节,所以便宜四个字节 :*((int *)a+1)即可获得成员变量。
但是这段代码其实是有问题的,在32位 centos下返回的仍然是0,这个跟编译器有关,所以用指针修改成员变量没啥意义。
总结:
1、如果有虚函数,对象的地址指向虚表(linux和windows下都一样)
2、如果有虚表,成员变量的地址为对象地址加一个虚表地址的偏移
3、相同类型的对象在内存里只有一份虚表
4、在子类的虚表里只会存子类的虚函数,没有父类的信息
5、this指针和对象的地址是同一个
6、通过对象地址获得成员地址,然后修改成员变量,这个在windows下可行,但linux下没效果