C++对象内存布局测试总结

 

  http://hi.baidu.com/%D6%F2%C7%EF/blog/item/826d38ff13c32e3a5d6008e8.html

  上文是半年前对虚函数、虚拟继承的理解。可能有一些错漏。而且只是理解了比较简单的部分,表达也不够清晰,这次决定花的时间再做一次总结。

  对于普通的C++对象内存布局,简单得不得了,就不做总结了。这里只总结涉及到虚拟继承的情况。

    因为不同编译器对虚拟继承的实现采用不同的方式,所以要完整的分析是不可能的。这里只考虑VS2005/2008,还有简单涉及GCC编译器。

1、 单个虚拟继承

只是为了分析而已,实际中并没有太大的作用。跟虚拟继承相关的派生类对象的内存布局跟具体的编译器相关。

   (1)VS编译器:无论有无虚函数,必然含有虚基类表指针。虚基类表中的内容为本类实例的偏移和基类实例的相对偏移值。如果有虚函数,那么基类的虚函数表跟派生类的虚函数表是分开的。

在内存布局上,地址从低到高,顺序如下:派生类的虚函数表指针+虚基类表指针+派生类的成员变量+“间隔”(4个字节)+基类的虚函数表指针+基类的成员变量。派生类跟基类实例的位置关系跟普通继承正好相反。

    说明:“间隔”产生的原因是派生类重写了基类的虚函数。如果没重写,则这一项没有。"本类地址"指的是包含有虚基类的对象(或部分对象),也就是继承链上的直接子类对象的地址,本例比较简单,就是派生类对象地址。“本类地址跟虚基类表指针地址只差”,这个值经常是-4、0,-4表明“本类”还有一个虚函数表指针;0则表明“本类”的第一个4字节保存的就是虚基类表指针,没有虚函数表指针。

    

图 1 VS编译器—单个虚拟继承

   (2)GNU的GCC编译器:跟VS的编译器类似,有不同的地方是,虚基类表跟派生类的虚函数表合并。另外通过虚基类表指针往正负两个方向寻址,可以获得不同偏移值,也就是说有两个功能一样的虚函数表。不过在实际应用的时候,不知道虚基类表是否真的有用,测试了简单的情况发现编译器做了优化,根本就没有用虚基类表来寻址虚基类实例。

 

图 2 GCC编译器—单个虚拟继承

2、 虚拟继承多个基类

虚基类表要增加内容,有N个虚基类就有N项基类实例偏移值,再加上1项本类实例的偏移值,也就是N+1。

假设C虚拟继承了A类和B类,考虑最复杂的情况(都有虚函数),那么C类对象的内存布局如下

 

(VS编译器):

C类虚函数表指针+虚基类表指针+C类成员变量+A类间隔(4个字节) + A类虚函数表指针+ A类成员变量+ B类间隔(4个字节)+B类虚函数表指针+ B类成员变量。

说明:当派生类重写了该基类的虚函数,才会有“间隔”。“间隔”属于虚函数被重新实现了的虚基类,可能是一个标志,也有可能是在函数调用的时候用上。不是很清楚。

 

图 3 VS编译器—虚拟继承多个基类

(GCC编译器):

C类虚函数表指针(包含虚基类表) + C类成员变量 + A类虚函数表指针 +  A类成员变量 + B类虚函数表指针 + B类成员变量。

相比较执行,使用GCC编译器,派生类对象小一些。(图略)

3、 虚拟继承之菱形继承

这里的菱形继承指的是:B、C虚拟继承A,然后D普通继承B、C。

D类的对象的内存布局如下

(VS编译器)

B类虚函数表指针(该虚函数表包含D类独有的虚函数的地址)+B类虚基类表指针+B类成员变量+C类虚函数表指针+C类虚基类表指针+C类成员变量+D类成员变量+“间隔”+A类虚函数表指针+A类成员变量。

说明:如果A类的虚函数没有被重写,那么就没有“间隔”。

 

图 4 VS编译器—菱形继承

(GCC编译器)

把B、C类的虚函数表跟虚基类表合并就是了。(图略)

4、VS编译器,“间隔”的疑问

 “间隔”的问题,在没有虚函数的情况下,重写是没有“间隔”的,所以觉得可能跟虚函数有关,也就是说是为了实现多态,具体是用在哪个地方,做了简单的反汇编调试(父类指针指向子类对象,调用被子类重写了的虚函数),并没有发现哪里用到了“间隔”,可能要在复杂的调用才会用上吧,目前搞不清楚。

5、虚基类表的问题

通过反汇编调试发现在使用多态的时候,VS编译器会去使用虚基类表,用于寻址虚基类地址。而GCC编译器则没有这么做,测试了比较简单的情况,发现它做了优化,并没有利用虚基类表,而是直接在派生类对象地址上加上一个常数,获得虚基类实例的地址。

 

 

学习参考:

http://blog.csdn.net/haoel/archive/2008/10/15/3081328.aspx

http://blog.csdn.net/BlueDog/archive/2009/10/22/4711169.aspx

《深度探索C++对象模型》也是非常好的资料,只是该书较旧,而类对象布局又是编译器的个性属性,手上的编译器有可能不是按照书里的来实现(譬如书中强调虚函数表指针放在类对象的结尾,以便兼容C,而实际上编译器没有这么做,还有,数据成员的地址也没有像书中所说的那样),所以动手实践才有真理。

 

2010.8.19

cs_wuyg@126.com

posted on 2010-08-20 02:07  烛秋  阅读(5203)  评论(5编辑  收藏  举报