【转载】C++对象的内存分析(1)
思考:
1、本文提到了对象的内存结构中,没有成员函数,那么究竟对象又是如何找到成员函数的地址,从而来调用成员函数的呢?
---------------------------------------
转自:http://www.cnblogs.com/Binhua-Liu/archive/2010/06/16/1759019.html,感谢原作者!
介绍
虚函数表、虚指针、多态、重写(override)、虚析构、指针调整… 这些概念大家应该都不陌生,不过,除了了解概念和用法,你了解他们背后的实现的机制吗。 本文通过的C++类的对象内存进行分析,来讲解这些面向对象的特性是怎么实现的。本文的目的是为了更好的理解C++面向对象的特性的实质,而对于编译器实现的细节并不会过多的涉及。本文所指的C++,在没有特别说明时,特指VC++。
本文将首先将进行4个课题的研究,分别分析4个C++类的对象内存,最后,讲解构造函数,析构函数和虚析构函数的原理。将分为以下几个章节:
- Subject1:一个带虚函数的基类
- Subject2:从带虚函数的基类继承的子类
- Subject3:从不带虚函数的基类继承的子类
- Subject4:多重继承
- 构造函数与析构函数
Subject1:一个带虚函数的基类。
首先我们来分析一个带虚函数的基类对象的内存布局,厘清一些基本的问题,对于我们接下来分析继承以及多重继承的情况,是很有帮助的。
下面是我们要分析的CBasic类:
代码如下:
class CBasic { public : CBasic() { Array= new int [2]; } int i; int *Array; virtual int add( int a, int b) { return a+b; } virtual int minus( int a, int b) { return a-b; } void HelloWorld() { cout<< "hello world" <<endl; } }; |
并且在main函数中构造这个类的一个对象:
CBasic *b= new CBasic; |
分析该对象内存的最好工具是Visual Studio的Watch窗口。按F5进行调试,然后点击主菜单的Debug->Windows->Watch->Watch1,在Watch窗口中输入以下元素并观察:
仔细观察Watch1表中元素地址我们发现:b对象在内存中占12个字节;b对象的第一个元素为4字节长的虚函数指针(因此,虚函数指针地址和对象地址相同),第2个元素为4字节的整型i,第3个元素为4字节的整型指针Array。
我们还发现:虚函数指针指向一个虚函数表(虚函数表并不在b对象内存地址内),虚函数表是一个函数指针数组,第一个元素指向虚函数add,第二个元素指向虚函数minus。
我们可以画出b对象的内存结构图了:
很显然,在调用b对象的2个虚函数add和minus时,是通过虚函数指针找到虚函数表,再从虚函数表索引到函数的地址的。但是,下面2个问题我们还没有结论:
1)普通成员函数HelloWorld是怎么被调用的呢?在对象的内存和虚表中,我们没有看到任何关于它的信息。
2)CBasic类每个对象都有一个独有的虚函数表吗?还是所有对象共有一个呢?
首先来看第一个问题,看下面的代码:
CBasic *b= new CBasic; b->HelloWorld(); b->add(1,2); b->minus(3,4); |
下面我们查看这段代码的汇编:按F5调试,鼠标放在以上代码处,点击右键,选择Go to disassembly,我们可以看到以下片段
b->HelloWorld(); 012146F6 mov ecx,dword ptr [ebp-14h] 012146F9 call CBasic::HelloWorld (12112B2h) |
从汇编代码我们看出:对普通成员函数HelloWord的调用在编译时转换为对函数地址的直接调用了。因此,在对象内存中不需要保持关于它的地址信息。
这和虚函数的调用方式是完全不同的,下面的汇编代码片段再次证明了虚函数是通过虚函数指针和虚函数表来调用的:
b->add(1,2); 012146FE mov esi,esp 01214700 push 2 01214702 push 1 01214704 mov eax,dword ptr [ebp-14h] 01214707 mov edx,dword ptr [eax] 01214709 mov ecx,dword ptr [ebp-14h] 0121470C mov eax,dword ptr [edx] 0121470E call eax |
b->minus(3,4); 01214717 mov esi,esp 01214719 push 4 0121471B push 3 0121471D mov eax,dword ptr [ebp-14h] 01214720 mov edx,dword ptr [eax] 01214722 mov ecx,dword ptr [ebp-14h] 01214725 mov eax,dword ptr [edx+4] 01214728 call eax |
因此,上面的调用可以认为被编译器翻译成了如下代码
CBasic *b= new CBasic; b->12112B2h(); b->__vfptr[0](1,2); b->__vfptr[1](3,4); |
我们来看第二个问题,构造2个CBasic对象:b和b1,在wacth窗口中观察b->__vfptr和b1->__vfptr的值,它们是相同的,说明它们指向同一个虚表。很显然,类所有对象使用的虚表的结构都是相同的,编译器没有必要为每个对象都独立地分配一个虚表,而是让他们公用一个。
SUBJECT1:总结
让我们对一个带虚函数的类的内存布局做一个总结:
1)虚函数指针存储在对象内存的最开始4个字节中(这是C++规范决定的)【1】,虚函数指针指向虚表地址。
2)虚函数表是存储函数指针的数组,按照虚函数定义的顺序存储了所有虚函数的实际地址。虚函数被调用时,程序通过虚函数指针索引到虚函数表,再通过虚函数表索引到虚函数的实际地址。虚表并不是对象内存的一部分,类的所有对象共有一个虚函数表【2】.
3)对普通成员函数的调用,编译时就直接编译成对函数地址的直接调用。
4)‘值类型’数据直接存储在对象的内存中;’引用类型’(对象,堆上分配的数组等)只有指向其实际地址的指针存储在对象内存中。
注释
[1]在没有继承或者单继承的情况下,虚函数指针只有一个且始终处于最前端。在多重继承下,可能有好几个虚函数指针,主基类的虚函数指针位于最前端。
[2]虚函数表是类关联信息的一部分。