1. 程式人生 > >Item 51:寫new和delete時請遵循慣例

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具有同樣的引數和返回值, 要注意的是你不要假設其中有幾個物件,以及每個物件的大小是多少,所以不要操作這些還不存在的物件。因為:

  1. 你不知道物件大小是什麼。上面也提到了當繼承發生時size不一定等於sizeof(Base)。
  2. 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。