Delphi 类与对象内存结构浅析(上)

来源:互联网 发布:郑杭生 知乎 编辑:程序博客网 时间:2024/04/28 22:37

参考资料
李战.《悟透delphi》,第1章“DELPHI的原子世界”
 “探索Delphi类与对象的内存结构”

基本知识

动态内存与静态内存

当程序运行时,系统首先将所有数据装载入内存,完成初始化,然后从入口地址开始执行代码。

程序装载后即存在于内存空间中的数据我们称之为静态内存,运行过程中分配的内存我们称之为动态内存。

Delphi的类是由编译期间决定的,编译完成后即固定在程序中,所以类是存在于静态内存中(类信息应该是位于数据段,类的方法实现应该是位于代码段)。对象是由运行期间创建的,所以对象属于动态内存。

 

对象

DELPHI中的对象是一个指针,这个指针指向该对象在内存中所占据的一块空间。我们可以试着用sizeof函数获取对象的大小,结果是4字节,这正是一个32位指针的大小。而对象的真正大小应该用MyObject.InstanceSize获得。

 

对象空间

我们将对象指针指向的内存空间称为对象空间。对象空间的头4个字节是指向该对象直属类的虚方法地址表(VMT Vritual Method Table)。接下来的空间就是存储对象本身成员数据的空间,并按从该对象最原始祖先类的数据成员到该对象具体类的数据成员的总顺序,和每一级类中定义数据成员的排列顺序存储。

当一个对象产生时,系统会为该对象分配一块内存空间,然后将空间的头4个字节存储为指向类的VMT的指针,从而将该对象与对象的直属类联系起来。即使类并未定义任何虚方法,对象仍然会保存指向虚方法地址表的指针,只是VMT地址项的长度为零。

 

类空间

我们将类指针指向的内存空间称为类空间。类指针与VMT指针地址相同。在TObject中定义的虚方法地址(如DestroyFreeInstance等等)存储在相对VMT指针负方向偏移的空间中。在VMT的负方向偏移有76个字节的数据信息,它们是对象类的基本数据结构。而VMT是存储我们自己为类定义的虚方法地址的地方,它只是类数据结的构扩展部分。VMT前的76个字节的数据结构是DELPHI内定的,与编译器相关的,并且在将来的DELPHI版本中有可能被改变。

对我们的应用程序来说,类的数据是静态的数据。当编译器编译完成我们的应用程序之后,这些数据信息已经确定并已初始化。我们编写的程序语句可访问类数据中的相关信息,获得诸如对象的尺寸、类名或运行时刻的属性资料等等信息,或者调用虚方法以及读取方法的名称与地址等等操作。

 

VMT

每一个类都有对应的一个虚方法地址表(VMT Vritual Method Table),类的VMT保存从该类的原始祖先类派生到该类的所有类的虚方法的过程地址。虚拟方法表包括本身以及以上的父类所有的虚拟方法的地址,调用时直接指向地址即可,好处在于速度极快,不需要查询,缺点在于占用了额外的内存。

 

DMT

每一个类都有对应的一个动态方法地址表(DMT Dynamic Method Table),类的DMT只保存本身所包含的动态方法的过程地址,如果调用者的动态方法不属于自己,则根据索引号往上级父类遍历查询得到方法的地址,好处在于不用保存父类的动态方法从而节省了内存,缺点在于搜索带来的效率下降。

 

类的类型

DELPHI中我们用TObjectTComponent等等标识符表示类,它们在DELPHI的内部实现为各自的VMT数据。而用class of保留字定义的类的类型,实际就是指向相关VMT数据的指针。

 

TClass

System.pas单元中,TClass是这样定义的:

  TClass = class of TObject;

它的意思是说,TClassTObject的类。从概念上说,TClassTObject类的类型。但是,我们知道DELPHI的一个类代表着一个VMT数据项。因此,TClass可以认为是为VMT数据项定义的类型,实际上它就是一个指向VMT数据的指针类型!

有了类的类型,我们就可以将类赋值给使用“类的类型”声明的变量(即类变量),从而将类作为变量来使用。可以将类变量理解为一种特殊的对象,你可以象访问对象那样访问类变量的方法。

 

类方法

类方法,就是指在类的层次上调用的方法,它是用保留字class声明的方法。

在类方法中你也可使用self这一标识符,不过其所代表的含义与对象方法中的self是不同的。类方法中的self表示的是自身的类,即指向VMT的指针,而对象方法中的self表示的是对象本身,即指向对象数据空间的指针。

虽然,类方法只能在类层次上使用,但你仍可通过一个对象去调用类方法。例如,可以通过语句aObject.ClassName调用对象TObject的类方法ClassName,因为对象指针所指向的对象数据空间中的头4个字节又是指向类VMT的指针。相反,你不可能在类层次上调用对象方法,象TObject.Free的语句一定是非法的。

值得注意的是,构造函数是类方法,而析构函数是对象方法!原因很简单,在构造对象之前,对象还不存在,只存在类,创建对象只能用类方法。相反,删除对象一定是删除已经存在的对象,是对象被释放,而不是类被释放。

 

非静态方法

虚方法(Virtual)和动态方法(Dynamic)均为非静态方法,它们是用来实现面对对象的多态性的关键特性。通过这种特性,开发者可以根据需要在不同的子类中拥有不同的实现,从而使设计变得更加灵活。

 

虚方法

类的虚方法,就是用保留字vritual声明的方法。虚方法是实现对象多态性的基本机制。

 

动态方法

类的动态方法,就是用保留字dynamic声明的方法。动态方法只是Object Pascal语言提供的另一种可节约类存储空间的多态实现机制,但却是以牺牲调用速度为代价的。

 

内存结构
类空间

内存结构

类的内存结构是固定的,编译完成后就无法改变。它主要存储了类的基本信息,派生对象内存大小,虚方法列表,动态方法列表,公开属性和方法列表(published),接口列表,TObject类的一些方法等等有关于构建对象所必须的信息。结构如图所示:

 

类信息

信息的存储位置如下(SYSTEM单元中定义

标志

偏移量

含义

vmtSelfPtr

-76

指向虚方法表的指针

vmtIntfTable

-72

指向接口表的指针

vmtAutoTable

-68

指向自动化信息表的指针

vmtInitTable

-64

指向实例初始化表的指针

vmtTypeInfo

-60

指向类型信息表的指针,这里的数据对于RTTI来说非常重要,它指向一个PTypeInfo类型的指针,有兴趣可以看看TypInfo单元

vmtFieldTable

-56

指向域定义表的指针Published Field

vmtMethodTable

-52

指向方法定义表的指针(Published Method

vmtDynamicTable

-48

指向动态方法表的指针

vmtClassName

-44

指向类名字符串的指针

vmtInstanceSize

-40

对象实例的大小

vmtParent

-36

指向父类的指针

以下都是TOBJECT类的一些虚拟方法指针

vmtSafeCallException

-32 deprecated

 

vmtAfterConstruction

-28 deprecated

 

vmtBeforeDestruction

-24 deprecated

 

vmtDispatch

-20 deprecated

 

vmtDefaultHandler

-16 deprecated

 

vmtNewInstance

-12 deprecated

 

vmtFreeInstance

-8 deprecated

 

vmtDestroy

-4 deprecated

 

 

静态方法

类的静态方法在编译期间就决定了它的地址,类只为所有的派生的对象提供统一的一份静态方法表,不会为每个对象复制一份,所以不必关心静态方法的存储(实际上静态方法也是和动态方法有序的排列在一块的,顺序与方法的实现顺序有关)

 

非静态方法

虚方法(Virtual)和动态方法(Dynamic)均为非静态方法,它们是用来实现面对对象的多态性的关键特性。从语法上讲虚拟方法和动态方法是没有任何区别的,凡是声明了该两种类型的方法,在子类中都可以通过override关键字进行覆盖。但实际上二者的实现是存在巨大差别的:

vmtSelfPtr(虚方法表的指针)实际上就是指向TObject位置,所以类的虚拟方法是依次排在TObject所指向的位置之后。虚拟方法表包括本身以及以上的父类所有的虚拟方法的地址,调用时直接指向地址即可,好处在于速度极快,不需要查询,缺点在于占用了额外的内存。

 

 

vmtDynamicTable(动态方法表的指针)指向的是动态方法表,动态方法表的结构与虚方法表的结构有所不同。动态方法表则只保存自己本身所包含的动态方法表,如果调用者的动态方法不属于自己,则根据索引号往上级父类遍历查询得到方法的地址,好处在于不用保存父类的动态方法从而节省了内存,缺点在于搜索带来的效率下降。

 

注意:

只有定义为published访问级别的数据成员和方法才可以使用名称去访问,而定义为privateprotectedpublic访问级别的除外。

只有类型是类或接口的数据成员才可定义为published的访问级别,方法都是可以定义为published的。

凡是声明在类的 Published 部分的方法都可以通过调用 TObject.MethodName 获得方法的名字。

 

Published Field

vmtFieldTablePublished Field表)指向Published Field表有序排列,只存储当前类的PublishedField表,得到父类的Published Field表需要往上遍历。

注意:只有类型是类或接口的数据成员才可定义为published的访问级别。

 

 

接口表的指针

vmtIntfTable(接口表的指针)指向一块PInterfaceTable类型的接口信息表空间,vmtIntfTable只保存当前类所实现的接口表信息,不保存父类的接口表信息,创建对象时会根据vmtParent父类指针遍历获取所有父类的接口表信息插入对象内存空间。

 

 

原创粉丝点击