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;
begin
end;

哈哈!空的!这让我们为难了,那 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 := ;
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( );
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+]
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++*]
MOV EAX,[EAX].TExcFrame.ConstructedObject
TEST EAX,EAX
JE @@skip
MOV ECX,[EAX]
MOV DL,$
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._ClassCreate
System._ClassDestroy System._AfterConstruction
System._BeforeDestruction TObject.AfterConstruction(virtual)
TObject.BeforeDestruction(virtual) TObject.NewInstance(virtual)
TObject.FreeInstance(virtual) TObject.InitInstance
TObject.CleanupInstance MemoryManager.GetMem
MemoryManager.FreeMem

 

Object Pascal对象模型中构造函数之研究的更多相关文章

  1. Object Pascal中文手册 经典教程

    Object Pascal 参考手册 (Ver 0.1)ezdelphi@hotmail.com OverviewOverview(概述)Using object pascal(使用 object p ...

  2. Object Pascal 面向对象的特性

    2 面向对象的特性 在软件系统开发过程中,结构分析技术和结构设计技术具有很多优点,但同时也存在着许多难以克服的缺点.因为结构分析技术和结构设计技术是围绕着实现处理功能来构造系统的,而在系统维护和软件升 ...

  3. Object Pascal 数据类型

     数据类型与定义变量 Object Pascal 语言的最大特点是对数据类型的要求非常严谨.传递给过程或函数的参数值必须与形参的类型一致.在Object Pascal 语言中不会看到像C 语言编译器提 ...

  4. Object Pascal 语言基础

    Delphi 是以Object Pascal 语言为基础的可视化开发工具,所以要学好Delphi,首先要掌握的就是Object Pascal 语言.Object Pascal语言是Pascal之父在1 ...

  5. Object Pascal 语法之异常处理

    http://www.cnblogs.com/spider518/archive/2010/12/30/1921298.html 3 结构化异常处理 结构化异常处理(SHE)是一种处理错误的手段,使得 ...

  6. C#中构造函数的作用

    C#中构造函数的作用 共同点: 都是实例化对象,初始化数据的 默认构造是说所有的类都从祖先object那继承了空参的构造方法,你不写与写空参构造都存在,而有参数的构造一般是自己写的,写就有不写就没有, ...

  7. 深入理解Javascript中构造函数和原型对象的区别

    在 Javascript中prototype属性的详解 这篇文章中,详细介绍了构造函数的缺点以及原型(prototype),原型链(prototype chain),构造函数(constructor) ...

  8. Object Pascal 过程与函数

    过程与函数 过程与函数是实现一定功能的语句块,是程序中的特定功能单元.可以在程序的其他地方被调用,也可以进行递归调用.过程与函数的区别在于过程没有返回值,而函数有返回值. 1.过程与函数的定义 过程与 ...

  9. Object Pascal 运算符

          Object Pascal 的运算符 运算符是程序代码中对各种类型的数据进行计算的符号,通常分为算数运算符.逻辑运算符.比较运算符和按位运算符. 1.算术运算符Object Pascal ...

随机推荐

  1. 4. 2D绘制与控件绘制

    绘制基本图形和文本 绘制图形和文本的基本方法 drawPoint(绘制点).drawLine(绘制直线).drawCircle(绘制圆) drawArc(绘制弧).drawText(绘制文本) pac ...

  2. loadrunner下检查点乱码情况处理

    对于很多用过LR的人来说,乱码一直是很纠结的事情,尤其是对新手来说.网上给的解决方法是在录制的时候勾选UTF-8选项,但是似乎并没有解决. 对于用户名为中文或者检查点为中文的情况,我们又该如何去处理呢 ...

  3. C#单元测试

    简单来说,单元测试就是局部测试,即是对项目中的某个静态类测试.静态方法测试.类的实例化测试以及类的方法测试.当您有一个具体的项目时您可以通过运行查看结果的方式进行测试,但当您只有一个类而没有完整的项目 ...

  4. Python 以指定概率获取元素

    这是Python cookbook的示例 1 def random_pick(some_list,probabilities): 2 x=random.uniform(0,1) 3 cumulativ ...

  5. c 按范围快速指定整数

    以前用过octave, 和matlab类似的软件, 指定范围非常方便 i = 1:10:100;  就可以得到 10 20 30 ... 100 这一系列的数据, 但是在c里面, 必须手动写循环, 太 ...

  6. select多个字段赋值给多个变量

    在存储过程中定义了变量v1 int;v2 int;v3 int;从表tab1选择3个字段f1,f2,f3赋值给这三个变量,要如何写 如果单个变量可以  select f1 into v1 from t ...

  7. BOX2D测试

    ; ; Box2DTestLayer = cc.Layer.extend({ world:null, //GLESDebugDraw *m_debugDraw; ctor:function () { ...

  8. 关于网站编码显示问题 效果是 访问 带有中文注释的sass文件出现编码报错。

    首先查看环境变量 export declare -x HOME="/home/piperck" declare -x LANG="en_US.UTF-8" de ...

  9. LAMP最新源码一键安装脚本

    Linux+Apache+MySQL+PHP (脚本可以选择是否安装+Pureftpd+User manager for PureFTPd+phpMyAdmin+memcache),添加虚拟主机请执行 ...

  10. 通过Microsoft Azure服务设计网络架构的经验分享(转)

    原文:http://www.infoq.com/cn/articles/azure-networking-tips 本文从产品设计和架构角度分享了 Microsoft Azure 网络服务方面的使用经 ...