确实,这是一个有趣的问题。
我们看看Delphi在Create一个对象之前做了什么。
在执行创建函数的时候,我看看它的汇编码。
mov eax,[xxxxx] //第一个参数,就是VMT指针
test dl,dl //比较第二个参数,如果是True的话就跳到add esp,-$10指令处
jz +$08 //为false(0)的话跳过下面两行代码,其实什么都没有做,具体还可以看后面的相关代码
add esp,-$10 //栈中分配16字节,主要放置异常处理的一些处理信息,后面的代码中还要用到(也可以忽略这部分)
call @ClassCreate//关键部分,调用_ClassCreate方法
test dl,dl
......
下面看看关键的代码:
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 //如果为false的话调用下面的@@noAlloc不分配内存
JL @@noAlloc
CALL dword ptr [EAX].vmtNewInstance //这里是关键,下面会分析!调用NewInstance方法(这个方法就是平时看到的虚方法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].vmtDestroy
POP EAX
CALL _ClassDestroy
@@skip:
{ reraise the exception }
CALL _RaiseAgain
{$ENDIF}
end;
//如果真的要分配的话,下面这个函数会被调用!
class function TObject.NewInstance: TObject;
begin
Result := InitInstance(_GetMem(InstanceSize));
//很简单啊,就是根据对象大小(InstanceSize)调用_GetMem分配内存!注意这里我们还没有到相应的Create方法哟,内存中对象框架已经搭好了!
end;
现在可以简单总结一下:
在delphi中,真正创建对象的方法是_ClassCreate,它会调用虚方法NewInstance方法,这个方法默认会分配内存(其实你可以不让它分配的,有兴趣的可以试试),由于有一个类后,编译器就知道类对象的大小(InstanceSize)并可以在编译的时候确定VMT的地址,所以利用虚方法来创建对象是可以的。
当所有的工作都做好了,它才会调用我们的构造器函数(例如:Create方法),到这里,这个函数好像不再具有它名字赋予的意义了,其实它的确可以改为任何名字,这个方法已经变成一个初始化的过程了(因为内存构造在这个函数之前就完成了,这里只是在这片分配好的内存中放入适当的数据而已)。那什么时候编译器会调用到_ClassCreate方法呢??其实这里有一个玄机:就是Constructor关键字了,你可以认为它是一个编译器关键字,只要编译器看到你调用的方法是一个Constructor类型的,它就会在这个函数之前插入一些代码(当然就是调用_ClassCreate之类的了),这就是所谓的“编译器魔法”,所以你可以定义类似下面的函数:
Constructor TMyClass.Delete;
虽然名字是删除的意思,但你如果这样调用:
a:TMyClass;
a:=TMyClass.Delete;
a变量其实已经指向了一个存在的实例了,编译器会在delete函数前分配好内存,然后再调用Delete方法初始化,这就是Constructor的作用了,^_^
至于各种编译器使用不同方式的构造方法其实不必关心,因为各种编译器工作机制不同,其中掺杂了编译器和语言创造人自己的喜好。
说了这么多,不知道说清楚了没有,呵呵!