Item 51: 編寫 new 和 delete 時要遵守慣例
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發佈:http://blog.csdn.net/fatalerror99/
Item 50 講解了什麼時候你可能需要編寫 operator new 和 operator delete 的你自己的版本,但是沒有講解當你這樣做時必須遵循的慣例。這些規則並不難以遵循,但有一些不那麼直觀,所以瞭解它們是什麼非常重要。
我們從 operator new 開始。實現一個符合慣例的 operator new 需要有正確的返回值,在沒有足夠的內存可用時調用 new-handling function(參見 Item 49),並做好應付無內存請求的準備。你還要避免無意中對 new 的“常規”形式的覆蓋,雖然這更多的是一個 class interface(類接口)的問題,而並非是一個實現的需求,它將在 Item 52 討論。
operator new 的返回值部分很容易。如果你能提供所請求的內存,你就返回一個指向它的指針。如果你不能,你應該遵循 Item 49 描述的規則並拋出一個 bad_alloc 類型的 exception(異常)。
然而,它也不完全那麼簡單,因爲 operator new 實際上不止一次設法分配內存,每次失敗後調用 new-handling function。在此假設 new-handling function 能做些事情釋放一些內存。只有當指向 new-handling function 的指針爲空時,operator new 才拋出一個 exception(異常)。
奇怪的是,C++ 要求即使請求零字節,operator new 也要返回一個合理的指針。(需要這種怪異的行爲來簡化語言的其它部分。)在這種情況下,一個 non-member(非成員)的 operator new 的僞代碼如下:
void * operator new(std::size_t size) throw(std::bad_alloc)
{ // your operator new might
using namespace std; // take additional params
if (size == 0) { // handle 0-byte requests
size = 1; // by treating them as
} // 1-byte requests
while (true) {
attempt to allocate size bytes;
if (the allocation was successful)
return (a pointer to the memory);
// allocation was unsuccessful; find out what the
// current new-handling function is (see below)
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if (globalHandler) (*globalHandler)();
else throw std::bad_alloc();
}
}
將零字節請求當作他們真的請求了一個字節來處理的竅門看起來很齷齪,但是它簡單,合法,有效,無論如何,你估摸着的請求零字節這種事情發生的頻率有多大呢?
你可能在不經意中還看到僞代碼中將 new-handling function pointer 設置爲空,然後又馬上重置爲它原來的值。遺憾的是,沒有辦法直接得到 new-handling function pointer,所以你必須調用 set_new_handler 以得知它是什麼。拙劣,的確,但是也有效,至少對 single-threaded(單線程)代碼沒問題。在 multithreaded(多線程)環境中,你可能需要某種鎖以便安全地擺佈隱藏在 new-handling function 後面的(全局的)data structures(數據結構)。
Item 49 談及 operator new 包含一個無限循環,而上面的代碼明確地展示了這個循環,"while (true)" 差不多會盡其所能地無限做下去。跳出循環的唯一出路是內存被成功分配或 new-handling function 做了 Item 49 中描述的事情之一:使得更多的內存可用,安裝一個不同的 new-handler,卸載 new-handler,拋出一個 bad_alloc 或從 bad_alloc 派生的 exception(異常),或不再返回。現在,new-handler 爲什麼要做這些事情之一已經很清楚了。如果它不這樣做,operator new 內的循環永遠不會停止。
很多人沒有意識到 operator new member functions(成員函數)會被 derived classes(派生類)繼承。這會引起一些有趣的複雜性。在前面的 operator new 僞代碼中,注意那個函數設法分配 size 個字節(除非 size 是零)。因爲它是傳遞給這個函數的 argument(實參),所以它有着明確的意義。然而,就像 Item 50 所講的,編寫一個自定義的內存管理器的最常見的原因之一是爲了優化某個特定 class 的 objects 的分配,而不是某個 class 或它的任何 derived classes(派生類)的。也就是說,給定一個 class X 的 operator new,這個函數的行爲通常是爲大小爲 sizeof(X) 的 objects 調諧的——絕不會更大或者更小。然而,由於 inheritance(繼承),就有可能一個 base class(基類)中的 operator new 被調用來爲一個 derived class(派生類)的 object 分配內存:
class Base {
public:
static void * operator new(std::size_t size) throw(std::bad_alloc);
...
};
class Derived: public Base // Derived doesn't declare
{ ... }; // operator new
Derived *p = new Derived; // calls Base::operator new!
如果 Base 的 class-specific(類專用)的 operator new 不是被設計成應付這種情況的——它很可能不是——它處理這種局面的最佳辦法就是把這個請求“錯誤”內存量的調用甩給 standard operator new,就像這樣:
void * Base::operator new(std::size_t size) throw(std::bad_alloc)
{
if (size != sizeof(Base)) // if size is "wrong,"
return ::operator new(size); // have standard operator
// new handle the request
... // otherwise handle
// the request here
}
“不許動!”我聽到你喊,“你忘了檢查 size 是零這種 pathological-but-nevertheless-possible(病態然而可能)的情況!”實際上,我沒有,還有,當你大聲抱怨的時候拜託不要使用連字符。測試依然在那,它只是與 size 和 sizeof(Base) 的比較合在了一起。C++ 工作在一些神祕的方式中,這些方式之一就是強制規定所有的獨立 objects 都具有非零的大小(參見 Item 39)。根據定義,sizeof(Base) 絕不會是零,所以如果 size 是零,請求將轉發給 ::operator new,而以一種合理的方式處置這個請求就成爲那個函數的職責。
如果你想要在每一個 class 的基礎上控制數組的內存分配,你需要實現 operator new 的專用於數組的兄弟,operator new[]。(這個 function 通常被叫做 "array new",因爲要確定 "operator new[]" 如何發音實在是太難了。)如果你決定要編寫 operator new[],記住你所做的全部是分配一大塊 raw memory(裸內存)——你不能針對還不存在的數組中的 objects 做任何事情。實際上,你甚至不能確定數組中會有多少個 objects。首先,你不知道每個 object 有多大。畢竟,一個 base class(基類)的 operator new[] 通過繼承可以被調用來爲一個 derived class objects(派生類對象)的數組分配內存,而 derived class objects(派生類對象)通常都比 base class objects(基類對象)更大。
因此,在 Base::operator new[] 中,你不能斷定每一個加到數組中的 object 的大小一定是 sizeof(Base),而這就意味着,你不能斷定數組中的 objects 的數量是 (bytes requested)/sizeof(Base)。第二,傳遞給 operator new[] 的 size_t 參數可能比充滿 objects 的內存還要大一些,因爲,就像 Item 16 講到的,dynamically allocated arrays(動態分配數組)可能包括額外的空間用於存儲數組元素的數量。
編寫 operator new 時,你需要遵循的慣例也就到此爲止了。對於 operator delete,事情就更簡單了,你需要記住的全部大約就是 C++ 保證刪除空指針總是安全的,所以你需要遵循這個保證。下面是一個非成員的 operator delete 的僞代碼:
void operator delete(void *rawMemory) throw()
{
if (rawMemory == 0) return; // do nothing if the null
// pointer is being deleted
deallocate the memory pointed to by rawMemory;
}
這個函數的成員版本也很簡單,只是你必須確保檢查被刪除東西的大小。假設你的 class-specific(類專用)的 operator new 將“錯誤”大小的請求轉發給 ::operator new,你也可以將“錯誤大小”的刪除請求轉發給 ::operator delete:
class Base { // same as before, but now
public: // operator delete is declared
static void * operator new(std::size_t size) throw(std::bad_alloc);
static void operator delete(void *rawMemory, std::size_t size) throw();
...
};
void Base::operator delete(void *rawMemory, std::size_t size) throw()
{
if (rawMemory == 0) return; // check for null pointer
if (size != sizeof(Base)) { // if size is "wrong,"
::operator delete(rawMemory); // have standard operator
return; // delete handle the request
}
deallocate the memory pointed to by rawMemory;
return;
}
有趣的是,如果被刪除的 object 是從一個缺少 virtual destructor(虛擬析構函數)的 base class(基類)派生出來的,C++ 傳遞給 operator delete 的 size_t 值也許是不正確的。這已經足夠作爲“確保你的 base classes(基類)擁有 virtual destructors(虛擬析構函數)”的原因了,除此之外,Item 7 描述了另一個,論證得更好的原因。至於當前,簡單地記住如果你在 base classes(基類)中遺漏了 virtual destructors(虛擬析構函數),operator delete functions 可能無法正確工作。
Things to Remember
- operator new 應該包含一個設法分配內存的無限循環,如果它不能滿足一個內存請求,應該調用 new-handler,還應該處理零字節請求。class-specific(類專用)版本應該處理對比預期更大的區塊的請求。
- operator delete 如果收到一個空指針應該什麼都不做。class-specific(類專用)版本應該處理比預期更大的區塊。