c++虚函数、成员变量内存布局

http://blog.csdn.net/ywh147/article/details/9378601


大家都知道c++的虚函数有个虚表,那这个需要具体在哪呢,在程序的代码段还是数据段的?一个类有成员变量,成员变量在代码段内是怎么分布的呢?

如何根据一个对象指针调用某个虚函数?如何根据一个对象的指针直接修改成员变量?如果生成多个对象,那是不是会为每一个对象都生成一份虚表?


ok,问了这么多,咱们来一一解答吧。虚表其实在数据段。对于一个具体类型来说,它的行为是确定的,即对于同一个类型的所有实例,它们的虚函数表的内容相同,在编译时可以确定。因此,如果把虚函数表的全部内容附着在对象实例上,这样的对象模型显然是浪费内存的。因此,在对象实例起始处,放的是一个指针,指向其虚函数表,因此每个对象的虚函数表在实例中仅占一个指针(4 bytes / 32位系统)空间。如果一个类型有多个实例,它们指向的是同一份虚函数表(典型情况是位于进程空间的 .rdata section)。虚函数表指针的初始化由编译器生成的各种构造函数负责。如果一个函数不包含虚函数,则对象实例中不包含虚函数表指针。虚函数表可以认为是由函数指针组成的数组,数组元素由该类型的所有虚函数的地址组成,用 NULL 表示结尾(取决于编译器对模型的实现)。如果该类型实现了自己的虚函数,它将覆盖从父类继承下来的元素。编译器知道表中每个元素对应是那个虚函数,因此调用时取出元素,通过 call 指令实现调用。观察 VC debug 版本的汇编代码,虚函数表的内容被编译到只读的 section(和其他常量字符串一起),每个元素代表的是函数的地址(这些元素是代码段起始部的一组 jump 语句的地址,类似中断向量表,用于跳到真正的函数体)。由于虚函数表位于只读 section 中,所以其元素(函数指针)是不能直接改写的。


看下面一个简单的例子,来看看虚函数:

[cpp]  view plain copy
  1. class Base  
  2. {  
  3. public:  
  4. virtual void func1(){}  
  5. };  
  6.   
  7. class Derive: public Base  
  8. {  
  9. public:  
  10. virtual void func1(){}  
  11. virtual void func2(){}  
  12. };  
  13.   
  14. int main()   
  15. {  
  16. Base base;  
  17. Derive derive1;  
  18. Derive derive2;  
  19. derive1.func1();  
  20. }  

在vc 2010下debug一下,可以看到一个Base对象和两个Derive对象的虚函数情况:



可以看到,derive1和derive2对应的虚表地址是一样的,都是0x00415844,说明相同类型的实例确实只有一份虚表,而不同类型的虚表当然是不一样的,

上图Base对象和Derive对象对应的虚表地址就不一样。


那如何获得虚表的地址呢,能直接通过对象的地址调用虚函数吗?(细节可以看看陈浩的博客:http://blog.csdn.net/haoel/article/details/1948051

下面再给给例子:


[cpp]  view plain copy
  1. class Base  
  2. {  
  3. public:  
  4.     virtual void func1()  
  5.     {  
  6.         cout<<"Base::func1"<<endl;  
  7.     }  
  8. };  
  9.   
  10. class Derive: public Base  
  11. {  
  12. public:  
  13.     virtual void func1()  
  14.     {   
  15.         cout<<"Derive::func1"<<endl;  
  16.     }  
  17.     virtual void func2()  
  18.     {   
  19.         cout<<"Derive::func2"<<endl;  
  20.     }  
  21. };  
  22. typedef void(*PFunc)(void);  
  23. int main()   
  24. {  
  25.     Derive *derive=new Derive;  
  26.     PFunc ptr1=(PFunc)*(int*)(*(int*)derive);  
  27.     ptr1();  
  28. }  

输出为:


可以看到,上面成功调用了子类的虚函数。上面的指针转换是啥意思呢,其实很好懂,先看一下debug时的这个信息:



可以看到,new出来的子类对象指针其实是指向虚表的,我们通过子类对象的地址就能直接找到虚表。*(int*)derive 的意思是取得虚表地址,

(指针的值是个地址,这个地址指向虚表),然后*(int*)(*(int*)derive) 是指把虚表地址转换成int* ,然后取虚表里的第一个值(数组首地址代表数组第一个元素,有印象没?)

ok,这个时候已经去到虚表里虚函数的地址了,然后再转换成对应的函数指针(PFunc)*(int*)(*(int*)derive)  ,最后再调用一把这个函数ptr1();


如果虚表里有多个函数呢,我该怎么调用呢,只需要在上面转换时加上便宜即可,比如,如果虚表里有两个函数,第二个函数的地址是:

PFunc ptr1=(PFunc)*(int*)(*(int*)derive+1);


如何通过对象指针获得成员变量呢,看看下面的代码:

[cpp]  view plain copy
  1. class A    
  2. {    
  3.     int value;    
  4. public:    
  5.     A(int n = 0) : value(n) {}    
  6.     virtual int GetValue()  {  return value;  }    
  7. };    
  8. int main() {  
  9.     A a;    
  10.     *((int *)&a+1) = 5;    
  11.     cout<<a.GetValue()<<endl;  
  12.     return 0;  
  13. }  

我故意将函数弄成虚函数,看看有虚函数时的成员是如何布局的:


可以看到成员变量在对象地址上加一个便宜即可,因为虚函数的指针占了前四个字节,所以便宜四个字节 :*((int *)a+1)即可获得成员变量。

但是这段代码其实是有问题的,在32位 centos下返回的仍然是0,这个跟编译器有关,所以用指针修改成员变量没啥意义。



总结:

1、如果有虚函数,对象的地址指向虚表(linux和windows下都一样)

2、如果有虚表,成员变量的地址为对象地址加一个虚表地址的偏移

3、相同类型的对象在内存里只有一份虚表

4、在子类的虚表里只会存子类的虚函数,没有父类的信息

5、this指针和对象的地址是同一个

6、通过对象地址获得成员地址,然后修改成员变量,这个在windows下可行,但linux下没效果 


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值