| --------------------------------------------------------- 原創文章,如轉載,請注明出處 --------------------------------------------------------- 以下內容為備忘,你可能早已知道或者早已注意。 1, 區分類的forward聲明和繼承自TObject的子類 例如: type TFirst = class;?? //forward聲明, 類TFirst的具體聲明在該聲明區域后部 TSecond = class?? //一個完整的類定義,定義了類型TSecond end; TThird = class(TObject); //同樣是一個完整的類型定義,定義了類型TThird, ???? //這個類不包含任何數據成員和方法 2, constructor 與 destructor 我談兩點:用對象名和類名調用create的不同、構造和析構的虛與實 首先需要說的是,對象被動態分配內存塊,內存塊結構由類型決定,同時類型也決定了對該類型的“合法”操作! 一個對象的構造函數用來得到這個內存塊。 <1>, 用對象名和類名調用create的不同 構造函數是一個類方法,通常應該由類來調用,如下: AMan := TMan.Create; 這條語句,會在堆中分配內存塊(當然,它不僅僅干這些,實際上它還會把類型中所有的有序類型字段置0, 置所有字符串為空,置所有指針類型為nil,所有variant為Unassigned;實際上,構造函數只是把內存塊進行 了清零,內存塊清零意味著對所有的數據成員清零),并把這個內存塊的首地址給AMan。 但如果你用下面的方式調用, AMan2 := AMan.Create; //假設AMan已經被構造, 如未被構造,會產生運行時異常, ???????? //本質上,對未構造的對象的操作是對非法內存的操作,結果不 ???????? //可預知! 這實際上相當于調用一個普通的方法,不會分配內存塊,當然也不會自動初始化。這和你調用下面的方法 類似, ??????? AMan.DoSomething; //DoSomething 為類 TMan的普通方法 當然,構造函數畢竟是函數,它會有一個返回值,這個返回值就是對象的地址,所以AMan2和AMan指向同 一地址。 Note:不要試圖通過用對象名調用create方法來實現對象初始化!!因為把構造函數當做普通的方法來調用 并不會實現自動初始化,所以用對象名來調用create方法就顯得很雞肋了,當然,某些場合,這是一種技巧。 <2>, 構造和析構的虛與實 構造函數和析構函數應該是虛的還是實的?這很疑惑! 看代碼: --------------------------------- type TMan = class(TObject) public ?? constructor Create; ?? destructor Destroy; end; TChinese = class(TMan) public ?? constructor create; ?? destructor Destroy; end; TBeijing = class(TChinese) public ?? constructor Create; ?? destructor Destroy; end; .... ??????? var ?? AMan, AMan2, AMan3: TMan; ?? AChinese: TChinese; ?? ABeijing: TBeijing; begin ?? AMan := TChinese.Create; ?? AMan2 := TMan.Create; ?? AMan3 := TBeijing.Create; ?? ?? AMan.Free; ?? AMan2.Free; ?? AMan3.Free; end; 如果構造都是“實”的,無論如何,對于上面的用法,對象都可以被正確構造。但是對于析構,上述代碼有 問題!如果加入測試代碼,你會發現所有的析構方法根本不會被執行。知道,Free方法繼承自TObject,其 源碼如下: procedure TObject.Free; begin ?? if Self <> nil then ?? Destroy; end; constructor TObject.Create; begin end; destructor TObject.Destroy; begin end; Note:通常,self內置參數是指向對象本身,而在類方法中self是指向類本身。 很顯然,在前面的代碼中, AMan, AMan2, AMan3不是nil,free方法執行了。但是很遺憾的是,它所執行的 Destroy其實是基類TObject.Destroy; 而TObject.Destroy什么也不做。要理解這一點,需要理解類的內存組織 結構,理解“隱藏”的概念。所謂隱藏,指的是基類和子類有同名的方法(不考慮虛、動態的情況), 子類的該名 方法隱藏基類的方法名。運行時,到底調用哪一個方法取決于調用者類型。如下代碼: type TCountry = class public ?? procedure Test; end; TChina = class(TCountry) public ?? procedure Test; end; .... var ?? ACoun, ACoun1: TCountry; ?? AChina: TChina; begin ?? ACoun := TCountry.Create; ?? ACoun1 := TChina.Create; ?? AChina := TChina.Create; ?? ACoun.Test; //調用TCountry.Test ?? ACoun1.Test; //調用TCountry.Test ?? AChina.Test; //調用TChina.Test ?? ACoun.Free; ?? AChina.Free; end; 對于隱藏的情況,具體調用哪一個方法,取決于調用者本身的類型。 很顯然,一個實的析構方法有問題!可能會造成內存泄露。那么它們是虛的好了。問題來了, type TMan = class(TObject) public ?? constructor Create; ?? destructor Destroy; virtual; override; end; TChinese = class(TMan) public ?? constructor create; ?? destructor Destroy; virtual; override; end; TBeijing = class(TChinese) public ?? constructor Create; ?? destructor Destroy; virtual; override; end; 語法錯誤!上述的代碼編譯器會提示語法錯誤。ok,正確的應該是下面 TMan = class(TObject) public ?? constructor Create; ?? destructor Destroy; override; end; TChinese = class(TMan) public ?? constructor create; ?? destructor Destroy; override; end; TBeijing = class(TChinese) public ?? constructor Create; ?? destructor Destroy; override; end; 疑問來了,不是只有virtual 和 dynamic 才能被覆蓋么?在Delphi中為了保證對象被完全的析構, 所有的Destroy方法“天生”為虛方法,可以在任何地方被覆蓋!雖然這破壞了語言語法上的一致性, 但這保證一個對象不管從哪里(繼承)來,都只有唯一的析構方法。但是,編譯器不會強制你使用 override關鍵字,所以,你可以像下面這樣來定義析構。 TChinaese = class(TMan) public ?? destructor Destroy; end; 這可能會帶來問題,雖然不總是會帶來問題。因為在TChinese類中存在多個Destroy方法,分別是: TObject.Destroy; TMan.Destroy; TChinese.Destroy; 所以,結論是,無論在什么地方定義Destroy,覆蓋它!!以確保整個類譜中只有一個Destroy; **** 至此,我們確信:一個實的構造方法可以工作,一個實的析構會有問題,析構應該總被override! **** 那么,虛的構造可以正常工作么?看代碼: type TMan = class(TObject) public ?? constructor Create; virtual; end; TChinese = class(TMan) public ?? constructor create; override; end; TBeijing = class(TChinese) public ?? constructor Create; override; end; .... ??????? var ?? AMan, AMan2, AMan3: TMan; ?? AChinese: TChinese; ?? ABeijing: TBeijing; begin ?? AMan := TChinese.Create; ?? AMan2 := TMan.Create; ?? AMan3 := TBeijing.Create; ?? ?? AMan.Free; ?? AMan2.Free; ?? AMan3.Free; end; Ok,可以,工作正常。原因在于我們對對象的使用方法。我們總是用"確定"的類名來構建的,這總可以保證 我們調用正確的方法。但如果我們使用類引用的話,情況不同了,如下: type ?? TManClass = class of TMan; var ?? AManClass: TManClass; ?? AObj, AObj2, AObj3: TMan; begin ?? AManClass := TMan;?? //AManClass is TMan ?? AObj := AManClass.Create; //調用TMan.create ?? AManClass := TChinese; //AManClass is TChinese ?? AObj2 := AManClass.Create;??? //調用那一個create?? ?? AManClass := TBeijing; ?? AObj3 := AManClass.Create;?? //which create??? ?? .... end; 和前面討論析構的情況類似,這取決于方法的內存布局,注意我在最初提到過類型決定布局。方法的內存布局 取決于它是virtual, override, overload, dynamic.當TMan.create 是virtual, 并且,TChinese.create是 override時,AObj2 := AManClass.Create 調用的是 TChinese.Create; 如果不是,則是TMan.Create; 上面的解釋仍然是疑惑的,問題的關鍵在于什么是“類引用”!從語義上說,類引用是這樣一個類型:它代表了 一系列相關的存在繼承關系的類型,一個類引用變量代表一系列類型。區別于一般的變量,一般的變量表示的 是一系列實體,如:var count: integer; 意思是你定義了一個實例count,它的類型是integer; count 對應 于堆或棧中的一個內存塊,count是數據。這是一般情況下我們定義一個變量所隱含的意思。但類引用變量稍有 不同,如上,AManClass變量,它代表的是一系列和TMan兼容的“類型”,它的類型是TManClass,它沒有內存塊 (你沒辦法說,一個類型的類型對應什么內存塊),實際上,可以認為類引用類型指向了一個“代碼塊”。類引用 變量的值是“類”!這很重要(稍后會進一步解釋)!Delphi對類引用的實現實際上就是一個32位的指針(一個普通 指針),它指向了類的虛方法表(VMT). 類引用的值是“類”,這決定了任何時候使用類引用變量只能調用類方法!一個構造函數是類方法,所以使用類 引用調用Create方法是合理的,但是你不可以使用類引用調用非類方法,比如Destroy方法,實際上在類引用中 你也找不到Free方法。 需要提及的是,構造函數雖然很接近于類方法,甚至于你也可以使用類方法來模擬構造函數: TMan = class public ?? class function Create: TMan; end; class function TMan.Create: TMan; //模擬構造函數 begin ?? result := inherited Create; end; 但構造函數做的更多,除了前面提到的自動初始化外,構造函數還會在構造對象時將對象的VMT指針指向類的VMT。 此外,構造函數中的self指的是對象本身,類方法中self指的是類本身??傊?#xff0c;我們可以認為構造函數是一個 特殊的類函數。 Ok,到此,我們確信:類引用可以調用類方法,構造函數可以使用類引用來調用。 問題產生:由于類引用表示的是“一系列”類型,那么調用構造方法的時候,到底調用的是那一個構造方法呢? 對于TManClass類型變量而言,它只能調用的一定是TMan類的create方法,但如果TMan類的子類覆蓋了TMan的create 方法,調用TMan類的create方法實際上就是調用子類的create方法。這里的關鍵是理解“覆蓋”的概念。這實際上意味 著對使用類引用調用構造方法,虛的構造方法和實的構造方法是不同的。對于前面的例子而言, AManClass := TChinese; AObj2 := AManClass.create; //調用了TChinese.Create; AManClass := TBeijing; AObj3 := AManClass.create; //調用了TBeijing.Create; 但如果構造不是覆蓋虛函數,那么它們統統是調用TMan.Create;顯然這不是我們期望的。 Note:每個類有一個虛方法表(VMT),而不是每個對象有一個! 結論:構造函數是可以被類本身、類引用、對象調用,在這三種情況下構造函數的表現是不同的,在使用類引用的情況 下虛的構造通常是必要的。 那么,構造函數總是虛的是否更合理呢?我個人傾向于總是虛的更合理,因為這樣可以保證一個對象總是被正確的構造。 實際上,VCL中組件繼承的大多Create都被聲明成虛方法了。但Delphi并未將TObject的構造實現為虛,也許是出于兼容的 考慮,我不太清楚。我聽說,Java中的構造總是虛的。當然,總是虛的也有弊端,例如:你不可以通過調用Create方法來 只初始化父類的成員,因為在類的繼承體系中只有一個Create方法。但是靜態的構造可以實現父類的成員初始化。私下里, 我猜想也許是因為構造函數承擔了太多的義務,使得Delphi設計者沒有像析構那樣以總是虛來處理。 另,在C++中有一條法則,不要在構造中使用虛!但在Delphi中可以隨便,根本原因在于RTTI(運行時類型信息),Delphi在 運行時保存了完整的類型信息。這是C++和Delphi的重要不同之處。 ? 總結:對析構函數而言,無論是否聲明為virtual,它總是虛的,你總需要使用override;對構造函數而言,虛和實取決 于的使用方法,但一般而言使用虛更安全,在使用類引用的情況下一定要保證子類覆蓋基類的構造方法。 |