# Item 50: 領會何時替換 new 和 delete 才有意義
作者:[Scott Meyers](http://aristeia.com/)
譯者:[fatalerror99 (iTePub's Nirvana)](mailto:fatalerror9999@hotmail.com?subject=Item%2049)
發布:[http://blog.csdn.net/fatalerror99/](http://blog.csdn.net/fatalerror99/)
讓我們先回顧一下基礎。為什么有些人想要替換編譯器提供的 operator new 或 operator delete 版本呢?有三個最主要的原因:
* **為了監測使用錯誤。**對由 new 產生的內存沒有實行 delete 會導致內存泄漏。在 new 出的內存上實行多于一次的 delete 會引發未定義行為。如果 operator new 保存一個已分配地址的列表,而 operator delete 從這個列表中移除地址,這樣就很容易監測到上述使用錯誤。同樣,某種編程錯誤會導致 data overruns(數據上溢)(在一個已分配塊的末端之后寫入)和 underruns(下溢)(在一個已分配塊的始端之前寫入)。在對于客戶可用的內存的之前和之后,自定義 operator news 可以跨越分配塊,在這些空間放置已知的字節模式 ("signatures")。operator deletes 會去檢查這些 signatures 是否依舊保持原樣。如果不是,在這個分配塊的生存期間的某個時刻發生了一個上溢或者下溢,而且 operator deletes 可以記錄這件事以及那個討厭的指針的值。
* **為了提升性能。**由編譯器加載的 operator new 和 operator delete 版本是為了多種用途而設計的。它們必須被長時間運行的程序(例如,web servers),接受,但是,它們也必須被運行時間少于一秒的程序接受。它們必須處理大內存塊,小內存塊,以及兩者混合的請求序列。它們必須適應廣泛的分配模式,從存在于整個程序的持續期間的少數幾個區塊的動態分配到大量短壽命 objects 的持續不斷的分配和釋放。它們必須為堆碎片化負責,對這個過程,如果不進行控制,最終會導致不能滿足對大內存塊的請求,即使有足夠的自由內存分布在大量小塊中。
> 由于內存管理器的特定需求,由編譯器加載的 operator news 和 operator deletes 采取了 middle-of-the-road strategy(中間路線策略)不值得大驚小怪。它們的工作對每一個人來說都說得過去,但是對誰都不是最合適的。如果你對你的程序的動態內存的應用模式有充分的理解,你可能經常發現 operator new 和 operator delete 的自定義版本勝于缺省版本。對于“勝于”,我的意思是它們運行更快——有時會有數量級的提升——而且它們需要更少的內存——最高會少于 50%。對于某些(盡管不意味著全部)應用程序,用自定義版本取代普通的 new 和 delete 是獲得重大性能提升的一個簡單方法。
* **為了收集使用方法的統計數據。**在一頭扎入編寫自定義 news 和 deletes 的道路之前,收集一下你的軟件如何使用動態內存的信息還是比較明智的。被分配區塊大小的分布如何?生存期的分布如何?它們的分配和釋放的順序是趨向于 FIFO ("first in, first out")(“先進先出”),或者 LIFO ("last in, first out")(“后進先出”)的順序,還是某種接近于隨機的順序?使用模式會隨著時間而變化嗎?例如,你的軟件是不是在不同的運行階段有不同的分配/釋放模式?在任一時間內使用中的動態分配內存的最大值(也就是說,它的“最高水位”)是多少?operator new 和 operator delete 的自定義版本使得收集這類信息變得容易。
在概念上,編寫一個自定義 operator new 相當簡單。例如,這是一個便于 under- 和 overruns 的檢測的 global operator new 的主要部分。這里有很多小麻煩,但是我們馬上就來關注一下它們。
```
static const int signature = 0xDEADBEEF;
typedef unsigned char Byte;
// this code has several flaws—see below
void* operator new(std::size_t size) throw(std::bad_alloc)
{
? using namespace std;
? size_t realSize = size + 2 * sizeof(int);??? // increase size of request so2
?????????????????????????????????????????????? // signatures will also fit inside
? void *pMem = malloc(realSize);?????????????? // call malloc to get theactual
? if (!pMem) throw bad_alloc();??????????????? // memory
? // write signature into first and last parts of the memory
? *(static_cast<int*>(pMem)) = signature;
? *(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof(int))) =
? signature;
? // return a pointer to the memory just past the first signature
? return static_cast<Byte*>(pMem) + sizeof(int);
}
```
這個 operator new 的大多數缺陷都與它沒有遵循叫這個名字的函數的 C++ 慣例有關。例如,[Item 51](http://blog.csdn.net/fatalerror99/archive/2006/12/28/1466315.aspx) 闡明:所有的 operator new 都應該包含一個調用 new-handling function 的循環,但是這里沒有。但是,[Item 51](http://blog.csdn.net/fatalerror99/archive/2006/12/28/1466315.aspx) 正是專用于這樣的慣例,所以我在這里忽略它們。我現在要關注一個更微妙的問題:_alignment_(排列對齊)。
很多計算機架構要求特定類型的數據要放置在內存中具有特定性質的地址中。例如,一種架構可能要求 pointers(指針)要出現在四的倍數的地址上(也就是說,按照四字節對齊)或者 doubles(雙精度浮點型)必須出現在八的倍數的地址上(也就是說,按照八字節對齊)。不遵守這樣的約束會導致 hardware exceptions at runtime(運行時硬件異常)。其它的架構可能會寬容一些,但是如果滿足了排列對齊的次序會得到更好的性能。例如,在 Intel x86 架構上 doubles(雙精度浮點型)可以按照任意字節分界排列,但是如果他們按照八字節對齊,訪問速度會快得多。
alignment(排列對齊)在這里有重大意義,因為 C++ 要求所有的 operator news 返回適合任何數據類型的排列的指針。malloc 也工作于同樣的要求下,所以,讓 operator new 返回它從 malloc 得到的指針是安全的。然而,在上面的 operator news 中,我們沒有返回我們從 malloc 得到的指針,我們返回的指針比我們從 malloc 得到的指針偏移了一個 int 大小。無法保證這是安全的!如果客戶調用 operator new 為一個 double(或者,如果我們正在編寫 operator new[],一個 doubles 的數組)申請足夠的內存,而且我們正在運行一臺 ints 是四個字節大小而 doubles 需要八字節對齊的機器,我們就可能返回對齊不恰當的指針。這可以導致程序崩潰。或者,它只是導致運行速度變慢。無論哪種情況,這或許都不是我們想要的。
像 alignment(排列對齊)這樣的細節可以用于區分專業品質的內存管理器和那些由需要解決其它任務而心煩意亂的程序員匆匆拼湊出來的東西。編寫一個幾乎能工作的自定義內存管理器相當容易。編寫一個工作得很好的要困難得多。作為一個一般規則,我建議你不要致力于此,除非你不得不做。
很多情況下,你并非不得不做。有些編譯器提供選項開關用為它們的 memory management functions(內存管理函數)打開調試和記錄的功能。快速瀏覽一下你的編譯器的文檔也許可以打消你編寫 new 和 delete 的念頭。在很多平臺上,商用產品可以替代隨編譯器提供的 memory management functions(內存管理函數)。為了利用它們的增強的功能以及(或許會有的)更好的性能,你需要做的全部就是重新鏈接。(當然,你還必須把它們買回來。)
另一個選擇是開源的內存管理器。它們可用于多種平臺,所以你可以下載并試用。出自于 Boost(參見 Item 55)的 Pool library 就是一個這樣的開源分配器。Pool library 提供了針對自定義內存管理能提供幫助的最通常的情況之一(大數量 small objects(小對象)的分配)進行了調諧的分配器。很多 C++ 書籍,包括本書的早期版本,展示了一個 high-performance small-object allocator(高性能小對象分配器)的代碼,但是它們通常忽略了可移植性和排列對齊的考慮以及線程安全等等諸如此類的麻煩的細節。真正的庫會注意用健壯得多的代碼。即使你決定編寫你自己的 news 和 deletes,看一下開源版本很可能會為你提供對“區分幾乎起作用和真正起作用”的容易忽略的細節的洞察力。(已知 alignment(排列對齊)就是一個這樣的細節,值得一提的是,TR1(參見 Item 54)包含了對已發現的類型特定的排列對齊要求的支持。)
這個 Item 的主題是了解何時替換 new 和 delete 的缺省版本(無論是基于全局的還是 per-class 的)才有意義。我們現在應該比前面更詳細地總結一下時機問題。
* **為了監測使用錯誤**(如前)。
* **為了收集有關動態分配內存的使用的統計數據**(如前)。
* **為了提升分配和回收的速度。**general-purpose allocators(通用目的的分配器)通常(雖然不總是)比自定義版本慢很多,特別是如果自定義版本是為某種特定類型的 objects 專門設計的。class-specific allocators(類專用分配器)是 fixed-size allocators(固定大小分配器)(就像 Boost 的 Pool library 所提供的那些)的一種典范應用。如果你的程序是 single-threaded(單線程)的,而你的編譯器缺省的內存管理例程是 thread-safe(線程安全)的,通過編寫 thread-unsafe allocators(非線程安全分配器)你可以獲得相當的速度提升。當然,在得出 operator new 和 operator delete 對速度提升有價值的結論之前,確實測定你的程序以保證這些函數是真正的瓶頸。
* **為了減少缺省內存管理的空間成本。**general-purpose memory managers(通用目的的內存管理器)通常(雖然不總是)不僅比自定義版本慢,而且還經常使用更多的內存。這是因為它們經常為每個已分配區塊招致某些成本。針對 small objects(小對象)調諧的分配器(諸如 Boost 的 Pool library 中的那些)從根本上消除了這樣的成本。
* **為了調整缺省分配器不適當的排列對齊。**就像我前面提到的,在 x86 架構上,當 doubles 按照八字節對齊時訪問速度是最快的。哎呀,有些隨編譯器提供的 operator news 不能保證 doubles 的動態分配按照八字節對齊。在這種情況下,用保證按照八字節對齊的 operator new 替換掉缺省版本,可以使程序性能得到較大提升。
* **為了聚集相關的 objects,使它們彼此靠近。**如果你知道特定的 data structures(數據結構)通常會在一起使用,而且你想將在這些數據上工作時的頁錯誤頻率降到最低,那么為這些 data structures(數據結構)創建一個獨立的 heap(堆)以便讓它們盡可能地聚集在不多的幾個頁上就是有意義的。new 和 delete 的 placement versions(參見 [Item 52](http://blog.csdn.net/fatalerror99/archive/2007/01/21/1489466.aspx))使得完成這樣的聚集成為可能。
* **為了獲得不同尋常的行為。**有時你想讓 operators new 和 delete 做一些編譯器裝備版本沒有提供的事情。例如,你可能想在共享內存中分配和回收區塊,但是只能通過一個 C API 來管理那片內存。編寫 new 的 delete 的自定義版本(或許是 placement versions——再次參見 [Item 52](http://blog.csdn.net/fatalerror99/archive/2007/01/21/1489466.aspx))允許你用 C++ 衣服來遮住那個 C API。作為另一個例子,你可以編寫一個自定義的 operator delete 用 zeros 復寫被回收的內存以提高應用程序數據的安全性。
**Things to Remember**
* 有很多正當的編寫 new 和 delete 的自定義版本的理由,包括改進性能,調試 heap(堆)用法錯誤,以及收集 heap(堆)用法信息。
- Preface(前言)
- Introduction(導言)
- Terminology(術語)
- Item 1: 將 C++ 視為 federation of languages(語言聯合體)
- Item 2: 用 consts, enums 和 inlines 取代 #defines
- Item 3: 只要可能就用 const
- Item 4: 確保 objects(對象)在使用前被初始化
- Item 5: 了解 C++ 為你偷偷地加上和調用了什么函數
- Item 6: 如果你不想使用 compiler-generated functions(編譯器生成函數),就明確拒絕
- Item 7: 在 polymorphic base classes(多態基類)中將 destructors(析構函數)聲明為 virtual(虛擬)
- Item 8: 防止因為 exceptions(異常)而離開 destructors(析構函數)
- Item 9: 絕不要在 construction(構造)或 destruction(析構)期間調用 virtual functions(虛擬函數)
- Item 10: 讓 assignment operators(賦值運算符)返回一個 reference to *this(引向 *this 的引用)
- Item 11: 在 operator= 中處理 assignment to self(自賦值)
- Item 12: 拷貝一個對象的所有組成部分
- Item 13: 使用對象管理資源
- Item 14: 謹慎考慮資源管理類的拷貝行為
- Item 15: 在資源管理類中準備訪問裸資源(raw resources)
- Item 16: 使用相同形式的 new 和 delete
- Item 17: 在一個獨立的語句中將 new 出來的對象存入智能指針
- Item 18: 使接口易于正確使用,而難以錯誤使用
- Item 19: 視類設計為類型設計
- Item 20: 用 pass-by-reference-to-const(傳引用給 const)取代 pass-by-value(傳值)
- Item 21: 當你必須返回一個對象時不要試圖返回一個引用
- Item 22: 將數據成員聲明為 private
- Item 23: 用非成員非友元函數取代成員函數
- Item 24: 當類型轉換應該用于所有參數時,聲明為非成員函數
- Item 25: 考慮支持不拋異常的 swap
- Item 26: 只要有可能就推遲變量定義
- Item 27: 將強制轉型減到最少
- Item 28: 避免返回對象內部構件的“句柄”
- Item 29: 爭取異常安全(exception-safe)的代碼
- Item 30: 理解 inline 化的介入和排除
- Item 31: 最小化文件之間的編譯依賴
- Item 32: 確保 public inheritance 模擬 "is-a"
- Item 33: 避免覆蓋(hiding)“通過繼承得到的名字”
- Item 34: 區分 inheritance of interface(接口繼承)和 inheritance of implementation(實現繼承)
- Item 35: 考慮可選的 virtual functions(虛擬函數)的替代方法
- Item 36: 絕不要重定義一個 inherited non-virtual function(通過繼承得到的非虛擬函數)
- Item 37: 絕不要重定義一個函數的 inherited default parameter value(通過繼承得到的缺省參數值)
- Item 38: 通過 composition(復合)模擬 "has-a"(有一個)或 "is-implemented-in-terms-of"(是根據……實現的)
- Item 39: 謹慎使用 private inheritance(私有繼承)
- Item 40: 謹慎使用 multiple inheritance(多繼承)
- Item 41: 理解 implicit interfaces(隱式接口)和 compile-time polymorphism(編譯期多態)
- Item 42: 理解 typename 的兩個含義
- Item 43: 了解如何訪問 templatized base classes(模板化基類)中的名字
- Item 44: 從 templates(模板)中分離出 parameter-independent(參數無關)的代碼
- Item 45: 用 member function templates(成員函數模板) 接受 "all compatible types"(“所有兼容類型”)
- Item 46: 需要 type conversions(類型轉換)時在 templates(模板)內定義 non-member functions(非成員函數)
- Item 47: 為類型信息使用 traits classes(特征類)
- Item 48: 感受 template metaprogramming(模板元編程)
- Item 49: 了解 new-handler 的行為
- Item 50: 領會何時替換 new 和 delete 才有意義
- Item 51: 編寫 new 和 delete 時要遵守慣例
- Item 52: 如果編寫了 placement new,就要編寫 placement delete
- 附錄 A. 超越 Effective C++
- 附錄 B. 第二和第三版之間的 Item 映射