转至繁体中文版     | 网站首页 | 图文教程 | 资源下载 | 站长博客 | 图片素材 | 武汉seo | 武汉网站优化 | 
最新公告:     敏韬网|教学资源学习资料永久免费分享站!  [mintao  2008年9月2日]        
您现在的位置: 学习笔记 >> 图文教程 >> 软件开发 >> Delphi程序 >> 正文
我的文章-《剖析Delphi中的构造和析构》         ★★★★

我的文章-《剖析Delphi中的构造和析构》

作者:闵涛 文章来源:闵涛的学习笔记 点击数:1363 更新时间:2009/4/23 18:30:22

剖析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要这么做呢?因为它要保证在多态情况下对象能正确地被析构。如果不使用虚方法,则可能只析构了基类子对象,从而造成所谓的“

[1] [2]  下一页


[Delphi程序]我的文章-《剖析Delphi中的多态》  
教程录入:mintao    责任编辑:mintao 
  • 上一篇教程:

  • 下一篇教程:
  • 【字体: 】【发表评论】【加入收藏】【告诉好友】【打印此文】【关闭窗口
      注:本站部分文章源于互联网,版权归原作者所有!如有侵权,请原作者与本站联系,本站将立即删除! 本站文章除特别注明外均可转载,但需注明出处! [MinTao学以致用网]
      网友评论:(只显示最新10条。评论内容只代表网友观点,与本站立场无关!)

    同类栏目
    · C语言系列  · VB.NET程序
    · JAVA开发  · Delphi程序
    · 脚本语言
    更多内容
    热门推荐 更多内容
  • 没有教程
  • 赞助链接
    更多内容
    闵涛博文 更多关于武汉SEO的内容
    500 - 内部服务器错误。

    500 - 内部服务器错误。

    您查找的资源存在问题,因而无法显示。

    | 设为首页 |加入收藏 | 联系站长 | 友情链接 | 版权申明 | 广告服务
    MinTao学以致用网

    Copyright @ 2007-2012 敏韬网(敏而好学,文韬武略--MinTao.Net)(学习笔记) Inc All Rights Reserved.
    闵涛 投放广告、内容合作请Q我! E_mail:admin@mintao.net(欢迎提供学习资源)

    站长:MinTao ICP备案号:鄂ICP备11006601号-18

    闵涛站盟:医药大全-武穴网A打造BCD……
    咸宁网络警察报警平台