探索Delphi類與對象的內存結構
初次接觸DELPHI對它提供的RAD快速編程模式頗感神奇,隨手拖放及格控件設定些屬性一個應用程序就誕生了,我正是被這種特性所吸引。隨着深入,慢慢的窺探到了DELPHI的VCL體系,知道了隨手拖放背後隱藏的祕密:一切都起源於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表
vmtMethodTable(Published 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;
內存結構如圖:
如圖,TMyObject2的VirtualMethod1方法覆蓋了父類的VirtualMethod1方法,故虛方法表中VirtualMethod1的指針指向了TMyObject2的VirtualMethod1方法。TMyObject2的DynamicMethod1方法覆蓋了父類的DynamicMethod1方法,由於動態方法表只保存當前類的動態方法表,故表中只有一個DynamicMethod1方法指針,如果要訪問DynamicMethod2方法則需要特定的方法到父類去搜索。
後記
面對對象的三個特性封裝、繼承、多態均體現在以上過程中,DELPHI正是通過這些機制滿足面對對象編程語言的需要。當然還有更多複雜的情況,本人並未一一列舉,有興趣的朋友可以和我聯繫,一起繼續探討。另外以上分析均通過源代碼所得到的結果,BORLAND官方並未有明確的文檔顯示這些結果的正確性,也無法保證在將來的版本中不會出現變動,以上結果都是在DELPHI7下取得。在實際應用當中請儘量根據RTTI開發,以免帶來隱患。
書呆子
QQ:7878906 EMAIL:[email protected]