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