條款8: 寫operator new和operator delete時要遵循常規
自己重寫operator new時(條款10解釋了為什麼有時要重寫它),很重要的一點是函數提供的行為要和系統預設的operator new一致。實際做起來也就是:要有正確的傳回值;可用記憶體不夠時要調用出錯處理函數(見條款7);處理好0位元組記憶體請求的情況。此外,還要避免不小心隱藏了標準形式的new,不過這是條款9的話題。
有關傳回值的部分很簡單。如果記憶體配置請求成功,就返回指向記憶體的指標;如果失敗,則遵循條款7的規定拋出一個std::bad_alloc類型的異常。
但事情也不是那麼簡單。因為operator new實際上會不只一次地嘗試著去分配記憶體,它要在每次失敗後調用出錯處理函數,還期望出錯處理函數能想辦法釋放別處的記憶體。只有在指向出錯處理函數的指標為空白的情況下,operator new才拋出異常。
另外,c++標準要求,即使在請求分配0位元組記憶體時,operator new也要返回一個合法指標。(實際上,這個聽起來怪怪的要求確實給c++語言其它地方帶來了簡便)
這樣,非類成員形式的operator new的虛擬碼看起來會象下面這樣:
void * operator new(size_t size) // operator new還可能有其它參數
{
if (size == 0) { // 處理0位元組請求時,
size = 1; // 把它當作1個位元組請求來處理
}
while (1) {
分配size位元組記憶體;
if (分配成功)
return (指向記憶體的指標);
// 分配不成功,找出當前出錯處理函數
new_handler globalhandler = set_new_handler(0);
set_new_handler(globalhandler);
if (globalhandler) (*globalhandler)();
else throw std::bad_alloc();
}
}
處理零位元組請求的技巧在於把它作為請求一個位元組來處理。這看起來也很怪,但簡單,合法,有效。而且,你又會多久遇到一次零位元組請求的情況呢?
你又會奇怪上面的虛擬碼中為什麼把出錯處理函數置為0後又立即恢複。這是因為沒有辦法可以直接得到出錯處理函數的指標,所以必須通過調用set_new_handler來找到。辦法很笨但也有效。
條款7提到operator new內部包含一個無限迴圈,上面的代碼清楚地說明了這一點——while (1)將導致無限迴圈。跳出迴圈的唯一辦法是記憶體配置成功或出錯處理函數完成了條款7所描述的事件中的一種:得到了更多的可用記憶體;安裝了一個新的new-handler(出錯處理函數);卸載了new-handler;拋出了一個std::bad_alloc或其衍生類別型的異常;或者返回失敗。現在明白了為什麼new-handler必須做這些工作中的一件。如果不做,operator new裡面的迴圈就不會結束。
很多人沒有認識到的一點是operator new經常會被子類繼承。這會導致某些複雜性。上面的虛擬碼中,函數會去分配size位元組的記憶體(除非size為0)。size很重要,因為它是傳遞給函數的參數。但是大多數針對類所寫的operator new(包括條款10中的那種)都是只為特定的類設計的,不是為所有的類,也不是為它所有的子類設計的。這意味著,對於一個類x的operator new來說,函數內部的行為在涉及到對象的大小時,都是精確的sizeof(x):不會大也不會小。但由於存在繼承,基類中的operator new可能會被調用去為一個子類對象分配記憶體:
class base {
public:
static void * operator new(size_t size);
...
};
class derived: public base // derived類沒有聲明operator new
{ ... };
derived *p = new derived; // 調用base::operator new
如果base類的operator new不想費功夫專門去處理這種情況——這種情況出現的可能性不大——那最簡單的辦法是把這個“錯誤”數量的記憶體配置請求轉給標準operator new來處理,象下面這樣:
void * base::operator new(size_t size)
{
if (size != sizeof(base)) // 如果數量“錯誤”,讓標準operator new
return ::operator new(size); // 去處理這個請求
//
... // 否則處理這個請求
}
“停!”我聽見你在叫,“你忘了檢查一種雖然不合理但是有可能出現的一種情況——size有可能為零!”是的,我沒檢查,但拜託下次再叫出聲的時候不要這麼文縐縐的。:)但實際上檢查還是做了,只不過融合到size != sizeof(base)語句中了。c++標準很怪異,其中之一就是規定所以獨立的(freestanding)類的大小都是非零值。所以sizeof(base)永遠不可能是零(即使base類沒有成員),如果size為零,請求會轉到::operator new,由它來以一種合理的方式對請求進行處理。(有趣的是,如果base不是獨立的類,sizeof(base)有可能是零,詳細說明參見"my article on counting objects")。
如果想控制基於類的數組的記憶體配置,必須實現operator new的數組形式——operator new[](這個函數常被稱為“數組new”,因為想不出"operator new[]")該怎麼發音)。寫operator new[]時,要記住你面對的是“原始”記憶體,不能對數組裡還不存在的對象進行任何操作。實際上,你甚至還不知道數組裡有多少個對象,因為不知道每個對象有多大。基類的operator new[]會通過繼承的方式被用來為子類對象的數組分配記憶體,而子類對象往往比基類要大。所以,不能想當然認為base::operator new[]裡的每個對象的大小都是sizeof(base),也就是說,數組裡對象的數量不一定就是(請求位元組數)/sizeof(base)。關於operator new[]的詳細介紹參見條款m8。
重寫operator new(和operator new[])時所有要遵循的常規就這些。對於operator delete(以及它的夥伴operator delete[]),情況更簡單。所要記住的只是,c++保證刪除null 指標永遠是安全的,所以你要充分地應用這一保證。下面是非類成員形式的operator delete的虛擬碼:
void operator delete(void *rawmemory)
{
if (rawmemory == 0) return; //如果指標為空白,返回
釋放rawmemory指向的記憶體;
return;
}
這個函數的類成員版本也簡單,只是還必須檢查被刪除的對象的大小。假設類的operator new將“錯誤”大小的分配請求轉給::operator new,那麼也必須將“錯誤”大小的刪除請求轉給::operator delete:
class base { // 和前面一樣,只是這裡聲明了
public: // operator delete
static void * operator new(size_t size);
static void operator delete(void *rawmemory, size_t size);
...
};
void base::operator delete(void *rawmemory, size_t size)
{
if (rawmemory == 0) return; // 檢查null 指標
if (size != sizeof(base)) { // 如果size"錯誤",
::operator delete(rawmemory); // 讓標準operator來處理請求
return;
}
釋放指向rawmemory的記憶體;
return;
}
可見,有關operator new和operator delete(以及他們的數組形式)的規定不是那麼麻煩,重要的是必須遵守它。只要記憶體配置程式支援new-handler函數並正確地處理了零記憶體請求,就差不多了;如果記憶體釋放程式又處理了null 指標,那就沒其他什麼要做的了。至於在類成員版本的函數裡增加繼承支援,那將很快就可以完成。
出處:《Effective C++》
轉自:http://www.leftworld.net/online/effectivec/file/ch02c.htm