对Object Pascal编译器给类对象分配堆内存细节的一种大胆猜测(下)

    技术2022-05-11  117

    到了这里,你也许会说,说了半天,都是猜测,或许,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中继承来的XY不会变,还是0,但我们发现,S中的XY已经改变,所以也可以证明Tbase.Create没有分配新的内存,只是对原有内存中的XY进行设置。

    第三点,跟踪。这是最简单,最一目了然的方法,看看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


    最新回复(0)