(一)
當我們寫下了下面這個語句:
Widget* pw = new Widget;
這個時候,共有兩個函數被調用:一個分配內存的operator new,另外一個是Widget的default構造函數。
假設第一個調用成功,第二個卻拋出異常。步驟一所分配內存必須取消並恢復舊觀,否則會造成內存洩漏。這時,客戶沒能力歸還內存,因為Widget構造函數拋出異常,pw尚未被賦值,客戶手上也就沒有指針指向該被歸還的內存。
這個時候,取消步驟一,並恢復舊觀的責任就落到C++運行系統身上。
運行期系統會高興的調用步驟一所調用operator new的相應的operator delete版本,前提是他必須知道哪一個operator delete被調用(可能有許多個)。
如果目前面對的是擁有正常簽名式的new和delete,並不是問題,
正常的new和對應正常的delete:
void* operator new(std::size_t) throw(std::bad_alloc); void operator delete(void* rawMemory) throw();//global 作用域中的正常簽名式 void operator delete(void* rawMemory, std::size_t size) throw();//class作用域中的典型簽名式。
(二)
但是當你開始聲明非正常形式的operator new,也就是帶附加參數的operator new。
這個時候,“究竟哪一個delete伴隨這個new”的問題就浮現了:
比如說:
class專屬的operator new要求接受一個ostream,志記相關分配信息,同時又寫了個正常形式的class專屬operator delete:
class Widget{ public: static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc); static void* operator delete(void* pMemory, std::size_t size) throw(); };
如果operator new接受的參數除了一定會有的那個size_t之外還有其他,這個便是所謂的placement new。
眾多placement new中特別有一個是“接受一個指針指向對象該被構造之處”,那樣的operator new長相如下:
void* operator new(std::size_t, void* pMemory) throw();
Widget* pw = new(std::cerr) Widget;
這個如果Widget構造函數拋出異常,運行期系統無法知道真正被調用的那個new如何運作,因此無法取消分配並恢復舊觀。運行期系統尋找“參數個數和類型都與operator new相同的”某個operator delete。應該是:
void operator delete(void*, std::ostream&) throw();
被稱為placement deletes。
現在,既然Widget沒有聲明placement版本的operator delete,所以運行期系統什麼也不做。如果Widget構造函數拋出異常,不會有任何operator delete被調用。
所以會造成內存洩漏。
(三)
規則很簡單:如果一個帶額外參數的operator new沒有“帶相同額外參數”的對應版operator delete,那麼當new的內存分配動作需要取消並恢復時就沒有任何operator delete會被調用。所以有必要聲明一個placement delete,對應於那個有志記功能的placement new:
class Widget{ public: static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc); static void* operator delete(void* pMemory, std::size_t size) throw(); static void* operator delete(void* pMemory, std::ostream& logStream) throw(); };
這樣改變後,如果Widget構造函數拋出異常:
Widget* pw = new (std::cerr) Widget;
對應的placement delete會被自動調用,讓Widget有機會確保不洩露內存。
這個語句:
delete pw;//調用正常的operator delete
placement delete只有在“伴隨placement new調用而觸發的構造函數”出現異常時才會被調用。對一個指針施行delete絕不會導致調用placement delete。
這意味對所有placement new我們必須同時提供一個正常的delete和一個placement版本。
(四)
由於成員函數的名稱會掩蓋其外圍作用域中的相同名稱,你必須小心讓class專屬的news掩蓋客戶期望的其他news(包括正常版本)。
例如你有一個base class,其中聲明唯一一個placement operator new,客戶會發現他們無法使用正常形式的new:
class Base { public: ... static void* operator new(size_t size, ostream& logStream) throw(bad_alloc); //這個new會遮掩正常的global形式 }; Base* pb = new Base; //錯誤!因為正常形式的operator new被遮掩 Base* pb = new (std::cerr) Base; //正確,調用Base的placement new.
同樣的道理,derived classes中的operator news會遮掩global版本和繼承而得的operator new版本:
class Derived : public Base { //繼承先前的Base public: ... static void* operator new(std::size_t size) throw(std::bad_alloc); 重新聲明正常形式的new ... }; Derived* pd = new(std::clog) Derived; //錯誤,因為Base的placement被遮掩了。 Derived* pd = new Derived; //沒問題,調用Derived的operator new.
(五)
缺省情況下c++在global作用域內提供以下形式的operator new:
void* operator new(std::size_t) throw(std::bad_alloc); //normal new void* operator new(std::size_t, void*) throw(); //placement new void* operator new(std::size_t, const std::nothrow_t&) throw();// nothrow new
如果你在class內聲明任何operator news,它會遮掩上述這些標准形式。除非你的意思就是要阻止class的客戶使用這些形式,否則確保他們在你生成的任何定制型operator new之外還可用。
解決所有問題的一個簡單的做法是,建立一個base class,內含所有正常形式的new和delete:
class StandardNewDeleteForms{ public: //normal new/delete static void* operator new(std::size_t size)throw(std::bad_alloc) {return ::operator new(size);} static void operator delete(void* pMemory) throw() {::operator delete(pMemory);} //placement new/delete static void* operator new(std::size_t size, void* ptr) throw() {return ::operator new(size, ptr);} static void operator delete(void* pMemory, void* ptr)throw() {return ::operator delete(pMemory, ptr);} //nothrow new/delete static void* operator new(std::size_t size, const std::nothrow_t& nt)throw() {return ::operator new(size, nt);} static void operator delete(void* pMemory, const std::nothrow_t&) throw() {::operator delete(pMemory);} };
凡是想自定義形式擴充標准形式的客戶,可利用繼承機制及using聲明式取得標准形式:
class Widget: public StandardNewDeleteForms{ public: using StandardNewDeleteForms::operator new; using StandardNewDeleteForms::operator delete; static void* operator new(std::size_t size, std::ostream& logStream)throw(std::bad_alloc); static void operator delete(void* pMemory, std::ostream& logStream) throw(); };
請記住:
(1)當你寫一個placement operator new,請確定也寫出了對應的placement operator delete.如果沒有這樣做。你的程序可能會發生隱微而時斷時續的內存洩漏.
(2)當你聲明placement new和placement delete。請確定不要無意識地遮掩它們的正常版本.