在使用DELPHI開發軟體的過程中,我們就像草原上一群快樂牛羊,無憂無慮地享受著Object Pascal語言為我們帶來的陽光和各種VCL控制提供的豐富的水草。抬頭望無邊無際蔚藍的天空,低頭品嚐大地上茂密的青草,誰會去想宇宙有多大,比分子和原子更小的東西是什麼?那是哲學家的事。而哲學家此時正坐在高高的山頂上,仰望宇宙星雲變換,凝視地上小蟲的爬行,驀然回頭,對我們這群吃草的牛羊點頭微笑。隨手扯起一根小草,輕輕地含在嘴裡,閉上眼睛細細品嚐,不知道這根青草在哲學家的嘴裡是什麼味道?只是,他的臉上一直帶著滿意的微笑。
認識和了解DELPHI微觀的原子世界,可以使我們徹底理解DELPHI的宏觀應用程式結構,從而在更廣闊的思想空間中開發我們的軟體。這就好像,牛頓發現了宏觀物體的運動,卻因為搞不清物體為什麼會這樣運動而苦惱,相反,愛因斯坦卻在基本粒子規律和宏觀物體運動之間體驗著相對論的快樂生活!
第一節TObject原子
TObject是什麼?
是Object Pascal語言體系結構的基本核心,也是各種VCL控制項的起源。我們可以認為,TObject是構成DELPHI應用的原子之一,當然,他們又是由基本Pascal語法元素等更細微的粒子所構成。
說TObject是DELPHI程式的原子,是因為TObject是DELPHI編譯器內部支援的。所有的物件類別都是從TObject派生的,即使你並未指定TObject為祖先類別。 TObject被定義在System單元,它是系統的一部分。在System.pas單元的開頭,有這樣的註解文字:
{ PRedefined constants, types, procedures, }
{ and functions (such as True, Integer, or }
{ Writeln) do not have actual declarations.}
{ Instead they are built into the compiler }
{ and are treated as if they were declared }
{ at the beginning of the System unit.}
它的意思說,這一單元包含預先定義的常數、類型、過程和函數(諸如:Ture、Integer或Writeln),它們並沒有實際的聲明,而是編譯器內建的,並在編譯的開始就被認為是已經聲明的定義。你可以將Classes.pas或Windows.pas等其他原始程式檔案加入你的專案檔案中進行編譯和偵錯其原始程式碼,但你絕對無法將System.pas原始程式檔案加入到你的專案檔案中進行編譯! DELPHI將報告重複定義System的編譯錯誤!
因此,TObject是編譯器內部提供的定義,對於我們使用DELPHI開發程式的人來說,TObject是原子性的東西。
TObject在System單元中的定義是這樣的:
TObject = class
constructor Create;
procedure Free;
class function InitInstance(Instance: Pointer): TObject;
procedure CleanupInstance;
function ClassType: TClass;
class function ClassName: ShortString;
class function ClassNameIs(const Name: string): Boolean;
class function ClassParent: TClass;
class function ClassInfo: Pointer;
class function InstanceSize: Longint;
class function InheritsFrom(AClass: TClass): Boolean;
class function MethodAddress(const Name: ShortString): Pointer;
class function MethodName(Address: Pointer): ShortString;
function FieldAddress(const Name: ShortString): Pointer;
function GetInterface(const IID: TGUID; out Obj): Boolean;
class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;
class function GetInterfaceTable: PInterfaceTable;
function SafeCallException(ExceptObject: TObject;
ExceptAddr: Pointer): HResult; virtual;
procedure AfterConstruction; virtual;
procedure BeforeDestruction; virtual;
procedure Dispatch(var Message); virtual;
procedure DefaultHandler(var Message); virtual;
class function NewInstance: TObject; virtual;
procedure FreeInstance; virtual;
destructor Destroy; virtual;
end;
下面,我們將逐步敲開TObject原子的大門,看看裡面到底是什麼結構。
我們知道,TObject是所有物件的基本類,那麼,一個物件到底是什麼?
DELPHI中的任何物件都是指針,這個指針指明該物件在記憶體中所佔據的一塊空間!雖然,物件是一個指針,但我們引用物件的成員時卻不用寫成這樣的程式碼MyObject^.GetName,而只能寫成MyObject.GetName,這是Object Pascal語言擴充的語法,是由編譯器支援的。使用C++ Builder的朋友就很清楚物件與指標的關係,因為在C++ Builder的物件都要定義為指標。物件指標指向的地方就是物件儲存資料的物件空間,我們來分析一下物件指標所指向的記憶體空間的資料結構。
物件空間的頭4個位元組是指向該物件類別的虛擬方法位址表(VMT ?C Vritual Method Table)。接下來的空間就是儲存物件本身成員資料的空間,並依照從該物件最原始祖先類別的資料成員到該物件類別的資料成員的總順序,以及每一層類別中資料成員的定義順序儲存。
類別的虛擬方法位址表(VMT)保存從該類別的原始祖先類別派生到該類別的所有類別的虛擬方法的過程位址。類別的虛擬方法,就是用保留字vritual宣告的方法,虛方法是實現物件多態性的基本機制。雖然,以保留字dynamic聲明的動態方法也可實現物件的多態性,但這樣的方法不保存在虛擬方法位址表(VMT)中,它只是Object Pascal提供的另一種可節約類別儲存空間的多態實作機制,但卻是以犧牲呼叫速度為代價的。
即使,我們自己並未定義任何類別的虛擬方法,但該類別的物件仍然存在指向虛擬方法位址表的指針,只是位址項目的長度為零。可是,那些在TObject中定義的那些虛擬方法,如Destroy、FreeInstance等等,又儲存在哪裡呢?原來,他們的方法位址儲存在相對VMT指針負方向偏移的空間中。其實,在VMT表的負方向偏移76個位元組的資料空間是物件類別的系統資料結構,這些資料結構是與編譯器相關的,並且在將來的DELPHI版本中有可能被改變。
因此,你可以認為,VMT是一個從負偏移位址空間開始的資料結構,負偏移資料區是VMT的系統資料區,VMT的正偏移資料是使用者資料區(自訂的虛擬方法位址表)。 TObject中定義的有關類別資訊或物件運行時刻資訊的函數和過程,一般都與VMT的系統資料有關。
一個VMT資料就代表一個類,其實VMT就是類!在Object Pascal中我們用TObject、TComponent等等識別碼來表示類,它們在DELPHI的內部實作為各自的VMT資料。而用class of保留字定義的類別的類型,實際上就是指向相關VMT資料的指標。
對我們的應用程式來說,VMT數據是靜態的數據,當編譯器編譯完成我們的應用程式之後,這些數據資訊已經確定並已初始化。我們編寫的程序語句可訪問VMT相關的信息,獲得諸如對象的尺寸、類名或運行時刻的屬性資料等等信息,或者調用虛擬方法或讀取方法的名稱與地址等等操作。
當一個物件產生時,系統會為該物件分配一塊記憶體空間,並將該物件與相關的類別連結起來,於是,在為物件分配的資料空間中的頭4個位元組,就成為指向類別VMT數據的指針。
我們再來看看對像是如何誕生和滅亡的。看著我三歲的兒子在草地上活蹦亂跳,正是由於親眼目睹生命的誕生過程,我才能真真體會到生命的意義和偉大。也只有那些經歷過死別的人,才會更理解、珍惜生命。那麼,就讓我們來理解一下對象的產生與消亡的過程吧!
我們都知道,用下面的語句可以建構一個最簡單物件:
AnObject := TObject.Create;
編譯器將其編譯實現為:
以TObject對應的VMT為依據,呼叫TObject的Create建構子。而在Create建構函式呼叫了系統的ClassCreate過程,系統的ClassCreate過程又透過儲存在類別VMT呼叫NewInstance虛擬方法。呼叫NewInstance方法的目的是要建立物件的實例空間,因為我們沒有重載該方法,所以,它就是TObject類別的NewInstance。 TObjec類別的NewInstance方法將根據編譯器在VMT表中初始化的物件實例尺寸(InstanceSize),呼叫GetMem過程為該物件分配內存,然後調用InitInstance方法將分配的空間初始化。 InitInstance方法先將物件空間的頭4個位元組初始化為指向物件類別對應VMT的指針,然後將其餘的空間清除。建立物件實例之後,也呼叫了一個虛擬方法AfterConstruction。最後,將物件實例資料的位址指標儲存到AnObject變數中,這樣,AnObject物件就誕生了。
同樣,用下面的語句可以消滅一個物件:
AnObject.Destroy;
TObject的析構函數Destroy被宣告為虛方法,它也是系統固有的虛方法之一。 Destory方法先呼叫了BeforeDestruction虛方法,然後呼叫系統的ClassDestroy過程。 ClassDestory過程又透過類別VMT呼叫FreeInstance虛方法,由FreeInstance方法呼叫FreeMem過程釋放物件的記憶體空間。就這樣,一個物件就在系統中消失。
對象的析構過程比對象的構造過程簡單,就好像生命的誕生是一個漫長的孕育過程,而死亡卻相對的短暫,這似乎是一種必然的規律。
在物件的建構和析構過程中,呼叫了NewInstance和FreeInstance兩個虛函數,來建立和釋放物件實例的記憶體空間。之所以將這兩個函數宣告為虛函數,是為了能讓使用者在編寫需要使用者自己管理記憶體的特殊物件類別時(如在一些特殊的工業控製程式中),有擴充的空間。
而將AfterConstruction和BeforeDestruction宣告為虛函數,也是為了將來派生的類別在產生物件之後,有機會讓新誕生的物件呼吸第一口新鮮空氣,而在物件消亡之前可以允許物件完成善後事宜,這都是合情合理的事。其實,TForm物件和TDataModule物件的OnCreate事件和OnDestroy事件,就是在TForm和TDataModule重載的這兩個虛擬函數過程分別觸發的。
此外,TObjec也提供了一個Free方法,它不是虛方法,它是為了那些搞不清物件是否為空(nil)的情況下能安全釋放物件而專門提供的。其實,搞不清楚物件是否為空,本身就有程序邏輯不清晰的問題。不過,任何人都不是完美的,都可能犯錯,使用Free能避免偶然的錯誤也是件好事。然而,編寫正確的程式不能一味依靠這樣的解決方法,還是應該以保證程式的邏輯正確性為程式設計的第一個目標!
有興趣的朋友可以閱讀System單元的原始程式碼,其中,大量的程式碼是用組合語言書寫的。細心的朋友可以發現,TObject的建構子Create和析構函數Destory竟然沒有寫任何程式碼,其實,在調試狀態下通過Debug的CPU窗口,可清楚反映出Create和Destory的彙編程式碼。因為,締造DELPHI的大師門不想將過多複雜的東西提供給用戶,他們希望用戶在簡單的概念上編寫應用程序,將複雜的工作隱藏在系統的內部由他們承擔。所以,在發布System.pas單元時特別將這兩個函數的程式碼去掉,讓使用者認為TObject是萬物之源,用戶衍生的類別完全從虛無開始,這本身並沒有錯。雖然,閱讀DELPHI的這些最本質的程式碼需要少量的組合語言知識,但閱讀這樣的程式碼,可以讓我們更深刻認識DELPHI世界的起源和發展的基本規律。即使看不太懂,能起碼了解一些基本東西,對我們寫DELPHI程式也是大有幫助。
第二節TClass原子
在System.pas單元中,TClass是這樣定義的:
TClass = class of TObject;
它的意思是說,TClass是TObject的類別。因為TObject本身就是一個類,所以TClass就是所謂的類別的類別。
從概念上來說,TClass是類別的類型,即,類別之類。但是,我們知道DELPHI的一個類,代表一項VMT資料。因此,類之類可以認為是為VMT資料項定義的類型,其實,它就是一個指向VMT資料的指標類型!
在以前的傳統C++語言中,是不能定義類別的型別的。物件一旦編譯就固定下來,類別的結構資訊已經轉化為絕對的機器碼,在記憶體中將不存在完整的類別資訊。一些較高級的物件導向語言才能支援對類別資訊的動態存取和調用,但往往需要一套複雜的內部解釋機制和較多的系統資源。而DELPHI的Object Pascal語言吸收了一些高階物件導向語言的優秀特徵,又保留可將程式直接編譯成機器碼的傳統優點,比較完美地解決了高階功能與程式效率的問題。
正是由於DELPHI在應用程式中保留了完整的類信息,才能提供諸如as和is等在運行時刻轉換和判別類的高級面向對像功能,而類的VMT數據在其中起了關鍵性的核心作用。有興趣的朋友可以閱讀System單元的AsClass和IsClass兩個彙編過程,他們是as和is操作符的實作程式碼,以加深對類別和VMT資料的理解。
.....
後面的內容還有對虛構造函數的理解,Interface的實現機制和異常處理的實現機制,等等DLPHI的基本原理。希望五一之後能寫完,再貢獻給大家。