到了这里,你也许会说,说了半天,都是猜测,或许,OP编译器根本就不会调用那个TObject.NewInstance方法呢!
问得好,再做实验!
还是以上面的那个Tbase类为例,重载TObject.NewInstance方法,如下:
TBase = class(TObject)
x : Integer;
y : Double;
class function NewInstance: TObject; override;
procedure FreeInstance; override;
constructor Create;
end;
{实现}
constructor TBase.Create;
begin
self.x := 2;
self.y := 3.14;
end;
procedure TBase.FreeInstance;
begin
inherited;
ShowMessage(Format('Call %s.FreeInstance!!!',[self.ClassName]));
end;
class function TBase.NewInstance: TObject;
begin
ShowMessage(Format('call %s.NewInstance',[self.ClassName]));
result := inherited NewInstance;
end;
之后进行简单的声明对象:
var
b : Tbase;
begin
b := Tbase.Create; ß在这里设断点!
b.Free;
end;
通过对代码进行跟踪果然在一进入Create就马上调用NewInstance方法。
[说明:一定要重载它才能跟踪到它,在断点处,观察CPU,从反汇编后的代码中可以发现,是先调用一个_ClassCreate,然后才调用NewInstance]
用同样的方法可以分析出b.Free会最终调用到FreeInstance;来释放对象。
我想基本上大的问题已经说请了,Object Pascal为了实现分配堆内存,在你调用构造器的时候:
b := Tbase.Create;
在构造方法内你的代码前,安插了代码调用NewInstance方法,析构时,则在析构函数中你的代码后,调用FreeInstance函数。
那么,现在再来看这种情况:派生
TBase = class(TObject)
x : Integer;
y : Double;
class function NewInstance: TObject; override;
procedure FreeInstance; override;
constructor Create;
end;
TSub = class (TBase)
m : Integer;
n : Double;
constructor Create;
end;
{实现}
constructor TBase.Create;
begin
self.x := 2;
self.y := 3.14;
end;
procedure TBase.FreeInstance;
begin
inherited;
ShowMessage(Format('Call %s.FreeInstance!!!',[self.ClassName]));
end;
class function TBase.NewInstance: TObject;
begin
ShowMessage(Format('call %s.NewInstance',[self.ClassName]));
result := inherited NewInstance;
end;
{ TSub }
constructor TSub.Create;
begin
inherited Create; ß注意这里!
self.m := 4;
self.n := 12.32;
end;
我们已经知道,
var
s : Tsub;
s := Tsub.Create;
时,在进入Tsub.Create内部马上得到了它想要的内存[这里是32字节],那么当:
inherited Create;时,在Tbase.Create内部,还有内存分配的动作吗?我们可以通过三点证明:这里,Tbase.Create只是完成程序员给出的初始化代码,没有进行内存分配的动作。
第一点,ReturnValue := inherited Create;所得到的返回地址和调用Tsub.Create所得到的返回地址相同。
第二点,如果在Tbase.Create内部又分配新的内存,那么
self.x := 2;
self.y := 3.14;
只是针对新的内存操作,而原来的S对象中从TBASE中继承来的X,Y不会变,还是0,但我们发现,S中的X,Y已经改变,所以也可以证明Tbase.Create没有分配新的内存,只是对原有内存中的X,Y进行设置。
第三点,跟踪。这是最简单,最一目了然的方法,看看inherited Create;到底有没有调用NewInstance,实验证明,跟本没有调用。
但是,如果把Tsub.Create中的inherited Create;改为Tbase.Create;情况则大不同了,用上面三种方式发现,它又分配了新的堆内存,这样不但没有达到程序员初始化数据的目的,反而造成了内存泄漏,而这样的BUG是很难找到的。
也就是说,编译器发现如果是通过类来调用构造函数,就会当成是新的类对象进行构造、分配堆内存,如果是在构造器内部inherited Create;只是按常规的处理 类方法 的方式进行处理。我想,对于Anders Hejlsberg[DELPHI设计者],想在编译器中实现这样的功能并非一件难事[实际上,我们通过查看汇编代码也能分析出个中原由,有兴趣者请注意其中的TEST d1,d1指令和其下的跳转指令]。
PS:刚才被网友告知有本书叫《delphi的原子世界》,我很想得到它,如果您手上有它的E-BOOK版,希望您能发给我: coder@vip.sina.com