探索Delphi类与对象的内存结构

探索Delphi类与对象的内存结构

                                                                                                        

 

初次接触DELPHI对它提供的RAD快速编程模式颇感神奇,随手拖放及格控件设定些属性一个应用程序就诞生了,我正是被这种特性所吸引。随着深入,慢慢的窥探到了DELPHIVCL体系,知道了随手拖放背后隐藏的秘密:一切都起源于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

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

对象的内存结构

运行期是如何创建对象的呢,过程如下:

首先读取InstanceSize对象实例内存大小分配内存

class function TObject.NewInstance: TObject;

begin

  Result := InitInstance(_GetMem(InstanceSize));

end;

 

然后初始化对象的数据结构,将属性置为空,将接口方法表(包括父亲的)插入对象内存空间

class function TObject.InitInstance(Instance: Pointer): TObject;

{$IFDEF PUREPASCAL}

var

  IntfTable: PInterfaceTable;

  ClassPtr: TClass;

  I: Integer;

begin

  FillChar(Instance^, InstanceSize, 0);

  PInteger(Instance)^ := Integer(Self);  //将类地址存放在开始的四个字节中

  ClassPtr := Self;

  while ClassPtr <> nil do

  begin

    IntfTable := ClassPtr.GetInterfaceTable;

    if IntfTable <> nil then

      for I := 0 to IntfTable.EntryCount-1 do

  with IntfTable.Entries[I] do

  begin

    if VTable <> nil then

      PInteger(@PChar(Instance)[IOffset])^ := Integer(VTable); //根据接口表提供的偏移地址,在对象的相应位置存储接口的虚方法表的地址

  end;

    ClassPtr := ClassPtr.ClassParent;

  end;

  Result := Instance;

end;

随后会调用类的构造方法完成创建。

 

从对象的创建过程我们可以分析出对象的基本内存结构,如下面的类实例:

  TMyObject = class(TObject, IInterface)

  private

    FField1: Integer;

    FField2: Boolean;

  protected

    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

    function _AddRef: Integer; stdcall;

    function _Release: Integer; stdcall;

  public

    procedure DynamicMethod1; dynamic;

    procedure DynamicMethod2; dynamic;

    procedure VirtualMethod1; virtual;

    procedure VirtualMethod2; virtual;

  end;

 

内存结构如图:

                                     当一个派生一个子类后,由子类生成的对象又是什么情形呢,如下面实例:

                                       TMyObject2 = class(TMyObject)

  private

       FField3: Integer;

public

                                         procedure DynamicMethod1; override;

                                           procedure VirtualMethod1; override;

                                      end;

内存结构如图:

                                     如图,TMyObject2VirtualMethod1方法覆盖了父类的VirtualMethod1方法,故虚方法表中VirtualMethod1的指针指向了TMyObject2VirtualMethod1方法。TMyObject2DynamicMethod1方法覆盖了父类的DynamicMethod1方法,由于动态方法表只保存当前类的动态方法表,故表中只有一个DynamicMethod1方法指针,如果要访问DynamicMethod2方法则需要特定的方法到父类去搜索。

后记

面对对象的三个特性封装、继承、多态均体现在以上过程中,DELPHI正是通过这些机制满足面对对象编程语言的需要。当然还有更多复杂的情况,本人并未一一列举,有兴趣的朋友可以和我联系,一起继续探讨。另外以上分析均通过源代码所得到的结果,BORLAND官方并未有明确的文档显示这些结果的正确性,也无法保证在将来的版本中不会出现变动,以上结果都是在DELPHI7下取得。在实际应用当中请尽量根据RTTI开发,以免带来隐患。

 

                                                                                                           书呆子

                                                                                                           QQ:7878906  EMAIL7878906@qq.com

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值