麻豆小视频在线观看_中文黄色一级片_久久久成人精品_成片免费观看视频大全_午夜精品久久久久久久99热浪潮_成人一区二区三区四区

首頁 > 編程 > Delphi > 正文

我的文章-《剖析Delphi中的構造和析構》

2019-11-18 18:10:42
字體:
來源:轉載
供稿:網友
 

剖析Delphi中的構造和析構

1 Delphi中的對象模型: 2
1.1 對象名表示什么? 2
1.2 對象存儲在哪里? 2
1.3 對象中存儲了什么?它們是如何存儲的? 3
2 構造函數與創建對象 5
2.1 什么是構造函數?(“特殊的”類方法) 5
2.2 對象的創建的全過程 5
2.3構造函數另類用法(使用類引用實現構造函數的多態性) 6
3 析構函數與銷毀對象 7
3.1 什么是析構函數(“天生的”虛方法) 7
3.2 對象銷毀的全過程 7
3.3 destroy, free, freeAndNil, release用法和區別 7
4 VCL構造&析構體系結構 8
5 正確使用構造函數和析構函數 9

剖析Delphi中的構造和析構
摘  要: 本文通過對VCL/RTL的研究,來剖析構造函數和析構函數的實現機制和VCL中對象的體系結構,并說明如何正確地創建和釋放對象。
關鍵字: 構造,析構,創建對象,銷毀對象,堆,棧,多態。
作  者: majorsoft
 
問題
      Delphi中構造函數和析構函數的實現機制是什么?和C++有何不同?如何做到正確地創建和釋放對象?
解決思路
如何正確使用構造和析構是我們在使用Delphi過程中經常遇到的問題,在大富翁論壇中的Oriented Pascal欄目有不少相關帖子(詳見相關問題),本人也曾遇到過類似的問題,下面通過對VCL/RTL源代碼的研究,來理解構造函數和析構函數的實現機制。
1 Delphi中的對象模型:
1.1 對象名表示什么?
與C++不同,Delphi中的對象名(也可以稱做變量)表示對象的引用,并不表示對象本身,相當于指向對象的指針,這就所謂的“對象引用模型”。如圖所示:
         Obj(對象名)                實際的對象
 
Vmt 入口地址   

數據成員
 

 

 

                         圖1對象名引用內存中的對象
1.2 對象存儲在哪里?
每個應用程序將分配給其運行的內存分為四個區域:
 
代碼區(Code area)   
全局數據區(data area)   
堆區(heap area)   
棧區(stack area) 

 

 

 

                      圖2  程序內存空間
代碼區:存儲程序中程序代碼,包括所有的函數代碼
全局數據區:存儲全局數據。
堆區:又叫“自由存儲區”,存儲動態數據(在Delphi中包括對象和字符串)。作用域為整個應用程序的整個生命周期直到調用了析構方法。
棧區:又叫“自動存儲區”存儲程序中的局部數據,在C++中,局部變量實際上是auto類型的變量。作用域為函數內部,函數調用完系統就立即回收棧空間。
在C++中,對象既可創建在堆(heap)上,也可以創建在棧(stack)中,還可以在全局數據中創建對象,故C++有全局對象、局部對象、靜態對象和堆對象四種對象之說。而在Delphi中,所有的對象都是建立堆(heap)存儲區上,所以Delphi構造函數不能自動被調用,而必須由程序員自己調用(在設計器拖動組件,此時對象由Delphi創建)。下面的程序說明Delphi和C++中創建對象的區別:
在Delphi中:
PRocedure CreateObject(var FooObjRef:TFooObject);
   begin
FooObjRef:=TfooObject.create;
//由程序員調用,過程調用完之后,對象依然存在.不需要進行拷貝
FooObject.caption=’I am created in stack of CreateObject()’;
   End;
   而在C++中:
   TfooObject CreateObject(void);
   { 
TfooObject FooObject;//創建局部對象
// static TfooObject FooObject;//創建靜態局部對象
 //對象自動調用默認的構造函數進行創建,對象此時在函數棧中創建
FooObject.caption=’I am created in stack of CreateObject()’;
return FooObject;
//返回的時候進行了對象拷貝,原來創建的對象隨函數的調用結束后,自動銷毀}
   TfooObject fooObject2;//創建全局對象。
void main();
   { TFooObject* PfooObjec=new TfooObject;
 //創建堆對象。函數調用完之后,對象依然存在,不需要進行拷貝。}
1.3 對象中存儲了什么?它們是如何存儲的?
 與C++不同的是,Delphi中的對象只存儲了數據成員和虛擬方法表(vmt)的入口地址,而沒有存儲方法,如圖所示:
             對  象              虛擬方法表             代碼段
 
Vmt地址   
name:String
width:integer;
 ch1:char;
…   
Proc1   
Func1    
…   
procn   
funcn 
                                    

 

                                                            …
                 
                                 圖 3 對象的結構                …
也許你對上面的說法存在著些疑問,請看下面的程序:
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');
結果顯示:
16:InstanceSize
14630724<-start Addr
14630728<-sizeTest.i
14630732<-sizeTest.ch1
14630733<-sizeTest.ch2
14630736<-sizeTest.j
數據成員和vmt入口地址就占了16個字節!,兩個成員函數showMsg, virtMtd在對象的存儲區中根本沒占空間。
那么成員函數到底存儲在哪兒呢?由于Delphi是基于RTL(運行時類型庫)的,所有的成員函數都在類中存儲,成員函數實際上就是方法指針,它指向成員函數的入口地址,該類的所有對象共享這些成員函數。那么怎樣找到成員函數的入口地址呢?對于靜態函數,這個工作由編譯器來完成的,在編譯過程中,根據類對象引用/指針的類型,即直接在類來中找到成員函數的入口地址(此時并不需要對象存在),這也就是所謂的靜態綁定;而對于虛方法(包括動態方法),則是通過在運行時的對象的虛擬方法表vmt入口地址(即對象的前四個字節,此時對象一定要存在,否則就會導致指針訪問出錯),來找到成員函數的入口地址,這也就是所謂的動態綁定。
 注  意
上面提到,所有的成員函數都在類中存儲,實際上也包括虛擬方法表Vmt。從Delphi的代碼自動完成功能(它依賴于編譯信息)可以看出,當我們在輸入完對象名,再輸入“.“之后,此時Delphi重新編譯了一遍,列出所有的數據成員和所有的靜態方法,所有的虛方法,所有的類方法,所有的構造函數和析構函數,大家可以動手試試看是不是這樣的。
 
類虛方法表vmt入口地址   
數據成員模板信息   
靜態方法表等   
虛方法表vmt 
              對 象
 
Vmt入口地址   
數據成員
 

 


上面的程序還演示了對象數據成員的對齊方式(物理數據結構),以4字節對齊(windows默認的對齊方式),如下圖所示:
 
Vmt Entrance Addr   
i   
Ch1 Ch2    

 

 

2 構造函數與創建對象
2.1 什么是構造函數?(“特殊的”類方法)
從OO(面向對象)思想的語義上講,構造函數負責對象的創建,但就OOP語言的實現上講,無論Delphi還是C++,構造函數充其量只做了對象的初始化工作(包含調用內部子對象的構造函數),并沒有負責創建對象的全過程(參考2.2)。
另外,與C++中不同的是,Delphi為構造函數定義了另一種方法類型(mkConstructor,參見Delphi安裝目錄下的/Source/RTL/Common/typInfo.pas,125行),我們可以把它理解為 “特殊的”類方法。它只能通過類(類名/類引用/類指針)來調用,而一般的類方法既可以通過類也可以通過對象來調用;還有一點特殊就是構造函數中內置的self參數是指向對象的,而在類方法中self是指向類的,我們通常在其中對其數據成員進行初始化工作,使其成為真正意義上的對象,這都得益于self這個參數。
在默認情況下,構造函數是靜態函數,我們可以把它設為虛方法,在其派生類中對其覆載(Override),這樣可以實現構造函數的多態性(參見2.4),也可以對其進行重載(Overload),創建多個構造函數,還可以在派生類直接覆蓋(Overlay)父類的構造函數,這樣在派生類屏蔽了父類的構造函數,在VCL中就采用了這些技術,形成一個構造&析構的“體系結構”(參見4)
2.2 對象的創建的全過程
對象的創建完整過程應該包括分配空間、構造物理數據結構、初始化、內部子對象的創建。上面提到,構造函數只是負責初始化工作以及調用內部子對象的構造函數,那么分配空間和構造物理結構是怎么完成的呢?這由于編譯器在做了額外的事情,我們不知道而已。編譯到構造函數時,會構造函數之前,會在插入一行“call @ClassCreate”匯編代碼,它實際上就是system 單元中的_ClassCreate函數,下面看看_ClassCreate函數的部分源碼
function _ClassCreate(AClass: TClass; Alloc: Boolean): TObject;
asm
        { ->    EAX = pointer to VMT      }
        { <-    EAX = pointer to instance }
        …
        CALL  dWord ptr [EAX].vmtNewInstance  //調用NewInstance

End; {/Source/RTL/sys/system.pas,第8939行}
VmtNewInstance=-12; 它是NewInstance 函數在類中的偏移量,則“CALL dword ptr [EAX].vmtNewInstance”實際上就是調用NewInstance,請看TObject.NewInstance:源碼:
class function NewInstance: TObject; virtual;
class function TObject.NewInstance: TObject;
begin
  Result := InitInstance(_GetMem(InstanceSize));
end;
  “InitInstance(_GetMem(InstanceSize))”依次調用了三個函數:
1) 首先調用InstanceSize(),返回實際類的對象大小
  class function TObject.InstanceSize: Longint; //相當于一個虛方法
begin
    Result := PInteger(Integer(Self) + vmtInstanceSize)^;//返回實際類的對象大小
end;
2) 調用_GetMem()在堆上分配Instance大小的內存,并返回對象的引用
3) 調用InitInstance()進行構造物理數據結構,并把成員設置默認值,比如把整型的數據成員的值設為0,指針設為nil等。如果有虛方法,把虛擬方法表Vmt的入口地址賦給對象的前四個字節。
在調用完NewInstance之后,這個時候的對象,只有“空殼”,而沒有實際的“內容”,所以就需要要調用定制的構造函數對對象進行有意義的初始化,以及調用內部子對象的構造函數,使程序中的對象能真實反映現實世界的對象。這就是對象創建的全過程。
   2.3構造函數另類用法(使用類引用實現構造函數的多態性)
在Delphi中,類也是作為對象存儲的,所以同樣存在著多態性,它是借助類引用和虛類方法來實現的,這樣提供了類一級的多態的實現。把類方法設為虛方法,在其派生類中覆載(override)它,再通過基類的引用/指針調用它,這樣根據類引用/指針指向實際類來構造對象。請看下面的程序:
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為無類型,和任何類型都不兼容,所以使用時必須顯式強制轉換(cast)
//Aclass為類引用,統一的函數接口,不同的實現。
//它會根據Aclass引用/指向的實際類來構造對象。
 End;

CreateObj(TmyClass,Obj);
CreateObj(TmyClassSub,subObj);
3 析構函數與銷毀對象
  3.1 什么是析構函數(“天生的”虛方法)
從OOP思想的語義上講,析構函數負責銷毀對象,釋放資源。在Delphi中,同義。
Delphi為析構函數也定義了一種方法類型(mkConstructor,參見Delphi安裝目錄下的/Source/RTL/Common/typInfo.pas,125行),在VCL中,它實際是一種“天生的”虛方法,在VCL類所有的祖先-Tobject中定義了“destructor Destroy; virtual; ”。為什么VCL要這么做呢?因為它要保證在多態情況下對象能正確地被析構。如果不使用虛方法,則可能只析構了基類子對象,從而造成所謂的“內存泄露”。所以為了保證正確地析構對象,析構函數都需要加override聲明。
3.2 對象銷毀的全過程
先銷毀派生類子對象,再銷毀基類子對象。
 提  示
在派生類中,基類子對象指從基類中繼承的部分,派生類中子對象是指新增的部分。
3.3 destroy, free, freeAndNil, release用法和區別
destroy:虛方法
 釋放內存,在Tobject中聲明為virtual,通常是在其子類中override 它,且要加上inherited關鍵字,才能保證派生類對象正確地被銷毀;
但destroy一般不能直接用,為什么?
假如當一個對象為nil,我們仍然調用destroy,此時會產生錯誤。因為destroy是虛方法,它要根據對象中的頭四個字節找到虛擬方法表Vmt的入口地址,從而找到destroy的入口地址,所以此時對象一定要存在。但free就是靜態方法,它只需根據對象引用/指針的類型來確定,即使對象本身不存在也沒問題,而且在free中有判斷對象是否存在的操作, 所以用free比用destroy安全。
2)free:靜態方法
測試對象是否為nil, 非nil則調用destroy。下面是free的Delphi代碼:
procedure Tobject.Free;
 begin
   if Self <> nil then
     Destroy;
  end;
 一靜一動,取長補短,豈不妙哉!
不過,調用Destroy只是把對象銷毀了,但并沒有把對象的引用設為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中定義的靜態方法。
當窗口中所有的事件處理完之后,才調用free函數。常用在銷毀窗口,而在這個窗口中事件處理需要一定的時間的時候,用這個方法能確保窗口事件處理完之后才銷毀窗口。下面是TCustomForm.Release的Delphi源代碼:
procedure TCustomForm.Release;
begin
  PostMessage(Handle, CM_RELEASE, 0, 0);
//向窗口發CM_RELEASE消息到消息隊列,當所有的窗口事件消息處理完之后,
//再調用CM_RELEASE消息處理過程CMRelease
end;
再看看下面CM_RELEASE消息處理過程CMRelease的定義:
procedure CMRelease(var Message: TMessage); message CM_RELEASE;
procedure TCustomForm.CMRelease;
begin
Free; //最后還是free;
end;
4 VCL構造&析構體系結構
 
TObject   
constructor Create;//靜態方法
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中的構造和析構的源代碼,以Tcontrol為例:
constructor TControl.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);//創建基類子對象,并把析構權移交給AOwner。放在最前面
   //這樣就保證了“先創建基類子對象,再創建派生類子對象”的順序
  …//初始化,以及調用內部子對象的構造函數
end;

destructor TControl.Destroy;
begin
  …//析構派生類中內部子對象
  inherited Destroy;//析構基類對象,放在最后面
  //這樣就保證了“先析構派生類子對象,再析構基類子對象”的順序
end;
5 正確使用構造函數和析構函數
   經過上面的分析,下面總結一下使用構造函數和析構函數的原則:
在使用對象之前,必須先建立一個對象時,并且及時銷毀對象,以釋放資源。
兩個對象引用賦值時,要確保出現的無名對象(指沒有被引用的對象)能被釋放。
當創建一個組件時,建議設置一個宿主組件(即使用AOwner參數,通常是窗體),由Aowner來管理對象的銷毀,那么就不必惦記著銷毀該組件了,這是Delphi在窗體上/數據模塊設計并創建組件是采用的方法。所以我們不必書寫調用該組件的析構函數。
當函數的返回類型為對象時,那么Result也是對象的引用,確保Result引用的對象要存在。
若要使用obj<>nil 或assigned(nil)測試對象存在時,在調用析構之后還應obj:=nil。

請參考演示程序的源代碼
說明(建議要有)
所有的Delphi程序已在win2k+Delphi6 sp2 上通過,對于C++程序,只是為了說明與Delphi中不同,并不保證能直接運行。為了加深對本篇文章的理解,建議參考演示程序。
這篇文章包括了我在學習VCL/RTL中的一些經驗和體會,加上本人的個人能力有限,難免出現錯誤,請大家不吝指正!
在閱讀本篇文章之前,需要讀者對Oriented Pascal語言有一定的了解,并能理解多態,如果您對其中一些概念還不是很清楚的話,請參考相關文章。
通過本篇文章,你應該能比較清楚地理解Delphi中的對象模型、構造&析構實現機制以及VCL中構造&析構 體系結構,并能掌握使用構造&析構的使用方法。Delphi中的構造&析構相當于C++中的算是簡單多了,我們應該能掌握它。


上一篇:兩個delphi下遍歷指定目錄下指定類型文件的函數

下一篇:我的文章-《剖析Delphi中的多態》

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
學習交流
熱門圖片

新聞熱點

疑難解答

圖片精選

網友關注

主站蜘蛛池模板: 欧美wwwsss9999| 91精品欧美一区二区三区 | 国产18成人免费视频 | 在线91视频| 美国av片在线观看 | 51国产偷自视频区视频小蝌蚪 | 欧美性生交xxxxx久久久缅北 | 免费观看黄视频 | 亚洲午夜激情网 | 国产精品亚洲综合 | 亚洲成人福利电影 | 午夜视频在线观看91 | 91精品国产日韩91久久久久久360 | 久草手机在线视频 | 久精品久久 | 国产精品视频自拍 | 欧美国产日韩在线观看成人 | 主播粉嫩国产在线精品 | 一区二区久久精品66国产精品 | 久久国语对白 | 精品一区二区三区免费 | 国产精品一区二区视频 | 精品久久久久99 | 国产剧情v888av | 久久精品视频16 | 久久艹国产精品 | 国产污污视频 | av在线播放亚洲 | 国产免费www| 亚洲啊v在线观看 | 视频一区二区国产 | 欧美一级无毛 | xnxx 美女19| 国产亚洲精品久久久久久久 | 毛片免费试看 | 99精品视频一区二区三区 | www成人在线观看 | 久久久久久久久久亚洲精品 | 主播粉嫩国产在线精品 | 国产精品一区二区三区在线 | 在线91观看 |