operator new 和 operator delete

來源: http://topic.csdn.net/u/20081204/11/5848283a-c1b9-4efc-8a26-0878aed14a5c.html


C++裏允許用戶通過自定義operator new的方式來更改new表達式的行爲,這給了程序員定製內存管理方案的自由。但是享受這種自由的時候必須遵守一定的規範,具體可以參見《Effective C++ 2nd》的相關條款。本文補充解釋一些特別容易引起誤解的問題。


  operator new和operator delete都有其正規形式(normal signature):

void* operator new(size_t size);
void operator delete(void *p);
void operator delete(void *p,size_t size);

  普通的new與delete表達式在分配與釋放內存時調用的就是它們。一般來說operator delete(void*)的優先級比operator delete(void*,size_t)要高,這意味着如果在同一空間(scope)定義了這兩種形式的delete,擁有單一參數者優先被編譯器選擇。這一點在VC7.1中得到驗證,不知其它編譯器如何? 

  除了上面的正規形式外,我們還可以定義擁有更多參數的operator new和operator delete,只要保證前者的返回值和第一個參數分別是void*和size_t類型,而後者的分別是void和void*就行了。比如:

void* operator new(size_t size,const char* szFile,int nLine);
void operator delete(void *p,const char*,int);

  表達式new("xxx",20) SomeClass實際上就是告訴編譯器調用上面的多參數operator new來分配內存。但是不要依此類推出 delete("xxx",20) pObj,這是非法的。那麼怎麼才能調用到這個多參數的operator delete呢?實話告訴你,你沒有這個權利。呵呵,別吃驚,容我慢慢解釋。當兩個operator new和operator delete有相等的參數個數,並且除了第一個參數之外其餘參數的類型依次完全相同之時,我們稱它們爲一對匹配的operator new和operator delete。按照這個標準,上面兩位就是匹配的一對了。在我們使用 SomeClass *pObj = new("xxx",20) SomeClass 於堆中構建一個對象的過程中,如果在執行SomeClass的構造函數時發生了異常,並且這個異常被捕獲了,那麼C++的異常處理機制就會自動用與被使用的operator new匹配的operator delete來釋放內存(補充一點:在operator new中拋出異常不會導致這樣的動作,因爲系統認爲這標誌着內存分配失敗)。編譯期間編譯器按照以下順序尋找匹配者:首先在被構建對象類的類域中尋找,然後到父類域中,最後到全局域,此過程中一旦找到即停止搜尋並用它來生成正確的內存釋放代碼,如果沒有找到,當發生上述異常情況時將不會有代碼用來釋放分配的內存,這就造成內存泄漏了。而如果一切正常,delete pObj 則總是會去調用operator delete的正規形式。現在明白了吧,多參數的operator delete不是給我們而是給系統調用的,它平常默默無聞,但在最危急的關頭卻能挺身而出,保證程序的健壯性。爲了有個感性的認識,讓我們看看下面的代碼(試驗環境是VC7.1):

#include <malloc.h>

struct Base
{
  Base()
  {
  throw int(3);
  }

  ~Base() {}

  void* operator new( size_t nSize, const char*,int)
  {
  void* p = malloc( nSize );

  return p;
  } 

  void operator delete( void *p)
  {
  free(p);
  }

  void operator delete( void* p,const char*,int)
  {
  free( p );
  }
};

#define NULL 0
#define new new(__FILE__, __LINE__)

int main( void )
{
  Base* p = NULL;

  try
  {
  p = new Base;

  delete p;
  }
  catch(...)
  {
  }

  return 0;
}

  跟蹤執行會發現:程序在 p = new Base 處拋出一個異常後馬上跳去執行operator delete(void*,const char*,int)。註釋掉Base構造函數中的throw int(3)重來一遍,則new成功,然後執行delete p,這時實際調用的是Base::operator delete(void*)。以上試驗結果符合我們的預期。注意,operator new和operator delete是可以被繼承和重定義的,那接下來就看看它們在繼承體系中的表現。引進一個Base的派生類(代碼加在#define NULL 0的前面):

struct Son : public Base
{
  Son()
  {
  }

  void* operator new( size_t nSize, const char*,int)
  {
  // class Son

  void* p = malloc( nSize );
  return p;
  }

  void operator delete( void *p)
  {
  // class Son
  free(p);
  }

  void operator delete( void* p,const char*,int)
  {
  // class Son
  free( p );
  }
};
  然後將main函數中的p = new Base改成p = new Son並且取消對Base()中的throw int(3)的註釋,跟蹤執行,發現這回new表達式調用的是Son重定義的operator new,拋出異常後也迅速進入了正確的operator delete,即Son重定義的多參數版本。一切都如所料,是嗎?呵呵,別急着下結論,讓我們把拋異常的語句註釋掉再跑一次吧。很明顯,有些不對勁。這次delete p沒有如我們所願去調用Son::operator delete(void*),而是找到了在Base中定義的版本。怎麼回事?我願意留一分鐘讓好奇的你仔細想想。

  找到答案了嗎?沒錯,罪魁禍首就是那愚蠢的Base析構函數聲明。作爲一個領導着派生類的基類,析構函數竟然不聲明成virtual函數,這簡直就是瀆職。趕緊糾正,在~Base()前加上一個神聖的virtual,rebuild and run.....。謝天謝地,世界終於完美了。

  可能你會疑惑,在沒有給基類析構函數加virtual之前,當發生異常時C++爲什麼知道正確地調用派生類定義的多參數operator delete,而不是基類的?其實很簡單,new一個對象時必須提供此對象的確切類型,所以編譯器能夠在編譯期確定new表達式拋出異常後應該調用哪個類定義的operator delete。對於正常的delete p來說,如果p被聲明爲非基類類型的指針,編譯器就會在編譯時決定調用這種聲明類型定義的operator delete(靜態綁定),而如果p是某種基類類型指針,編譯器就會聰明地把到底調用哪個類定義的operator delete留待運行期決定(動態綁定)。那麼編譯器如何判斷p是否是基類指針呢?實際上它的根據就是p的聲明類型中定義的析構函數,只有在析構函數是虛擬的情況下p才被看成基類指針。這就可以解釋上面碰到的問題。當時p被聲明爲Base*,程序中它實際指向一個Son對象,但我們並沒有把~Base()聲明爲虛擬的,所以編譯器大膽地幫我們做了靜態綁定,也即生成調用Base::operator delete(void*)的代碼。不過千萬不要以爲所有編譯器都會這樣做,以上分析僅僅是基於VC7.1在本次試驗中的表現。事實上在C++標準中,經由一個基類指針刪除一個派生類對象,而此基類卻有一個非虛擬的析構函數,結果未定義。明白了吧老兄,編譯器在這種情況下沒有生成引爆你電腦的代碼已經算是相當客氣與負責了。現在你該能夠體會Scott Meyers勸你"總是讓base class擁有virtual detructor"時的苦心吧。

  至於數組版本的operator new[]和opeator delete[],情況一樣。朋友們可以自己做試驗確認一下。

  最後要指出的是,試驗代碼中對operator new和operator delete的實現相當不規範,負責任的做法仍然請大家參考Scott Meyers的著作




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