探索Delphi类与对象的内存结构
初次接触DELPHI对它提供的RAD快速编程模式颇感神奇,随手拖放及格控件设定些属性一个应用程序就诞生了,我正是被这种特性所吸引。随着深入,慢慢的窥探到了DELPHI的VCL体系,知道了随手拖放背后隐藏的秘密:一切都起源于VCL的对象体系,一切都是面对对象的编程思想。Object pascal就是是怎样实现这个体系的呢,它究竟是如何将面对对象的特性表现出来的呢,Delphi的类和对象究竟是以什么样的形式存在的呢。带着这些问题我翻阅了一些书籍,也借鉴了一些网友的成果,做了下面的探索。
动态内存与静态内存
程序需要执行必须先装载入内存,任何程序表现的数据都存在内存中。当程序运行时,系统首先将所有数据装载入内存,完成初始化,然后从入口地址开始执行代码。程序装载后即存在于内存空间中的数据我们称之为静态内存,运行过程中分配的内存我们称之为动态内存。Delphi的类是由编译期间决定的,编译完成后即固定在程序中,所以类是存在于静态内存中。对象是由运行期间创建的,所以对象属于动态内存。
注意:后面所提到的TObject均为泛指所有类,而非真正的TObject类
程序运行示意图
类的内存结构
类的内存结构是固定的,编译完成后就无法改变。它主要存储了类的基本信息,派生对象内存大小,虚方法列表,动态方法列表,公开属性和方法列表(published),接口列表,TObject类的一些方法等等有关于构建对象所必须的信息。这些信息的存储位置在SYSTEM单元中有定义:
vmtSelfPtr = -76; 指向虚方法表的指针
vmtIntfTable = -72; 指向接口表的指针
vmtAutoTable = -68; 指向自动化信息表的指针
vmtInitTable = -64; 指向实例初始化表的指针
vmtTypeInfo = -60; 指向类型信息表的指针,这里的数据对于RTTI来说非常重要,它指向一个PTypeInfo类型的指针,有兴趣可以看看TypInfo单元
vmtFieldTable = -56; 指向域定义表的指针(我开始认为是Published Field,但实际查询时却为NIL)
vmtMethodTable = -52; 指向方法定义表的指针(Published)
vmtDynamicTable = -48; 指向动态方法表的指针
vmtClassName = -44; 指向类名字符串的指针
vmtInstanceSize = -40; 对象实例的大小
vmtParent = -36; 指向父类的指针
vmtSafeCallException = -32 deprecated; 以下都是TOBJECT类的一些虚拟方法指针
vmtAfterConstruction = -28 deprecated;
vmtBeforeDestruction = -24 deprecated;
vmtDispatch = -20 deprecated;
vmtDefaultHandler = -16 deprecated;
vmtNewInstance = -12 deprecated;
vmtFreeInstance = -8 deprecated;
vmtDestroy = -4 deprecated;
如果获取对象大小,可以使用以下代码:
Result := PInteger(Integer(TObject) + vmtInstanceSize)^;
其他各项可以依此类推。
l 静态方法
类的静态方法在编译期间就决定了它的地址,类只为所有的派生的对象提供统一的一份静态方法表,不会为每个对象复制一份,所以不必关心静态方法的存储(实际上静态方法也是和动态方法有序的排列在一块的,顺序与方法的实现顺序有关)
l 非静态方法
虚方法(Virtual)和动态方法(Dynamic)均为非静态方法,它们是用来实现面对对象的多态性的关键特性。通过这种特性,开发者可以根据需要在不同的子类中拥有不同的实现,从而使设计变得更加灵活。在具体实现中,编译器只需要将简单的改变表中方法指针的指向即可达到目的。从语法上讲虚拟方法和动态方法是没有任何区别的,凡是声明了该两种类型的方法,在子类中都可以通过override关键字进行覆盖。但实际上二者的实现是存在巨大差别的:
vmtSelfPtr(虚方法表的指针)实际上就是指向TObject位置,所以类的虚拟方法是依次排在TObject所指向的位置之后。
vmtDynamicTable(动态方法表的指针)指向的是动态方法表,动态方法表的结构与虚方法表的结构有所不同。
两者实现方式的不同体现了两者作用的不同。虚拟方法表包括本身以及以上的父类所有的虚拟方法的地址,调用时直接指向地址即可,好处在于速度极快,不需要查询,缺点在于占用了额外的内存。动态方法表则只保存自己本身所包含的动态方法表,如果调用者的动态方法不属于自己,则根据索引号往上级父类遍历查询得到方法的地址,好处在于不用保存父类的动态方法从而节省了内存,缺点在于搜索带来的效率下降。
l 接口表的指针
vmtIntfTable(接口表的指针)指向一块PInterfaceTable类型的接口信息表空间,vmtIntfTable只保存当前类所实现的接口表信息,不保存父类的接口表信息,创建对象时会根据vmtParent父类指针遍历获取所有父类的接口表信息插入对象内存空间。
l Published Method表
vmtMethodTable(Published Method表)指向Published Method表有序排列,只存储当前类的Published Method表,得到父类的Published Method表需要往上遍历。