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中,你会发现构造函数的定义为:

  1. constructor TObject.Create;
  2. begin
  3. end;

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

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

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

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

  1. program Project1;
  2.  
  3. {$APPTYPE CONSOLE}
  4.  
  5. uses
  6. SysUtils;
  7.  
  8. type
  9. TDerive = class( TObject )
  10. public
  11. constructor Create; overload;
  12. private
  13. x : integer;
  14. y : double;
  15. end;
  16.  
  17. TDerive1 = class( TDerive )
  18. public
  19. constructor Create( i : integer ); overload;
  20. private
  21. c : integer;
  22. end;
  23.  
  24. var
  25. Obj : TObject;
  26. Der : TDerive;
  27. Der1 : TDerive1;
  28.  
  29. { TDerive }
  30.  
  31. constructor TDerive.Create;
  32. begin
  33. x := ;
  34. y := 0.1;
  35. end;
  36.  
  37. { TTDerive1 }
  38.  
  39. constructor TDerive1.Create( i : integer );
  40. begin
  41. inherited Create;
  42. c := i;
  43. end;
  44.  
  45. begin
  46. Obj := TObject.Create;
  47. Obj.Free;
  48. Der := TDerive.Create;
  49. Der.Free;
  50. Der1 := TDerive1.Create( );
  51. Der1.Free;
  52.  
  53. end.

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

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

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

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

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

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

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

  1. function _ClassCreate( AClass : TClass; Alloc : Boolean ) : TObject;
  2. asm
  3. { -> EAX = pointer to VMT }
  4. { <- EAX = pointer to instance }
  5. PUSH EDX
  6. PUSH ECX
  7. PUSH EBX
  8. TEST DL,DL
  9. JL @@noAlloc
  10. CALL DWORD PTR [EAX] + VMTOFFSET TObject.NewInstance
  11. @@noAlloc:
  12. {$IFNDEF PC_MAPPED_EXCEPTIONS}
  13. XOR EDX,EDX
  14. LEA ECX,[ESP+]
  15. MOV EBX,FS:[EDX]
  16. MOV [ECX].TExcFrame.next,EBX
  17. MOV [ECX].TExcFrame.hEBP,EBP
  18. MOV [ECX].TExcFrame.desc,offset @desc
  19. MOV [ECX].TexcFrame.ConstructedObject,EAX { trick: remember copy to instance }
  20. MOV FS:[EDX],ECX
  21. {$ENDIF}
  22. POP EBX
  23. POP ECX
  24. POP EDX
  25. RET
  26.  
  27. {$IFNDEF PC_MAPPED_EXCEPTIONS}
  28. @desc:
  29. JMP _HandleAnyException
  30.  
  31. { destroy the object }
  32.  
  33. MOV EAX,[ESP++*]
  34. MOV EAX,[EAX].TExcFrame.ConstructedObject
  35. TEST EAX,EAX
  36. JE @@skip
  37. MOV ECX,[EAX]
  38. MOV DL,$
  39. PUSH EAX
  40. CALL DWORD PTR [ECX] + VMTOFFSET TObject.Destroy
  41. POP EAX
  42. CALL _ClassDestroy
  43. @@skip:
  44. { reraise the exception }
  45. CALL _RaiseAgain
  46. {$ENDIF}
  47. end;
  48.  
  49. function _AfterConstruction( Instance : TObject ) : TObject;
  50. begin
  51. Instance.AfterConstruction;
  52. Result := Instance;
  53. 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]:

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

  1. 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等时,编译器为我们作了如下的展开工作:

  1. function TSomething.Create(Alloc: Boolean): TSomething;
  2. begin
  3. if Alloc then
  4. Self := _ClassCreate(True);
  5.  
  6. // 真正的初始化代码
  7. inherited Create(False); //如果有基类的构造器,别忘了加上这行
  8. // ....
  9. if Alloc then
  10. _AfterConstruction;
  11. result := Self;
  12. 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。

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

  1. System._ClassCreate
  2. System._ClassDestroy
  3.  
  4. System._AfterConstruction
  5. System._BeforeDestruction
  6.  
  7. TObject.AfterConstruction(virtual)
  8. TObject.BeforeDestruction(virtual)
  9.  
  10. TObject.NewInstance(virtual)
  11. TObject.FreeInstance(virtual)
  12.  
  13. TObject.InitInstance
  14. TObject.CleanupInstance
  15.  
  16. MemoryManager.GetMem
  17. 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. Shell教程4-Shell替换

    如果表达式中包含特殊字符,Shell 将会进行替换.例如,在双引号中使用变量就是一种替换,转义字符也是一种替换. 举个例子: 复制纯文本新窗口   #!/bin/bash a=10 echo -e & ...

  2. invalid initialization of non-const reference of type与discards qualifiers

    参数传递          函数参数的传递是初始化语义:用调用者的实参去初始化函数的形参,如果参数是对象,需要调用该类的拷贝构造函数,如果没有显式定义的拷贝构造函数,则执行默认的按成员拷贝      ...

  3. N人报数第M人出列游戏问题(约瑟夫问题)

    这是一道华为的机试题,后来才知道也叫约瑟夫问题,题目是这样的:有n个人围成一圈,玩一个游戏,规则为将该n个人编号为1,2,......n, 从编号为1的人开始依次循环报数,报道第m的时候将第m个人从队 ...

  4. Effective java笔记7--线程

    一.对可共享数据的同步访问 synchronized关键字可以保证在同一时刻,只有一个线程在执行一条语句,或者一段代码块.正确地使用同步可以保证其他任何方法都不会看到对象处于不一致的状态中,还能保证通 ...

  5. 请教下 Yii 和 Ajax来验证用户名是否存在

    添加一个 Custom, Model页面: CustomForm中: public function rules() { // 使用ajax 校验数据 return array( array('nam ...

  6. new 动态分配数组空间 .xml

    pre{ line-height:1; color:#3c3c3c; background-color:#d2c39b; font-size:16px;}.sysFunc{color:#627cf6; ...

  7. Ubuntu14.04LTS安装记录(办公室联想台式机)

    一.用UltraISO制作U盘启动器,被安装的电脑要设置成从U盘启动. 二.傻瓜式安装简体中文版 三.安装更新 sudo apt-get update sudo apt-get upgrade 四.安 ...

  8. 【转】 Linux Shell 命令--rename

    重命名文件,经常用到mv命令,批量重命名文件rename是最好的选择,Linux的rename 命令有两个版本,一个是C语言版本的,一个是Perl语言版本的,判断方法:输入man rename 看到第 ...

  9. Arduino uno R3 ISP刷Rootloader for arduino pro mini

    找了好久才发现的,好东西.介绍怎么使用uno对mini 刷Rootloader **SOLUTION** Reinstall the Arduino Pro Mini Bootloader using ...

  10. cubieboard中使用py-kms与dnsmasq搭建局域网内全自动KMS激活环境

    众所周知,KMS激活方式是当前广大网民“试用”windows,office的最广泛的激活方式.几乎可以用于微软的全线产品. 但是在本机使用KMS类的激活工具总是有些不放心,一方面每隔180天都要重新激 ...