【effective c++读书笔记】【第8章】定制new和delete(2)
條款50:了解new和delete的合理替換時機
有人會想要替換掉編譯器提供的operator new或operator delete,以下是幾個理由:
a、用來檢測運用上的錯誤。編程過程中會出現各種錯誤。這些錯誤導致內存泄露(memory leaks)、不確定行為產生、overruns(寫入點在分配區塊尾端之后)、underruns(寫入點在分配區塊尾端之前)等的發生。定制的operator new和operator delete可以避免這樣的問題。
b、為了強化效能。編譯器帶的operator new和operator delete必須處理開辟大塊內存、小塊內存、大小混合型內存,必須考慮內存碎片問題,可能會導致無法滿足大區快內存需求,即使有足夠但分散的小區塊自由內存。定制的operator new和operator delete可以避免這樣的問題,有時還可以提升性能。
c、為了收集使用上的統計數據。自定義的operator new和operator delete使我們得以輕松收集你的軟件如何使用其動態內存,分配區塊的大小,壽命分布,內存以FIFO次序或LIFO次序或隨機次序來分配和歸還等信息。
d、為了增加分配和歸還的速度。使用定制分配器,有加快程序速度的價值。
e、為了降低缺省內存管理器帶來的空間額外開銷。泛用型分配器往往(雖然并非總是)不只比定制型慢,還使用更多空間,因為它們常常在每一個分配區塊上招引某些額外開銷。針對小型對象開放的分配器,例如Boost庫的Pool,本質上消除了這樣的額外開銷。
f、為了彌補缺省分配器中的非最佳齊位(suboptimal alignment)。X86體系結構上的double訪問最快——如果它們是8-byte對齊。但是編譯器自帶的operator new并不保證分配double是8-byte對齊。這種情況下將內存分配方案替換為8-byte齊位保證版本,可導致程序效率大大提升。
g、為了將相關對象成簇集中。如果特定的某個數據結構往往被一起使用,我們希望在處理這些數據時將“內存頁錯誤”(page faults)的頻率降至最低,那么為此數據結構創建另一個heap就有意義,這樣就可以將它們成簇集中到盡可能少的內存也上。
h、為了獲得非傳統的行為。有時候我們希望operator new和delete做編譯器附帶版沒做沒做的某些事。例如,在歸還內存時將其數據覆蓋為0,以此增加應用程序的數據安全。
請記住:
- 有許多理由需要寫個自定義的new和delete,包括改善效能、對heap運用錯誤進行調試、收集heap使用信息。
條款51:編寫new和delete時需固守常規
1、實現一致性operator new必須返回正確的值,內存不足時必得調用new-handling函數,必須有對付零內存需求的準備,還需避免不慎掩蓋正常形式的new。
下面是個non-memberoperator new偽碼:??void* operator new(std::size_t size) throw(std::bad_alloc){using namespace std;if (size == 0) { //處理0byte申請size = 1; //將它視為1byte申請}while (true) {嘗試分配size byteif (分配成功)return (一個指針,指向分配得來的內存)// 分配失敗:找出目前的new-handling函數new_handler globalHandler = set_new_handler(0);set_new_handler(globalHandler);if (globalHandler) (*globalHandler)();else throw std::bad_alloc();} }
上述偽碼中將new-handling函數指針設為null后又立刻恢復原樣,因為沒有任何辦法直接取得new_handling函數指針,所以必須調用set_new_handler找它出來。
operator new有個無窮循環,退出此循環的唯一辦法是:內存被成功分配或new-handling函數做了一件描述于條款49的事情:讓更多內存可用、安裝另一個new_handler、卸除new-handler、拋出bad_alloc異常,或是承認失敗而直接return。
2、operator new成員函數會被derived classes繼承。針對class X而設計的operator new,其行為很典型地只為大小為sizeof(X)的對象而設計。一旦被繼承下去,有可能base class的operator new被調用用以分配derived class對象:
class Base { public:static void* operator new(std::size_t size) throw(std::bad_alloc);... }; class Derived : public Base {//假設Derived未聲明operator new... }; Derived* p = new Derived;//這里調用的是Base::operator new如果base class專屬的operatornew并非被設計用來對付上述情況,處理此情勢最佳做法是將“內存申請量錯誤”的調用行為改采用標準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專屬“array內存分配行為”,那么你要實現operator new的array兄弟版:operator new[]。如果決定寫個operator new[],唯一要做的一件事就是分配一塊未加工內存,因為你無法對array之內迄今尚未存在的元素對象做任何事情。實際上,你甚至無法計算那個array將含多少個元素對象。你不知道每個對象多大,畢竟base class的operator new[]有可能經由繼承被調用,將內存分配給“元素為derived class對象”的array使用。
3、operator delete的情況更簡單,需要記住的唯一事情就是C++保證“刪除null指針永遠安全”,所以你必須兌現這項保證。
下面是non-member operator delete的偽碼:
void operator delete(void *rawMemory) throw() {if (rawMemory == 0) return;現在,歸還rawMemory所指內存 }這個函數的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* rawMemory, std::size_t size) throw(); };void Base::operator delete(void* rawMemory, std::size_t size) throw() {if (rawMemory == 0) return 0; //檢查null指針if (size != sizeof(Base)) { //如果大小錯誤,令標準版operator delete處理此一申請::operator delete(rawMemory);return;}現在,歸還rawMemory所指的內存;return; }如果即將被刪除的對象派生自某個base class而后者欠缺virtual析構函數,那么c++傳給operatordelete的size_t數值可能不正確,operatordelete可能無法正確運作。
請記住:
- operator new應該內含一個無窮循環,并在其中嘗試分配內存,如果它無法滿足內存需求,就該調用new-handler。它也應該有能力處理0bytes申請。class專屬版本則還應該處理“比正確大小更大的(錯誤)申請”。
- operator delete應該在收到null指針時不做任何事情。class專屬版本則還應該處理“比正確大小更大的(錯誤)申請”。
條款 52:寫了placement new也要寫placement delete
1、當寫了下面這樣一個new表達式:
Widget* pw = new Widget;共有兩個函數被調用:一個是用以分配內存的operator new,一個是Widget的default構造函數。
假設第一個調用成功,第二個卻拋出異常。步驟一所分配內存必須取消并恢復舊觀,否則會造成內存泄漏。這時,客戶沒能力歸還內存,因為Widget構造函數拋出異常,pw尚未被賦值,客戶手上也就沒有指針指向該被歸還的內存。這個時候,取消步驟一,并恢復舊觀的責任就落到C++運行系統身上。
運行期系統會高興的調用步驟一所調用operator new的相應的operator delete版本,前提是它必須知道哪一個operator delete被調用(可能有許多個)。
如果目前面對的是擁有正常簽名式的new和delete,并不是問題,因為正常的operator 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作用域中的典型簽名式。2、如果operator new接受的參數除了一定會有的那個size_t之外還有其他,這個便是所謂的placement new。眾多placement new中特別有一個是“接受一個指針指向對象該被構造之處”,那樣的operator new長相如下:
void* operator new(std::size_t, void* pMemory) throw();這個版本的new已被納入C++標準程序庫,只要#include<new>就可以取用它。這個new的用途之一是負責在vector的未使用空間上創建對象。
3、類似于new的placement版本,operator delete如果接受額外參數,便稱為placement deletes。如果一個帶額外參數的operator new沒有“帶相同額外參數”的對應版operator delete,那么當new的內存分配動作需要取消并恢復時就沒有任何operator delete會被調用。placement delete只有在“伴隨placement new調用而觸發的構造函數”出現異常時才會被調用。對著一個指針施行delete時絕不會調用placement delete。這意味對所有placement new我們必須同時提供一個正常的operator delete(用于構造期間無任何異常被拋出)和一個placement版本(用于構造期間有異常被拋出)。
例子:
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有機會確保不泄漏任何內存。
4、由于成員函數的名稱會掩蓋其外圍作用域中的相同名稱,derived classes中的operator news會掩蓋global版本和繼承而得的operator new版本,必須小心避免讓class專屬的news掩蓋客戶期望的其他news(包括正常版本)。
5、缺省情況下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請記住:
- 當你寫一個placement operator new,請確定也寫出了對應的placement operator delete。如果沒有這樣做,你的程序可能會發生隱微而時斷時續的內存泄漏。
- 當你聲明placement new和placement delete,請確定不要無意識地遮掩它們的正常版本。
版權聲明:本文為博主原創文章,未經博主允許不得轉載。
轉載于:https://www.cnblogs.com/ruan875417/p/4785431.html
總結
以上是生活随笔為你收集整理的【effective c++读书笔记】【第8章】定制new和delete(2)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: hdu 2602 Bone Collec
- 下一篇: MySql 事务+异常处理+异常抛出