探索Delphi類與對象的內存結構

探索Delphi類與對象的內存結構

                                                                                                        

 

初次接觸DELPHI對它提供的RAD快速編程模式頗感神奇,隨手拖放及格控件設定些屬性一個應用程序就誕生了,我正是被這種特性所吸引。隨着深入,慢慢的窺探到了DELPHIVCL體系,知道了隨手拖放背後隱藏的祕密:一切都起源於VCL的對象體系,一切都是面對對象的編程思想。Object pascal就是是怎樣實現這個體系的呢,它究竟是如何將面對對象的特性表現出來的呢,Delphi的類和對象究竟是以什麼樣的形式存在的呢。帶着這些問題我翻閱了一些書籍,也借鑑了一些網友的成果,做了下面的探索。

動態內存與靜態內存

程序需要執行必須先裝載入內存,任何程序表現的數據都存在內存中。當程序運行時,系統首先將所有數據裝載入內存,完成初始化,然後從入口地址開始執行代碼。程序裝載後即存在於內存空間中的數據我們稱之爲靜態內存,運行過程中分配的內存我們稱之爲動態內存。Delphi的類是由編譯期間決定的,編譯完成後即固定在程序中,所以類是存在於靜態內存中。對象是由運行期間創建的,所以對象屬於動態內存。

注意:後面所提到的TObject均爲泛指所有類,而非真正的TObject

                                                        程序運行示意圖

類的內存結構

       類的內存結構是固定的,編譯完成後就無法改變。它主要存儲了類的基本信息,派生對象內存大小,虛方法列表,動態方法列表,公開屬性和方法列表(published),接口列表,TObject類的一些方法等等有關於構建對象所必須的信息。這些信息的存儲位置在SYSTEM單元中有定義:

  vmtSelfPtr           = -76;         指向虛方法表的指針

  vmtIntfTable         = -72;          指向接口表的指針

  vmtAutoTable         = -68;        指向自動化信息表的指針

  vmtInitTable         = -64;          指向實例初始化表的指針

  vmtTypeInfo          = -60;        指向類型信息表的指針,這裏的數據對於RTTI來說非常重要,它指向一個PTypeInfo類型的指針,有興趣可以看看TypInfo單元

  vmtFieldTable        = -56;          指向域定義表的指針(我開始認爲是Published Field,但實際查詢時卻爲NIL

  vmtMethodTable       = -52;        指向方法定義表的指針(Published

  vmtDynamicTable      = -48;        指向動態方法表的指針

  vmtClassName         = -44;       指向類名字符串的指針

  vmtInstanceSize      = -40;          對象實例的大小

  vmtParent            = -36;        指向父類的指針

  vmtSafeCallException = -32 deprecated;  以下都是TOBJECT類的一些虛擬方法指針

  vmtAfterConstruction = -28 deprecated; 

  vmtBeforeDestruction = -24 deprecated;

  vmtDispatch          = -20 deprecated;

  vmtDefaultHandler    = -16 deprecated;

  vmtNewInstance       = -12 deprecated;

  vmtFreeInstance      = -8 deprecated;

  vmtDestroy           = -4 deprecated;

 

如果獲取對象大小,可以使用以下代碼:

Result := PInteger(Integer(TObject) + vmtInstanceSize)^;

其他各項可以依此類推。

l       靜態方法

類的靜態方法在編譯期間就決定了它的地址,類只爲所有的派生的對象提供統一的一份靜態方法表,不會爲每個對象複製一份,所以不必關心靜態方法的存儲(實際上靜態方法也是和動態方法有序的排列在一塊的,順序與方法的實現順序有關)

l       非靜態方法

虛方法(Virtual)和動態方法(Dynamic)均爲非靜態方法,它們是用來實現面對對象的多態性的關鍵特性。通過這種特性,開發者可以根據需要在不同的子類中擁有不同的實現,從而使設計變得更加靈活。在具體實現中,編譯器只需要將簡單的改變表中方法指針的指向即可達到目的。從語法上講虛擬方法和動態方法是沒有任何區別的,凡是聲明瞭該兩種類型的方法,在子類中都可以通過override關鍵字進行覆蓋。但實際上二者的實現是存在巨大差別的:

 

vmtSelfPtr(虛方法表的指針)實際上就是指向TObject位置,所以類的虛擬方法是依次排在TObject所指向的位置之後。

vmtDynamicTable(動態方法表的指針)指向的是動態方法表,動態方法表的結構與虛方法表的結構有所不同。

兩者實現方式的不同體現了兩者作用的不同。虛擬方法表包括本身以及以上的父類所有的虛擬方法的地址,調用時直接指向地址即可,好處在於速度極快,不需要查詢,缺點在於佔用了額外的內存。動態方法表則只保存自己本身所包含的動態方法表,如果調用者的動態方法不屬於自己,則根據索引號往上級父類遍歷查詢得到方法的地址,好處在於不用保存父類的動態方法從而節省了內存,缺點在於搜索帶來的效率下降。

l       接口表的指針

vmtIntfTable(接口表的指針)指向一塊PInterfaceTable類型的接口信息表空間,vmtIntfTable只保存當前類所實現的接口表信息,不保存父類的接口表信息,創建對象時會根據vmtParent父類指針遍歷獲取所有父類的接口表信息插入對象內存空間。

l       Published Method

vmtMethodTablePublished Method表)指向Published Method表有序排列,只存儲當前類的Published Method表,得到父類的Published Method表需要往上遍歷。

對象的內存結構

運行期是如何創建對象的呢,過程如下:

首先讀取InstanceSize對象實例內存大小分配內存

class function TObject.NewInstance: TObject;

begin

  Result := InitInstance(_GetMem(InstanceSize));

end;

 

然後初始化對象的數據結構,將屬性置爲空,將接口方法表(包括父親的)插入對象內存空間

class function TObject.InitInstance(Instance: Pointer): TObject;

{$IFDEF PUREPASCAL}

var

  IntfTable: PInterfaceTable;

  ClassPtr: TClass;

  I: Integer;

begin

  FillChar(Instance^, InstanceSize, 0);

  PInteger(Instance)^ := Integer(Self);  //將類地址存放在開始的四個字節中

  ClassPtr := Self;

  while ClassPtr <> nil do

  begin

    IntfTable := ClassPtr.GetInterfaceTable;

    if IntfTable <> nil then

      for I := 0 to IntfTable.EntryCount-1 do

  with IntfTable.Entries[I] do

  begin

    if VTable <> nil then

      PInteger(@PChar(Instance)[IOffset])^ := Integer(VTable); //根據接口表提供的偏移地址,在對象的相應位置存儲接口的虛方法表的地址

  end;

    ClassPtr := ClassPtr.ClassParent;

  end;

  Result := Instance;

end;

隨後會調用類的構造方法完成創建。

 

從對象的創建過程我們可以分析出對象的基本內存結構,如下面的類實例:

  TMyObject = class(TObject, IInterface)

  private

    FField1: Integer;

    FField2: Boolean;

  protected

    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

    function _AddRef: Integer; stdcall;

    function _Release: Integer; stdcall;

  public

    procedure DynamicMethod1; dynamic;

    procedure DynamicMethod2; dynamic;

    procedure VirtualMethod1; virtual;

    procedure VirtualMethod2; virtual;

  end;

 

內存結構如圖:

                                     當一個派生一個子類後,由子類生成的對象又是什麼情形呢,如下面實例:

                                       TMyObject2 = class(TMyObject)

  private

       FField3: Integer;

public

                                         procedure DynamicMethod1; override;

                                           procedure VirtualMethod1; override;

                                      end;

內存結構如圖:

                                     如圖,TMyObject2VirtualMethod1方法覆蓋了父類的VirtualMethod1方法,故虛方法表中VirtualMethod1的指針指向了TMyObject2VirtualMethod1方法。TMyObject2DynamicMethod1方法覆蓋了父類的DynamicMethod1方法,由於動態方法表只保存當前類的動態方法表,故表中只有一個DynamicMethod1方法指針,如果要訪問DynamicMethod2方法則需要特定的方法到父類去搜索。

後記

面對對象的三個特性封裝、繼承、多態均體現在以上過程中,DELPHI正是通過這些機制滿足面對對象編程語言的需要。當然還有更多複雜的情況,本人並未一一列舉,有興趣的朋友可以和我聯繫,一起繼續探討。另外以上分析均通過源代碼所得到的結果,BORLAND官方並未有明確的文檔顯示這些結果的正確性,也無法保證在將來的版本中不會出現變動,以上結果都是在DELPHI7下取得。在實際應用當中請儘量根據RTTI開發,以免帶來隱患。

 

                                                                                                           書呆子

                                                                                                           QQ:7878906  EMAIL[email protected]

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章