程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Delphi >> Delphi2009初體驗 - 語言篇 - 智能指針(Smart Pointer)的實現

Delphi2009初體驗 - 語言篇 - 智能指針(Smart Pointer)的實現

編輯:Delphi

一、回顧歷史

在c++中,對象可以創建在棧裡,也可以創建在堆裡。如:

class CTestClass
{
public:
   CTestClass()
   {
     printf(“Create”);
   }
   void DoPrint() {}
   ~CTestClass()
   {
     printf(“destroy”);
   }
}

// 以下代碼創建棧對象

CTestClass test;
test.DoPrint();

棧對象生命周期由後台管理。當方法結束時,棧對象會從棧中彈出,編譯器會自動銷毀棧所彈出的對象。

// 以下代碼創建堆對象

CTestClass* test = new CTestClass();
test->DoPrint();

堆對象保存在堆中,堆對象生命周期不受後台管理,程序員必須自己手動的釋放堆對象,否則會造成內存洩露:

delete test;
test = NULL;

Pascal語言從OOP Pascal開始支持面向對象,也就是說,OOP Pascal支持創建對象了。OOP Pascal和c++一樣,也可以分別創建棧對象和堆對象:

我們最常見的OOP Pascal堆對象的定義和創建:

type
   THeapObject = class // 注意此處的聲明,class表名為堆對象
   public
     constructor Create; virtual;
     destructor Destroy; override;
     procedure DoPrint;
   end;
var
   heapObj: THeapObject;
{ TStackObject }
constructor THeapObject.Create;
begin
   Writeln('Create');
end;
destructor THeapObject.Destroy;
begin
   Writeln('Destroy');
end;
procedure THeapObject.DoPrint;
begin
   Writeln('DoPrint');
end;
begin
   heapObj := THeapObject.Create;
   heapObj.DoPrint;
   FreeAndNil(heapObj);
   Readln;
end.

運行結果:

OOP Pascal也有棧對象,棧對象的定義和創建:

type
   TStackObject = object // 注意此處的聲明,object為保留字表明為棧對象
   public
     constructor Create;
     destructor Destroy;
     procedure DoPrint;
   end;
var
   stackObj: TStackObject;
{ TStackObject }
constructor TStackObject.Create;
begin
   Writeln('Create');
end;
destructor TStackObject.Destroy;
begin
   Writeln('Destroy');
end;
procedure TStackObject.DoPrint;
begin
   Writeln('DoPrint');
end;
begin
   // 注意此處的代碼,不需要使用TStackObject.Create
   stackObj.DoPrint;
   Readln;
end.

運行結果:

從結果我們可以看到,與c++不同的是,OOP Pascal所謂的棧對象的構造和析構,不受constructor方法和destructor方法控制,我們不能捕獲到OOP Pascal棧對象的構造和析構。

二、智能指針簡介

經過前面分析,我們知道,棧對象的聲明周期由後台管理,棧對象在聲明時進行構造,當方法退出或者類被銷毀時(此時棧對象為類的成員變量),棧對象的生命周期也會隨著結束,後台自動會調用它們的析構函數並釋放棧空間。

而堆對象必須由程序員手動的釋放,如果一個方法只有一兩個堆對象我們還能應付的過來,但是當堆對象非常多,而且堆對象一般都要經過多個方法的傳遞、賦值,傳遞到最後,非常容易忘了delete,造成內存洩露。

能不能讓後台也去自動管理堆對象的釋放呢?前輩們想到一個辦法,就是讓一個棧對象包含一個堆對象的引用,當棧對象被後台自動釋放時,會調用棧對象的析構函數,於是,在棧對象的析構函數裡寫下delete堆對象指針的語句。這樣,就完成了後台間接管理堆對象,以上就是stl中的智能指針auto_ptr的處理方法。

三、Delphi中的interface

從智能指針的簡介中我們可以了解到,要使用智能指針,我們必須得捕獲到棧對象的構造函數,將堆對象的指針傳入棧對象,由棧對象保存堆對象的指針;還必須捕獲到棧對象的析構函數,在棧對象的析構函數裡進行對構造函數所傳入堆對象指針delete。在c++很容易做到這一點,但是經上面分析,我們無法對Delphi的棧對象進行構造和析構的捕獲。

我們可以換一種角度思考,不一定非要是棧對象,只要在Delphi中能有一種東西,只要出了它的作用域,它就能自動析構!

Delphi中的interface能間接滿足我們這個需要,請看以下例子:

program TestInterface;
{$APPTYPE CONSOLE}
uses
   SysUtils;
type
   ITestInterface = interface
   ['{ED2517D5-FB77-4DD6-BC89-DF9182B335AE}']
     procedure DoPrint;
   end;
   TTestInterface = class(TInterfacedObject, ITestInterface)
   public
     constructor Create; virtual;
     destructor Destroy; override;
     procedure DoPrint;
   end;
{ TTestInterface }
constructor TTestInterface.Create;
begin
   Writeln('Create');
end;
destructor TTestInterface.Destroy;
begin
   Writeln('Destroy');
   inherited;
end;
procedure TTestInterface.DoPrint;
begin
   Writeln('DoPrint');
end;
procedure DoTest;
var
   testInter: ITestInterface;   // 1*
begin
   testInter := TTestInterface.Create;
   testInter.DoPrint;
end;
begin
   DoTest;
   Readln;
end.

有結果可以看到,代碼中沒有釋放testInter指向的對象,對象由後台釋放了。如果將1*處改為testInter: TTestInterface;則結果如下,我們將看到如果不聲明為接口,即使創建同一個對象,Delphi是不會自動釋放對象的。

在此,我們利用了接口的自動管理功能,它自己維護著一個引用計數,當引用計數為0時接口自己會調用析構函數。關於Delphi接口的一些概念以及為什麼後台會自動釋放接口,可以參考以下兩篇文章,在此不做多余敘述。

1、Delphi 的接口機制淺探http://www.d99net.net/article.asp?id=206

2、淺談引用計數http://www.moon-soft.com/doc/13056.htm

四、Delphi中智能指針的實現

有了以上經驗,我們就可以實現我們的智能指針了!

首先,我們要創建一個繼承於TInterfacedObject的對象,在構造函數中傳入要管理的堆對象的引用,在析構函數裡FreeAndNil這個堆對象的引用。代碼如下:

unit ClassicalAutoPtr;
interface
uses
   SysUtils;
type
   TClassicalAutoPtr = class(TInterfacedObject)
   private
     fObj: TObject;
   public
     constructor Create(aObj: TObject); virtual;
     destructor Destroy; override;
     class function New(aObj: TObject): IInterface;
   end;
implementation
{ TClassicalAutoPtr }
constructor TClassicalAutoPtr.Create(aObj: TObject);
begin
   fObj := aObj;
end;
destructor TClassicalAutoPtr.Destroy;
begin
   // 智能指針在方法退出時銷毀,同時銷毀所管理的堆對象
   FreeAndNil(fObj);
   inherited;
end;
class function TClassicalAutoPtr.New(aObj: TObject): IInterface;
begin
   // 外部必須使用此方法創建智能指針
   // 因為此方法會暴露給外部一個接口
   // 後台碰到接口後會自動調用接口的析構函數
   Result := TClassicalAutoPtr.Create(aObj);
end;
end.

然後我們寫一個控制台程序做試驗:

program TestClassicalAutoPtr;
{$APPTYPE CONSOLE}
uses
   SysUtils,
   ClassicalAutoPtr in 'ClassicalAutoPtr.pas';
type
   TTestClass = class
   public
     constructor Create; virtual;
     destructor Destroy; override;
     procedure DoPrint;
   end;
{ TTestClass }
constructor TTestClass.Create;
begin
   Writeln('Create');
end;
destructor TTestClass.Destroy;
begin
   Writeln('Destroy');
   inherited;
end;
procedure TTestClass.DoPrint;
begin
   Writeln('DoPrint');
end;
procedure DoTest;
var
   tt: TTestClass;
begin
   // 首先創建一個堆對象
   tt := TTestClass.Create;
   // 創建一個智能指針,並把堆對象的引用傳入智能指針,由智能指針管理堆對象
   TClassicalAutoPtr.New(tt);  // 2*
   tt.DoPrint;
end;
begin
   DoTest;
   Readln;
end.

代碼執行結果如下圖所示:

如果我們將代碼2*處替換成

TClassicalAutoPtr.Create (tt);

執行結果將看不到Destroy,析構函數沒有被調用。因為由TClassicalAutoPtr.New返回的是一個interface,而TClassicalAutoPtr.Create返回的是一個Object。

這樣,我們一個簡單的智能指針就完成了。

五、interface + 泛型 = 強類型的智能指針

D2009引入了泛型,我們把程序稍微改動一下,就可以支持強類型的智能指針了!

關於D2009對泛型的支持的分析,請參看我另外兩篇隨筆:

http://www.cnblogs.com/felixYeou/archive/2008/08/22/1273989.html

http://www.cnblogs.com/felixYeou/archive/2008/08/22/1274202.html

我們以stl的auto_ptr作為參照物,要是咱們的智能指針看起來“優雅”,必須還要實現以下幾個方法:

1、Get:返回智能指針所指向的對象

2、Release:釋放智能指對堆對象的管理,智能指針被自動釋放後,不對堆對象進行釋放

3、Reset:為智能指針指向其它堆對象,同時釋放原來指向的堆對象

對於auto_ptr一些運算符重載,這裡不考慮在內,因為Delphi2009還沒有支持類的運算符重載。

話不多說了,直接上代碼:

智能指針類代碼:

unit AutoPtr;
interface
uses
   SysUtils;
type
   IAutoPtr<T: class> = interface(IInterface)
     ['{BD098FDB-728D-4CAC-AE40-B12B8B556AD3}']
     function Get: T;
     function Release: T;
     procedure Reset(aObj: T);
   end;
   TAutoPtr<T: class> = class(TInterfacedObject, IAutoPtr<T>)
   private
      fObj: T;
   public
      class function New(aObj: T): IAutoPtr<T>;
      constructor Create(aObj: T); virtual;
      destructor Destroy; override;
     function Get: T;
     function Release: T;
     procedure Reset(aObj: T);
   end;
implementation
{ TAutoPtr<T> }
constructor TAutoPtr<T>.Create(aObj: T);
begin
   fObj := aObj;
end;
class function TAutoPtr<T>.New(aObj: T): IAutoPtr<T>;
begin
   Result := TAutoPtr<T>.Create(aObj) as IAutoPtr<T>;

// 注意:此處如果不加as IAutoPtr<T>,程序運行時會報錯,第一次我沒有加as IAutoPtr<T>程序運行一切正常,到後面就不行了,不知道是為什麼

function TAutoPtr<T>.Release: T;
begin
   Result := fObj;
   fObj := nil;
end;
procedure TAutoPtr<T>.Reset(aObj: T);
begin
   if aObj <> fObj then
   begin
     FreeAndNil(fObj);
     fObj := aObj;
   end;
end;
destructor TAutoPtr<T>.Destroy;
begin
   if fObj <> nil then
   begin
     FreeAndNil(fObj);
   end;
   inherited;
end;
function TAutoPtr<T>.Get: T;
begin
   Result := fObj;
end;

測試代碼:

program TestAutoPtr;
{$APPTYPE CONSOLE}
uses
   SysUtils,
   AutoPtr in 'AutoPtr.pas';
type
   TTestClass = class
   private
     fInt: Integer;
   public
     constructor Create(aInt: Integer); virtual;
     destructor Destroy; override;
     procedure DoPrintInt;
   end;
{ TTestClass }
constructor TTestClass.Create(aInt: Integer);
begin
   fInt := aInt;
   Writeln('Create');
end;
destructor TTestClass.Destroy;
begin
   Writeln('Destroy');
   inherited;
end;
procedure TTestClass.DoPrintInt;
begin
   Writeln(fInt);
end;
procedure DoTestAutoPtr;
var
   ap: IAutoPtr<TTestClass>;
begin
   // 此處用Create和New都可以,因為ap對象是接口
   ap := TAutoPtr<TTestClass>.Create(TTestClass.Create(10));
   ap.Get.DoPrintInt; // 3*
end;
begin
   DoTestAutoPtr;
   Readln;
end.

測試結果為:

然而我們將3*處代碼改成

ap.Release.DoPrintInt,則輸出結果為

因為Release方法已經通知智能指針不管理堆對象了。

同時,我們還可以把DoTestAutoPtr方法寫成這樣,或許這樣創建TTestClass對象更優美一些:

procedure DoTestAutoPtr;
var
   tt: TTestClass;
begin
   // 注意,此處要用New
   tt := TAutoPtr<TTestClass>.New(TTestClass.Create(10)).Get;
   tt.DoPrintInt;

    // 不需要使用tt.Free;
end;

六、智能指針與集合

如果我們聲明一個全局變量:

var
   gAp: IAutoPtr<TTestClass>;

並從DoTestAutoPtr方法開始改變其下代碼:

procedure DoTestAutoPtr;
var
   tt: TTestClass;
   ap: IAutoPtr<TTestClass>;
begin
   ap := TAutoPtr<TTestClass>.New(TTestClass.Create(10));
   tt := ap.Get;
   tt.DoPrintInt;
   gAp := ap;
end;
begin
   DoTestAutoPtr;
   Writeln('Exit DoTestAutoPtr');
   Writeln('gAp nil'); 
   gAp := nil;  // 4*
   Readln;
end.

結果如下:

我們可以看到,當調用完畢DoTestAutoPtr方法後,方法內的堆對象tt並沒有銷毀,這說明智能指針ap並沒有銷毀。

因為在DoTestAutoPtr方法最後一行,將ap接口變量賦值給了全局變量gAp,此時接口的引用計數+1,方法退出後,ap變量被銷毀,接口的引用計數-1,但是gAp仍然引用著對象,所以引用計數不為0。當運行到第4*步時,強制把gAp指向空地址,對象的引用計數-1,為0,這個時候後台自動調用對象的析構函數Destroy(這有點像Java或.net的垃圾回收機制)。所以,我們使用智能指針,可以放心的創建,放心的引用,而不用去管什麼時候該銷毀,完全由後台幫我們實現。

下面把測試程序改一下,讓智能指針與集合結合測試:

program TestAutoPtrList;
{$APPTYPE CONSOLE}
uses
   SysUtils,
   Generics.Collections,
   AutoPtr in 'AutoPtr.pas';
type
   TTestClass = class
   private
     fInt: Integer;
   public
     constructor Create(aInt: Integer); virtual;
     destructor Destroy; override;
     procedure DoPrintInt;
   end;
var
   gList: TList<IAutoPtr<TTestClass>>;
   gAp: IAutoPtr<TTestClass>;
{ TTestClass }
constructor TTestClass.Create(aInt: Integer);
begin
   fInt := aInt;
   Writeln('Create');
end;
destructor TTestClass.Destroy;
begin
   Writeln('Destroy');
   inherited;
end;
procedure TTestClass.DoPrintInt;
begin
   Writeln(fInt);
end;
procedure DoTestAutoPtr;
var
   ap: IAutoPtr<TTestClass>;
   n: Integer;
begin
   gList := TList<IAutoPtr<TTestClass>>.Create;
   for n := 0 to 2 do
   begin
     ap := TAutoPtr<TTestClass>.New(TTestClass.Create(10));
     ap.Get.DoPrintInt;
     gList.Add(ap);
     ap := nil;
   end;
   Writeln('Save an AutoPtr');
   gAp := gList[1];
   Writeln('gList Destroy');
   gList.Free;
   Writeln('Set saved AutoPtr = nil');
   gAp := nil;
end;
begin
   DoTestAutoPtr;
   Readln;
end.

測試結果:

七、注意事項

1、智能指針與堆對象之間的循環引用

假如我們把TTestClass類進行如下修改,讓堆對象擁有指向它智能指針的引用:

TTestClass = class
   private
     fInt: Integer;
     fAp: IAutoPtr<TTestClass>;
   public
     constructor Create(aInt: Integer); overload; virtual;
     destructor Destroy; override;
     procedure DoPrintInt;
     property Ap: IAutoPtr<TTestClass> read fAp write fAp;
   end;

同時,把測試方法進行如下修改:

procedure DoTestAutoPtr;
var
   tt: TTestClass;
   ap: IAutoPtr<TTestClass>;
begin
   ap := TAutoPtr<TTestClass>.New(TTestClass.Create(10));
   tt := ap.Get;
   tt.Ap := ap;  // 5*
   tt.DoPrintInt;
end;

此時,我們得到了非常不靠譜的結果:

智能指針竟然沒有自動釋放!

從上面的分析和前面的代碼我們可以看到,接口的引用計數為0的時候,接口會自動釋放,我們要保證接口能夠被順利的釋放,必須保證接口的引用計數為0。

從第 5* 點代碼我們可以看到,tt.Ap := ap,使得智能指針與堆對象之間進行了循環引用,導致接口ap的引用計數+1為2。最後在方法退出的時候,雖然ap占用的引用已經被釋放了,引用-1,但是由於堆對象tt不會自己釋放,所以堆對象tt.Ap所占用的引用沒有釋放,方法在退出時,接口的引用數為1,接口沒有自動釋放。

2、什麼使用時候使用Release方法

首先我們為測試單元加入use:Generics.Collections,再將TTestClass類修改如下:

type
   TTestClass = class
   private
     fList: TList<Integer>;
   public
     constructor Create(aInt: Integer); overload; virtual;
     destructor Destroy; override;
     procedure DoPrintInt;
   end;
{ TTestClass }
constructor TTestClass.Create(aInt: Integer);
begin
    inherited Create;
   fList := TList<Integer>.Create;
   fList.Add(aInt);
   Writeln('Create');
end;
destructor TTestClass.Destroy;
begin
   Writeln('Destroy');
   FreeAndNil(fList);
   inherited;
end;
procedure TTestClass.DoPrintInt;
begin
   Writeln(fList[0]);
end;

此時,成員變量不再是一個值類型,而是一個引用類型。

將從DoTestAutoPtr方法開始代碼修改如下:

procedure DoTestAutoPtr;
begin
   gTt := TAutoPtr<TTestClass>.New(TTestClass.Create(10)).Get; // 6*
end;
begin
   DoTestAutoPtr;
   gTt.DoPrintInt;
   Readln;
end.

此時,我們在DoTestAutoPtr方法內部創建了智能指針,並將智能指針所指向的堆對象傳給全局變量,然後在DoTestAutoPtr方法執行結束後調用全局變量的DoPrintInt方法。運行結果:

運行失敗了,原因是在DoTestAutoPtr方法退出了以後,TAutoPtr<TTestClass>.New(TTestClass.Create(10))語句所創建的接口引用計數為0,此時它會調用TTestClass的Destroy方法將fList銷毀。此時,我們調用DoPrintInt方法,想得到fList第一個元素,但是fList本身已經被銷毀了,所以導致錯誤的發生。

我們將第6*行改為:

gTt:=TAutoPtr<TTestClass>.New(TTestClass.Create(10)).Release;

運行結果:

此時不會出現錯誤,因為Release方法已經通知智能指針堆對象已經不受智能指針管理,所以在TAutoPtr<TTestClass>銷毀的時候不會調用 TTestClass的析構函數,fList得以保留下來。

在此處我們可以看到,由於堆對象不再受到智能指針的管理,所以我們必須手動的將其釋放FreeAndNil(gTt),否則就會產生上圖所發生的結果:內存洩露。

八、總結

剛開始實現棧對象我考慮過使用record,Delphi的record非常類似於類,保存在棧中,支持方法、屬性和帶參數的構造函數,但是不支持析構函數,所以沒有辦法實現我們的智能指針。Delphi版的智能指針很早就在cnPack討論區中有前輩提出來過了(http://bbs.cnpack.org/viewthread.php?tid=1399),但是使用起來不方便導致這種寫法不怎麼流行。自從D2009支持泛型以後,以前很多實現起來很麻煩的功能現在都能很簡單的實現,如智能指針與泛型集合的結合。但是,在Delphi中使用智能指針是稍微有一些性能損失的,在目前電腦速度越來越快的今天,這點損失已經顯得微不足道了。

本文配套源碼

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