既然DELPHI对象是指向一块内存空间的指针,那么,代表对象的这快内存空间又有怎样的数据结构呢?就把南瓜切开来看看啰。
我们将对象指针指向的内存空间称为对象空间。对象空间的头4个字节是指向该对象直属类的虚方法地址表(VMT – Vritual Method Table)。接下来的空间就是存储对象本身成员数据的空间,并按从该对象最原始祖先类的数据成员到该对象具体类的数据成员的总顺序,和每一级类中定义数据成员的排列顺序存储。
每一个类都有对应的一张VMT,类的VMT保存从该类的原始祖先类派生到该类的所有类的虚方法的过程地址。类的虚方法,就是用保留字vritual声明的方法。虚方法是实现对象多态性的基本机制。虽然,用保留字dynamic声明的动态方法也可实现对象的多态性。但这样的方法不保存在VMT中。用保留字dynamic声明的动态方法只是Object Pascal语言提供的另一种可节约类存储空间的多态实现机制,但却是以牺牲调用速度为代价的。
即使,我们自己并未定义任何类的虚方法,但该类的对象仍然存在指向虚方法地址表的指针,只是地址项的长度为零。可是,在TObject中定义的那些虚方法,如Destroy、FreeInstance等等,又存储在什么地方呢?原来,他们的方法地址存储在相对VMT指针负方向偏移的空间中。在VMT的负方向偏移有76个字节的数据信息,它们是对象类的基本数据结构。而VMT是存储我们自己为类定义的虚方法地址的地方,它只是类数据结的构扩展部分。VMT前的76个字节的数据结构是DELPHI内定的,与编译器相关的,并且在将来的DELPHI版本中有可能被改变。
下面的对象和类的结构草图展示了对象和类之间的一些关系。
 TObject中定义的有关类信息或对象运行时刻信息的函数和过程,一般都与类的数据结构相关。
在DELPHI中我们用TObject、TComponent等等标识符表示类,它们在DELPHI的内部实现为各自的VMT数据。而用class of保留字定义的类的类型,实际就是指向相关VMT数据的指针。
对我们的应用程序来说,类的数据是静态的数据。当编译器编译完成我们的应用程序之后,这些数据信息已经确定并已初始化。我们编写的程序语句可访问类数据中的相关信息,获得诸如对象的尺寸、类名或运行时刻的属性资料等等信息,或者调用虚方法以及读取方法的名称与地址等等操作。
当一个对象产生时,系统会为该对象分配一块内存空间,并将该对象与相关的类联系起来。于是,在为对象分配的数据空间中的头4个字节,就成为指向类VMT数据的指针。
我们再来看看对象是怎样诞生和灭亡的。我们都知道,用下面的语句可以构造一个最简单对象:
  AnObject := TObject.Create;
编译器将其编译实现为,用TObject对应的类数据信息为依据,调用TObject的Create构造函数。而TObject的Create构造函数调用了系统的ClassCreate过程。系统的ClassCreate过程又通过调用TObject类的虚方法NewInstance。调用TObject的NewInstance方法的目的是要建立对象的实例空间。TObjec类的NewInstance方法将根据编译器在类信息数据中初始化的对象实例尺寸(InstanceSize),调用GetMem过程为该对象分配内存。然后调用TObject类InitInstance方法将分配的空间初始化。InitInstance方法首先将对象空间的头4个字节初始化为指向对象类的VMT的指针,然后将其余的空间清零。建立对象实例最后,还调用了一个虚方法AfterConstruction。最后,将对象实例数据的地址指针保存到AnObject变量中,这样,AnObject对象就诞生了。
同样,用下面的语句可以消灭一个对象:
  AnObject.Destroy;
TObject的析构函数Destroy被声明为虚方法,这可以让某些有个性的对象选择自己的死亡方法。Destory方法首先调用了BeforeDestruction虚方法,然后调用系统的ClassDestroy过程。ClassDestory过程又通过调用对象的FreeInstance虚方法。由FreeInstance方法调用FreeMem过程释放对象的内存空间。就这样,一个对象就在系统中消失。
对象的析构过程比对象的构造过程简单,就好像生命的诞生是一个漫长的孕育过程,而死亡却相对的短暂,这似乎是一种必然的规律。
在对象的构造和析构过程中,调用了NewInstance和FreeInstance两个虚函数,来创建和释放对象实例的内存空间。之所以将这两个函数声明为虚函数,是为了能让用户在编写需要用户自己管理内存的特殊对象类时(如在一些特殊的工业控制程序中),有扩展的空间。
而将AfterConstruction和BeforeDestruction声明为虚函数,也是为了将来派生的类在产生对象之后,有机会让新诞生的对象呼吸第一口新鲜空气,而在对象消亡之前可以允许对象交待最后的遗言,这都是合情合理的事。例如,我们熟悉的TForm对象和TdataModule对象的OnCreate事件和OnDestroy事件,就是分别在这两个重载的虚函数中触发的。
此外,TObjec还提供了一个Free方法。它不是虚方法,它是为了在搞不清对象指针是否为空(nil)的情况下,也能安全释放对象而专门提供的。当然,搞不清对象指针是否是否为空,本身就有程序逻辑不清晰的问题。不过,任何人都不是完美的,都可能犯错,使用Free能避免偶然的错误也是件好事。然而,编写正确的程序不能一味依靠这样的解决方法,还是应该以保证程序的逻辑正确性为编程的第一目标。
有兴趣的朋友可以读一读System单元的原代码,其中,大量的代码是用汇编语言书写的。细心的朋友可以发现,TObject的构造函数Create和析构函数Destory竟然没有写任何代码。其实,在调试状态下通过Debug的CPU窗口,可清楚地反映出Create和Destory的汇编代码。我想,可能是因为缔造DELPHI的大师门不想将过多复杂的东西提供给用户。他们希望用户在简单的概念上编写应用程序,将复杂的工作隐藏在系统的内部由他们来承担。所以,在编写System.pas单元时特别将这两个函数的代码去掉,让用户认为TObject是万物之源,用户派生的类完全从虚无中开始,这本身并没有错。第三节  TClass
在System.pas单元中,TClass是这样定义的:
  TClass = class of TObject;
它的意思是说,TClass是TObject的类。因为TObject本身就是一个类,所以TClass就是所谓的类的类。
从概念上说,TClass是类的类型,即,类之类。但是,我们知道DELPHI的一个类,代表着一项VMT数据。因此,类之类可以认为是为VMT数据项定义的类型,其实,它就是一个指向VMT数据的指针类型!
在以前传统的C++语言中,是不能定义类的类型的。对象一旦编译就固定下来,类的结构信息已经转化为绝对的机器代码,在内存中将不存在完整的类信息。一些较高级的面向对象语言才可支持对类信息的动态访问和调用,但往往需要一套复杂的内部解释机制和较多的系统资源。而DELPHI的Object Pascal语言吸收了一些高级面向对象语言的优秀特征,又保留可将程序直接编译成机器代码的传统优点,比较完美地解决了高级功能与程序效率的问题。
正是由于DELPHI在应用程序中保留了完整的类信息,才能提供诸如as和is等在运行时刻转换和判别的高级面向对象功能,而类的VMT数据在其中起了关键性的核心作用。有兴趣的朋友可以读一读System单元的AsClass和IsClass两个汇编过程,他们是as和is操作符的实现代码,这样可以加深对类和VMT数据的理解。
有了类的类型,就可以将类作为变量来使用。可以将类的变量理解为一种特殊的对象,你可以象访问对象那样访问类变量的方法。例如:我们来看看下面的程序片段:
type
  TSampleClass = class of TSampleObject;
  TSampleObject = class( TObject )
  public
    constructor Create;
    destructor Destroy; override;
    class function GetSampleObjectCount:Integer;
    procedure GetObjectIndex:Integer;
  end;var
  aSampleClass : TSampleClass;
  aClass : TClass;在这段代码中,我们定义了一个类TSampleObject及其相关的类类型TSampleClass,还包括两个类变量aSampleClass和aClass。此外,我们还为TSampleObject类定义了构造函数、析构函数、一个类方法GetSampleObjectCount和一个对象方法GetObjectIndex。
首先,我们来理解一下类变量aSampleClass和aClass的含义。
显然,你可以将TSampleObject和TObject当作常量值,并可将它们赋值给aClass变量,就好象将123常量值赋值给整数变量i一样。所以,类类型、类和类变量的关系就是类型、常量和变量的关系,只不过是在类的这个层次上而不是对象层次上的关系。当然,直接将TObject赋值给aSampleClass是不合法的,因为aSampleClass是TObject派生类TSampleObject的类变量,而TObject并不包含与TSampleClass类型兼容的所有定义。相反,将TSampleObject赋值给aClass变量却是合法的,因为TSampleObject是TObject的派生类,是和TClass类型兼容的。这与对象变量的赋值和类型匹配关系完全相似。
然后,我们再来看看什么是类方法。
所谓类方法,就是指在类的层次上调用的方法,如上面所定义的GetSampleObjectCount方法,它是用保留字class声明的方法。类方法是不同于在对象层次上调用的对象方法的,对象方法已经为我们所熟悉,而类方法总是在访问和控制所有类对象的共同特性和集中管理对象这一个层次上使用的。
在TObject的定义中,我们可以发现大量的类方法,如ClassName、ClassInfo和NewInstance等等。其中,NewInstance还被定义为virtual的,即虚的类方法。这意味作你可以在派生的子类中重新编写NewInstance的实现方法,以便用特殊的方式构造该类的对象实例。
在类方法中你也可使用self这一标识符,不过其所代表的含义与对象方法中的self是不同的。类方法中的self表示的是自身的类,即指向VMT的指针,而对象方法中的self表示的是对象本身,即指向对象数据空间的指针。虽然,类方法只能在类层次上使用,但你仍可通过一个对象去调用类方法。例如,可以通过语句aObject.ClassName调用对象TObject的类方法ClassName,因为对象指针所指向的对象数据空间中的头4个字节又是指向类VMT的指针。相反,你不可能在类层次上调用对象方法,象TObject.Free的语句一定是非法的。
值得注意的是,构造函数是类方法,而析构函

解决方案 »

  1.   

    然而,正是由于在DELPHI中有动态的类的类型信息,有真正虚拟的类方法,以及构造函数是基于类实现的等等这些关键概念,才可实现虚拟的构造函数。对象是由类产生的,对象就好象成长中的婴儿,而类就是它的母亲,婴儿自己的确不知道自己将来会成为什么样的人,可是母亲们却用各自的教育方法培养出不同的人,道理是相通的。
    都知道强大的VCL是DELPHI得以成功的基础之一,而所有VCL的鼻祖是TComponent类。在TComponent类的定义中,构造函数Create被定义为虚拟的。这能使不同类型的控件实现各自的构造方法,这就是TClass创造的类之类概念的伟大,也是DELPHI的伟大。第四节  运用TObject的方法
    我们先来看看TObject的各个方法都是些什么东东。简要列示如下:
        constructor Create;
    TObject类的构造函数,用于建立对象。
        procedure Free;
    安全释放对象数据空间。
        class function InitInstance(Instance: Pointer): TObject;
    初始化新建对象的数据空间。
        procedure CleanupInstance;
    在对象被释放前清除对象的数据空间。
        function ClassType: TClass;
    获得对象直属的类。
        class function ClassName: ShortString;
    获得对象直属类的名称。
        class function ClassNameIs(const Name: string): Boolean;
    判断对象直属类的名称是否是指定的名称。
        class function ClassParent: TClass;
    获得对象或类的上一代类,即父类。
        class function ClassInfo: Pointer;
    获得对象类的运行时类型信息(RTTI),一般用于Tpersistent类。
        class function InstanceSize: Longint;
    获得对象实例的大小。
        class function InheritsFrom(AClass: TClass): Boolean;
    判断对象或类是否是从指定的类派生的。
        class function MethodAddress(const Name: ShortString): Pointer;
    获得对象或类指定方法名称的调用地址。该方法必须是published的。
        class function MethodName(Address: Pointer): ShortString;
    获得对象或类指定方法地址的方法名称。该方法必须是published的。
        function FieldAddress(const Name: ShortString): Pointer;
    获得对象指定属性名称的访问地址指针。
        function GetInterface(const IID: TGUID; out Obj): Boolean;
    获得对象支持指定接口标识的接口。
        class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;
    获得对象或类指定接口标识的接口项。
        class function GetInterfaceTable: PInterfaceTable;
    获得对象或类支持的所有接口项的信息表。
        function SafeCallException(ExceptObject: TObject;  ExceptAddr: Pointer): HResult; virtual;
    支持接口对象safecall调用异常处理虚方法,常被接口对象重载。
        procedure AfterConstruction; virtual;
    对象建立后首先被调用的虚方法,供派生类对象重载以初始化新对象。
        procedure BeforeDestruction; virtual;
    对象释放前最后被调用的虚方法,供派生类对象重载以清理对象数据。
        procedure Dispatch(var Message); virtual;
    对象的消息处理方法,支持Windows等消息处理。
        procedure DefaultHandler(var Message); virtual;
    缺省的消息处理方法。
        class function NewInstance: TObject; virtual;
    分配对象实例空间的虚方法。
        procedure FreeInstance; virtual;
    释放对象实例空间的虚方法。
        destructor Destroy; virtual;
    对象的析构虚方法,用于消灭对象。这些方法在DELPHI的帮助文档中都有描述。有些方法已在前面简单介绍过。由于TObject的方法也比较多,一时也讲不完。就挑一两个说说其用法吧,其他的在后面用到时再细细道来也不迟。
    就说说MethodAddress和FieldAddress对象方法吧。
    在使用DELPHI开发程序的过程中,我们经常会与VCL的属性和事件打交道。我们添加元件到设计窗口中,设置元件的相关属性,为元件的各种事件编制处理事件的方法。然后,轻轻松松地编译,程序就诞生了,一切都是可视化的。
    我们知道,在设计时DELPHI将元件的数据成员(包括字段、属性和事件)等信息存储在*.DFM文件中,并将其作为资源数据编译到最终的执行程序中。DELPHI的编译过程同时也将源程序中类的结构信息和代码也编译到执行程序中,这些信息在运行时可以由程序访问的。
    DELPHI的程序在运行时创建的Form或DataModule等对象时,首先建立该对象。接着,从相应的资源数据中读取设计时保留的数据成员信息,并使用FieldAddress方法获取数据成员的访问地址。然后,用设计时定义的值初始化该数据成员。如果是事件,则再调用MethodAddress获取事件处理程序的调用地址,并初始化该事件。这样就完成了设计时的数据代码关系到运行时的数据代码关系的映射,有点儿象动态连接过程。
    原来,元件的某些的数据成员和方法是可以用名称去访问的,就是使用FieldAddress和MethodAddress方法。其实,这种功能是在最基础的TObject中就支持的。当然,只有定义为published访问级别的数据成员和方法才可以使用名称去访问,而定义为private、protected和public访问级别的除外。
    注意,只有类型是类或接口的数据成员才可定义为published的访问级别,方法都是可以定义为published的。对于从TPersistent继承的那些对象类,如果没有特别声明数据成员和方法的访问级别的,则缺省是published的。例如,TForm类是Tpersistent派生下来的,一个典型的Form类的定义中,由DELPHI的IDE自动维护和生成的那些数据成员和方法,缺省都是published的。因为,TPersistent类使用了特殊的{$M+}编译选项。
    知道这层内幕之后,我们也可以自己使用这些方法来实现一些有意义的功能。第五节  对象的消息处理机制
    TObject的定义中,有两个方法值得我们注意,就是:
        procedure Dispatch(var Message); virtual;
        procedure DefaultHandler(var Message); virtual;
    这两个方法是DELPHI的VCL强大的消息处理机制的基础,Windows的各种消息最终都是通过这两个个方法处理掉的。
    在讲述这一问题之前,有必要先说明一下什么是消息。
    从广义上将,消息就是信息的传递,一个对象将自己知道的事情通知其他对象。每个对象可以根据得到的消息做出相应的反应。消息在现实世界中普遍存在,故事、新闻、命令、报告等等,当然也包括流言蜚语。在程序中表现为数据访问、过程调用、方法调用、事件触发和通讯协议等等。
    而我们今天讨论的消息是狭义的消息,这种消息就是对象间的一种通讯协议。这种消息沟通机制的特点是,相关对象之间不会象变量访问和方法调用那样是固定的耦合关系,而是非常自由和松散的关系。采用这种消息机制,对象之间是的通讯方式是统一的,而消息的内容是多种多样的,一组通讯的对象之间可以约定自己的消息格式和含义。虽然,一个对象可以和将消息发送给任何对象,也可以接收任何对象发来的消息,但对象一般只处理和发送自己关心的消息。
    在Windows中的窗口、任务和进程等对象间的信息沟通,都普遍采用这种消息机制。实际上,消息机制是Windows的基础之一。而DELPHI对象的消息处理机制一开始就是为了支持Windows消息而设计的,特别是用于窗口类的控件(即从TWinControl继承的控件)。但这种消息机制已经能够让所有的TObject对象采用这种方式通讯。如,我们熟悉的TLabel虽然不是一个窗口控件,但仍然能收到Windows发来的消息。当然,Windows是不会给一个TLabel发送消息的,那是DELPHI帮的忙。而TObject的Dispatch方法在这一个过程中起了关键性作用。
    我们知道,DELPHI将Windows的消息描述为是一个联合结构,也叫变体结构。消息结构的第一个成员是一个四字节的整数,是区分消息类别的标识。其余的数据成员是根据消息类别的不同而有不同的定义。正是因为其余的成员是可以自由定义的,才使得消息处理机制有良好的扩展性。要知道,Windows有几千种不同类型的消息,DELPHI也自己扩展了若干种消息。随着软件版本的发展,消息的种类还会不断增加。
    关心某种消息的对象类会为指定的消息定义一个消息处理方法,消息处理方法是用保留字message来声明的。例如:
      TMouseObject = class(TObject)
      public
        procedure WMMouseMove(var Msg:TMessage); message WM_MOUSEMOVE;
        procedure WMLButtonDown(var Msg:TMessage); message WM_LBUTTONDOWN;
      end;
    DELPHI的编译器将根据message保留字识别消息处理方法,并生成一个消息标识到该对象方法的映射表,连接到最终的执行程序中。事实上在DELPHI的内部,消息处理方法是用dynamic方法的机制实现的。前面我们说过,dynamic类型的方法是DELPHI的另一种虚方法,是可以重载以实现对象类的多态性。事实上,dynamic方法就是根据方法的序号找到调用地址的,这与根据消息ID找到各自的消息处理地址是没有什么本质区别的。因此,消息处理方法是可以由子类重载的,这可以让继承的对象实现自己的消息处理。不过,这种重载的语义与dynamic的重载有些不同。消息处理方法是按消息标识来重载的,即按message保留字后面的值。虽然,子类的消息处理方法的名称可以不同,只要消息标识相同即可实现重载。例如:
      TNewMouseObject = class(TMouseObject)
      public
        procedure MouseMove(var Msg:TMessage); message WM_MOUSEMOVE;
        procedure MouseDown(var Msg:TMessage); message WM_LBUTTONDOWN;
      end;
    其中,MouseMove方法重载了父类的WMMouseMove方法,而MouseDown重载了WMLButtonDown方法。当然,你也可以完全按dynamic的语义来定义重载:
      TNewMouseObject = class(TMouseObject)
      public
        procedure WMMouseMove(var Msg:TMessage); override;
        procedure WMLButtonDown(var Msg:TMessage); override;
      end;
    虽然,这没有任何错误,但我们很少这样写。这里只是要向大家说明message与dynamic的本质相同之处,以加
      

  2.   

    好,长知识,收藏,以后有什么好文章可以邮寄给我吗?
    My E_mail:[email protected]  谢谢!!
      

  3.   

    多谢,这对我很有帮助![email protected]