用C++的new操作符,分配記憶體,什麼時候會失敗?【轉】
Effective C++ 中文第二版 50個有效改善程式設計效率的辦法
條款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標頭檔(或與它相等價的用到了名字空間的版本,見條款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函數,它在標頭檔裡大致是象下面這樣定義的:
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。也就是傳遞null 指標給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
假設處理類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函數)
你只要建立一個“混合風格”(mixin-style)的基類,這種基類允許子類繼承它某一特定的功能——這裡指的是建立一個類的new-handler的功能。之所以設計一個基類,是為了讓所有的子類可以繼承set_new_handler和operator new功能,而設計範本是為了使每個子類有不同的currenthandler資料成員。這聽起來很複雜,不過你會看到代碼其實很熟悉。區別只不過是它現在可以被任何類重用了。
template // 提供類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
new_handler newhandlersupport::set_new_handler(new_handler p)
{
new_handler oldhandler = currenthandler;
currenthandler = p;
return oldhandler;
}
template
void * newhandlersupport::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
new_handler newhandlersupport::currenthandler;
有了這個模板類,對類x加上set_new_handler功能就很簡單了:只要讓x從newhandlersupport繼承:
// 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 {
… // as before, but no declarations for
}; // set_new_handler or operator new
使用x的時候依然不用理會它幕後在做些什麼;老代碼依然工作。這很好!那些你常不去理會的東西往往是最可信賴的。