Item 51:寫new和delete時請遵循慣例
Item 51: Adhere to convention when writing new and delete.
Item 50介紹瞭如何自定義new和delete但沒有解釋你必須遵循的慣例, 這些慣例中有些並不直觀,所以你需要記住它們!
- operator new需要無限迴圈地獲取資源,如果沒能獲取則呼叫”new handler”,不存在”new handler”時應該丟擲異常;
- operator new應該處理size == 0的情況;
- operator delete應該相容空指標;
- operator new/delete作為成員函式應該處理size > sizeof(Base)的情況(因為繼承的存在)。
外部operator new
Item 49指出瞭如何將operator new過載為類的成員函式,在此我們先看看如何實現一個外部(非成員函式)的operator new: operator new應當有正確的返回值,在記憶體不足時應當呼叫”new handler”,請求申請大小為0的記憶體時也可以正常執行,避免隱藏全域性的(”normal form”)new。
- 給出返回值很容易。當記憶體足夠時,返回申請到的記憶體地址;當記憶體不足時,根據Item 49描述的規則返回空或者丟擲bad_alloc異常。
- 每次失敗時呼叫”new handler”,並重復申請記憶體卻不太容易。只有當”new handler”為空時才應丟擲異常。
- 申請大小為零時也應返回合法的指標。允許申請大小為零的空間確實會給程式設計帶來方便。
考慮到上述目標,一個非成員函式的operator new大致實現如下:
void * operator new(std::size_t size) throw(std::bad_alloc){
if(size == 0) size = 1;
while(true){
// 嘗試申請
void *p = malloc(size);
// 申請成功
if(p) return p;
// 申請失敗,獲得new handler
new_handler h = set_new_handler(0);
set_new_handler(h);
if(h) (*h)();
else throw bad_alloc();
}
}
- size == 0時申請大小為1看起來不太合適,但它非常簡單而且能正常工作。況且你不會經常申請大小為0的空間吧?
- 兩次set_new_handler呼叫先把全域性”new handler”設定為空再設定回來,這是因為無法直接獲取”new handler”,多執行緒環境下這裡一定需要鎖。
- while(true)意味著這可能是一個死迴圈。所以Item 49提到,”new handler”要麼釋放更多記憶體、要麼安裝一個新的”new handler”,如果你實現了一個無用的”new handler”這裡就是死迴圈了。
成員operator new
過載operator new為成員函式通常是為了對某個特定的類進行動態記憶體管理的優化,而不是用來給它的子類用的。 因為在實現Base::operator new()時,是基於物件大小為sizeof(Base)來進行記憶體管理優化的。
當然,有些情況你寫的Base::operator new是通用於整個class及其子類的,這時這一條規則不適用。
class Base{
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
};
class Derived: public Base{...};
Derived *p = new Derived; // 呼叫了 Base::operator new !
子類繼承Base::operator new()之後,因為當前物件不再是假設的大小,該方法不再適合管理當前物件的記憶體了。 可以在Base::operator new中判斷引數size,當大小不為sizeof(Base)時呼叫全域性的new:
void *Base::operator new(std::size_t size) throw(std::bad_alloc){
if(size != sizeof(Base)) return ::operator new(size);
...
}
上面的程式碼沒有檢查size == 0!這是C++神奇的地方,大小為0的獨立物件會被插入一個char(見Item 39)。 所以sizeof(Base)永遠不會是0,所以size == 0的情況交給::operator new(size)去處理了。
這裡提一下operator new[],它和operator new具有同樣的引數和返回值, 要注意的是你不要假設其中有幾個物件,以及每個物件的大小是多少,所以不要操作這些還不存在的物件。因為:
- 你不知道物件大小是什麼。上面也提到了當繼承發生時size不一定等於sizeof(Base)。
- size實參的值可能大於這些物件的大小之和。因為Item 16中提到,陣列的大小可能也需要儲存。
外部operator delete
相比於new,實現delete的規則要簡單很多。唯一需要注意的是C++保證了delete一個NULL總是安全的,你尊重該慣例即可。
同樣地,先實現一個外部(非成員)的delete:
void operator delete(void *rawMem) throw(){
if(rawMem == 0) return;
// 釋放記憶體
}
成員operator delete
成員函式的delete也很簡單,但要注意如果你的new轉發了其他size的申請,那麼delete也應該轉發其他size的申請。
class Base{
public:
static void * operator new(std::size_t size) throw(std::bad_alloc);
static void operator delete(void *rawMem, std::size_t size) throw();
};
void Base::operator delete(void *rawMem, std::size_t size) throw(){
if(rawMem == 0) return; // 檢查空指標
if(size != sizeof(Base)){
::operator delete(rawMem);
}
// 釋放記憶體
}
注意上面的檢查的是rawMem為空,size是不會為空的。
其實size實參的值是通過呼叫者的型別來推導的(如果沒有虛解構函式的話):
Base *p = new Derived; // 假設Base::~Base不是虛擬函式
delete p; // 傳入`delete(void *rawMem, std::size_t size)`的`size == sizeof(Base)`。
如果Base::~Base()宣告為virtual,則上述size就是正確的sizeof(Derived)。 這也是為什麼Item 7指出解構函式一定要宣告virtual。