C++ -- 內存管理 之 重載new delete

寫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 
*= 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++保證刪除空指針永遠是安全的,所以你要充分地應用這一保證。下面是非類成員形式的operator delete的僞代碼:

void operator delete(void *rawmemory)
{
  
if (rawmemory == 0return;    //如果指針爲空,返回

  釋放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 == 0return;      // 檢查空指針

  
if (size != sizeof(base)) {      // 如果size"錯誤",
    ::operator delete(rawmemory);  // 讓標準operator來處理請求
    return;                        
  }

  釋放指向rawmemory的內存;

  
return;
}

可見,有關operator new和operator delete(以及他們的數組形式)的規定不是那麼麻煩,重要的是必須遵守它。只要內存分配程序支持new-handler函數並正確地處理了零內存請求,就差不多了;如果內存釋放程序又處理了空指針,那就沒其他什麼要做的了。至於在類成員版本的函數裏增加繼承支持,那將很快就可以完成。






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章