天天看點

More Effective C++之 Item M9:使用析構函數防止資源洩漏

對指針說再見。必須得承認:你永遠都不會喜歡使用指針。

    Ok,你不用對所有的指針說再見,但是你需要對用來操縱局部資源(local resources)的指針說再見。假設,你正在為一個小動物收容所編寫軟體,小動物收容所是一個幫助小狗小貓尋找主人的組織。每天收容所建立一個檔案,包含當天它所管理的收容動物的資料資訊,你的工作是寫一個程式讀出這些檔案然後對每個收容動物進行适當的處理(appropriate processing)。

    完成這個程式一個合理的方法是定義一個抽象類,ALA("Adorable Little Animal"),然後為小狗和小貓建立派生類。一個虛拟函數processAdoption分别對各個種類的動物進行處理:

 class ALA {

public:

  virtual void processAdoption() = 0;

  ...

};

class Puppy: public ALA {

public:

  virtual void processAdoption();

  ...

};

class Kitten: public ALA {

public:

  virtual void processAdoption();

  ...

};

    你需要一個函數從檔案中讀去資訊,然後根據檔案中的資訊産生一個puppy(小狗)對象或者kitten(小貓)對象。這個工作非常适合于虛拟構造器(virtual constructor),在條款M25較長的描述了這種函數。為了完成我們的目标,我們這樣聲明函數:

// 從s中讀去動物資訊, 然後傳回一個指針

// 指向建立立的某種類型對象

ALA * readALA(istream& s);

    你的程式的關鍵部分就是這個函數,如下所示:

void processAdoptions(istream& dataSource)

{

  while (dataSource) {                  // 還有資料時,繼續循環

    ALA *pa = readALA(dataSource);      //得到下一個動物

    pa->processAdoption();             //處理收容動物

    delete pa;                         //删除readALA傳回的對象

  }                                  

}

    這個函數循環周遊dataSource内的資訊,處理它所遇到的每個項目。唯一要記住的一點是在每次循環結尾處删除pa。這是必須的,因為每次調用readALA都建立一個堆對象。如果不删除對象,循環将産生資源洩漏。

    現在考慮一下,如果pa->processAdoption抛出了一個異常,将會發生什麼?processAdoptions沒有捕獲異常,是以異常将傳遞給processAdoptions的調用者。傳遞中,processAdoptions函數中的調用pa->processAdoption語句後的所有語句都被跳過,這就是說pa沒有被删除。結果,任何時候pa->processAdoption抛出一個異常都會導緻processAdoptions記憶體洩漏。

    堵塞洩漏很容易 :

void processAdoptions(istream& dataSource)

{

  while (dataSource) {

    ALA *pa = readALA(dataSource);

  try {

      pa->processAdoption();

  }

  catch (...) {              // 捕獲所有異常

    delete pa;               // 避免記憶體洩漏

                             // 當異常抛出時

    throw;                   // 傳送異常給調用者

  }

  delete pa;                 // 避免資源洩漏

}                           // 當沒有異常抛出時

}

    但是你必須用try和catch對你的代碼進行小改動。更重要的是你必須寫雙份清除代碼,一個為正常的運作準備,一個為異常發生時準備。在這種情況下,必須寫兩個delete代碼。象其它重複代碼一樣,這種代碼寫起來令人心煩又難于維護,而且它看上去好像存在着問題。不論我們是讓processAdoptions正常傳回還是抛出異常,我們都需要删除pa,是以為什麼我們必須要在多個地方編寫删除代碼呢?

    (WQ加注,VC++支援try…catch…final結構的SEH。)

    我們可以把總被執行的清除代碼放入processAdoptions函數内的局部對象的析構函數裡,這樣可以避免重複書寫清除代碼。因為當函數傳回時局部對象總是被釋放,無論函數是如何退出的。(僅有一種例外就是當你調用longjmp時。Longjmp的這個缺點是C++率先支援異常處理的主要原因)

    具體方法是用一個對象代替指針pa,這個對象的行為與指針相似。當pointer-like對象(類指針對象)被釋放時,我們能讓它的析構函數調用delete。替代指針的對象被稱為smart pointers(靈巧指針),參見條款M28的解釋,你能使得pointer-like對象非常靈巧。在這裡,我們用不着這麼聰明的指針,我們隻需要一個pointer-lik對象,當它離開生存空間時知道删除它指向的對象。

    寫出這樣一個類并不困難,但是我們不需要自己去寫。标準C++庫函數包含一個類模闆,叫做auto_ptr,這正是我們想要的。每一個auto_ptr類的構造函數裡,讓一個指針指向一個堆對象(heap object),并且在它的析構函數裡删除這個對象。下面所示的是auto_ptr類的一些重要的部分:

template<class T>

class auto_ptr {

public:

  auto_ptr(T *p = 0): ptr(p) {}        // 儲存ptr,指向對象

  ~auto_ptr() { delete ptr; }          // 删除ptr指向的對象

private:

  T *ptr;                              // raw ptr to object

};

    auto_ptr類的完整代碼是非常有趣的,上述簡化的代碼實作不能在實際中應用。(我們至少必須加上拷貝構造函數,指派operator和将在條款M28講述的pointer-emulating函數),但是它背後所蘊含的原理應該是清楚的:用auto_ptr對象代替raw指針,你将不再為堆對象不能被删除而擔心,即使在抛出異常時,對象也能被及時删除。(因為auto_ptr的析構函數使用的是單對象形式的delete,是以auto_ptr不能用于指向對象數組的指針。如果想讓auto_ptr類似于一個數組模闆,你必須自己寫一個。在這種情況下,用vector代替array可能更好。)

    使用auto_ptr對象代替raw指針,processAdoptions如下所示:

void processAdoptions(istream& dataSource)

{

  while (dataSource) {

    auto_ptr<ALA> pa(readALA(dataSource));

    pa->processAdoption();

  }

}

    這個版本的processAdoptions在兩個方面差別于原來的processAdoptions函數。第一,pa被聲明為一個auto_ptr<ALA>對象,而不是一個raw ALA*指針。第二,在循環的結尾沒有delete語句。其餘部分都一樣,因為除了析構的方式,auto_ptr對象的行為就象一個普通的指針。是不是很容易。

    隐藏在auto_ptr後的思想是:用一個對象存儲需要被自動釋放的資源,然後依靠對象的析構函數來釋放資源,這種思想不隻是可以運用在指針上,還能用在其它資源的配置設定和釋放上。想一下這樣一個在GUI程式中的函數,它需要建立一個window來顯式一些資訊:

// 這個函數會發生資源洩漏,如果一個異常抛出

void displayInfo(const Information& info)

{

  WINDOW_HANDLE w(createWindow());

  在w對應的window中顯式資訊

  destroyWindow(w);

}

    很多window系統有C-like接口,使用象like createWindow 和destroyWindow函數來擷取和釋放window資源。如果在w對應的window中顯示資訊時,一個異常被抛出,w所對應的window将被丢失,就象其它動态配置設定的資源一樣。

    解決方法與前面所述的一樣,建立一個類,讓它的構造函數與析構函數來擷取和釋放資源:

//一個類,擷取和釋放一個window 句柄

class WindowHandle {

public:

   WindowHandle(WINDOW_HANDLE handle): w(handle) {}

  ~WindowHandle() { destroyWindow(w); }

   operator WINDOW_HANDLE() { return w; }        // see below

private:

  WINDOW_HANDLE w;

  // 下面的函數被聲明為私有,防止建立多個WINDOW_HANDLE拷貝

  //有關一個更靈活的方法的讨論請參見條款M28。

  WindowHandle(const WindowHandle&);

  WindowHandle& operator=(const WindowHandle&);

};

    這看上去有些象auto_ptr,隻是指派操作與拷貝構造被顯式地禁止(參見條款M27),有一個隐含的轉換操作能把WindowHandle轉換為WINDOW_HANDLE。這個能力對于使用WindowHandle對象非常重要,因為這意味着你能在任何地方象使用raw WINDOW_HANDLE一樣來使用WindowHandle。(參見條款M5 ,了解為什麼你應該謹慎使用隐式類型轉換操作)

    通過給出的WindowHandle類,我們能夠重寫displayInfo函數,如下所示:

// 如果一個異常被抛出,這個函數能避免資源洩漏

void displayInfo(const Information& info)

{

  WindowHandle w(createWindow());

  在w對應的window中顯式資訊;

}

    即使一個異常在displayInfo内被抛出,被createWindow 建立的window也能被釋放。

    資源應該被封裝在一個對象裡,遵循這個規則,你通常就能避免在存在異常環境裡發生資源洩漏。但是如果你正在配置設定資源時一個異常被抛出,會發生什麼情況呢?例如當你正處于resource-acquiring類的構造函數中。還有如果這樣的資源正在被釋放時,一個異常被抛出,又會發生什麼情況呢?構造函數和析構函數需要特殊的技術。你能在條款M10和條款M11中擷取有關的知識。