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;
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 := 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._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

 

时间: 2024-10-11 03:12:43

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

Object Pascal 语法之语言基础(二)

1.5 数据类型与定义变量 Object Pascal 语言的最大特点是对数据类型的要求非常严谨.传递给过程或函数的参数值必须与形参的类型一致.在Object Pascal 语言中不会看到像C 语言编译器提示的“可疑的指针转换”等警告信息.由于Object Pascal 语言对数据类型比较严谨,因此它会对代码进行严格检查,以确保不会出现错误.变量是程序代码中代表一个内存地址的标识符,那么该地址的内存内容就可以在程序代码执行时被改变.每个变量都有一个名字和数据类型,名字可以用来引用变量,数据类型决

Object Pascal 语法之语言基础(一)

Delphi 是以Object Pascal 语言为基础的可视化开发工具,所以要学好Delphi,首先要掌握的就是Object Pascal 语言. 1 语言基础 Object Pascal 语言是在Pascal 语言的基础上发展起来的,它继承了Pascal 语言语法严谨.数据结构丰富等优点,同时融入了面向对象编程的语法要素,使之成为一个完善的面向对象的编程语言. 1.1 Object Pascal 语言编写环境 随着Windows 操作系统的普及,很少有人在DOS 环境下编写程序了,下面将为读

Javascript中构造函数的返回值问题和new对象的过程

首先明确一点:javascript中构造函数是不需要有返回值的,这一点跟java很类似.可以认为构造函数和普通函数的最大差别就是:构造函数中没有return语句,普通函数可以有return语句:构造函数中会使用this关键字定义成员变量和成员方法,普通的函数不会使用this关键字定义成员变量和方法. function Person(name,sex) { this.name = name; this.sex = sex; // return 1; //return true; //return

Delphi APP 開發入門(六)Object Pascal 語法初探

Delphi APP 開發入門(六)Object Pascal 語法初探 分享: Share on facebookShare on twitterShare on google_plusone_share 閲讀次數:3442 發表時間:2014/06/10 tags: 行動開發 教學 App Delphi XE6 Android iOS Delphi APP 開發入門(五)GPS 定位功能 << 前情 經過前面五週幾乎每週可以寫出一個簡單App後,大家都可以感受到Delphi強大的開發威力!

终于懂了:Delphi的函数名不是地址,取地址必须遵守Object Pascal的语法(Delphi和C的类比:指针、字符串、函数指针、内存分配等)good

这点是与C语言不一样的地方,以前我一直都没有明白这一点,所以总是不明白:函数地址再取地址算怎么回事? ---------------------------------------------------------------------------------------------------------------- 在学习Delphi的时候,一个很好的建议是和C/C++去类比着学习,从指针,到内存管理,到数组,到面向对象……各个方面,都是有很多可以相似和或者也有不同的方,类比着学习,一

Object Pascal 语法之语言基础(四)

1.8 过程与函数 过程与函数是实现一定功能的语句块,是程序中的特定功能单元.可以在程序的其他地方被调用,也可以进行递归调用.过程与函数的区别在于过程没有返回值,而函数有返回值. 1.过程与函数的定义过程与函数的定义包括过程原型或函数原型.过程体或函数体的定义.过程定义的形式如下: procedure ProcedureName(ParameterList); directives; var LocalDeclarations; begin statements end; ProcedureNa

Object Pascal 语法之语言基础(三)

1.6 Object Pascal 的运算符 运算符是程序代码中对各种类型的数据进行计算的符号,通常分为算数运算符.逻辑运算符.比较运算符和按位运算符. 1.算术运算符Object Pascal 语言的算术运算符,如表1-9 所示.表1-9 Object Pascal 语言算术运算符 操作符 操作 操作数据类型 结果类型 + 加 整型.实型 整型.实型 - 减 整型.实型 整型.实型 * 乘 整型.实型 整型.实型 / 除 整型.实型 整型.实型 mod 取余 整型 整型 div 整除 整型 整

【C++对象模型】构造函数语意学之一 默认构造函数

默认构造函数,如果程序员没有为类定义构造函数,那么编译器会在[需要的时候]为类合成一个构造函数,而[需要的时候]分为程序员需要的时候和编译器需要的时候,程序员需要的时候应该由程序员来做工作,编译器需要的时候则由编译器来做工作. C++中,全局变量 / 对象的内存会被清零(如果类对象没有程序员定义的构造函数的时候), 而堆heap 或 栈stack上的变量或对象则不会被清零,其内存只取决于上一次这段内存的值. 而编译器什么情况下会为类合成默认构造函数呢?会在以下四种情况合成默认的构造函数: 1.类

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

在 Javascript中prototype属性的详解 这篇文章中,详细介绍了构造函数的缺点以及原型(prototype),原型链(prototype chain),构造函数(constructor),instanceof运算符的一些特点.如果对prototype和构造函数不熟悉,可以前往Javascript中prototype属性的详解 和 Javascript 中构造函数与new命令的密切关系 仔细的品味品味.先来做一个简单的回顾. 首先,我们知道,构造函数是生成对象的模板,一个构造函数可以