程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Delphi >> DELPHI的原子世界

DELPHI的原子世界

編輯:Delphi

  在使用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 - 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數據的理解。 有了類的類型,就可以將類作為變量來使用。可以將類的變量理解為一種特殊的對象,你可以象訪問對象那樣訪問類變量的方法。例如:我們來看看下面的程序片段:
  type
    TSampleClass = class of TSampleObject;
  TSampleObject = class( TObject )
    public constructor Create;
  destructor Destroy; override;
  class function GetSampleObjectCount: Integer;
  procedure GetObjectIndex: Integer;
  end;
  var
    aSampleClass : TSampleClass;
  aClass : TClass;
  在這段代碼中,我們定義了一個類TSampleObject及其相關的類類型TSampleClass,還包括兩個類變量aSampleClass和aClass。此外,我們還為TSampleObject類定義了構造函數、析構函數、一個類方法GetSampleObjectCount和一個對象方法GetObjectIndex。
  首先,我們來理解一下類變量aSampleClass和aClass的含義。
  顯然,你可以將TSampleObject和TObject當作常量值,並可將它們賦值給aClass變量,就好象將123常量值賦值給整數變量i一樣。所以,類類型、類和類變量的關系就是類型、常量和變量的關系,只不過是在類的這個層次上而不是對象層次上的關系。當然,直接將TObject賦值給aSampleClass是不合法的,因為aSampleClass是TObject派生類TSampleObject的類變量,而TObject並不包含與TSampleClass類型兼容的所有定義。相反,將TSampleObject賦值給aClass變量卻是合法的,因為TSampleObject是TObject的派生類,是和TClass類型兼容的。這與對象變量的賦值和類型匹配關系完全相似。
  然後,我們再來看看什麼是類方法。
  所謂類方法,就是指在類的層次上調用的方法,如上面所定義的GetSampleObjectCount方法,它是用保留字class聲明的方法。類方法是不同於在對象層次上調用的對象方法的,對象方法已經為我們所熟悉,而類方法總是在訪問和控制所有類對象的共同特性和集中管理對象這一個層次上使用的。在TObject的定義中,我們可以發現大量的類方法,如ClassName、ClassInfo和NewInstance等等。其中,NewInstance還被定義為virtual的,即虛的類方法。這意味作你可以在派生的子類中重新編寫NewInstance的實現方法,以便用特殊的方式構造該類的對象實例。
  在類方法中你也可使用self這一標識符,不過其所代表的含義與對象方法中的self是不同的。類方法中的self表示的是自身的類,即指向VMT的指針,而對象方法中的self表示的是對象本身,即指向對象數據空間的指針。雖然,類方法只能在類層次上使用,但你仍可通過一個對象去調用類方法。例如,可以通過語句aObject.ClassName調用對象TObject的類方法ClassName,因為對象指針所指向的對象數據空間中的頭4個字節又是指向類VMT的指針。相反,你不可能在類層次上調用對象方法,象TObject.Free的語句一定是非法的。 值得注意的是,構造函數是類方法,而析構函數是對象方法!
  什麼?構造函數是類方法,析構函數是對象方法!有沒有搞錯?
  你看看,當你創建對象時分明使用的是類似於下面的語句:
  aObject := TObject.Create;
  分明是調用類TObject的Create方法。而刪除對象時卻用的下面的語句:
  aObject.Destroy;
  即使使用Free方法釋放對象,也是間接調用了對象的Destroy方法。
  原因很簡單,在構造對象之前,對象還不存在,只存在類,創建對象只能用類方法。相反,刪除對象一定是刪除已經存在的對象,是對象被釋放,而不是類被釋放。
  最後,順便討論一下虛構造函數的問題。
  在傳統的C++語言中,可以實現虛析構函數,但實現虛構造函數卻是一個難題。因為,在傳統的C++語言中,沒有類的類型。全局對象的實例是在編譯時就存在於全局數據空間中,函數的局部對象也是編譯時就在堆棧空間中映射的實例,即使是動態創建的對象,也是用new操作符按固定的類結構在堆空間中分配的實例,而構造函數只是一個對已產生的對象實例進行初始化的對象方法而已。傳統C++語言沒有真正的類方法,即使可以定義所謂靜態的基於類的方法,其最終也被實現為一種特殊的全局函數,更不用說虛擬的類方法,虛方法只能針對具體的對象實例有效。因此,傳統的C++語言認為,在具體的對象實例產生之前,卻要根據即將產生的對象構造對象本身,這是不可能的。的確不可能,因為這會在邏輯上產生自相矛盾的悖論!
  然而,正是由於在DELPHI中有動態的類的類型信息,有真正虛擬的類方法,以及構造函數是基於類實現的等等這些關鍵概念,才可實現虛擬的構造函數。對象是由類產生的,對象就好象成長中的嬰兒,而類就是它的母親,嬰兒自己的確不知道自己將來會成為什麼樣的人,可是母親們卻用各自的教育方法培養出不同的人,道理是相通的。
  正是在TComponent類的定義中,構造函數Create被定義為虛擬的,才能使不同類型的控件實現各自的構造方法。這就是TClass創造的類之類概念的偉大,也是DELPHI的偉大。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved