1. 程式人生 > >effective C++筆記--定製new和delete(二)

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宣告式取得標準形式