(转)剖析Delphi中的构造和析构
剖析Delphi中的構(gòu)造和析構(gòu)
1 Delphi中的對象模型: 2
1.1 對象名表示什么? 2
1.2 對象存儲在哪里? 2
1.3 對象中存儲了什么?它們是如何存儲的? 3
2 構(gòu)造函數(shù)與創(chuàng)建對象 5
2.1 什么是構(gòu)造函數(shù)?(“特殊的”類方法) 5
2.2 對象的創(chuàng)建的全過程 5
2.3構(gòu)造函數(shù)另類用法(使用類引用實現(xiàn)構(gòu)造函數(shù)的多態(tài)性) 6
3 析構(gòu)函數(shù)與銷毀對象 7
3.1 什么是析構(gòu)函數(shù)(“天生的”虛方法) 7
3.2 對象銷毀的全過程 7
3.3 destroy, free, freeAndNil, release用法和區(qū)別 7
4 VCL構(gòu)造&析構(gòu)體系結(jié)構(gòu) 8
5 正確使用構(gòu)造函數(shù)和析構(gòu)函數(shù) 9
剖析Delphi中的構(gòu)造和析構(gòu)
摘 要: 本文通過對VCL/RTL的研究,來剖析構(gòu)造函數(shù)和析構(gòu)函數(shù)的實現(xiàn)機(jī)制和VCL中對象的體系結(jié)構(gòu),并說明如何正確地創(chuàng)建和釋放對象。
關(guān)鍵字: 構(gòu)造,析構(gòu),創(chuàng)建對象,銷毀對象,堆,棧,多態(tài)。
作 者: majorsoft
問題
Delphi中構(gòu)造函數(shù)和析構(gòu)函數(shù)的實現(xiàn)機(jī)制是什么?和C 有何不同?如何做到正確地創(chuàng)建和釋放對象?
解決思路
如何正確使用構(gòu)造和析構(gòu)是我們在使用Delphi過程中經(jīng)常遇到的問題,在大富翁論壇中的Oriented Pascal欄目有不少相關(guān)帖子(詳見相關(guān)問題),本人也曾遇到過類似的問題,下面通過對VCL/RTL源代碼的研究,來理解構(gòu)造函數(shù)和析構(gòu)函數(shù)的實現(xiàn)機(jī)制。
1 Delphi中的對象模型:
1.1 對象名表示什么?
與C 不同,Delphi中的對象名(也可以稱做變量)表示對象的引用,并不表示對象本身,相當(dāng)于指向?qū)ο蟮闹羔?這就所謂的“對象引用模型”。如圖所示:
Obj(對象名) 實際的對象
Vmt 入口地址
數(shù)據(jù)成員
?
?
圖1對象名引用內(nèi)存中的對象
1.2 對象存儲在哪里?
每個應(yīng)用程序?qū)⒎峙浣o其運行的內(nèi)存分為四個區(qū)域:
代碼區(qū)(Code area)
全局?jǐn)?shù)據(jù)區(qū)(data area)
堆區(qū)(heap area)
棧區(qū)(stack area)
?
?
?
圖2 程序內(nèi)存空間
代碼區(qū):存儲程序中程序代碼,包括所有的函數(shù)代碼
全局?jǐn)?shù)據(jù)區(qū):存儲全局?jǐn)?shù)據(jù)。
堆區(qū):又叫“自由存儲區(qū)”,存儲動態(tài)數(shù)據(jù)(在Delphi中包括對象和字符串)。作用域為整個應(yīng)用程序的整個生命周期直到調(diào)用了析構(gòu)方法。
棧區(qū):又叫“自動存儲區(qū)”存儲程序中的局部數(shù)據(jù),在C 中,局部變量實際上是auto類型的變量。作用域為函數(shù)內(nèi)部,函數(shù)調(diào)用完系統(tǒng)就立即回收棧空間。
在C 中,對象既可創(chuàng)建在堆(heap)上,也可以創(chuàng)建在棧(stack)中,還可以在全局?jǐn)?shù)據(jù)中創(chuàng)建對象,故C 有全局對象、局部對象、靜態(tài)對象和堆對象四種對象之說。而在Delphi中,所有的對象都是建立堆(heap)存儲區(qū)上,所以Delphi構(gòu)造函數(shù)不能自動被調(diào)用,而必須由程序員自己調(diào)用(在設(shè)計器拖動組件,此時對象由Delphi創(chuàng)建)。下面的程序說明Delphi和C 中創(chuàng)建對象的區(qū)別:
在Delphi中:
Procedure CreateObject(var FooObjRef:TFooObject);
begin
FooObjRef:=TfooObject.create;
//由程序員調(diào)用,過程調(diào)用完之后,對象依然存在.不需要進(jìn)行拷貝
FooObject.caption=’I am created in stack of CreateObject()’;
End;
而在C 中:
TfooObject CreateObject(void);
{
TfooObject FooObject;//創(chuàng)建局部對象
// static TfooObject FooObject;//創(chuàng)建靜態(tài)局部對象
//對象自動調(diào)用默認(rèn)的構(gòu)造函數(shù)進(jìn)行創(chuàng)建,對象此時在函數(shù)棧中創(chuàng)建
FooObject.caption=’I am created in stack of CreateObject()’;
return FooObject;
//返回的時候進(jìn)行了對象拷貝,原來創(chuàng)建的對象隨函數(shù)的調(diào)用結(jié)束后,自動銷毀}
TfooObject fooObject2;//創(chuàng)建全局對象。
void main();
{ TFooObject* PfooObjec=new TfooObject;
//創(chuàng)建堆對象。函數(shù)調(diào)用完之后,對象依然存在,不需要進(jìn)行拷貝。}
1.3 對象中存儲了什么?它們是如何存儲的?
與C 不同的是,Delphi中的對象只存儲了數(shù)據(jù)成員和虛擬方法表(vmt)的入口地址,而沒有存儲方法,如圖所示:
對 象 虛擬方法表 代碼段
Vmt地址
name:String
width:integer;
ch1:char;
…
Proc1
Func1
…
procn
funcn
?
…
圖 3 對象的結(jié)構(gòu) …
也許你對上面的說法存在著些疑問,請看下面的程序:
TsizeAlignTest=class
private
i:integer;
ch1,ch2:char;
j:integer;
public
procedure showMsg;
procedure virtMtd; virtual;
end;
memo1.Lines.Add(inttostr(sizeTest.InstanceSize) '''':InstanceSize'''');
memo1.Lines.Add(inttostr(integer(sizeTest)) ''''<-start Addr'''');
memo1.Lines.Add(inttostr(integer(@(sizeTest.i))) ''''<-sizeTest.i'''');
memo1.Lines.Add(inttostr(integer(@(sizeTest.ch1))) ''''<-sizeTest.ch1'''');
memo1.Lines.Add(inttostr(integer(@(sizeTest.ch2))) ''''<-sizeTest.ch2'''');
memo1.Lines.Add(inttostr(integer(@(sizeTest.j))) ''''<-sizeTest.j'''');
結(jié)果顯示:
16:InstanceSize
14630724<-start Addr
14630728<-sizeTest.i
14630732<-sizeTest.ch1
14630733<-sizeTest.ch2
14630736<-sizeTest.j
數(shù)據(jù)成員和vmt入口地址就占了16個字節(jié)!,兩個成員函數(shù)showMsg, virtMtd在對象的存儲區(qū)中根本沒占空間。
那么成員函數(shù)到底存儲在哪兒呢?由于Delphi是基于RTL(運行時類型庫)的,所有的成員函數(shù)都在類中存儲,成員函數(shù)實際上就是方法指針,它指向成員函數(shù)的入口地址,該類的所有對象共享這些成員函數(shù)。那么怎樣找到成員函數(shù)的入口地址呢?對于靜態(tài)函數(shù),這個工作由編譯器來完成的,在編譯過程中,根據(jù)類對象引用/指針的類型,即直接在類來中找到成員函數(shù)的入口地址(此時并不需要對象存在),這也就是所謂的靜態(tài)綁定;而對于虛方法(包括動態(tài)方法),則是通過在運行時的對象的虛擬方法表vmt入口地址(即對象的前四個字節(jié),此時對象一定要存在,否則就會導(dǎo)致指針訪問出錯),來找到成員函數(shù)的入口地址,這也就是所謂的動態(tài)綁定。
?注 意
上面提到,所有的成員函數(shù)都在類中存儲,實際上也包括虛擬方法表Vmt。從Delphi的代碼自動完成功能(它依賴于編譯信息)可以看出,當(dāng)我們在輸入完對象名,再輸入“.“之后,此時Delphi重新編譯了一遍,列出所有的數(shù)據(jù)成員和所有的靜態(tài)方法,所有的虛方法,所有的類方法,所有的構(gòu)造函數(shù)和析構(gòu)函數(shù),大家可以動手試試看是不是這樣的。
類虛方法表vmt入口地址
數(shù)據(jù)成員模板信息
靜態(tài)方法表等
虛方法表vmt
對 象
Vmt入口地址
數(shù)據(jù)成員
?
?
上面的程序還演示了對象數(shù)據(jù)成員的對齊方式(物理數(shù)據(jù)結(jié)構(gòu)),以4字節(jié)對齊(windows默認(rèn)的對齊方式),如下圖所示:
Vmt Entrance Addr
i
Ch1 Ch2
j
?
?
2 構(gòu)造函數(shù)與創(chuàng)建對象
2.1 什么是構(gòu)造函數(shù)?(“特殊的”類方法)
從OO(面向?qū)ο?#xff09;思想的語義上講,構(gòu)造函數(shù)負(fù)責(zé)對象的創(chuàng)建,但就OOP語言的實現(xiàn)上講,無論Delphi還是C ,構(gòu)造函數(shù)充其量只做了對象的初始化工作(包含調(diào)用內(nèi)部子對象的構(gòu)造函數(shù)),并沒有負(fù)責(zé)創(chuàng)建對象的全過程(參考2.2)。
另外,與C 中不同的是,Delphi為構(gòu)造函數(shù)定義了另一種方法類型(mkConstructor,參見Delphi安裝目錄下的\Source\RTL\Common\typInfo.pas,125行),我們可以把它理解為 “特殊的”類方法。它只能通過類(類名/類引用/類指針)來調(diào)用,而一般的類方法既可以通過類也可以通過對象來調(diào)用;還有一點特殊就是構(gòu)造函數(shù)中內(nèi)置的self參數(shù)是指向?qū)ο蟮?#xff0c;而在類方法中self是指向類的,我們通常在其中對其數(shù)據(jù)成員進(jìn)行初始化工作,使其成為真正意義上的對象,這都得益于self這個參數(shù)。
在默認(rèn)情況下,構(gòu)造函數(shù)是靜態(tài)函數(shù),我們可以把它設(shè)為虛方法,在其派生類中對其覆載(Override),這樣可以實現(xiàn)構(gòu)造函數(shù)的多態(tài)性(參見2.4),也可以對其進(jìn)行重載(Overload),創(chuàng)建多個構(gòu)造函數(shù),還可以在派生類直接覆蓋(Overlay)父類的構(gòu)造函數(shù),這樣在派生類屏蔽了父類的構(gòu)造函數(shù),在VCL中就采用了這些技術(shù),形成一個構(gòu)造&析構(gòu)的“體系結(jié)構(gòu)”(參見4)
2.2 對象的創(chuàng)建的全過程
對象的創(chuàng)建完整過程應(yīng)該包括分配空間、構(gòu)造物理數(shù)據(jù)結(jié)構(gòu)、初始化、內(nèi)部子對象的創(chuàng)建。上面提到,構(gòu)造函數(shù)只是負(fù)責(zé)初始化工作以及調(diào)用內(nèi)部子對象的構(gòu)造函數(shù),那么分配空間和構(gòu)造物理結(jié)構(gòu)是怎么完成的呢?這由于編譯器在做了額外的事情,我們不知道而已。編譯到構(gòu)造函數(shù)時,會構(gòu)造函數(shù)之前,會在插入一行“call @ClassCreate”匯編代碼,它實際上就是system 單元中的_ClassCreate函數(shù),下面看看_ClassCreate函數(shù)的部分源碼:
function _ClassCreate(AClass: TClass; Alloc: Boolean): TObject;
asm
{ -> EAX = pointer to VMT }
{ <- EAX = pointer to instance }
…
CALL dword ptr [EAX].vmtNewInstance //調(diào)用NewInstance
…
End; {\Source\RTL\sys\system.pas,第8939行}
VmtNewInstance=-12; 它是NewInstance 函數(shù)在類中的偏移量,則“CALL dword ptr [EAX].vmtNewInstance”實際上就是調(diào)用NewInstance,請看TObject.NewInstance:源碼:
class function NewInstance: TObject; virtual;
class function TObject.NewInstance: TObject;
begin
Result := InitInstance(_GetMem(InstanceSize));
end;
“InitInstance(_GetMem(InstanceSize))”依次調(diào)用了三個函數(shù):
1) 首先調(diào)用InstanceSize(),返回實際類的對象大小
class function TObject.InstanceSize: Longint; //相當(dāng)于一個虛方法
begin
Result := PInteger(Integer(Self) vmtInstanceSize)^;//返回實際類的對象大小
end;
2) 調(diào)用_GetMem()在堆上分配Instance大小的內(nèi)存,并返回對象的引用
3) 調(diào)用InitInstance()進(jìn)行構(gòu)造物理數(shù)據(jù)結(jié)構(gòu),并把成員設(shè)置默認(rèn)值,比如把整型的數(shù)據(jù)成員的值設(shè)為0,指針設(shè)為nil等。如果有虛方法,把虛擬方法表Vmt的入口地址賦給對象的前四個字節(jié)。
在調(diào)用完NewInstance之后,這個時候的對象,只有“空殼”,而沒有實際的“內(nèi)容”,所以就需要要調(diào)用定制的構(gòu)造函數(shù)對對象進(jìn)行有意義的初始化,以及調(diào)用內(nèi)部子對象的構(gòu)造函數(shù),使程序中的對象能真實反映現(xiàn)實世界的對象。這就是對象創(chuàng)建的全過程。
2.3構(gòu)造函數(shù)另類用法(使用類引用實現(xiàn)構(gòu)造函數(shù)的多態(tài)性)
在Delphi中,類也是作為對象存儲的,所以同樣存在著多態(tài)性,它是借助類引用和虛類方法來實現(xiàn)的,這樣提供了類一級的多態(tài)的實現(xiàn)。把類方法設(shè)為虛方法,在其派生類中覆載(override)它,再通過基類的引用/指針調(diào)用它,這樣根據(jù)類引用/指針指向?qū)嶋H類來構(gòu)造對象。請看下面的程序:
TmyClass=class
constructor create;virtual;
end;
Ttmyclass=class of TmyClass;//基類的類引用
TmyClassSub=class(TmyClass)
constructor create; override;
end;
procedure CreateObj(Aclass:TTMyClass;var Ref);
begin
Tobject(Ref):=Aclass.create;
//ref為無類型,和任何類型都不兼容,所以使用時必須顯式強(qiáng)制轉(zhuǎn)換(cast)
//Aclass為類引用,統(tǒng)一的函數(shù)接口,不同的實現(xiàn)。
//它會根據(jù)Aclass引用/指向的實際類來構(gòu)造對象。
End;
…
CreateObj(TmyClass,Obj);
CreateObj(TmyClassSub,subObj);
3 析構(gòu)函數(shù)與銷毀對象
3.1 什么是析構(gòu)函數(shù)(“天生的”虛方法)
從OOP思想的語義上講,析構(gòu)函數(shù)負(fù)責(zé)銷毀對象,釋放資源。在Delphi中,同義。
Delphi為析構(gòu)函數(shù)也定義了一種方法類型(mkConstructor,參見Delphi安裝目錄下的\Source\RTL\Common\typInfo.pas,125行),在VCL中,它實際是一種“天生的”虛方法,在VCL類所有的祖先-Tobject中定義了“destructor Destroy; virtual; ”。為什么VCL要這么做呢?因為它要保證在多態(tài)情況下對象能正確地被析構(gòu)。如果不使用虛方法,則可能只析構(gòu)了基類子對象,從而造成所謂的“內(nèi)存泄露”。所以為了保證正確地析構(gòu)對象,析構(gòu)函數(shù)都需要加override聲明。
3.2 對象銷毀的全過程
先銷毀派生類子對象,再銷毀基類子對象。
提 示
在派生類中,基類子對象指從基類中繼承的部分,派生類中子對象是指新增的部分。
3.3 destroy, free, freeAndNil, release用法和區(qū)別
destroy:虛方法
釋放內(nèi)存,在Tobject中聲明為virtual,通常是在其子類中override 它,且要加上inherited關(guān)鍵字,才能保證派生類對象正確地被銷毀;
但destroy一般不能直接用,為什么?
假如當(dāng)一個對象為nil,我們?nèi)匀徽{(diào)用destroy,此時會產(chǎn)生錯誤。因為destroy是虛方法,它要根據(jù)對象中的頭四個字節(jié)找到虛擬方法表Vmt的入口地址,從而找到destroy的入口地址,所以此時對象一定要存在。但free就是靜態(tài)方法,它只需根據(jù)對象引用/指針的類型來確定,即使對象本身不存在也沒問題,而且在free中有判斷對象是否存在的操作, 所以用free比用destroy安全。
2)free:靜態(tài)方法
測試對象是否為nil, 非nil則調(diào)用destroy。下面是free的Delphi代碼:
procedure Tobject.Free;
begin
if Self <> nil then
Destroy;
end;
一靜一動,取長補(bǔ)短,豈不妙哉!
不過,調(diào)用Destroy只是把對象銷毀了,但并沒有把對象的引用設(shè)為nil,這需要程序員來完成,不過自從Delphi5之后,在sysUtils單元中提供了一個freeAndNil。
3)freeAndNil;一般方法,非對象方法,非類方法。
SysUtils單元中FreeAndNil 定義
procedure FreeAndNil(var Obj);
var
Temp: TObject;
begin
Temp := TObject(Obj);
Pointer(Obj) := nil;
Temp.Free;
end;
建議大家用它代替free/Destroy,以便確保正確地釋放對象。
4)release;TcustomForm中定義的靜態(tài)方法。
當(dāng)窗口中所有的事件處理完之后,才調(diào)用free函數(shù)。常用在銷毀窗口,而在這個窗口中事件處理需要一定的時間的時候,用這個方法能確保窗口事件處理完之后才銷毀窗口。下面是TCustomForm.Release的Delphi源代碼:
procedure TCustomForm.Release;
begin
PostMessage(Handle, CM_RELEASE, 0, 0);
//向窗口發(fā)CM_RELEASE消息到消息隊列,當(dāng)所有的窗口事件消息處理完之后,
//再調(diào)用CM_RELEASE消息處理過程CMRelease
end;
再看看下面CM_RELEASE消息處理過程CMRelease的定義:
procedure CMRelease(var Message: TMessage); message CM_RELEASE;
procedure TCustomForm.CMRelease;
begin
Free; //最后還是free;
end;
4 VCL構(gòu)造&析構(gòu)體系結(jié)構(gòu)
TObject
constructor Create;//靜態(tài)方法
destructor Destroy; virtual;
?
?
TPersistent
destructor Destroy; override;
?
TComponent
constructor Create(AOwner: TComponent); virtual;
destructor Destroy; override;
?
TControl
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
?
…
下面分析VCL中的構(gòu)造和析構(gòu)的源代碼,以Tcontrol為例:
constructor TControl.Create(AOwner: TComponent);
begin
inherited Create(AOwner);//創(chuàng)建基類子對象,并把析構(gòu)權(quán)移交給AOwner。放在最前面
//這樣就保證了“先創(chuàng)建基類子對象,再創(chuàng)建派生類子對象”的順序
…//初始化,以及調(diào)用內(nèi)部子對象的構(gòu)造函數(shù)
end;
destructor TControl.Destroy;
begin
…//析構(gòu)派生類中內(nèi)部子對象
inherited Destroy;//析構(gòu)基類對象,放在最后面
//這樣就保證了“先析構(gòu)派生類子對象,再析構(gòu)基類子對象”的順序
end;
5 正確使用構(gòu)造函數(shù)和析構(gòu)函數(shù)
經(jīng)過上面的分析,下面總結(jié)一下使用構(gòu)造函數(shù)和析構(gòu)函數(shù)的原則:
在使用對象之前,必須先建立一個對象時,并且及時銷毀對象,以釋放資源。
兩個對象引用賦值時,要確保出現(xiàn)的無名對象(指沒有被引用的對象)能被釋放。
當(dāng)創(chuàng)建一個組件時,建議設(shè)置一個宿主組件(即使用AOwner參數(shù),通常是窗體),由Aowner來管理對象的銷毀,那么就不必惦記著銷毀該組件了,這是Delphi在窗體上/數(shù)據(jù)模塊設(shè)計并創(chuàng)建組件是采用的方法。所以我們不必書寫調(diào)用該組件的析構(gòu)函數(shù)。
當(dāng)函數(shù)的返回類型為對象時,那么Result也是對象的引用,確保Result引用的對象要存在。
若要使用obj<>nil 或assigned(nil)測試對象存在時,在調(diào)用析構(gòu)之后還應(yīng)obj:=nil。
請參考演示程序的源代碼
說明(建議要有)
所有的Delphi程序已在win2k Delphi6 sp2 上通過,對于C 程序,只是為了說明與Delphi中不同,并不保證能直接運行。為了加深對本篇文章的理解,建議參考演示程序。
這篇文章包括了我在學(xué)習(xí)VCL/RTL中的一些經(jīng)驗和體會,加上本人的個人能力有限,難免出現(xiàn)錯誤,請大家不吝指正!
在閱讀本篇文章之前,需要讀者對Oriented Pascal語言有一定的了解,并能理解多態(tài),如果您對其中一些概念還不是很清楚的話,請參考相關(guān)文章。
通過本篇文章,你應(yīng)該能比較清楚地理解Delphi中的對象模型、構(gòu)造&析構(gòu)實現(xiàn)機(jī)制以及VCL中構(gòu)造&析構(gòu) 體系結(jié)構(gòu),并能掌握使用構(gòu)造&析構(gòu)的使用方法。Delphi中的構(gòu)造&析構(gòu)相當(dāng)于C 中的算是簡單多了,我們應(yīng)該能掌握它。
?
轉(zhuǎn)載于:https://www.cnblogs.com/chengxin1982/archive/2009/03/04/1403378.html
總結(jié)
以上是生活随笔為你收集整理的(转)剖析Delphi中的构造和析构的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: phpweb2.0 开发实战 ----
- 下一篇: 架构师之路(1)---面向过程和面向对象