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

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

文章目錄

. C++允許手動的管理記憶體,這是雙刃劍,你可以使程式更有效率,也可能面臨維護程式帶來的麻煩,所以瞭解C++記憶體管理的例程很是重要,其中的兩個主角是分配例程和歸還例程(也就是operator new 和 operator delete),配角是new-handler,這是當operator new無法滿足客戶的記憶體需求時呼叫的函式。
  額外有一點要注意的是:STL容器所使用的heap記憶體是由容器所擁有的分配器物件管理,不是被new和delete直接管理。這裡就不討論STL分配器了,這部分內容在C++ primer中有提到:

傳送門

瞭解new-handler的行為

. 當operator new無法滿足某一記憶體分配需求時,它會丟擲異常。以前是返回一個null指標,但這條條款最後在討論這個。
  當operator new丟擲異常以反映一個未獲滿足的記憶體需求之前,它會先呼叫一個客戶指定的錯誤處理函式,一個所謂的new-handler(其實operator new真正做到事稍微更復雜一點),為了指定這個new-handler函式,客戶必須呼叫set-new-handler,那是聲明於<new>的標準程式庫函式:

namespace std{
	typedef void (*new_handler)();				
	new_handler set_new_handler(new_handler p) throw();
};

. 如上所述,new_handler是一個typedef(ps:其實我不太懂為啥要在typedef裡用指標啊、括號啊什麼的,感覺不好閱讀),定義出一個指標指向一個函式,該函式沒有引數也不返回任何東西。set_new_handler宣告式尾部的throw()表示該函式不丟擲任何異常,其引數是一個指標,指向當operator new無法分配足夠記憶體時需要呼叫的函式,返回值也是一個指標,指向之前的new_handler函式。可以如下使用:

void outOfMem(){
	std::cerr<<"out of memory\n";
	std::abort();
}
int main(){
	std::set_new_handler(outOfMem);
	int* pBigDataArray = new int[100000000000000000];
	...
}

. 如果operator new 無法分配那麼多的記憶體,就會呼叫設定好的outOfMem,於是程式在發出一個資訊後夭折。
  當operator new無法滿足記憶體申請時,它會不斷呼叫new-handler函式,知道找到足夠的記憶體。一個設計良好的new-handler函式必須做以下事情:
  1.讓更多記憶體可被使用。這便造成operator new的下一次記憶體分配動作可能完成。實現這一策略的一種方法是,程式一開始執行就分配一大塊記憶體,而後當new-handler第一次被呼叫,將它們釋還給程式使用。
  2.安裝另一個new-handler。如果目前這個new-handler無法獲得更多的可用記憶體,或許它知道另外那個new-handler有這個能力。果真如此,目前這個new-handler就可以安裝另外的new-handler以替換自己。
  3.刪除new-handler。也就是傳遞null給set-new-handler。在沒有安裝new-handler的情況下,operator new會在記憶體分配不足時丟擲異常。
  4.丟擲bad_alloc(或派生自bad_alloc)的異常。這樣的異常不會被operator new捕捉,因此會被傳播到記憶體索求處。
  5.不返回。通常呼叫abort或exit。
  這樣的彈性為你在實現new-handler函式時擁有更多選擇。
  有時或許希望以不同的方式處理記憶體分配失敗的情況,比如每個類中有不同的outOfMem函式,在屬於該類的分配物出現分配不成功時呼叫該類的outOfMem,只需令每一個class提供自己的set_new_handler和operator new即可。例如:

class Widget{
public:
	static std::new_handler set_new_handler(std::new_handler p) throw();
	static void* operator new(std::size_t size) throw(std::bad_alloc);
private:
	static std::new_handler currentHandler;
};
//static成員需要在類外定義(除非它們是const且是整數型)
std::new_handler Widget::currentHandler = 0;				//初始化為null

std::new_handler Widget::set_new_handler(std::new_handler p) throw(){
	std::new_handler oldHandler = currentHandler;
	currentHandler = p;
	return oldHandler;
}

. 最後operator new需要做的事情就是:
  1.呼叫set_new_handler,安裝widget的new-handler;
  2.呼叫operator new,執行實際的記憶體分配,如果分配失敗,呼叫widget的new-handler,如果最終還是不能分配成功,會丟擲一個bad_alloc異常,在此情況下Widget的operator new必須回覆原本的new-handler函式,然後在傳播這異常。為確保原本的new-handler能被重新安裝回去,可以運用資源管理物件防止資源洩露
  3.如果operator new能夠分配足夠一個Widget物件所使用的記憶體,Widget的operator new會返回一個指標,指向分配所得。Widget的解構函式會管理new-handler,自動將安裝之前的那個new-handler恢復回來。
  從程式碼在闡述一次:

class NewHandlerHolder{
public:
	explicit NewHandlerHolder(std::new_handler nh)
		:handler(nh) {}
	~NewHandlerHolder(){
		std::set_new_handler(handler);
	}
private:
	std::new_handler handler;
	NewHandlerHolder(const NewHandlerHolder&);		//阻止copying
	NewHandlerHolder& operator=(const NewHandlerHolder&);
};

//這使得operator new的實現變得簡單
void* Widget::operator new(std::size_t size) throw(std::bad_alloc){
	NewHandlerHolder h(std::set_new_handler(currentHandler));		
	return ::operator new(size);							//分配記憶體或丟擲異常
}									//函式結束的時候恢復原來的new-handler

. 可以使用模板來少寫一些程式碼:

template <typename T>
class NewHandlerSupport{
public:
	static std::new_handler set_new_handler(std::new_handler p) throw();
	static void* operator new(std::size_t size) throw(std::bad_alloc);
	...
private:
	static std::new_handler currentHandler;
};
template <typename T>
std::new_handler 
NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw(){
	std::new_handler oldHandler = currentHandler;
	currentHandler = p;
	return currentHandler;
}
template <typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) 
throw(std::bad_alloc){
	NewHandlerHolder h(std::set_new_handler(currentHandler));		
	return ::operator new(size);							
}	

//初始化每個currenHandler
template <typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;

. 有了這個模板類,為Widget新增set_new_handler支援能力就輕而易舉了:只要令Widget繼承自NewHandlerSupport<Widget>就好。例如:

class Widget : public NewHandlerSupport<Widget>{
	...
}

. 新一代的operator new在無法分配足夠記憶體的時候會丟擲bad_alloc異常,但很多C++程式是在編譯器開始支援新規範前寫出來的。C++並不像拋棄那些偵測null的族群,於是提供另一個形式的operator new,這個形式被稱為“nothrow”形式:

Widget* p = new(std::nothrow) Widget;
if(p == 0){
	...
}

. 不過nothrow new對異常的強制保證性不高,因為如果這樣使用的時候,記憶體分配失敗的話會返回null,但是如果分配成功的話,Widget的建構函式將被呼叫,在建構函式中可能有出現分配記憶體的行為,這裡出現錯誤的時候將不會再強制使用nothrow new,這就可能丟擲異常了。

瞭解new和delete的合理替換時機

. 對於想要替換編譯器提供的operator new和operator delete的行為,一般有下面三種原因:
  1.用來檢測運用上的錯誤。如果將new分配的記憶體delete失敗的話將導致記憶體洩露,自己編寫的時候可以進行檢測是否釋放成功或者記錄下日誌;
  2.為了強化效能。編譯器所帶的operator new和operator delete主要用於一般的目的。它們的工作對每個人都是適度的好,但不對特定任何人有最佳表現。通常定製版的operator new和operator delete效能更高;
  3.為了收集使用上的統計資料。在定製new和delete之前,應該先收集你的軟體如何使用動態記憶體。比如分配區塊的大小分佈如何?壽命分佈如何?傾向於使用FIFO(先進先出)次序還是LIFO(後進先出)次序或是隨機次序?等等資訊,自定義operator new和operator delete使我們得以輕鬆收集這些資訊。
  一個簡單的定製型operator new,除了分配記憶體外還協助檢測“overruns”(寫入點在分配區塊尾端之後)或“underruns”(寫入點在分配區塊起點之前),其中還有一些小錯誤:

static const int signature = 0xDEADBEEF;
typedef unsigned char Byte;
void* operator new(std::size_t size) throw(std::bad_alloc){
	using namespace std;
	size_t realSize = size + 2 * sizeof(int);	//增加大小,
											//使得能夠塞入兩個signature
	void* pMem = malloc(realSize);			//呼叫malloc分配記憶體
	if(!pMem) throw bad_alloc();

	//將signature寫入最前和最後
	*(static_cast<int*>(pMem)) = signature;
	*(reinterpret_cast<int>(static_cast<int*>(pMem) 
		+ realSize - sizeof(int))) = signature;
	
	//返回指標,指向位於第一個signature之後的記憶體位置
	return static_cast<Byte*>(pMem) + sizeof(int);
}

. 這個operator new的缺點主要在於它疏忽了身為這個特殊函式所應該具備的“堅持C++規矩”的態度。比如所有的operator new都應該含有一個迴圈,不停的呼叫某個new-handler函式,這裡卻沒有。但這裡主要討論另一個主題:齊位。
  許多計算機體系結構要求特定的型別必須放在特定的記憶體地址上,例如它可能要求指標的地址是4的倍數或double的地址是8的倍數。在目前的這個主題中,齊位意義重大,因為C++要求所有的operator new返回的指標都有適當的對齊(取決於資料型別)。如果返回一個得自malloc的指標是安全的,但是上述返回的是一個得自malloc且偏移一個int大小的指標,沒人能保證它的安全。
  現在在總結一下使用定製的版本去替換預設的版本的目的:
  1.為了檢測運用錯誤。如前所述;
  2.為了收集動態分配記憶體的統計資訊。如前所述;
  3.為了增加分配和歸還的速度。泛用型分配器往往位元定型分配器慢,特別是當定製型分配器專門針對某特定型別的物件設計時。
  4.為了降低預設記憶體管理器帶來的空間額外開銷。泛用型分配器往往還佔用更多的記憶體,因為它們往往在每個分配區塊身上招引某些額外開銷。
  5.為了彌補預設分配器的非最佳齊位。在X86體系結構上doubles的訪問時最快的——如果它們都是8位齊位。但是編譯器自帶的operator new並不保證對動態分配而得的doubles採取8位齊位。這種情況下,將預設的operator new替換為一個8位齊位的保證版本,可導致效率大幅提升。
  6.為了將相關物件成簇幾種。如果知道某個資料結構常常被一起使用,而你又希望在處理這些資料的時候將“記憶體頁錯誤”的頻率降到最低,那麼為這個資料結構建立一個heap就有意義,這麼一來它們就可以被集中在儘可能少的記憶體頁上。
  7.為了獲得非傳統的行為。可以做點別的編譯器自帶的operator new版本沒有做到的事。