對象生死劫 - 構造函數和析構函數的異常

轉自http://www.cnblogs.com/leadzen/archive/2008/02/12/1067474.html

      在設計類時,構造函數和析構函數往往需要十分的小心。平時不太注意構造函數和析構函數拋出異常,但這卻會造成內存泄漏等問題。轉載李老師的博客,講的很清楚:

  構造函數和析構函數分別管理對象的建立和釋放,負責對象的誕生和死亡的過程。當一個對象誕生時,構造函數負責創建並初始化對象的內部環境,包括分配內存、創建內部對象和打開相關的外部資源,等等。
而當對象死亡時,析構函數負責關閉資源、釋放內部的對象和已分配的內存。
  在對象生死攸關的地方,如果程序代碼出現問題,常常會發生內存泄漏,從而產生可能危害系統運行的孤魂野鬼。大量的事實表明,業務邏輯代碼寫得非常嚴謹的程序在運行中仍然發現存在內存泄露,大都是構造和析構部分的代碼存在問題。
  而許多程序員都習慣於面向對象的編程,需要時就建立一個對象,不用時就將其釋放。這樣的習慣簡化了我們的思路,正是面向對象編程思想帶來的好處。也許由於太習慣了,很多程序員都忽略了在對象生死的瞬間也可能產生異常的問題,這種現象卻值得我們去認真反思。
  其實,對象生死間的異常問題是一個充滿爭議的問題。甚至不同的編程語言,在對象生死間的異常問題上也持不同的態度。
  C++語言說:一個對象在出生的過程中發生異常問題,那這個對象就是一個沒有生命的怪胎。既然它不是一個完整的對象,就根本不存在析構或釋放的 說 法。因此,C++在執行構造函數過程中產生異常時,是不會調用對象的析構函數的,而僅僅清理和釋放產生異常前的那些C++管理的變量空間等,之後就把異常 拋給程序員處理。
  DELPHI (Object Pascal)語言認爲:對象雖然在出生過程中出現異常,但它已經具有部分生命。既然是有生命的東西,都應該有死亡的權利。因此,DELPHI在執行構造函數時產生異常,一定會先調用該對象
的析構函數,然後再拋出異常給程序員去處理。
  那麼,誰的觀點對?誰的觀點錯?
  我想,這個問題爭論上九九八十一天也未必有結果。因此,我們不必糾纏於觀點的爭論。只要我們知道了不同編程程語言有不同的處理方法就夠了,當我們用哪種語言來編程時就尊重該種語言的觀點,這纔是務實的程序員應該做的!
  對於C++語言來說,由於構造函數產生異常時不會調用對應的析構函數,那麼在構造函數裏發生異常前的代碼所創建的其他東西就不能被析構函數內的相關釋放代碼所釋放。例如:
class TMyObject 
{
   private: 
     TOtherObject * OtherObject;
   public:
     TMyObject()
     {
       OtherObject = new TOtherObject();
       ......       //這後面的代碼發生異常將導致OtherObject不會被釋放!
     }
     ~TMyObject()
     {
       ......
       delete OtherObject;      //構造函數發生異常時析構函數根本不會被調用,此代碼也不會被執行! 
     }

  回想一下自己的程序中是否也存在類似的代碼?如果是這樣,那可就要注意了,這樣的代碼在C++中是不安全的!
  那麼,應該怎樣寫才安全呢?
  事實上,如果在C++的構造函數裏創建了其他東西,你就必須考慮構造函數發生異常的情況。在構造函數中發生異常時,已經創建的東西必須被釋放 掉,然後再重新拋出異常給上層調用代碼處理,這纔是C++構造函數中正確的異常處理方法。因此,前面的構造函數應該改寫成下面的形式:
     TMyObject()
     {
       OtherObject = new TOtherObject();
       try
       {
       ...... //這裏的代碼發生異常。
       }
       catch(...)
       {
         delete OtherObject; //確保發生異常時,能釋放掉已建立的東西。
         throw;   //再次拋出異常給上層調用代碼處理。
       };
     }
  如果,一個構造函數要創建很多其他東西的話,就應該編寫相應的try   try try ... catch catch catch形式的嵌套代碼(或者相同邏輯的代碼)來確保構造函數的正確性。當你看到某位C++程序員在構造函數中寫了一大堆壯觀的try   try try ... catch catch catch代碼時,請相信我說的話:他一定是一位非常嚴謹的C++程序員。不過,有些聰明的C++程序員還找到另外一種不用try...catch來處理構造異常的方法,那就是C++標準類庫中的那個著名的auto_ptr模板 類。auto_ptr又常常被稱爲智能指針,它巧妙地利用C++退出作用域時會自動釋放變量的機制,來清理其維護的對象。再看改寫的代碼:
#include <memory>
#include <iostream>

class TMyObject 
{
   private: 
     std::auto_ptr<TOtherObject>   OtherObject;
   public:
     TMyObject()
     {
       OtherObject = new TOtherObject();

       ...... //這後面的代碼發生異常,也可以確保OtherObject會被自動釋放!

     }
     ~TMyObject()
     {
       //一旦將對象交給auto_ptr來維護,就永遠不要自己釋放該對象。因此,這裏什麼都不用寫。
     }
}
  這樣的代碼那麼簡潔而且有效,也便於閱讀和維護。
  爲什麼這樣的機制有效?因爲,這裏的OtherObject是被定義爲一個成員變量而不是指針。從C++創建對象的機制上來說,一定會先分配對 象空間和創建成員對象,然後才調用對象的構造函數,進入構造函數的作用域。一 旦構造函數發生異常,必然退出構造函數的作用域,C++自然會釋放成員對象和空間。而OtherObject成員變量被釋放時,會調用auto_ptr的 析構函數,從而成功釋放其管理的真正對象。
  不過,atuo_ptr在使用過程中也有些副作用。比如,你把一個auto_ptr賦值給另一個auto_ptr,前一個auto_ptr就會 變成null值,這不符合正常的賦值語義。這是由於auto_ptr重載了賦值操作符的緣故,不懂auto_ptr實現原理的人就常常犯null指針錯 誤。使用auto_ptr還有一句名言:“別把一個對象賦給兩個auo_ptr變量”,因爲這會導致兩次釋放一個對象的錯誤。不管怎樣,如果嫌 try... catch麻煩,使用auto_ptr來保證不發生內存泄漏也是一個非常不錯的選擇。就看你喜歡不喜歡了。
  相比之下,DELPHI語言處理構造函數的異常就簡單多了。因爲DELPHI保證在構造函數發生異常後,會調用析構函數。不過並非所有的析構函數都滿足這一條件,只有Destroy可以,而且它是一個虛函數。
TMyObject = class 
   private 
     OtherObject: TOtherObject;
   public
     constructor Create;
     destructor Destroy; override;
end;

constructor TMyObject.Create;
begin
   OtherObject := TOtherObject.Create;
   ...... //這後面的代碼發生異常可以保證析構函數Destroy被調用,從而釋放OtherObject!
end;

destructor TMyObject.Destroy;
begin
   ......
   OtherObject.Free;   //這裏OtherObject將被正確釋放!
end;
  因爲DELPHI的“構造異常時確保析構的機制”是非常基礎的代碼,它只能給根類TObject設計一個虛析構函數Destroy來作爲析構函數調用。也就是說,如果你自己寫的析構函數不是從Destroy重載的,“構造異常時確保析構的機制”將失效!
  同時,DELPHI在根類TObject中提供了Free方法來方便對象釋放。這個Free方法保證即使對象併爲被創建(對象指針爲nil), 調用此方法也不會出錯。這樣,構造函數裏面創建語句就可以很簡潔地與析構函數的釋放語句對應起來,方便我們看代碼。看來,設計這個Free方法還是用心良 苦啊!
  那麼關於析構函數中的異常又會怎樣呢?
  對象在死亡的過程中發生異常又引出一個有趣的問題,“想死死不了”或者“死了一半又不能死了”!那麼,這個對象到底是死了還是活着?這種既死又活的對象,就像量子理論中的那隻“薛定諤的貓”一樣有趣。的確存在,卻難以琢磨!
     C++在處理析構函數的異常時,與一般異常沒有什麼不同,將異常交給上層調用程序處理。如果沒有任何地方處理這個異常,將直接導致當前執行線程異常終止!如果是主線程中發生析構異常,程序立即退出!
  由於,析構函數在發生異常時,如果異常點之後還有釋放內存的代碼,這些代碼將不會被執行,從而肯定會導致內存泄漏。因此,“永遠不要在析構函數中拋出異常”成了編寫C++代碼的一條鐵律!
  而在DELPHI中,析構函數發生異常和其他代碼發生異常也沒有什麼不同,發生異常之後的代碼將不會被執行(finally部分的除外),異常將由能找到的上層異常處理代碼來處理!如果異常最終沒有被處理,同樣導致當前線程終止。
  看來這個析構函數異常不是一時半會能想通的問題,DELPHI的方法也並不比C++高明。因此,“永遠不要在析構函數中拋出異常”依然可以作爲一個簡單的定律!
  有時候,面對複雜和困惑的糾纏,只要牢記簡單的原則,也能泰然處之。
  這世界有太多不如意,但你的生活還是要繼續,太陽每天依舊要升起,希望永遠種在你心裏...
李戰(leadzen).深圳 2007-9-7

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章