寫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的僞代碼看起來會象下面這樣:
{
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可能會被調用去爲一個子類對象分配內存:
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來處理,象下面這樣:
{
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++保證刪除空指針永遠是安全的,所以你要充分地應用這一保證。下面是非類成員形式的operator delete的僞代碼:
{
if (rawmemory == 0) return; //如果指針爲空,返回
釋放rawmemory指向的內存;
return;
}
這個函數的類成員版本也簡單,只是還必須檢查被刪除的對象的大小。假設類的operator new將“錯誤”大小的分配請求轉給::operator new,那麼也必須將“錯誤”大小的刪除請求轉給::operator delete:
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; // 檢查空指針
if (size != sizeof(base)) { // 如果size"錯誤",
::operator delete(rawmemory); // 讓標準operator來處理請求
return;
}
釋放指向rawmemory的內存;
return;
}
可見,有關operator new和operator delete(以及他們的數組形式)的規定不是那麼麻煩,重要的是必須遵守它。只要內存分配程序支持new-handler函數並正確地處理了零內存請求,就差不多了;如果內存釋放程序又處理了空指針,那就沒其他什麼要做的了。至於在類成員版本的函數裏增加繼承支持,那將很快就可以完成。