文章详情

  • 游戏榜单
  • 软件榜单
关闭导航
热搜榜
热门下载
热门标签
php爱好者> php文档>剖析delphi中的构造和析构

剖析delphi中的构造和析构

时间:2010-09-17  来源:chulia

http://www.sudu.cn/info/html/edu/20071227/53749.html

 

  • 剖析Delphi中的构造和析构

    1 Delphi中的对象模型: 2
    1.1 对象名表示什么? 2
    1.2 对象存储在哪里? 2
    1.3 对象中存储了什么?他们是怎么存储的? 3
    2 构造函数和创建对象 5
    2.1 什么是构造函数?(“特别的”类方法) 5
    2.2 对象的创建的全过程 5
    2.3构造函数另类用法(使用类引用实现构造函数的多态性) 6
    3 析构函数和销毁对象 7
    3.1 什么是析构函数(“天生的”虚方法) 7
    3.2 对象销毁的全过程 7
    3.3 destroy, free, freeAndNil, release用法和差别 7
    4 VCL构造&析构体系结构 8
    5 正确使用构造函数和析构函数 9

    剖析Delphi中的构造和析构
    摘  要: 本文通过对VCL/RTL的研究,来剖析构造函数和析构函数的实现机制和VCL中对象的体系结构,并说明怎么正确地创建和释放对象。
    关键字: 构造,析构,创建对象,销毁对象,堆,栈,多态。
    作  者: majorsoft
     
    问题
          Delphi中构造函数和析构函数的实现机制是什么?和C++有何不同?怎么做到正确地创建和释放对象?
    解决思路
    怎么正确使用构造和析构是我们在使用Delphi过程中经常遇见的问题,在大富翁论坛中的Oriented Pascal栏目有不少相关帖子(详见相关问题),本人也曾遇见过类似的问题,下面通过对VCL/RTL原始码的研究,来理解构造函数和析构函数的实现机制。
    1 Delphi中的对象模型:
    1.1 对象名表示什么?
    和C++不同,Delphi中的对象名(也能称做变量)表示对象的引用,并不表示对象本身,相当于指向对象的指针,这就所谓的“对象引用模型”。如图所示:
             Obj(对象名)                实际的对象
     
    Vmt 入口地址   

    数据成员

     

     

                             图1对象名引用内存中的对象
    1.2 对象存储在哪里?
    每个应用程式将分配给其运行的内存分为四个区域:
     
    代码区(Code area)   
    全局数据区(data area)   
    堆区(heap area)   
    栈区(stack area) 

     

     

     

                          图2  程式内存空间
    代码区:存储程式中程式代码,包括所有的函数代码
    全局数据区:存储全局数据。
    堆区:又叫“自由存储区”,存储动态数据(在Delphi中包括对象和字符串)。作用域为整个应用程式的整个生命周期直到调用了析构方法。
    栈区:又叫“自动存储区”存储程式中的局部数据,在C++中,局部变量实际上是auto类型的变量。作用域为函数内部,函数调用完系统就即时回收栈空间。
    在C++中,对象既可创建在堆(heap)上,也能创建在栈(stack)中,还能在全局数据中创建对象,故C++有全局对象、局部对象、静态对象和堆对象四种对象之说。而在Delphi中,所有的对象都是建立堆(heap)存储区上,所以Delphi构造函数不能自动被调用,而必须由程式员自己调用(在设计器拖动组件,此时对象由Delphi创建)。下面的程式说明Delphi和C++中创建对象的差别:
    在Delphi中:
    Procedure CreateObject(var FooObjRef:TFooObject);
       begin
    FooObjRef:=TfooObject.create;
    //由程式员调用,过程调用完之后,对象依然存在.不必进行拷贝
    FooObject.caption=’I am created in stack of CreateObject()’;
       End;
       而在C++中:
       TfooObject CreateObject(void);
       { 
    TfooObject FooObject;//创建局部对象
    // static TfooObject FooObject;//创建静态局部对象
     //对象自动调用默认的构造函数进行创建,对象此时在函数栈中创建
    FooObject.caption=’I am created in stack of CreateObject()’;
    return FooObject;
    //返回的时候进行了对象拷贝,原来创建的对象随函数的调用结束后,自动销毁}
       TfooObject fooObject2;//创建全局对象。
    void main();
       { TFooObject* PfooObjec=new TfooObject;
     //创建堆对象。函数调用完之后,对象依然存在,不必进行拷贝。}
    1.3 对象中存储了什么?他们是怎么存储的?
     和C++不同的是,Delphi中的对象只存储了数据成员和虚拟方法表(vmt)的入口地址,而没有存储方法,如图所示:
                 对  象              虚拟方法表             代码段
     
    Vmt地址   
    name:String
    width:integer;
     ch1:char;
    …   
    Proc1   
    Func1    
    …   
    procn   
    funcn 

     

                                                                …
                     
                                     图 3 对象的结构                …
    也许你对上面的说法存在着些疑问,请看下面的程式:
    TsizeAlignTest=class
     private
       i:integer;
       ch1,ch2:char;
       j:integer;
     public
       procedure showMsg;
       procedure virtMtd; virtual;
     end;

     memo1.Lines.Add(inttostr(sizeTest.InstanceSize)+:InstanceSize);
     memo1.Lines.Add(inttostr(integer(sizeTest))+<-start Addr);
     memo1.Lines.Add(inttostr(integer(@(sizeTest.i)))+<-sizeTest.i);
     memo1.Lines.Add(inttostr(integer(@(sizeTest.ch1)))+<-sizeTest.ch1);
     memo1.Lines.Add(inttostr(integer(@(sizeTest.ch2)))+<-sizeTest.ch2);
     memo1.Lines.Add(inttostr(integer(@(sizeTest.j)))+<-sizeTest.j);
    结果显示:
    16:InstanceSize
    14630724<-start Addr
    14630728<-sizeTest.i
    14630732<-sizeTest.ch1
    14630733<-sizeTest.ch2
    14630736<-sizeTest.j
    数据成员和vmt入口地址就占了16个字节!,两个成员函数showMsg, virtMtd在对象的存储区中根本没占空间。
    那么成员函数到底存储在哪儿呢?由于Delphi是基于RTL(运行时类型库)的,所有的成员函数都在类中存储,成员函数实际上就是方法指针,他指向成员函数的入口地址,该类的所有对象共享这些成员函数。那么怎样找到成员函数的入口地址呢?对于静态函数,这个工作由编译器来完成的,在编译过程中,根据类对象引用/指针的类型,即直接在类来中找到成员函数的入口地址(此时并不必对象存在),这也就是所谓的静态绑定;而对于虚方法(包括动态方法),则是通过在运行时的对象的虚拟方法表vmt入口地址(即对象的前四个字节,此时对象一定要存在,否则就会导致指针访问出错),来找到成员函数的入口地址,这也就是所谓的动态绑定。
     注  意
    上面提到,所有的成员函数都在类中存储,实际上也包括虚拟方法表Vmt。从Delphi的代码自动完成功能(他依赖于编译信息)能看出,当我们在输入完对象名,再输入“.“之后,此时Delphi重新编译了一遍,列出所有的数据成员和所有的静态方法,所有的虚方法,所有的类方法,所有的构造函数和析构函数,大家能动手试试看是不是这样的。
     
    类虚方法表vmt入口地址   
    数据成员模板信息   
    静态方法表等   
    虚方法表vmt 
                  对 象
     
    Vmt入口地址   
    数据成员

     


    上面的程式还演示了对象数据成员的对齐方式(物理数据结构),以4字节对齐(windows默认的对齐方式),如下图所示:
     
    Vmt Entrance Addr   
    i   
    Ch1 Ch2    

     

     

    2 构造函数和创建对象
    2.1 什么是构造函数?(“特别的”类方法)
    从OO(面向对象)思想的语义上讲,构造函数负责对象的创建,但就OOP语言的实现上讲,无论Delphi还是C++,构造函数充其量只做了对象的初始化工作(包含调用内部子对象的构造函数),并没有负责创建对象的全过程(参考2.2)。
    另外,和C++中不同的是,Delphi为构造函数定义了另一种方法类型(mkConstructor,参见Delphi安装目录下的\Source\RTL\Common\typInfo.pas,125行),我们能把他理解为 “特别的”类方法。他只能通过类(类名/类引用/类指针)来调用,而一般的类方法既能通过类也能通过对象来调用;更有一点特别就是构造函数中内置的self参数是指向对象的,而在类方法中self是指向类的,我们通常在其中对其数据成员进行初始化工作,使其成为真正意义上的对象,这都得益于self这个参数。
    在默认情况下,构造函数是静态函数,我们能把他设为虚方法,在其派生类中对其覆载(Override),这样能实现构造函数的多态性(参见2.4),也能对其进行重载(Overload),创建多个构造函数,还能在派生类直接覆盖(Overlay)父类的构造函数,这样在派生类屏蔽了父类的构造函数,在VCL中就采用了这些技术,形成一个构造&析构的“体系结构”(参见4)
    2.2 对象的创建的全过程
    对象的创建完整过程应该包括分配空间、构造物理数据结构、初始化、内部子对象的创建。上面提到,构造函数只是负责初始化工作及调用内部子对象的构造函数,那么分配空间和构造物理结构是怎么完成的呢?这由于编译器在做了额外的事情,我们不知道而已。编译到构造函数时,会构造函数之前,会在插入一行“call @ClassCreate”汇编代码,他实际上就是system 单元中的_ClassCreate函数,下面看看_ClassCreate函数的部分源码:
    function _ClassCreate(AClass: TClass; Alloc: Boolean): TObject;
    asm
            { ->    EAX = pointer to VMT      }
            { <-    EAX = pointer to instance }
            …
            CALL  dword ptr [EAX].vmtNewInstance  //调用NewInstance

    End; {\Source\RTL\sys\system.pas,第8939行}
    VmtNewInstance=-12; 他是NewInstance 函数在类中的偏移量,则“CALL dword ptr [EAX].vmtNewInstance”实际上就是调用NewInstance,请看TObject.NewInstance:源码:
    class function NewInstance: TObject; virtual;
    class function TObject.NewInstance: TObject;
    begin
      Result := InitInstance(_GetMem(InstanceSize));
    end;
      “InitInstance(_GetMem(InstanceSize))”依次调用了三个函数:
    1) 首先调用InstanceSize(),返回实际类的对象大小
      class function TObject.InstanceSize: Longint; //相当于一个虚方法
    begin
        Result := PInteger(Integer(Self) + vmtInstanceSize)^;//返回实际类的对象大小
    end;
    2) 调用_GetMem()在堆上分配Instance大小的内存,并返回对象的引用
    3) 调用InitInstance()进行构造物理数据结构,并把成员设置默认值,比如把整型的数据成员的值设为0,指针设为nil等。如果有虚方法,把虚拟方法表Vmt的入口地址赋给对象的前四个字节。
    在调用完NewInstance之后,这个时候的对象,只有“空壳”,而没有实际的“内容”,所以就需要要调用制定的构造函数对对象进行有意义的初始化,及调用内部子对象的构造函数,使程式中的对象能真实反映现实世界的对象。这就是对象创建的全过程。
       2.3构造函数另类用法(使用类引用实现构造函数的多态性)
    在Delphi中,类也是作为对象存储的,所以同样存在着多态性,他是借助类引用和虚类方法来实现的,这样提供了类一级的多态的实现。把类方法设为虚方法,在其派生类中覆载(override)他,再通过基类的引用/指针调用他,这样根据类引用/指针指向实际类来构造对象。请看下面的程式:
    TmyClass=class
        constructor create;virtual;
      end;
      Ttmyclass=class of TmyClass;//基类的类引用
      TmyClassSub=class(TmyClass)
        constructor create; override;
      end;

    procedure CreateObj(Aclass:TTMyClass;var Ref);
    begin
      Tobject(Ref):=Aclass.create;
    //ref为无类型,和所有类型都不兼容,所以使用时必须显式强制转换(cast)
    //Aclass为类引用,统一的函数接口,不同的实现。
    //他会根据Aclass引用/指向的实际类来构造对象。
     End;

    CreateObj(TmyClass,Obj);
    CreateObj(TmyClassSub,subObj);
    3 析构函数和销毁对象
      3.1 什么是析构函数(“天生的”虚方法)
    从OOP思想的语义上讲,析构函数负责销毁对象,释放资源。在Delphi中,同义。
    Delphi为析构函数也定义了一种方法类型(mkConstructor,参见Delphi安装目录下的\Source\RTL\Common\typInfo.pas,125行),在VCL中,他实际是一种“天生的”虚方法,在VCL类所有的祖先-Tobject中定义了“destructor Destroy; virtual; ”。为什么VCL要这么做呢?因为他要确保在多态情况下对象能正确地被析构。如果不使用虚方法,则可能只析构了基类子对象,从而造成所谓的“内存泄露”。所以为了确保正确地析构对象,析构函数都需要加override声明。
    3.2 对象销毁的全过程
    先销毁派生类子对象,再销毁基类子对象。
     提  示
    在派生类中,基类子对象指从基类中继承的部分,派生类中子对象是指新增的部分。
    3.3 destroy, free, freeAndNil, release用法和差别
    destroy:虚方法
     释放内存,在Tobject中声明为virtual,通常是在其子类中override 他,且要加上inherited关键字,才能确保派生类对象正确地被销毁;
    但destroy一般不能直接用,为什么?
    如果当一个对象为nil,我们仍然调用destroy,此时会产生错误。因为destroy是虚方法,他要根据对象中的头四个字节找到虚拟方法表Vmt的入口地址,从而找到destroy的入口地址,所以此时对象一定要存在。但free就是静态方法,他只需根据对象引用/指针的类型来确定,即使对象本身不存在也没问题,而且在free中有判断对象是否存在的操作, 所以用free比用destroy安全。
    2)free:静态方法
    测试对象是否为nil, 非nil则调用destroy。下面是free的Delphi代码:
    procedure Tobject.Free;
     begin
       if Self <> nil then
         Destroy;
      end;
     一静一动,取长补短,岂不妙哉!
    不过,调用Destroy只是把对象销毁了,但并没有把对象的引用设为nil,这需要程式员来完成,不过自从Delphi5之后,在sysUtils单元中提供了一个freeAndNil。
     3)freeAndNil;一般方法,非对象方法,非类方法。
    SysUtils单元中FreeAndNil 定义
    procedure FreeAndNil(var Obj);
    var
      Temp: TObject;
    begin
      Temp := TObject(Obj);
      Pointer(Obj) := nil;
      Temp.Free;
    end;
    建议大家用他代替free/Destroy,以便确保正确地释放对象。
    4)release;TcustomForm中定义的静态方法。
    当窗口中所有的事件处理完之后,才调用free函数。常用在销毁窗口,而在这个窗口中事件处理需要一定的时间的时候,用这个方法能确保窗口事件处理完之后才销毁窗口。下面是TCustomForm.Release的Delphi原始码:
    procedure TCustomForm.Release;
    begin
      PostMessage(Handle, CM_RELEASE, 0, 0);
    //向窗口发CM_RELEASE消息到消息队列,当所有的窗口事件消息处理完之后,
    //再调用CM_RELEASE消息处理过程CMRelease
    end;
    再看看下面CM_RELEASE消息处理过程CMRelease的定义:
    procedure CMRelease(var Message: TMessage); message CM_RELEASE;
    procedure TCustomForm.CMRelease;
    begin
    Free; //最后还是free;
    end;
    4 VCL构造&析构体系结构
     
    TObject   
    constructor Create;//静态方法
    destructor Destroy; virtual; 

     


     
    TPersistent   
    destructor Destroy; override; 

     


    TComponent   
    constructor Create(AOwner: TComponent); virtual;
    destructor Destroy; override; 

     


     
    TControl   
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override; 

     


                                           …
    下面分析VCL中的构造和析构的原始码,以Tcontrol为例:
    constructor TControl.Create(AOwner: TComponent);
    begin
      inherited Create(AOwner);//创建基类子对象,并把析构权移交给AOwner。放在最前面
       //这样就确保了“先创建基类子对象,再创建派生类子对象”的顺序
      …//初始化,及调用内部子对象的构造函数
    end;

    destructor TControl.Destroy;
    begin
      …//析构派生类中内部子对象
      inherited Destroy;//析构基类对象,放在最后面
      //这样就确保了“先析构派生类子对象,再析构基类子对象”的顺序
    end;
    5 正确使用构造函数和析构函数
       经过上面的分析,下面总结一下使用构造函数和析构函数的原则:
    在使用对象之前,必须先建立一个对象时,并且及时销毁对象,以释放资源。
    两个对象引用赋值时,要确保出现的无名对象(指没有被引用的对象)能被释放。
    当创建一个组件时,建议设置一个宿主组件(即使用AOwner参数,通常是窗体),由Aowner来管理对象的销毁,那么就不必惦记着销毁该组件了,这是Delphi在窗体上/数据模块设计并创建组件是采用的方法。所以我们不必书写调用该组件的析构函数。
    当函数的返回类型为对象时,那么Result也是对象的引用,确保Result引用的对象要存在。
    若要使用obj<>nil 或assigned(nil)测试对象存在时,在调用析构之后还应obj:=nil。

    请参考演示程式的原始码
    说明(建议要有)
    所有的Delphi程式已在win2k+Delphi6 sp2 上通过,对于C++程式,只是为了说明和Delphi中不同,并不确保能直接运行。为了加深对本篇文章的理解,建议参考演示程式。
    这篇文章包括了我在学习VCL/RTL中的一些经验和体会,加上本人的个人能力有限,难免出现错误,请大家不吝指正!
    在阅读本篇文章之前,需要读者对Oriented Pascal语言有一定的了解,并能理解多态,如果你对其中一些概念还不是非常清晰的话,请参考相关文章。
    通过本篇文章,你应该能比较清晰地理解Delphi中的对象模型、构造&析构实现机制及VCL中构造&析构 体系结构,并能掌控使用构造&析构的使用方法。Delphi中的构造&析构相当于C++中的算是简单多了,我们应该能掌控他。


  • 以上内容由 华夏名网 搜集整理,如转载请注明原文出处,并保留这一部分内容。

      “华夏名网” http://www.sudu.cn 和 http://www.bigwww.com 是成都飞数科技有限公司的网络服务品牌,专业经营虚拟主机,域名注册,VPS,服务器租用业务。公司创建于2002年,经过6年的高速发展,“华夏名网”已经成为我国一家知名的互联网服务提供商,被国外权威机构webhosting.info评价为25大IDC服务商之一。

    华夏名网网址导航: 虚拟主机 双线主机 主机 域名注册 cn域名 域名 服务器租用 酷睿服务器 vps vps主机

  • (阅读次数:519)

 

 

 

 

 

相关阅读 更多 +
排行榜 更多 +
马里奥赛车世界游戏手机版下载

马里奥赛车世界游戏手机版下载

赛车竞速 下载
无畏契约皮肤开箱器手游下载

无畏契约皮肤开箱器手游下载

休闲益智 下载
旭日之城官方正版下载

旭日之城官方正版下载

策略塔防 下载