首页 > 代码库 > Object Pascal对象模型中构造函数之研究

Object Pascal对象模型中构造函数之研究

http://www.delphi2007.net/delphiblog/html/delphi_2004511950333715.html

前言
近期,一直在使用 C++ 与 Object Pascal (后面简称 OP)深入学习面向对象编程(Object-Oriented Programming 后面简称 OOP)。

说到 OOP ,其实我早在四年前就已经开始接触这个概念了,用 Delphi 作为开发平台,语言是 OP,

因为当时是我学习编程的初级阶段,感觉 Delphi 学习起来比较容易,拖动几下鼠标,在窗体上放几个可视化控件,

再添加几行代码就可以完成一个很漂亮的 Windows 桌面程序。

所以那时的我认为这就是所谓的 OOP,如此简单。

现在看来,那时的思想有些幼稚,这些简单的程序实现,只能说明是 Delphi 的功能强大,

造就它的 Borland 工程师们的伟大,真正的 OOP 还是相当复杂的!

在后来的日子里,由于对破解的热衷和朋友的建议,我又将学习的重点转到了ASM, C语言学习当中。

直到今天开始学习 C++,越来越发现我当时的想法是如此的浅薄。

与面向过程编程(Procedural Programming)相比,OOP 更接近现实世界,你甚至可以用类来表示自然界中存在的各种实物,

从而体现 OOP 的一些特点诸如:封装性,继承性、多态性。编程也由此变得更加方便、快捷、条理清晰。

不过随着对 OOP 学习的深入,你会发现 OOP 内部其实很复杂,代码方面,自己开发一个类时(例如一个控件),你会知道那真不是一个简单的工作!

而内部处理方面,凡是 OOP 类型的语言,它们的编译器都会在幕后为你作很多工作。

由于以上特点,你会感到OOP是个让你又爱又恨的家伙。

这里,我仅从 OP 的构造函数作为切入点来讲讲 Object Pascal 对象模型中的一小方面,权当是我的 Object Pascal 学习笔记。

正文

学习语言的最好方式是理论加实践,当你在为代码的结果感到迷茫时,最好的了解方式是调试(Debug)。

今天我说的这几种学习方式都会在后面的讨论中体现出来。在这里我假设你很熟悉 OP 的语法、Delphi的用法,还有最关键的是 ASM。

Delphi 中万物之源是 TObject,不管你自定义的类是否指明了所继承的父类,一定都是TObject的子孙,一样具有TObject定义的所有特性。

想知道构造函数是怎么回事,先从它入手吧。

想查看 TObject 的源代码请到 \Delphi 安装路径\Source\Rtl\Sys\System.pas 文件中查询。

在TObject中,你会发现构造函数的定义为:

constructor TObject.Create;beginend;

 

 哈哈!空的!这让我们为难了,那 TObject 及继承类的实例到底是怎么创建的呢?

可以肯定的是,编译器为我们作了一些幕后工作,让我们看看它到底做了些什么?

为此我特地设计了一个 Demo 程序,基本可以全方面地了解 OP 对象模型中的构造器工作方式。

首先你需要在Delphi 中新建一个 Console Application,将文件 Project1.dpr 中的内容替换成下面的代码:

program Project1;{$APPTYPE CONSOLE}uses  SysUtils;type  TDerive = class( TObject )  public    constructor Create; overload;  private    x : integer;    y : double;  end;  TDerive1 = class( TDerive )  public    constructor Create( i : integer ); overload;  private    c : integer;  end;var  Obj : TObject;  Der : TDerive;  Der1 : TDerive1;  { TDerive }constructor TDerive.Create;begin  x := 7;  y := 0.1;end;{ TTDerive1 }constructor TDerive1.Create( i : integer );begin  inherited Create;  c := i;end;begin  Obj := TObject.Create;  Obj.Free;  Der := TDerive.Create;  Der.Free;  Der1 := TDerive1.Create( 5 );  Der1.Free;end.

让我们先看看最简单的 TObject 的实例是如何创建的。

请在 Delphi IDE 编辑器中第46行设置断点,然后运行,在编译器的右键菜单中选择命令 Debug-->View CPU,调出Debug CPU窗口。

你会看到下面的代码片断(你的实际程序可能跟我的代码地址不一致,但不妨碍理解):

继续跟入 TObject.Create(0x00407E27):

从代码中我们可以清楚地看到程序先调用了系统级函数 @ClassCreate 然后是 @AfterConstruction,

让我们看看能否幸运地在 system.pas 里找到这两个函数。哈哈,找到了!

但函数名称是 _ClassCreate 和 _AfterConstruction,让我们仔细看看他们的实现方式:

function _ClassCreate( AClass : TClass; Alloc : Boolean ) : TObject;asm  { -> EAX = pointer to VMT }  { <- EAX = pointer to instance }  PUSH EDX  PUSH ECX  PUSH EBX  TEST DL,DL  JL @@noAlloc  CALL DWORD PTR [EAX] + VMTOFFSET TObject.NewInstance@@noAlloc:  {$IFNDEF PC_MAPPED_EXCEPTIONS}  XOR EDX,EDX  LEA ECX,[ESP+16]  MOV EBX,FS:[EDX]  MOV [ECX].TExcFrame.next,EBX  MOV [ECX].TExcFrame.hEBP,EBP  MOV [ECX].TExcFrame.desc,offset @desc  MOV [ECX].TexcFrame.ConstructedObject,EAX { trick: remember copy to instance }  MOV FS:[EDX],ECX  {$ENDIF}  POP EBX  POP ECX  POP EDX  RET  {$IFNDEF PC_MAPPED_EXCEPTIONS}@desc:  JMP _HandleAnyException  { destroy the object }  MOV EAX,[ESP+8+9*4]  MOV EAX,[EAX].TExcFrame.ConstructedObject  TEST EAX,EAX  JE @@skip  MOV ECX,[EAX]  MOV DL,$81  PUSH EAX  CALL DWORD PTR [ECX] + VMTOFFSET TObject.Destroy  POP EAX  CALL _ClassDestroy@@skip:  { reraise the exception }  CALL _RaiseAgain  {$ENDIF}end;function _AfterConstruction( Instance : TObject ) : TObject;begin  Instance.AfterConstruction;  Result := Instance;end;

 

没想到吧,函数 _ClassCreate 的实现完全是内嵌汇编代码,可见其重要性。

代码要表达的最主要目的是要调用虚拟方法表(Vitual Method Table,以后简称 VMT)[1]中的虚函数

NewInstance,以完成对象实例的创建、部分初始化。

另外,设置函数 _AfterConstruction 是调用 VMT 中的虚函数AfterConstruction,

关于 _NewInstance 和 _AfterConstruction 的实现请自行查阅相关代码,限于篇幅这里不再列出。

好了,现在在我们的头脑里应该有一幅大致的流程图了。结合代码现总结如下[1]:

调用TObject的Create构造函数,而TObject的Create构造函数调用了系统的ClassCreate过程。

系统的ClassCreate过程又通过调用TObject类的虚方法NewInstance。

调用TObject的NewInstance方法的目的是要建立对象的实例空间。

TObjec类的NewInstance方法将根据编译器在类信息数据中初始化的对象实例尺寸(InstanceSize),

调用GetMem过程为该对象分配内存。

然后调用TObject类InitInstance方法将分配的空间初始化。

InitInstance方法首先将对象空间的头4个字节初始化为指向对象类的VMT的指针,然后将其余的空间清零。

建立对象实例最后,还调用了一个虚方法AfterConstruction。

最后,将对象实例数据的地址指针保存到Obj变量中,这样,Obj对象就诞生了。

综上所述,该流程可以使用以下代码表示[3]:

程序员调用(代码级调用) 系统内部调用(编译器级调用)

TObject.Create; => @ClassCreate;  => TObject.NewInstance; @AfterConstruction; => TObject.AfterConstruction; 

 

 

TObject.Create 调用 System._ClassCreate 又调用 TObject.NewInstance  调用 TObject.InitInstance,  最后又调用了 TObject.AfterConstruction.

 

让我们再看看运用继承机制时,构造器是如何工作的,来看看TDerive.Create的实现,

请在源代码的第48行设断点:调试结果显示与TObject.Create 的过程区别只在于图2部分,现只贴与图2对应部分的代码:

从代码可以分析出:

由于 TObject 中的成员函数 NewInstance 和 AfterConstruction 被 TDerive 继承,

所以 Der 可以象 Obj 那样在堆(Heap)里被成功创建,另外编译器只将 TDerive.Create 的实现部分(从0x407D38到0x00407D46),

即真正你自己写的构造函数代码放在了系统级调用函数 @ClassCreate 和 @AfterConstruction 两个函数之间

并且以插入代码方式实现(C++ 术语中叫 inline 成员函数),这完成了对 Der 对象成员的初始化工作。

特别提示:除非你知道自己在做什么,否则不要在继承类中覆盖 TObject 的 NewInstance

以及重载它间接调用的 InitInstance,这些至关重要的函数应该由系统内部调用。

让我们再继续看看 TDerive 的继承类 TDerive1 的 Create 的实现,跟哪里设断点就不用我说了吧?

请与图2、图3对应的部分直接比较:

从代码可以分析出:编译器将TDerive1.Create的实现部分(从0x407D73到0x00407D82)

放在了系统级调用函数 @ClassCreate 和 @AfterConstruction 两个函数之间,

且 inherited Create 这一行代码造成对 TDerive.Create 的调用,

但你会发现这里的再次调用与图1所示的调用有了明显的区别,图4中的这一行代码:


00407D79 33D2 xor edx, edx


会造成寄存器 dl = 0,再看看图1这一行:


00407E20 B201 mov dl, $01


会造成寄存器 dl = 1,寄存器dl用来表示系统级函数 _ClassCreate 中的参数 Alloc: Boolean

(但你会发现在 _ClassCreate 的实现代码中并没有出现对 Alloc 的任何操作,

这只是编译器将代码进行了优化处理,或者说是一个隐含参数,不用管它),

它代表在对对象的创建过程是否需要调用系统级函数 _ClassCreate 和 _AfterConstruction,

即在构造基于 TObject 类的派生类对象实例时,第一次调用构造器,参数 Alloc 设为 true,

代表需要为对象实例分配内存空间,并进行初始化以及一些创建后的工作,

当继承的构造函数里调用父类的构造器时,这个隐含参数又被设为 false,

这样 @ClassCreate 和 @AfterConstruction 两个函数不会被再次调用,

这代表不再需要为对象分配内存空间,可以想象如果再次对对象分配内存空间,会造成什么样的恶果。

我们得到的结论是无论创建一个继承链有多长的类时,

@ClassCreate 和 @AfterConstruction 只会被调用一次,

循环调用父类的构造函数,只是为了初始化父类中声明的对象成员,这与我们的设计目的完全吻合!

现在回过头来再想 TObject.Create 的实现代码为什么是空的,是不是觉得不奇怪啦。

作为所有类的基类,它没有任何成员数据需要初始化,因此就无需再画蛇添足,等待着继承类去重载它的构造器。

你可以试着将源代码中的TDerive1.Create的实现部分的inherited Create这一行(即第41行)注释掉,

再次编译程序,再次 Debug,你会发现编译器并没有自动调用 TDerive 的构造器,

这与C++中的实现方式不同,Delphi总是先构造派生的类,仅当派生类调用了继承的构造器时才去构造基类。

在C++中次序相反,从祖先类开始构建,最后才是派生的类。[4]

只要你遵守 OP 的规矩,即写继承类的构造器时,别忘了先通过 inherited 保留字来达到对父类的构造器的调用,

这样当创建一个继承链很长的类时,就可以保证 Create 是从父类到子类的链式初始化。

最后再让我们以 OP 语言模拟写出一个系统级的 OP 构造器。

当系统遇到 constructor 保留字或 inherited Create等时,编译器为我们作了如下的展开工作:

function TSomething.Create(Alloc: Boolean): TSomething;begin  if Alloc then     Self := _ClassCreate(True);        // 真正的初始化代码  inherited Create(False); //如果有基类的构造器,别忘了加上这行  // ....  if Alloc then     _AfterConstruction;  result := Self;end; 

 

 

TObject简要说明-对象的创建流程

http://www.xuebuyuan.com/1784386.html

一个类实例的生成需要经过对象内存分配、内存初始化、设置对象执行框架三个步骤。

编译器首先调用 System._ClassCreate 进行对象内存分配、内存初始化的工作。

而 System._ClassCreate 调用 TObject 类的虚方法 NewInstance 建立对象的实例空间,

继承类通常不需要重载 TObject.NewInstance,除非你使用自己的内存管理器,

因此缺省是调用 TObject.NewInstance。

TObject.NewInstance 方法将根据编译器在类信息数据中初始化的对象实例尺寸(TObject.InstanceSize),

调用系统缺省的 MemoryManager.GetMem 过程为该对象在堆(Heap)中分配内存,

然后调用 TObject.InitInstance 方法将分配的空间初始化。

InitInstance 方法首先将对象空间的头4个字节初始化为指向对象类的 VMT 的指针,

然后将其余的空间清零。如果类中还设计了接口,它还要初始化接口表格(Interface Table)。

当对象实例在内存中分配且初始化后,开始设置执行框架。

所谓设置执行框架就是执行你在 Create 方法里真正写的代码。

设置执行框架的规矩是先设置基类的框架,然后再设置继承类的,通常用 Inherited 关键字来实现。

上述工作都做完后,编译器还要调用 System._AfterConstruction

让你有最后一次机会进行一些事务的处理工作。

System._AfterConstruction 是调用虚方法 AfterConstruction 实现的。

在 TObject 中 AfterConstruction 中只是个 Place Holder,

你很少需要重载这个方法,重载这个方法通常只是为了与 C++ Builder 对象模型兼容。

最后,编译器返回对象实例数据的地址指针。

对象释放服务其实就是对象创建服务的逆过程,可以认为对象释放服务就是回收对象在创建过程中分配的资源。

当编译器遇到 destructor 关键字通常会这样编码:

首先调用 System._BeforeDestruction,而 System._BeforeDestruction 继而调用虚方法 BeforeDestruction,

在 TObject 中 BeforeDestruction 中只是个 Place Holder,你很少需要重载这个方法,

重载这个方法通常只是为了与 C++ Builder 对象模型兼容。

这之后,编译器调用你在 Destroy 中真正写的代码,如果当前你在撰写的类是继承链上的一员,

不要忘记通过 inherited 调用父类的析构函数以释放父类分配的资源,

但规矩是,先释放当前类的资源,然后再调用父类的,这和对象创建服务中设置对象执行框架的顺序恰好相反。

当前类及继承链中所有类中分配的资源全部释放后,最后执行的就是释放掉对象本身及一些特别数据类型占用的内存空间。

编译器调用 System._ClassDestroy 来完成这件工作。

System._ClassDestroy 继而调用虚方法 FreeInstance,继承类通常不需要重载 TObject.FreeInstance,

除非你使用自己的内存管理器,因此缺省是调用 TObject.FreeInstance。

TObject.FreeInstance 继而调用 TObject.CleanupInstance 完成对于

字符串数组、宽字符串数组、Variant、未定义类型数组、记录、接口和动态数组这些特别数据类型占用资源的释放[4],

最后 TObject.FreeInstance 调用 MemoryManager.FreeMem 释放对象本身占用的内存空间。

还有一点要注意,通常我们不会直接调用 Destroy 来释放对象,

而是调用 TObject.Free,它会在释放对象之前检查对象引用是否为 nil。

 

 

很有意思的是,对象释放服务与对象创建服务所用方法、函数是一一对应的,是不是有一种很整齐的感觉?

System._ClassCreateSystem._ClassDestroySystem._AfterConstructionSystem._BeforeDestructionTObject.AfterConstruction(virtual)TObject.BeforeDestruction(virtual)TObject.NewInstance(virtual)TObject.FreeInstance(virtual)TObject.InitInstanceTObject.CleanupInstanceMemoryManager.GetMemMemoryManager.FreeMem

 

 

 

 

Object Pascal对象模型中构造函数之研究