注 意
上面提到,所有的成员函数都在类中存储,实际上也包括虚拟方法表Vmt。从Delphi的代码自动完成功能(它依赖于编译信息)可以看出,当我们在输入完对象名,再输入“.“之后,此时Delphi重新编译了一遍,列出所有的数据成员和所有的静态方法,所有的虚方法,所有的类方法,所有的构造函数和析构函数,大家可以动手试试看是不是这样的。
类虚方法表vmt入口地址
数据成员模板信息
静态方法表等
虚方法表vmt
对 象
Vmt入口地址
数据成员
上面的程序还演示了对象数据成员的对齐方式(物理数据结构),以4字节对齐(windows默认的对齐方式),如下图所示:
Vmt Entrance Addr
i
Ch1 Ch2
j
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要这么做呢?因为它要保证在多态情况下对象能正确地被析构。如果不使用虚方法,则可能只析构了基类子对象,从而造成所谓的“内存泄露”。所以为了保证正确地析构对象,析构函数都需要加override声明。
3.2 对象销毁的全过程
文章整理:西部数码--专业提供域名注册、虚拟主机服务
http://www.west263.com
以上信息与文章正文是不可分割的一部分,如果您要转载本文章,请保留以上信息,谢谢!




