天天看點

Effective C++ 2e 條款7:預先準備好記憶體不夠的情況

條款7:預先準備好記憶體不夠的情況

  operator new在無法完成記憶體配置設定請求時會抛出異常(以前的做法一般是傳回0,一些舊一點的編譯器還這麼做。你願意的話也可以把你的編譯器設定成這樣。關于這個話題我将推遲到本條款的結尾處讨論)。大家都知道,處理記憶體不夠所産生的異常真可以算得上是個道德上的行為,但實際做起來又會象刀架在脖子上那樣痛苦。是以,你有時會不去管它,也許一直沒去管它。但你心裡一定還是深深地隐藏着一種罪惡感:萬一new真的産生了異常怎麼辦?

  你會很自然地想到處理這種情況的一種方法,即回到以前的老路上去,使用預處理。例如,C的一種常用的做法是,定義一個類型無關的宏來配置設定記憶體并檢查配置設定是否成功。對于C++來說,這個宏看起來可能象這樣:

   #define NEW(PTR, TYPE) try { (PTR) = new TYPE; } catch (std::bad_alloc&) { assert(0); }

  (“慢!std::bad_alloc是做什麼的?”你會問。bad_alloc是operator new不能滿足記憶體配置設定請求時抛出的異常類型,std是bad_alloc所在的名字空間(見條款28)的名稱。“好!”你會繼續問,“assert又有什麼用?”如果你看看标準C頭檔案<assert.h>(或與它相等價的用到了名字空間的版本<cassert>,見條款49),就會發現assert是個宏。這個宏檢查傳給它的表達式是否非零,如果不是非零值,就會發出一條出錯資訊并調用abort。assert隻是在沒定義标準宏NDEBUG的時候,即在調試狀态下才這麼做。在産品釋出狀态下,即定義了NDEBUG的時候,assert什麼也不做,相當于一條空語句。是以你隻能在調試時才能檢查斷言(assertion))。

  NEW宏不但有着上面所說的通病,即用assert去檢查可能發生在已釋出程式裡的狀态(然而任何時候都可能發生記憶體不夠的情況),同時,它還在C++裡有另外一個缺陷:它沒有考慮到new有各種各樣的使用方式。例如,想建立類型T對象,一般有三種常見的文法形式,你必須對每種形式可能産生的異常都要進行處理:

   new T;

   new T(constructor arguments);

   new T[size];

  這裡對問題大大進行了簡化,因為有人還會自定義(重載)operator new,是以程式裡會包含任意個使用new的文法形式。

  那麼,怎麼辦?如果想用一個很簡單的出錯處理方法,可以這麼做:當記憶體配置設定請求不能滿足時,調用你預先指定的一個出錯處理函數。這個方法基于一個正常,即當operator new不能滿足請求時,會在抛出異常之前調用客戶指定的一個出錯處理函數——一般稱為new-handler函數。(operator new實際工作起來要複雜一些,詳見條款8)

  指定出錯處理函數時要用到set_new_handler函數,它在頭檔案<new>裡大緻是象下面這樣定義的:

   typedef void (*new_handler)();

   new_handler set_new_handler(new_handler p) throw();

  可以看到,new_handler是一個自定義的函數指針類型,它指向一個沒有輸入參數也沒有傳回值的函數。set_new_handler則是一個輸入并傳回new_handler類型的函數。

  set_new_handler的輸入參數是operator new配置設定記憶體失敗時要調用的出錯處理函數的指針,傳回值是set_new_handler沒調用之前就已經在起作用的舊的出錯處理函數的指針。

  可以象下面這樣使用set_new_handler:

  // function to call if operator new can't allocate enough memory

  void noMoreMemory()

  {

   cerr << "Unable to satisfy request for memory\n";

   abort();

  }

  int main()

  {

   set_new_handler(noMoreMemory);

   int *pBigDataArray = new int[100000000];

   ...

  }

  假如operator new不能為100,000,000個整數配置設定空間,noMoreMemory将會被調用,程式發出一條出錯資訊後終止。這就比簡單地讓系統核心産生錯誤資訊來結束程式要好。(順便考慮一下,假如cerr在寫錯誤資訊的過程中要動态配置設定記憶體,那将會發生什麼...)

  operator new不能滿足記憶體配置設定請求時,new-handler函數不隻調用一次,而是不斷重複,直至找到足夠的記憶體。實作重複調用的代碼在條款8裡可以看到,這裡我用描述性的的語言來說明:一個設計得好的new-handler函數必須實作下面功能中的一種。

   ·産生更多的可用記憶體。這将使operator new下一次配置設定記憶體的嘗試有可能獲得成功。實施這一政策的一個方法是:在程式啟動時配置設定一個大的記憶體塊,然後在第一次調用new-handler時釋放。釋放時伴随着一些對使用者的警告資訊,如記憶體數量太少,下次請求可能會失敗,除非又有更多的可用空間。

   ·安裝另一個不同的new-handler函數。如果目前的new-handler函數不能産生更多的可用記憶體,可能它會知道另一個new-handler函數可以提供更多的資源。這樣的話,目前的new-handler可以安裝另一個new-handler來取代它(通過調用set_new_handler)。下一次operator new調用new-handler時,會使用最近安裝的那個。(這一政策的另一個變通辦法是讓new-handler可以改變它自己的運作行為,那麼下次調用時,它将做不同的事。方法是使new-handler可以修改那些影響它自身行為的靜态或全局資料。)

   ·卸除new-handler。也就是傳遞空指針給set_new_handler。沒有安裝new-handler,operator new配置設定記憶體不成功時就會抛出一個标準的std::bad_alloc類型的異常。

   ·抛出std::bad_alloc或從std::bad_alloc繼承的其他類型的異常。這樣的異常不會被operator new捕捉,是以它們會被送到最初進行記憶體請求的地方。(抛出别的不同類型的異常會違反operator new異正常範。規範中的預設行為是調用abort,是以new-handler要抛出一個異常時,一定要确信它是從std::bad_alloc繼承來的。想更多地了解異正常範,參見條款M14。)

   ·沒有傳回。典型做法是調用abort或exit。abort/exit可以在标準C庫中找到(還有标準C++庫,參見條款49)。

  上面的選擇給了你實作new-handler函數極大的靈活性。

  處理記憶體配置設定失敗的情況時采取什麼方法,取決于要配置設定的對象的類:

  class X {

  public:

   static void outOfMemory();

   ...

  };

  class Y {

  public:

   static void outOfMemory();

   ...

  };

  X* p1 = new X; // 若配置設定成功,調用X::outOfMemory

  Y* p2 = new Y; // 若配置設定不成功,調用Y::outOfMemory

  C++不支援專門針對于類的new-handler函數,而且也不需要。你可以自己來實作它,隻要在每個類中提供自己版本的set_new_handler和operator new。類的set_new_handler可以為類指定new-handler(就象标準的set_new_handler指定全局new-handler一樣)。類的operator new則保證為類的對象配置設定記憶體時用類的new-handler取代全局new-handler。

  假設處理類X記憶體配置設定失敗的情況。因為operator new對類型X的對象配置設定記憶體失敗時,每次都必須調用出錯處理函數,是以要在類裡聲明一個new_handler類型的靜态成員。那麼類X看起來會象這樣:

   class X {

   public:

   static new_handler set_new_handler(new_handler p);

   static void * operator new(size_t size);

   private:

   static new_handler currentHandler;

   };

  類的靜态成員必須在類外定義。因為想借用靜态對象的預設初始化值0,是以定義X::currentHandler時沒有去初始化。

  new_handler X::currentHandler; // 預設設定currentHandler為0(即null)

  類X中的set_new_handler函數會儲存傳給它的任何指針,并傳回在調用它之前所儲存的任何指針。這正是标準版本的set_new_handler所做的:

  new_handler X::set_new_handler(new_handler p)

  {

   new_handler oldHandler = currentHandler;

   currentHandler = p;

   return oldHandler;

  }

  最後看看X的operator new所做的:

  1. 調用标準set_new_handler函數,輸入參數為X的出錯處理函數。這使得X的new-handler函數成為全局new-handler函數。注意下面的代碼中,用了"::"符号顯式地引用std空間(标準set_new_handler函數就存在于std空間)。

  2. 調用全局operator new配置設定記憶體。如果第一次配置設定失敗,全局operator new會調用X的new-handler,因為它剛剛(見1.)被安裝成為全局new-handler。如果全局operator new最終未能配置設定到記憶體,它抛出std::bad_alloc異常,X的operator new會捕捉到它。X的operator new然後恢複最初被取代的全局new-handler函數,最後以抛出異常傳回。

  3. 假設全局operator new為類型X的對象配置設定記憶體成功,, X的operator new會再次調用标準set_new_handler來恢複最初的全局出錯處理函數。最後傳回配置設定成功的記憶體的指針。

  C++是這麼做的:

  void * X::operator new(size_t size)

  {

   new_handler globalHandler = // 安裝X的new_handler

   std::set_new_handler(currentHandler);

   void *memory;

   try { // 嘗試配置設定記憶體

   memory = ::operator new(size);

   }

   catch (std::bad_alloc&) { // 恢複舊的new_handler

   std::set_new_handler(globalHandler);

   throw; // 抛出異常

   }

   std::set_new_handler(globalHandler); // 恢複舊的new_handler

  

   return memory;

  }

  如果你對上面重複調用std::set_new_handler看不順眼,可以參見條款M9來除去它們。

  使用類X的記憶體配置設定處理功能時大緻如下:

  void noMoreMemory(); // X的對象配置設定記憶體失敗時調用的

   // new_handler函數的聲明

   //

  X::set_new_handler(noMoreMemory);

   // 把noMoreMemory設定為X的

   // new-handling函數

  X *px1 = new X; // 如記憶體配置設定失敗,

   // 調用noMoreMemory

  string *ps = new string; // 如記憶體配置設定失敗,

   // 調用全局new-handling函數

   //

  X::set_new_handler(0); // 設X的new-handling函數為空

   //

  X *px2 = new X; // 如記憶體配置設定失敗,立即抛出異常

   // (類X沒有new-handling函數)

   //

  你會注意到,處理以上類似情況,如果不考慮類的話,實作代碼是一樣的,這就很自然地想到在别的地方也能重用它們。正如條款41所說明的,繼承和模闆可以用來設計可重用代碼。在這裡,我們把兩種方法結合起來使用,進而滿足了你的要求。

  你隻要建立一個“混合風格”(mixin-style)的基類,這種基類允許子類繼承它某一特定的功能——這裡指的是建立一個類的new-handler的功能。之是以設計一個基類,是為了讓所有的子類可以繼承set_new_handler和operator new功能,而設計模闆是為了使每個子類有不同的currentHandler資料成員。這聽起來很複雜,不過你會看到代碼其實很熟悉。差別隻不過是它現在可以被任何類重用了。

  template<class T> // 提供類set_new_handler支援的

  class NewHandlerSupport { // “混合風格”的基類

  public:

   static new_handler set_new_handler(new_handler p);

   static void * operator new(size_t size);

  private:

   static new_handler currentHandler;

  };

  template<class T>

  new_handler NewHandlerSupport<T>::set_new_handler(new_handler p)

  {

   new_handler oldHandler = currentHandler;

   currentHandler = p;

   return oldHandler;

  }

  template<class T>

  void * NewHandlerSupport<T>::operator new(size_t size)

  {

   new_handler globalHandler =

   std::set_new_handler(currentHandler);

   void *memory;

   try {

   memory = ::operator new(size);

   }

   catch (std::bad_alloc&) {

   std::set_new_handler(globalHandler);

   throw;

   }

   std::set_new_handler(globalHandler);

   return memory;

  }

  // this sets each currentHandler to 0

  template<class T>

  new_handler NewHandlerSupport<T>::currentHandler;

  有了這個模闆類,對類X加上set_new_handler功能就很簡單了:隻要讓X從newHandlerSupport<X>繼承:

  // note inheritance from mixin base class template. (See

  // my article on counting objects for information on why

  // private inheritance might be preferable here.)

  class X: public NewHandlerSupport<X> {

   ... // as before, but no declarations for

  }; // set_new_handler or operator new

  使用X的時候依然不用理會它幕後在做些什麼;老代碼依然工作。這很好!那些你常不去理會的東西往往是最可信賴的。

  使用set_new_handler是處理記憶體不夠情況下一種友善,簡單的方法。這比把每個new都包裝在try子產品裡當然好多了。而且,NewHandlerSupport這樣的模闆使得向任何類增加一個特定的new-handler變得更簡單。“混合風格”的繼承不可避免地将話題引入到多繼承上去,在轉到這個話題前,你一定要先閱讀條款43。

  1993年前,C++一直要求在記憶體配置設定失敗時operator new要傳回0,現在則是要求operator new抛出std::bad_alloc異常。很多C++程式是在編譯器開始支援新規範前寫的。C++标準委員會不想放棄那些已有的遵循傳回0規範的代碼,是以他們提供了另外形式的operator new(以及operator new[]——見條款8)以繼續提供傳回0功能。這些形式被稱為“無抛出”,因為他們沒用過一個throw,而是在使用new的入口點采用了nothrow對象:

  class Widget { ... };

  Widget *pw1 = new Widget; // 配置設定失敗抛出std::bad_alloc if

  if (pw1 == 0) ... // 這個檢查一定失敗

  Widget *pw2 =

   new (nothrow) Widget; // 若配置設定失敗傳回0

  if (pw2 == 0) ... // 這個檢查可能會成功

  不管是用“正規”(即抛出異常)形式的new還是“無抛出”形式的new,重要的是你必須為記憶體配置設定失敗做好準備。最簡單的方法是使用set_new_handler,因為它對兩種形式都有用

繼續閱讀