effective C++筆記--定製new和delete(二)

編寫new和delete時需固守常規

. 在編寫自己的operator new和operator delete時,需要遵守一些規則,先從operator new開始:實現一致性的operator new必需要返回正確的值;內存不足時必須調用new-handler函數;必須有應對零內存需求的準備;還需要避免不慎掩蓋正常形式的new。
  operator new的返回值看上去非常單純,如果它有能力供應客戶申請的內存,就返回指針指向那塊內存,如果沒有能力,就遵循本文的第一個條款描述的內容,並拋出一個bad_alloc異常。
  然而其實也不是很單純,因爲operator new實際上並不只嘗試分配一次內存,並在每次失敗後調用new-handler函數。假設new-handler函數也許能做一些動作將某些內存釋放出來。只有當指向new-handler函數的指針是null的時候,operator new才返回異常。
  C++規定,即使客戶要求分配0bytes,operator new也得返回一個合法的指針,一個小伎倆是將申請0byte視爲申請1byte。以下是一個non-member operator new的僞碼:

void* operator new(std::size_t size) throw(std::bad_alloc){
	using namespace std;
	if(size == 0){
		size = 1;
	}
	while(true){
		嘗試分配size bytes;
		if(分配成功)
		return (一個指針,指向分配來的內存);

		//分配失敗,找出目前的new-handler函數
		new_handler globalHandler = set_new_handler(0);
		set_new_handler(globalHandler);

		if(globalHandler) 
			(*globalHandler)();
		else 
			throw std::bad_alloc();
	}
}

. 這段僞碼中將new-handler函數指針設爲null後又恢復原樣,其實是因爲沒有辦法能直接取得當前的new-handler指針(你應該還記得set_new_handler是返回之前指向的new-handler函數的指針吧)。
  operator new成員函數是會被派生類繼承的,這會導致一些有趣的複雜度,如先前所說,寫出定製版的operator new是爲了某特定的class,而不是爲了它的派生類,然而一旦被繼承下去,可能基類的operator new被調用用來分配派生類的對象,處理這種行爲的最佳做法是將“內存申請錯誤”的調用行爲改爲採用標準的operator new:

void* Base::operator new(std::size_t size) throw(std::bad_alloc){
	if(size != sizeof(Base))				//如果大小錯誤,	
		return ::operator new(size);		//讓標準的operator new處理
	...									//否則在這裏處理
}

. 如果打算控制class專屬之“arrays內存分配行爲”,那麼需要實現operator new[]。這個函數通常被稱爲“array new”。對此,唯一需要做的事就是分配一塊未加工的內存,因爲你無法對array之內迄今爲止尚未存在的元素對象做任何事。實際上甚至無法計算這個array將含有多少個元素,首先你不知道每個對象多大,畢竟基類的operator new[]可能經由繼承被調用,將內存分配給“元素爲派生類對象”的array使用,而派生類對象通常比基類對象大。
  因此不能在Base::operator new[]內假設每個元素對象的大小是sizeof(Base),此外傳遞給operator new[]的size_t參數,其值有可能比“將被填以對象”的內存數量更多,因爲動態分配的arrays有可能含有額外的空間來存放元素個數。
  以上是撰寫operator new時需要注意的規則。對operator delete來說情況更加簡單,唯一需要記住的事情就是C++保證“刪除null指針永遠安全”,所以在編寫時要兌現這一規則,以下是operator delete的僞碼:

void operator delete(void* pMem) throw(){
	if(pMem == 0) return;				//如果將被刪除的是null指針,就啥也不做
	現在,歸還pMem所指的內存;
}

. 這個函數的member版本也很簡單,只需要多加一個動作檢查刪除數量。萬一class專屬的operator new將大小有誤的分配行爲轉交給::operator new執行,你也必須將大小有誤的刪除行爲轉交給::operator delete執行:

class Base{
public:
	static void* operator new(std::size_t size) throw(std::bad_alloc);
	static void operator delete(void* pMem,std::size_t size) throw();
	...
};
void Base::operator delete(void* pMem,std::size_t size) throw(){
	if(pMem == 0) return;
	if(size != sizeof(Base)){
		::operator delete(pMem);
		return;
	}
}

寫了placement new也要寫 placement delete

. 當寫一個new表達式像這樣:Widget* pw = new Widget;共有兩個函數被調用:一個是用以分配內存的operator new,一個是Widget的默認構造函數。
  假設第一個函數調用成功,第二個函數卻拋出異常。那麼步驟一的內存分配所得必須取消並恢復原樣,否則會造成內存泄露。在這個時候,客戶沒有能力去歸還內存,因爲如果Widget的構造函數拋出異常,pw尚未被賦值,客戶手上也就沒有指向該被歸還的內存。取消步驟一的內存分配所得並恢復原樣的責任因此落到C++運行期系統身上。
  運行期系統會調用步驟一所調用的operator new的相應的operator delete版本,前提是它知道哪個版本該被調用,如果面對的是正常簽名式的new和delete,這並不是問題,因爲正常的operator new對應正常的operator 的delete:

void* operator new(std::size_t size) throw(std::bad_alloc);
void operator delete(void* pMem) throw();			//global作用域中的正常簽名式
void operator delete(void* pMem,std::size_t size) throw();	//class作用域中的正常簽名式

. 因此,當只是用正常形式的new和delete,運行期系統毫無問題的可以找出那個“知道如何取消new所作所爲並恢復原狀”的delete,但是當聲明非正常形式的operator new,就不知道如何挑選delete版本了。比如,假設有一個class專屬的operator new,要求接受一個ostream,用來打日誌,同時又寫了一個正常形式的operator delete:

class Widget{
public:
	...
	static void* operator new(std::size_t size,std::ostream& logStream) 
		throw(std::bad_alloc);						//非正常形式的new
	static void operator delete(void* pMem,std::size_t size) throw();
													//正常形式的delete
};

//考慮到如下調用
Widget* pw = new(std::cerr) Widget;			

. 以上的客戶調用將會在Widget的構造函數拋出異常的時候導致內存泄露,因爲在內存分配成功後,而Widget的構造函數拋出異常,運行期系統有責任取消operator new的內存分配並恢復原狀,然而運行期系統無法知道真正被調用的那個operator new是如何運作的,因此它無法取消內存分配並恢復原狀。因此需要提供與調用的operator new相同的參數個數與類型的operator delete版本,否則將沒有任何operator delete被調用:

void operator delete(void* pMem,std::ostream& ) throw();

. 那如果提供了對應版本的operator delete之後,並在代碼中使用了delete,比如:delete pw;會發生什麼事呢?其實它會調用正常的operator delete。這意味着如果要避免placement new帶來的內存泄露麻煩,我們必須同時提供一個正常版本的operator delete(用於構造期間無異常拋出)和一個placement版本(用於構造期間有異常拋出)。後者的額外參數必須和placement new的一樣。
  順帶一提的是,由於成員函數的名稱會掩蓋其外圍作用域中的相同名稱,比如類中的專有new版本會遮掩外部全局中的new版本,派生類的new版本會遮掩全局和基類的new版本。有一個可行的辦法是:建立一個基類,內含所有的正常形式的new和delete版本,凡是想用自定義形式括充標準形式的客戶,可利用繼承機制和using聲明式取得標準形式

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