章節回顧:
《Effective C++》第1章 讓自己習慣C++-讀書筆記
《Effective C++》第2章 構造/析構/賦值運算(1)-讀書筆記
《Effective C++》第2章 構造/析構/賦值運算(2)-讀書筆記
《Effective C++》第3章 資源管理(1)-讀書筆記
《Effective C++》第3章 資源管理(2)-讀書筆記
《Effective C++》第4章 設計與宣告(1)-讀書筆記
《Effective C++》第4章 設計與宣告(2)-讀書筆記
《Effective C++》第8章 定製new和delete-讀書筆記
條款49:瞭解new-handler的行為
當operator new無法滿足某一記憶體分配需求時,它會丟擲異常,當其丟擲異常以反應一個未獲滿足的記憶體需求之前,會先呼叫一個客戶指定的錯誤處理函式,一個所謂的new-handler。為了指定這個“用以處理記憶體不足”的函式,客戶必須呼叫set_new_handler。
namespace std { typedef void (*new_handler)(); new_handler set_new_handler(new_handler p) throw(); }
說明:
(1)set_new_handler是宣告於<new>的一個標準庫函式。
(2)throw()是一份異常明細,表示該函式不丟擲任何異常。
(3)形參p指向operator new無法分配足夠記憶體時該被呼叫的函式,返回指標指向set_new_handler被呼叫前正在執行(但馬上就要被替換)的那個new-handler函式。
(4)set_new_handler的例子:
void OutOfMem() { cerr << "Unable to satisfy request for memory" << endl; abort(); } int main() { set_new_handler(OutOfMem); int *ptr = new int[100000000L]; return 0; }
(5)當operator new無法滿足記憶體申請時,它會不斷呼叫new-handler函式,直到找到足夠記憶體。
1、一個設計良好的new-handler函式必須做以下事情:
(1)讓更多記憶體被使用。
這可能使得operator new中下一次記憶體分配的嘗試成功。實現這一策略的一個方法是在程式啟動時分配一大塊記憶體,然後在new-handler第一次被呼叫時釋放它供程式使用。
(2)安裝另一個new-handler。
如果當前的new-handler不能做到使更多的記憶體可用,或許它知道有一個不同的 new-handler 可以做到。
(3)解除安裝new-handler。
即將空指標傳給set_new_handler,當記憶體分配不成功時,operator new將丟擲一個異常。
(4)丟擲bad_alloc(或派生自bad_alloc)的異常。
這樣的異常不會被operator new捕獲,因此會被傳播到記憶體請求的地方。
(5)不返回。
通常呼叫abort或exit。
2、以不同方式處理分配記憶體失敗
只要讓每一個class提供set_new_handler和operator new的自己的版本即可。operator new無法分配足夠記憶體時應丟擲bad_alloc異常,但也可能返回null(傳統形式仍然保留),這種傳統形式稱為“nothrow”。
class Widget { ... }; Widget *pw1 = new Widget; // throws bad_alloc if allocation fails if (pw1 == 0) ... // this test must fail Widget *pw2 =new (std::nothrow) Widget; // returns 0 if allocation for the Widget fails if (pw2 == 0) ... // this test may succeed
請記住:
(1)set_new_handler允許客戶指定一個函式,在記憶體分配無法獲得滿足時被呼叫。
(2)nothrow new是一個侷限的工具,因為它只適用於記憶體分配,後繼的建構函式呼叫還是可能丟擲異常。
———————————————————————————————————————————————————————
條款50:瞭解new和delete的合理替換時機
為什麼有些人想要替換編譯器提供的operator new或operator delete 版本呢?原因有:
(1)為了檢測運用錯誤;(2)為了收集動態分配記憶體之使用統計資訊;(3)為了增加分配和歸還的速度;(4)為了降低預設記憶體管理器帶來的空間額外開銷;(5)為了彌補預設分配器中的非最佳對齊;(6)為了將相關物件成簇集中;(7)為了獲得非傳統的行為。
———————————————————————————————————————————————————————
條款51:編寫new和delete時需固守常規
1、operator new
實現一致性operator new需要考慮:
(1)必須返回正確的值,記憶體不足時呼叫new-handling函式。
(2)必須有對付0記憶體需求的準備。
(3)避免不慎掩蓋正常形式的new。
下面分別說明:
(1)如果operator new有能力供應客戶申請的記憶體,就返回一個指標指向那塊記憶體,如果沒有就丟擲一個bad_alloc異常。
注意:只有當指向new-handling函式的指標是null,operator new才會丟擲異常。
(2)即使客戶要求0bytes,operator new也要返回一個合法指標,這種行為是為了簡化語言其他部分。下面是個operator new偽碼:
void * operator new(std::size_t size) throw(std::bad_alloc) { // your operator new might using namespace std; // take additional params if (size == 0) { // handle 0-byte requests size = 1; // by treating them as 1-byte requests } while (true) { attempt to allocate size bytes; if (the allocation was successful) return (a pointer to the memory); // allocation was unsuccessful; find out what the // current new-handling function is (see below) new_handler globalHandler = set_new_handler(0); set_new_handler(globalHandler); if (globalHandler) (*globalHandler)(); else throw std::bad_alloc(); } }
說明:
1)把0bytes申請量視為1byte申請量,畢竟客戶多久才會發出一個0bytes申請。
2)將new-handling函式指標設為null而後又立即恢復原樣,是因為沒有辦法可以直接取得new-handling函式指標,所以必須呼叫set_new_handler找出它來。這種做法在單執行緒環境下有效,多執行緒環境下或許需要某種鎖以便安全處置new-handling函式背後的資料結構。
下面考慮operator new成員函式被繼承會發生什麼情況:
如果Base class專屬的operator new並非被設計用來處理上述情況。最佳做法是採用標準operator new:
void * Base::operator new(std::size_t size) throw(std::bad_alloc) { if (size != sizeof(Base)) // if size is "wrong," return ::operator new(size) ; // have standard operator // new handle the request ... // otherwise handle // the request here }
2、operator delete
C++保證“刪除null指標永遠安全”,所以你必須兌現這項保證。
class Base { // same as before, but now public: // operator delete is declared 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; // check for null pointer if (size != sizeof(Base)) { // if size is "wrong," ::operator delete(rawMemory); // have standard operator return; // delete handle the request } deallocate the memory pointed to by rawMemory; return; }
請記住:
(1)operator new應該內含一個無窮迴圈,並在其中嘗試分配記憶體,如果它無法滿足記憶體需求,就該呼叫new-handler。它也應該有能力處理0bytes申請。class專屬版本還應該處理“比正確大小更大的(錯誤)申請”。
(2)operator delete應該在收到null指標時不做任何事。class專屬版本還應該處理“比正確大小更大的(錯誤)申請”。
———————————————————————————————————————————————————————
條款52:寫了placement new也要寫placement delete
當你寫一個new表示式:
Widget *pw = new Widget;
共有兩個函式被呼叫:第一個是operator new用於分配記憶體,第二個是Widget的default 建構函式。
假設第一個呼叫成功,第二個呼叫導致丟擲一個異常。那麼在第1步中完成的記憶體分配必須被撤銷,否則記憶體洩漏。但客戶不可能回收這些記憶體,因為如果Widget的建構函式丟擲一個異常,pw根本就沒有被賦值。對於客戶來說無法得到指向應該被回收的記憶體指標。所以撤銷第1步的職責必然落在了C++ 執行時系統身上。C++ 執行時系統會呼叫第1步operator new相應的operator delete,但只有在它知道哪一個operator delete(可能有許多個)該被呼叫。
(1)對於正常的operator new,執行時系統可以找到對應的operator delete。常規的operator new
void* operator new(std::size_t) throw(std::bad_alloc);
對應常規的operator delete:
void operator delete(void *rawMemory) throw(); // normal signature at global scope void operator delete(void *rawMemory, std::size_t size) throw(); // typical normal signature at class scope
(2)當宣告operator new的非常規形式(帶有額外引數)的時候,問題就出現了。假設編寫了一個類專用的operator new,但編寫了一個常規的operator delete:
class Widget { public: ... static void* operator new(std::size_t size, // non-normal std::ostream& logStream) // form of new throw(std::bad_alloc); static void operator delete(void *pMemory // normal class- std::size_t size) throw(); // specific form // of delete ... };
說明:如果operator new接受的引數除了size_t之外還有其他,稱為placement new。比較有用的一個placement new版本是“接受一個指標指向物件該被構造之處”。
void* operator new(std::size_t, void *pMemory) throw();
考慮以下程式碼:
Widget *pw = new (std::cerr) Widget; // call operator new, passing cerr as // the ostream; this leaks memory // if the Widget constructor throws
如果記憶體分配成功,建構函式丟擲異常。執行時系統尋找“引數個數和型別都與operator new相同”的某個operator delete,如果找到則呼叫。顯然,這裡沒有找到,所以執行時系統無法取消分配的記憶體。對應於placement new的placement delete版本類似於:
void operator delete(void *, std::ostream&) throw();
然而如果建構函式沒有丟擲異常,客戶程式碼為delete pw,呼叫的是正常形式的operator delete,而非placement版本。
注意:只有在呼叫一個與placement new相關聯的建構函式丟擲異常,placement delete才會被呼叫。所以如果要處理所有與placement new相關的記憶體洩露,必須同時提供正常的operator delete和placement版本。
另外需要注意的是,成員函式名稱會掩蓋其外圍作用域中的相同名稱,所以要避免讓class專屬的news遮蓋客戶期望的其他news(包括正常版本)。
class Base { public: ... static void* operator new(std::size_t size, // this new hides the normal global forms std::ostream& logStream) throw(std::bad_alloc); ... }; Base *pb = new Base; // error! the normal form of // operator new is hidden Base *pb = new (std::cerr) Base; // fine, calls Base's // placement new
同樣道理,derived classes中的operator news會遮蓋globle版本和繼承而得到的operator new。
class Derived: public Base { // inherits from Base above public: ... static void* operator new(std::size_t size) // redeclares the normal throw(std::bad_alloc); // form of new ... }; Derived *pd = new (std::clog) Derived; // error! Base's placement // new is hidden Derived *pd = new Derived; // fine, calls Derived's // operator new
對於撰寫記憶體分配函式,需要注意的是,預設情況下C++在globle作用域內提供的operator news形式:
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, // nothrow new — see Item 49 const std::nothrow_t&) throw();
如果你在class內宣告任何operator news都會遮掩上述標準形式,除非你就是故意阻止客戶使用這些形式。如果你希望這些函式可用,只要令你的class專屬版本呼叫globle版本即可。
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); } }; class Widget: public StandardNewDeleteForms { // inherit std forms public: using StandardNewDeleteForms::operator new; // make those using StandardNewDeleteForms::operator delete; // forms visible static void* operator new(std::size_t size, // add a custom std::ostream& logStream) // placement new throw(std::bad_alloc); static void operator delete(void *pMemory, // add the corresponding placement delete std::ostream& logStream) throw(); ... };
請記住:
(1)當你寫operator new的placement版本時,確保同時編寫operator delete相應的placement版本。否則,你的程式可能會發生微妙的,斷續的記憶體洩漏。
(2)當你宣告new和delete的placement版本時,確保不會無意中覆蓋這些函式的常規版本。
———————————————————————————————————————————————————————
我感覺寫的有點流水賬。但其實每次拜讀這本書感受都有點不同,比如有的問題,自己知道這樣做肯定行,但是就是想不明白為什麼這麼麻煩著做。可能是沒有經驗吧。這篇總結,先記錄到這裡,我還要回顧修改的。看起來有點亂糟糟的。