使用說明1:
調用方法
使用時注意幾點:
1. ThreadJob 沒什么用,直接寫線程函數吧。
2. 線程函數(threadfunc)的入口參數void* 可以轉成自定義的類型對象,這個對象可以記錄下線程運行中的數據,并設置線程當前狀態,以此與線程進行交互。
3. 線程池有一個EndAndWait函數,用于讓線程池中所有計算正常結束。有時線程池中的一個線程可能要運行很長時間,怎么辦?可以通過線程函數threadfunc的入口參數對象來處理,比如:
在主線程中設置yourClass->cmd = 1,該線程就會自然結束。
使用說明2:
Critical section(臨界區)用來實現“排他性占有”。適用范圍是單一進程的各線程之間。它是:
· 一個局部性對象,不是一個核心對象。
· 快速而有效率。
· 不能夠同時有一個以上的critical section被等待。
· 無法偵測是否已被某個線程放棄。
Mutex
Mutex是一個核心對象,可以在不同的線程之間實現“排他性占有”,甚至幾十那些現成分屬不同進程。它是:
· 一個核心對象。
· 如果擁有mutex的那個線程結束,則會產生一個“abandoned”錯誤信息。
· 可以使用Wait…()等待一個mutex。
· 可以具名,因此可以被其他進程開啟。
· 只能被擁有它的那個線程釋放(released)。
Semaphore
Semaphore被用來追蹤有限的資源。它是:
· 一個核心對象。
· 沒有擁有者。
· 可以具名,因此可以被其他進程開啟。
· 可以被任何一個線程釋放(released)。
Event Object
Event object通常使用于overlapped I/O,或用來設計某些自定義的同步對象。它是:
· 一個核心對象。
· 完全在程序掌控之下。
· 適用于設計新的同步對象。
· “要求蘇醒”的請求并不會被儲存起來,可能會遺失掉。
· 可以具名,因此可以被其他進程開啟。
Interlocked Variable
如果Interlocked…()函數被使用于所謂的spin-lock,那么他們只是一種同步機制。所謂spin-lock是一種busy loop,被預期在極短時間內執行,所以有最小的額外負擔(overhead)。系統核心偶爾會使用他們。除此之外,interlocked variables主要用于引用技術。他們:
· 允許對4字節的數值有些基本的同步操作,不需動用到critical section或mutex之類。
· 在SMP(Symmetric Multi-Processors)操作系統中亦可有效運作。
"placement new"? 它
到底是什么東東呀?我也是最近幾天才聽說,看來對于C++我還差很遠呀!placement new 是重載operator
new的一個標準、全局的版本,它不能被自定義的版本代替(不像普通的operator new和operator
delete能夠被替換成用戶自定義的版本)。
它的原型如下:
void *operator new( size_t, void *p ) throw() { return p; }
首先我們區分下幾個容易混淆的關鍵詞:new、operator new、placement new
new和delete操作符我們應該都用過,它們是對堆中的內存進行申請和釋放,而這兩個都是不能被重載的。要實現不同的內存分配行為,需要重載operator new,而不是new和delete。
看如下代碼:
class MyClass {…};
MyClass * p=new MyClass;
這里的new實際上是執行如下3個過程:
1. 調用operator new分配內存 ;2. 調用構造函數生成類對象;3. 返回相應指針。
operator new就像operator+一樣,是可以重載的,但是不能在全局對原型為void operator new(size_t size)這個原型進行重載,一般只能在類中進行重載。如果類中沒有重載operator new,那么調用的就是全局的::operator new來完成堆的分配。同理,operator new[]、operator delete、operator delete[]也是可以重載的,一般你重載的其中一個,那么最后把其余的三個都重載一遍。
至于placement new才是本文的重點。其實它也只是operator new的一個重載的版本,只是我們很少用到它。如果你想在已經分配的內存中創建一個對象,使用new時行不通的。也就是說placement new允許你在一個已經分配好的內存中(棧或者堆中)構造一個新的對象。原型中void*p實際上就是指向一個已經分配好的內存緩沖區的的首地址。
我們知道使用new操作符分配內存需要在堆中查找足夠大的剩余空間,這個操作速度是很慢的,而且有可能出現無法分配內存的異常(空間不夠)。 placement new就可以解決這個問題。我們構造對象都是在一個預先準備好了的內存緩沖區中進行,不需要查找內存,內存分配的時間是常數;而且不會出現在程序運行中途 出現內存不足的異常。所以,placement new非常適合那些對時間要求比較高,長時間運行不希望被打斷的應用程序。
使用方法如下:
1. 緩沖區提前分配
可以使用堆的空間,也可以使用棧的空間,所以分配方式有如下兩種:
class MyClass {…};
char *buf=new char[N*sizeof(MyClass)+ sizeof(int) ] ; 或者char buf[N*sizeof(MyClass)+ sizeof(int) ];
2. 對象的構造
MyClass * pClass=new(buf) MyClass;
3. 對象的銷毀
一旦這個對象使用完畢,你必須顯式的調用類的析構函數進行銷毀對象。但此時內存空間不會被釋放,以便其他的對象的構造。
pClass->~MyClass();
4. 內存的釋放
如果緩沖區在堆中,那么調用delete[] buf;進行內存的釋放;如果在棧中,那么在其作用域內有效,跳出作用域,內存自動釋放。
注意:
本章首先簡單介紹自定義內存池性能優化的原理,然后列舉軟件開發中常用的內存池的不同類型,并給出具體實現的實例。
本 書主要針對的是 C++ 程序的性能優化,深入介紹 C++ 程序性能優化的方法和實例。全書由 4 個篇組成,第 1 篇介紹 C++ 語言的對象模型,該篇是優化 C++ 程序的基礎;第 2 篇主要針對如何優化 C++ 程序的內存使用;第 3 篇介紹如何優化程序的啟動性能;第 4 篇介紹了三類性能優化工具,即內存分析工具、性能分析工具和 I/O 檢測工具,它們是測量程序性能的利器。
在此我們推出了此書的第 2、6 章供大家在線瀏覽。更多推薦書籍請訪問 developerWorks 圖書頻道。
![]() ![]() |
![]()
|
![]() |
|
如 前所述,讀者已經了解到"堆"和"棧"的區別。而在編程實踐中,不可避免地要大量用到堆上的內存。例如在程序中維護一個鏈表的數據結構時,每次新增或者刪 除一個鏈表的節點,都需要從內存堆上分配或者釋放一定的內存;在維護一個動態數組時,如果動態數組的大小不能滿足程序需要時,也要在內存堆上分配新的內存 空間。
利用默認的內存管理函數new/delete或malloc/free在堆上分配和釋放內存會有一些額外的開銷。
系 統在接收到分配一定大小內存的請求時,首先查找內部維護的內存空閑塊表,并且需要根據一定的算法(例如分配最先找到的不小于申請大小的內存塊給請求者,或 者分配最適于申請大小的內存塊,或者分配最大空閑的內存塊等)找到合適大小的空閑內存塊。如果該空閑內存塊過大,還需要切割成已分配的部分和較小的空閑 塊。然后系統更新內存空閑塊表,完成一次內存分配。類似地,在釋放內存時,系統把釋放的內存塊重新加入到空閑內存塊表中。如果有可能的話,可以把相鄰的空 閑塊合并成較大的空閑塊。
默認的內存管理函數還考慮到多線程的應用,需要在每次分配和釋放內存時加鎖,同樣增加了開銷。
可見,如果應用程序頻繁地在堆上分配和釋放內存,則會導致性能的損失。并且會使系統中出現大量的內存碎片,降低內存的利用率。
默認的分配和釋放內存算法自然也考慮了性能,然而這些內存管理算法的通用版本為了應付更復雜、更廣泛的情況,需要做更多的額外工作。而對于某一個具體的應用程序來說,適合自身特定的內存分配釋放模式的自定義內存池則可以獲得更好的性能。
自 定義內存池的思想通過這個"池"字表露無疑,應用程序可以通過系統的內存分配調用預先一次性申請適當大小的內存作為一個內存池,之后應用程序自己對內存的 分配和釋放則可以通過這個內存池來完成。只有當內存池大小需要動態擴展時,才需要再調用系統的內存分配函數,其他時間對內存的一切操作都在應用程序的掌控 之中。
應用程序自定義的內存池根據不同的適用場景又有不同的類型。
從 線程安全的角度來分,內存池可以分為單線程內存池和多線程內存池。單線程內存池整個生命周期只被一個線程使用,因而不需要考慮互斥訪問的問題;多線程內存 池有可能被多個線程共享,因此則需要在每次分配和釋放內存時加鎖。相對而言,單線程內存池性能更高,而多線程內存池適用范圍更廣。
從內存池可分配內存單元大小來分,可以分為固定內存池和可變內存池。所謂固定內存池是指應用程序每次從內存池中分配出來的內存單元大小事先已經確定,是固定不變的;而可變內存池則每次分配的內存單元大小可以按需變化,應用范圍更廣,而性能比固定內存池要低。
下面以固定內存池為例說明內存池的工作原理,如圖6-1所示。
固定內存池由一系列固定大小的內存塊組成,每一個內存塊又包含了固定數量和大小的內存單元。
如 圖6-1所示,該內存池一共包含4個內存塊。在內存池初次生成時,只向系統申請了一個內存塊,返回的指針作為整個內存池的頭指針。之后隨著應用程序對內存 的不斷需求,內存池判斷需要動態擴大時,才再次向系統申請新的內存塊,并把所有這些內存塊通過指針鏈接起來。對于操作系統來說,它已經為該應用程序分配了 4個等大小的內存塊。由于是大小固定的,所以分配的速度比較快;而對于應用程序來說,其內存池開辟了一定大小,內存池內部卻還有剩余的空間。
例 如放大來看第4個內存塊,其中包含一部分內存池塊頭信息和3個大小相等的內存池單元。單元1和單元3是空閑的,單元2已經分配。當應用程序需要通過該內存 池分配一個單元大小的內存時,只需要簡單遍歷所有的內存池塊頭信息,快速定位到還有空閑單元的那個內存池塊。然后根據該塊的塊頭信息直接定位到第1個空閑 的單元地址,把這個地址返回,并且標記下一個空閑單元即可;當應用程序釋放某一個內存池單元時,直接在對應的內存池塊頭信息中標記該內存單元為空閑單元即 可。
可見與系統管理內存相比,內存池的操作非常迅速,它在性能優化方面的優點主要如下。
(1)針對特殊情況,例如需要頻繁分配釋放固定大小的內存對象時,不需要復雜的分配算法和多線程保護。也不需要維護內存空閑表的額外開銷,從而獲得較高的性能。
(2)由于開辟一定數量的連續內存空間作為內存池塊,因而一定程度上提高了程序局部性,提升了程序性能。
(3)比較容易控制頁邊界對齊和內存字節對齊,沒有內存碎片的問題。
![]() ![]() |
![]()
|
本節分析在某個大型應用程序實際應用到的一個內存池實現,并詳細講解其使用方法與工作原理。這是一個應用于單線程環境且分配單元大小固定的內存池,一般用來為執行時會動態頻繁地創建且可能會被多次創建的類對象或者結構體分配內存。
本節首先講解該內存池的數據結構聲明及圖示,接著描述其原理及行為特征。然后逐一講解實現細節,最后介紹如何在實際程序中應用此內存池,并與使用普通內存函數申請內存的程序性能作比較。
內存池類MemoryPool的聲明如下:
class MemoryPool |
MemoryBlock為內存池中附著在真正用來為內存請求分配內存的內存塊頭部的結構體,它描述了與之聯系的內存塊的使用信息:
struct MemoryBlock |
此內存池的數據結構如圖6-2所示。
此內存池的總體機制如下。
(1) 在運行過程中,MemoryPool內存池可能會有多個用來滿足內存申請請求的內存塊,這些內存塊是從進程堆中開辟的一個較大的連續內存區域,它由一個 MemoryBlock結構體和多個可供分配的內存單元組成,所有內存塊組成了一個內存塊鏈表,MemoryPool的pBlock是這個鏈表的頭。對每 個內存塊,都可以通過其頭部的MemoryBlock結構體的pNext成員訪問緊跟在其后面的那個內存塊。
(2) 每個內存塊由兩部分組成,即一個MemoryBlock結構體和多個內存分配單元。這些內存分配單元大小固定(由MemoryPool的 nUnitSize表示),MemoryBlock結構體并不維護那些已經分配的單元的信息;相反,它只維護沒有分配的自由分配單元的信息。它有兩個成員 比較重要:nFree和nFirst。nFree記錄這個內存塊中還有多少個自由分配單元,而nFirst則記錄下一個可供分配的單元的編號。每一個自由 分配單元的頭兩個字節(即一個USHORT型值)記錄了緊跟它之后的下一個自由分配單元的編號,這樣,通過利用每個自由分配單元的頭兩個字節,一個 MemoryBlock中的所有自由分配單元被鏈接起來。
(3)當有新的內存請求到來 時,MemoryPool會通過pBlock遍歷MemoryBlock鏈表,直到找到某個MemoryBlock所在的內存塊,其中還有自由分配單元 (通過檢測MemoryBlock結構體的nFree成員是否大于0)。如果找到這樣的內存塊,取得其MemoryBlock的nFirst值(此為該內 存塊中第1個可供分配的自由單元的編號)。然后根據這個編號定位到該自由分配單元的起始位置(因為所有分配單元大小固定,因此每個分配單元的起始位置都可 以通過編號分配單元大小來偏移定位),這個位置就是用來滿足此次內存申請請求的內存的起始地址。但在返回這個地址前,需要首先將該位置開始的頭兩個字節的 值(這兩個字節值記錄其之后的下一個自由分配單元的編號)賦給本內存塊的MemoryBlock的nFirst成員。這樣下一次的請求就會用這個編號對應 的內存單元來滿足,同時將此內存塊的MemoryBlock的nFree遞減1,然后才將剛才定位到的內存單元的起始位置作為此次內存請求的返回地址返回 給調用者。
(4)如果從現有的內存塊中找不到一個自由的內存分配單元(當第1次請求內存,以及現有的所有內存 塊中的所有內存分配單元都已經被分配時會發生這種情形),MemoryPool就會從進程堆中申請一個內存塊(這個內存塊包括一個MemoryBlock 結構體,及緊鄰其后的多個內存分配單元,假設內存分配單元的個數為n,n可以取值MemoryPool中的nInitSize或者nGrowSize), 申請完后,并不會立刻將其中的一個分配單元分配出去,而是需要首先初始化這個內存塊。初始化的操作包括設置MemoryBlock的nSize為所有內存 分配單元的大小(注意,并不包括MemoryBlock結構體的大小)、nFree為n-1(注意,這里是n-1而不是n,因為此次新內存塊就是為了滿足 一次新的內存請求而申請的,馬上就會分配一塊自由存儲單元出去,如果設為n-1,分配一個自由存儲單元后無須再將n遞減1),nFirst為1(已經知道 nFirst為下一個可以分配的自由存儲單元的編號。為1的原因與nFree為n-1相同,即立即會將編號為0的自由分配單元分配出去。現在設為1,其后 不用修改nFirst的值),MemoryBlock的構造需要做更重要的事情,即將編號為0的分配單元之后的所有自由分配單元鏈接起來。如前所述,每個 自由分配單元的頭兩個字節用來存儲下一個自由分配單元的編號。另外,因為每個分配單元大小固定,所以可以通過其編號和單元大小(MemoryPool的 nUnitSize成員)的乘積作為偏移值進行定位。現在唯一的問題是定位從哪個地址開始?答案是MemoryBlock的aData[1]成員開始。因 為aData[1]實際上是屬于MemoryBlock結構體的(MemoryBlock結構體的最后一個字節),所以實質上,MemoryBlock結 構體的最后一個字節也用做被分配出去的分配單元的一部分。因為整個內存塊由MemoryBlock結構體和整數個分配單元組成,這意味著內存塊的最后一個 字節會被浪費,這個字節在圖6-2中用位于兩個內存的最后部分的濃黑背景的小塊標識。確定了分配單元的起始位置后,將自由分配單元鏈接起來的工作就很容易 了。即從aData位置開始,每隔nUnitSize大小取其頭兩個字節,記錄其之后的自由分配單元的編號。因為剛開始所有分配單元都是自由的,所以這個 編號就是自身編號加1,即位置上緊跟其后的單元的編號。初始化后,將此內存塊的第1個分配單元的起始地址返回,已經知道這個地址就是aData。
(5) 當某個被分配的單元因為delete需要回收時,該單元并不會返回給進程堆,而是返回給MemoryPool。返回時,MemoryPool能夠知道該單 元的起始地址。這時,MemoryPool開始遍歷其所維護的內存塊鏈表,判斷該單元的起始地址是否落在某個內存塊的地址范圍內。如果不在所有內存地址范 圍內,則這個被回收的單元不屬于這個MemoryPool;如果在某個內存塊的地址范圍內,那么它會將這個剛剛回收的分配單元加到這個內存塊的 MemoryBlock所維護的自由分配單元鏈表的頭部,同時將其nFree值遞增1。回收后,考慮到資源的有效利用及后續操作的性能,內存池的操作會繼 續判斷:如果此內存塊的所有分配單元都是自由的,那么這個內存塊就會從MemoryPool中被移出并作為一個整體返回給進程堆;如果該內存塊中還有非自 由分配單元,這時不能將此內存塊返回給進程堆。但是因為剛剛有一個分配單元返回給了這個內存塊,即這個內存塊有自由分配單元可供下次分配,因此它會被移到 MemoryPool維護的內存塊的頭部。這樣下次的內存請求到來,MemoryPool遍歷其內存塊鏈表以尋找自由分配單元時,第1次尋找就會找到這個 內存塊。因為這個內存塊確實有自由分配單元,這樣可以減少MemoryPool的遍歷次數。
綜上所述,每個內 存池(MemoryPool)維護一個內存塊鏈表(單鏈表),每個內存塊由一個維護該內存塊信息的塊頭結構(MemoryBlock)和多個分配單元組 成,塊頭結構MemoryBlock則進一步維護一個該內存塊的所有自由分配單元組成的"鏈表"。這個鏈表不是通過"指向下一個自由分配單元的指針"鏈接 起來的,而是通過"下一個自由分配單元的編號"鏈接起來,這個編號值存儲在該自由分配單元的頭兩個字節中。另外,第1個自由分配單元的起始位置并不是 MemoryBlock結構體"后面的"第1個地址位置,而是MemoryBlock結構體"內部"的最后一個字節aData(也可能不是最后一個,因為 考慮到字節對齊的問題),即分配單元實際上往前面錯了一位。又因為MemoryBlock結構體后面的空間剛好是分配單元的整數倍,這樣依次錯位下去,內 存塊的最后一個字節實際沒有被利用。這么做的一個原因也是考慮到不同平臺的移植問題,因為不同平臺的對齊方式可能不盡相同。即當申請 MemoryBlock大小內存時,可能會返回比其所有成員大小總和還要大一些的內存。最后的幾個字節是為了"補齊",而使得aData成為第1個分配單 元的起始位置,這樣在對齊方式不同的各種平臺上都可以工作。
有了上述的總體印象后,本節來仔細剖析其實現細節。
(1)MemoryPool的構造如下:
MemoryPool::MemoryPool( USHORT _nUnitSize, |
從①處可以看出,MemoryPool創建時,并沒有立刻創建真正用來滿足內存申請的內存塊,即內存塊鏈表剛開始時為空。
②處和③處分別設置"第1次創建的內存塊所包含的分配單元的個數",及"隨后創建的內存塊所包含的分配單元的個數",這兩個值在MemoryPool創建時通過參數指定,其后在該MemoryPool對象生命周期中一直不變。
后 面的代碼用來設置nUnitSize,這個值參考傳入的_nUnitSize參數。但是還需要考慮兩個因素。如前所述,每個分配單元在自由狀態時,其頭兩 個字節用來存放"其下一個自由分配單元的編號"。即每個分配單元"最少"有"兩個字節",這就是⑤處賦值的原因。④處是將大于4個字節的大小 _nUnitSize往上"取整到"大于_nUnitSize的最小的MEMPOOL_ ALIGNMENT的倍數(前提是MEMPOOL_ALIGNMENT為2的倍數)。如_nUnitSize為11 時,MEMPOOL_ALIGNMENT為8,nUnitSize為16;MEMPOOL_ALIGNMENT為4,nUnitSize為 12;MEMPOOL_ALIGNMENT為2,nUnitSize為12,依次類推。
(2)當向MemoryPool提出內存請求時:
void* MemoryPool::Alloc() |
MemoryPool滿足內存請求的步驟主要由四步組成。
① 處首先判斷內存池當前內存塊鏈表是否為空,如果為空,則意味著這是第1次內存申請請求。這時,從進程堆中申請一個分配單元個數為nInitSize的內存 塊,并初始化該內存塊(主要初始化MemoryBlock結構體成員,以及創建初始的自由分配單元鏈表,下面會詳細分析其代碼)。如果該內存塊申請成功, 并初始化完畢,返回第1個分配單元給調用函數。第1個分配單元以MemoryBlock結構體內的最后一個字節為起始地址。
②處的作用是當內存池中已有內存塊(即內存塊鏈表不為空)時遍歷該內存塊鏈表,尋找還有"自由分配單元"的內存塊。
③ 處檢查如果找到還有自由分配單元的內存塊,則"定位"到該內存塊現在可以用的自由分配單元處。"定位"以MemoryBlock結構體內的最后一個字節位 置aData為起始位置,以MemoryPool的nUnitSize為步長來進行。找到后,需要修改MemoryBlock的nFree信息(剩下來的 自由分配單元比原來減少了一個),以及修改此內存塊的自由存儲單元鏈表的信息。在找到的內存塊中,pMyBlock->nFirst為該內存塊中自 由存儲單元鏈表的表頭,其下一個自由存儲單元的編號存放在pMyBlock->nFirst指示的自由存儲單元(亦即剛才定位到的自由存儲單元)的 頭兩個字節。通過剛才定位到的位置,取其頭兩個字節的值,賦給pMyBlock->nFirst,這就是此內存塊的自由存儲單元鏈表的新的表頭,即 下一次分配出去的自由分配單元的編號(如果nFree大于零的話)。修改維護信息后,就可以將剛才定位到的自由分配單元的地址返回給此次申請的調用函數。 注意,因為這個分配單元已經被分配,而內存塊無須維護已分配的分配單元,因此該分配單元的頭兩個字節的信息已經沒有用處。換個角度看,這個自由分配單元返 回給調用函數后,調用函數如何處置這塊內存,內存池無從知曉,也無須知曉。此分配單元在返回給調用函數時,其內容對于調用函數來說是無意義的。因此幾乎可 以肯定調用函數在用這個單元的內存時會覆蓋其原來的內容,即頭兩個字節的內容也會被抹去。因此每個存儲單元并沒有因為需要鏈接而引入多余的維護信息,而是 直接利用單元內的頭兩個字節,當其分配后,頭兩個字節也可以被調用函數利用。而在自由狀態時,則用來存放維護信息,即下一個自由分配單元的編號,這是一個 有效利用內存的好例子。
④處表示在②處遍歷時,沒有找到還有自由分配單元的內存塊,這時,需要重新向進程堆申 請一個內存塊。因為不是第一次申請內存塊,所以申請的內存塊包含的分配單元個數為nGrowSize,而不再是nInitSize。與①處相同,先做這個 新申請內存塊的初始化工作,然后將此內存塊插入MemoryPool的內存塊鏈表的頭部,再將此內存塊的第1個分配單元返回給調用函數。將此新內存塊插入 內存塊鏈表的頭部的原因是該內存塊還有很多可供分配的自由分配單元(除非nGrowSize等于1,這應該不太可能。因為內存池的含義就是一次性地從進程 堆中申請一大塊內存,以供后續的多次申請),放在頭部可以使得在下次收到內存申請時,減少②處對內存塊的遍歷時間。
可以用圖6-2的MemoryPool來展示MemoryPool::Alloc的過程。圖6-3是某個時刻MemoryPool的內部狀態。
因 為MemoryPool的內存塊鏈表不為空,因此會遍歷其內存塊鏈表。又因為第1個內存塊里有自由的分配單元,所以會從第1個內存塊中分配。檢查 nFirst,其值為m,這時pBlock->aData+(pBlock->nFirst*nUnitSize)定位到編號為m的自由分配 單元的起始位置(用pFree表示)。在返回pFree之前,需要修改此內存塊的維護信息。首先將nFree遞減1,然后取得pFree處開始的頭兩個字 節的值(需要說明的是,這里aData處值為k。其實不是這一個字節。而是以aData和緊跟其后的另外一個字節合在一起構成的一個USHORT的值,不 可誤會)。發現為k,這時修改pBlock的nFirst為k。然后,返回pFree。此時MemoryPool的結構如圖6-4所示。
可以看到,原來的第1個可供分配的單元(m編號處)已經顯示為被分配的狀態。而pBlock的nFirst已經指向原來m單元下一個自由分配單元的編號,即k。
(3)MemoryPool回收內存時:
void MemoryPool::Free( void* pFree ) |
如前所述,回收分配單元時,可能會將整個內存塊返回給進程堆,也可能將被回收分配單元所屬的內存塊移至內存池的內存塊鏈表的頭部。這兩個操作都需要修改鏈表結構。這時需要知道該內存塊在鏈表中前一個位置的內存塊。
①處遍歷內存池的內存塊鏈表,確定該待回收分配單元(pFree)落在哪一個內存塊的指針范圍內,通過比較指針值來確定。
運 行到②處,pMyBlock即找到的包含pFree所指向的待回收分配單元的內存塊(當然,這時應該還需要檢查pMyBlock為NULL時的情形,即 pFree不屬于此內存池的范圍,因此不能返回給此內存池,讀者可以自行加上)。這時將pMyBlock的nFree遞增1,表示此內存塊的自由分配單元 多了一個。
③處用來修改該內存塊的自由分配單元鏈表的信息,它將這個待回收分配單元的頭兩個字節的值指向該內存塊原來的第一個可分配的自由分配單元的編號。
④處將pMyBlock的nFirst值改變為指向這個待回收分配單元的編號,其編號通過計算此單元的起始位置相對pMyBlock的aData位置的差值,然后除以步長(nUnitSize)得到。
實 質上,③和④兩步的作用就是將此待回收分配單元"真正回收"。值得注意的是,這兩步實際上是使得此回收單元成為此內存塊的下一個可分配的自由分配單元,即 將它放在了自由分配單元鏈表的頭部。注意,其內存地址并沒有發生改變。實際上,一個分配單元的內存地址無論是在分配后,還是處于自由狀態時,一直都不會變 化。變化的只是其狀態(已分配/自由),以及當其處于自由狀態時在自由分配單元鏈表中的位置。
⑤處檢查當回收完畢后,包含此回收單元的內存塊的所有單元是否都處于自由狀態,且此內存是否處于內存塊鏈表的頭部。如果是,將此內存塊整個的返回給進程堆,同時修改內存塊鏈表結構。
注 意,這里在判斷一個內存塊的所有單元是否都處于自由狀態時,并沒有遍歷其所有單元,而是判斷nFree乘以nUnitSize是否等于nSize。 nSize是內存塊中所有分配單元的大小,而不包括頭部MemoryBlock結構體的大小。這里可以看到其用意,即用來快速檢查某個內存塊中所有分配單 元是否全部處于自由狀態。因為只需結合nFree和nUnitSize來計算得出結論,而無須遍歷和計算所有自由狀態的分配單元的個數。
另 外還需注意的是,這里并不能比較nFree與nInitSize或nGrowSize的大小來判斷某個內存塊中所有分配單元都為自由狀態,這是因為第1次 分配的內存塊(分配單元個數為nInitSize)可能被移到鏈表的后面,甚至可能在移到鏈表后面后,因為某個時間其所有單元都處于自由狀態而被整個返回 給進程堆。即在回收分配單元時,無法判定某個內存塊中的分配單元個數到底是nInitSize還是nGrowSize,也就無法通過比較nFree與 nInitSize或nGrowSize的大小來判斷一個內存塊的所有分配單元是否都為自由狀態。
以上面分配后的內存池狀態作為例子,假設這時第2個內存塊中的最后一個單元需要回收(已被分配,假設其編號為m,pFree指針指向它),如圖6-5所示。
不 難發現,這時nFirst的值由原來的0變為m。即此內存塊下一個被分配的單元是m編號的單元,而不是0編號的單元(最先分配的是最新回收的單元,從這一 點看,這個過程與棧的原理類似,即先進后出。只不過這里的"進"意味著"回收",而"出"則意味著"分配")。相應地,m的"下一個自由單元"標記為0, 即內存塊原來的"下一個將被分配出去的單元",這也表明最近回收的分配單元被插到了內存塊的"自由分配單元鏈表"的頭部。當然,nFree遞增1。
處理至⑥處之前,其狀態如圖6-6所示。
這 里需要注意的是,雖然pFree被"回收",但是pFree仍然指向m編號的單元,這個單元在回收過程中,其頭兩個字節被覆寫,但其他部分的內容并沒有改 變。而且從整個進程的內存使用角度來看,這個m編號的單元的狀態仍然是"有效的"。因為這里的"回收"只是回收給了內存池,而并沒有回收給進程堆,因此程 序仍然可以通過pFree訪問此單元。但是這是一個很危險的操作,因為首先該單元在回收過程中頭兩個字節已被覆寫,并且該單元可能很快就會被內存池重新分 配。因此回收后通過pFree指針對這個單元的訪問都是錯誤的,讀操作會讀到錯誤的數據,寫操作則可能會破壞程序中其他地方的數據,因此需要格外小心。
接著,需要判斷該內存塊的內部使用情況,及其在內存塊鏈表中的位置。如果該內存塊中省略號"……"所表示的其他部分中還有被分配的單元,即nFree乘以nUnitSize不等于nSize。因為此內存塊不在鏈表頭,因此還需要將其移到鏈表頭部,如圖6-7所示。
如果該內存塊中省略號"……"表示的其他部分中全部都是自由分配單元,即nFree乘以nUnitSize等于nSize。因為此內存塊不在鏈表頭,所以此時需要將此內存塊整個回收給進程堆,回收后內存池的結構如圖6-8所示。
一個內存塊在申請后會初始化,主要是為了建立最初的自由分配單元鏈表,下面是其詳細代碼:
MemoryBlock::MemoryBlock (USHORT nTypes, USHORT nUnitSize) |
這里可以看到,①處pData的初值是 aData,即0編號單元。但是②處的循環中i卻是從1開始,然后在循環內部的③處將pData的頭兩個字節值置為i。即0號單元的頭兩個字節值為1,1 號單元的頭兩個字節值為2,一直到(nTypes-2)號單元的頭兩個字節值為(nTypes-1)。這意味著內存塊初始時,其自由分配單元鏈表是從0號 開始。依次串聯,一直到倒數第2個單元指向最后一個單元。
還需要注意的是,在其初始化列表中,nFree初始 化為nTypes-1(而不是nTypes),nFirst初始化為1(而不是0)。這是因為第1個單元,即0編號單元構造完畢后,立刻會被分配。另外注 意到最后一個單元初始并沒有設置頭兩個字節的值,因為該單元初始在本內存塊中并沒有下一個自由分配單元。但是從上面例子中可以看到,當最后一個單元被分配 并回收后,其頭兩個字節會被設置。
圖6-9所示為一個內存塊初始化后的狀態。
當內存池析構時,需要將內存池的所有內存塊返回給進程堆:
MemoryPool::~MemoryPool() |
分 析內存池的內部原理后,本節說明如何使用它。從上面的分析可以看到,該內存池主要有兩個對外接口函數,即Alloc和Free。Alloc返回所申請的分 配單元(固定大小內存),Free則回收傳入的指針代表的分配單元的內存給內存池。分配的信息則通過MemoryPool的構造函數指定,包括分配單元大 小、內存池第1次申請的內存塊中所含分配單元的個數,以及內存池后續申請的內存塊所含分配單元的個數等。
綜上 所述,當需要提高某些關鍵類對象的申請/回收效率時,可以考慮將該類所有生成對象所需的空間都從某個這樣的內存池中開辟。在銷毀對象時,只需要返回給該內 存池。"一個類的所有對象都分配在同一個內存池對象中"這一需求很自然的設計方法就是為這樣的類聲明一個靜態內存池對象,同時為了讓其所有對象都從這個內 存池中開辟內存,而不是缺省的從進程堆中獲得,需要為該類重載一個new運算符。因為相應地,回收也是面向內存池,而不是進程的缺省堆,還需要重載一個 delete運算符。在new運算符中用內存池的Alloc函數滿足所有該類對象的內存請求,而銷毀某對象則可以通過在delete運算符中調用內存池的 Free完成。
為 了測試利用內存池后的效果,通過一個很小的測試程序可以發現采用內存池機制后耗時為297 ms。而沒有采用內存池機制則耗時625 ms,速度提高了52.48%。速度提高的原因可以歸結為幾點,其一,除了偶爾的內存申請和銷毀會導致從進程堆中分配和銷毀內存塊外,絕大多數的內存申請 和銷毀都由內存池在已經申請到的內存塊中進行,而沒有直接與進程堆打交道,而直接與進程堆打交道是很耗時的操作;其二,這是單線程環境的內存池,可以看到 內存池的Alloc和Free操作中并沒有加線程保護措施。因此如果類A用到該內存池,則所有類A對象的創建和銷毀都必須發生在同一個線程中。但如果類A 用到內存池,類B也用到內存池,那么類A的使用線程可以不必與類B的使用線程是同一個線程。
另外,在第1章中已經討論過,因為內存池技術使得同類型的對象分布在相鄰的內存區域,而程序會經常對同一類型的對象進行遍歷操作。因此在程序運行過程中發生的缺頁應該會相應少一些,但這個一般只能在真實的復雜應用環境中進行驗證。
![]() ![]() |
![]()
|
內 存的申請和釋放對一個應用程序的整體性能影響極大,甚至在很多時候成為某個應用程序的瓶頸。消除內存申請和釋放引起的瓶頸的方法往往是針對內存使用的實際 情況提供一個合適的內存池。內存池之所以能夠提高性能,主要是因為它能夠利用應用程序的實際內存使用場景中的某些"特性"。比如某些內存申請與釋放肯定發 生在一個線程中,某種類型的對象生成和銷毀與應用程序中的其他類型對象要頻繁得多,等等。針對這些特性,可以為這些特殊的內存使用場景提供量身定做的內存 池。這樣能夠消除系統提供的缺省內存機制中,對于該實際應用場景中的不必要的操作,從而提升應用程序的整體性能。
![]() |
||
![]() |
馮 宏華,清華大學計算機科學與技術系碩士。IBM 中國開發中心高級軟件工程師。 2003 年 12 月加入 IBM 中國開發中心,主要從事 IBM 產品的開發、性能優化等工作。興趣包括 C/C++ 應用程序性能調優,Windows 應用程序開發,Web 應用程序開發等。 |
![]() |
||
![]() |
徐 瑩,山東大學計算機科學與技術系碩士。2003 年 4 月加入 IBM 中國開發中心,現任 IBM 中國開發中心開發經理,一直從事IBM軟件產品在多個操作系統平臺上的開發工作。曾參與 IBM 產品在 Windows 和 Linux 平臺上的性能優化工作,對 C/C++ 編程語言和跨平臺的大型軟件系統的開發有較豐富的經驗。 |
![]() |
||
![]() |
程 遠,北京大學計算機科學與技術系碩士。IBM 中國開發中心高級軟件工程師。2003 年加入 IBM 中國開發中心,主要從事IBM Productivity Tools 產品的開發、性能優化等工作。興趣包括 C/C++ 編程語言,軟件性能工程,Windows/Linux 平臺性能測試優化工具等。 |
![]() |
||
![]() |
汪 磊,北京航空航天大學計算機科學與技術系碩士,目前是 IBM 中國軟件開發中心高級軟件工程師。從 2002 年 12 月加入 IBM 中國開發中心至今一直從事旨在提高企業生產效率的應用軟件開發。興趣包括 C\C++ 應用程序的性能調優,Java 應用程序的性能調優。 |
首先,C++標準中提到,一個編譯單元[translation
unit]是指一個.cpp文件以及它所include的所有.h文件,.h文件里的代碼將會被擴展到包含它的.cpp文件里,然后編譯器編譯該.cpp
文件為一個.obj文件,后者擁有PE[Portable
Executable,即windows可執行文件]文件格式,并且本身包含的就已經是二進制碼,但是,不一定能夠執行,因為并不保證其中一定有main
函數。當編譯器將一個工程里的所有.cpp文件以分離的方式編譯完畢后,再由連接器(linker)進行連接成為一個.exe文件。
舉個例子:
//---------------test.h-------------------//
void f();//這里聲明一個函數f
//---------------test.cpp--------------//
#i nclude”test.h”
void f()
{
…//do something
} //這里實現出test.h中聲明的f函數
//---------------main.cpp--------------//
#i nclude”test.h”
int main()
{
f(); //調用f,f具有外部連接類型
}
在
這個例子中,test.
cpp和main.cpp各被編譯成為不同的.obj文件[姑且命名為test.obj和main.obj],在main.cpp中,調用了f函數,然而
當編譯器編譯main.cpp時,它所僅僅知道的只是main.cpp中所包含的test.h文件中的一個關于void
f();的聲明,所以,編譯器將這里的f看作外部連接類型,即認為它的函數實現代碼在另一個.obj文件中,本例也就是test.obj,也就是
說,main.obj中實際沒有關于f函數的哪怕一行二進制代碼,而這些代碼實際存在于test.cpp所編譯成的test.obj中。在
main.obj中對f的調用只會生成一行call指令,像這樣:
call f [C++中這個名字當然是經過mangling[處理]過的]
在
編譯時,這個call指令顯然是錯誤的,因為main.obj中并無一行f的實現代碼。那怎么辦呢?這就是連接器的任務,連接器負責在其它的.obj中
[本例為test.obj]尋找f的實現代碼,找到以后將call
f這個指令的調用地址換成實際的f的函數進入點地址。需要注意的是:連接器實際上將工程里的.obj“連接”成了一個.exe文件,而它最關鍵的任務就是
上面說的,尋找一個外部連接符號在另一個.obj中的地址,然后替換原來的“虛假”地址。
這個過程如果說的更深入就是:
call f這行指令其實并不是這樣的,它實際上是所謂的stub,也就是一個
jmp
0x23423[這個地址可能是任意的,然而關鍵是這個地址上有一行指令來進行真正的call
f動作。也就是說,這個.obj文件里面所有對f的調用都jmp向同一個地址,在后者那兒才真正”call”f。這樣做的好處就是連接器修改地址時只要對
后者的call
XXX地址作改動就行了。但是,連接器是如何找到f的實際地址的呢[在本例中這處于test.obj中],因為.obj于.exe的格式都是一樣的,在這
樣的文件中有一個符號導入表和符號導出表[import table和export
table]其中將所有符號和它們的地址關聯起來。這樣連接器只要在test.obj的符號導出表中尋找符號f[當然C++對f作了mangling]的
地址就行了,然后作一些偏移量處理后[因為是將兩個.obj文件合并,當然地址會有一定的偏移,這個連接器清楚]寫入main.obj中的符號導入表中f
所占有的那一項。
這就是大概的過程。其中關鍵就是:
編譯main.cpp時,編譯器不知道f的實現,所有當碰到對它的調用時只是給出一個指示,指示連接器應該為它尋找f的實現體。這也就是說main.obj中沒有關于f的任何一行二進制代碼。
編譯test.cpp時,編譯器找到了f的實現。于是乎f的實現[二進制代碼]出現在test.obj里。
連接時,連接器在test.obj中找到f的實現代碼[二進制]的地址[通過符號導出表]。然后將main.obj中懸而未決的call XXX地址改成f實際的地址。
完成。
然而,對于模板,你知道,模板函數的代碼其實并不能直接編譯成二進制代碼,其中要有一個“具現化”的過程。舉個例子:
//----------main.cpp------//
template<class T>
void f(T t)
{}
int main()
{
…//do something
f(10); //call f<int> 編譯器在這里決定給f一個f<int>的具現體
…//do other thing
}
也就是說,如果你在main.cpp文件中沒有調用過f,f也就得不到具現,從而main.obj中也就沒有關于f的任意一行二進制代碼!!如果你這樣調用了:
f(10); //f<int>得以具現化出來
f(10.0); //f<double>得以具現化出來
這樣main.obj中也就有了f<int>,f<double>兩個函數的二進制代碼段。以此類推。
然而具現化要求編譯器知道模板的定義,不是嗎?
看下面的例子:[將模板和它的實現分離]
//-------------test.h----------------//
template<class T>
class A
{
public:
void f(); //這里只是個聲明
};
//---------------test.cpp-------------//
#i nclude”test.h”
template<class T>
void A<T>::f() //模板的實現,但注意:不是具現
{
…//do something
}
//---------------main.cpp---------------//
#i nclude”test.h”
int main()
{
A<int> a;
a. f(); //編譯器在這里并不知道A<int>::f的定義,因為它不在test.h里面
//于是編譯器只好寄希望于連接器,希望它能夠在其他.obj里面找到
//A<int>::f的實現體,在本例中就是test.obj,然而,后者中真有A<int>::f的
//二進制代碼嗎?NO!!!因為C++標準明確表示,當一個模板不被用到的時
//侯它就不該被具現出來,test.cpp中用到了A<int>::f了嗎?沒有!!所以實
//際上test.cpp編譯出來的test.obj文件中關于A::f的一行二進制代碼也沒有
//于是連接器就傻眼了,只好給出一個連接錯誤
//
但是,如果在test.cpp中寫一個函數,其中調用A<int>::f,則編譯器會將其//具現出來,因為在這個點上[test.cpp
中],編譯器知道模板的定義,所以能//夠具現化,于是,test.obj的符號導出表中就有了A<int>::f這個符號的地
//址,于是連接器就能夠完成任務。
}
關鍵是:在分離式編譯的環境下,編譯器編譯某一個.cpp文件時并不知道另一個.cpp文件的存在,也不會去查找[當遇到未決符號時它會寄希望于連 接器]。這種模式在沒有模板的情況下運行良好,但遇到模板時就傻眼了,因為模板僅在需要的時候才會具現化出來,所以,當編譯器只看到模板的聲明時,它不能 具現化該模板,只能創建一個具有外部連接的符號并期待連接器能夠將符號的地址決議出來。然而當實現該模板的.cpp文件中沒有用到模板的具現體時,編譯器 懶得去具現,所以,整個工程的.obj中就找不到一行模板具現體的二進制代碼,于是連接器也黔
/////////////////////////////////
http://dev.csdn.net/develop/article/19/19587.shtm
C++模板代碼的組織方式 ——包含模式(Inclusion Model) 選擇自 sam1111 的 Blog
關鍵字 Template Inclusion Model
出處 C++ Template: The Complete Guide
說明:本文譯自《C++ Template: The Complete Guide》一書的第6章中的部分內容。最近看到C++論壇上常有關于模板的包含模式的帖子,聯想到自己初學模板時,也為類似的問題困惑過,因此翻譯此文,希望對初學者有所幫助。
模板代碼有幾種不同的組織方式,本文介紹其中最流行的一種方式:包含模式。
鏈接錯誤
大多數C/C++程序員向下面這樣組織他們的非模板代碼:
·類和其他類型全部放在頭文件中,這些頭文件具有.hpp(或者.H, .h, .hh, .hxx)擴展名。
·對于全局變量和(非內聯)函數,只有聲明放在頭文件中,而定義放在點C文件中,這些文件具有.cpp(或者.C, .c, .cc, .cxx)擴展名。
這種組織方式工作的很好:它使得在編程時可以方便地訪問所需的類型定義,并且避免了來自鏈接器的“變量或函數重復定義”的錯誤。
由于以上組織方式約定的影響,模板編程新手往往會犯一個同樣的錯誤。下面這一小段程序反映了這種錯誤。就像對待“普通代碼”那樣,我們在頭文件中定義模板:
// basics/myfirst.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
// declaration of template
template <typename T>
void print_typeof (T const&);
#endif // MYFIRST_HPP
print_typeof()聲明了一個簡單的輔助函數用來打印一些類型信息。函數的定義放在點C文件中:
// basics/myfirst.cpp
#i nclude <iostream>
#i nclude <typeinfo>
#i nclude "myfirst.hpp"
// implementation/definition of template
template <typename T>
void print_typeof (T const& x)
{
std::cout << typeid(x).name() << std::endl;
}
這個例子使用typeid操作符來打印一個字符串,這個字符串描述了傳入的參數的類型信息。
最后,我們在另外一個點C文件中使用我們的模板,在這個文件中模板聲明被#i nclude:
// basics/myfirstmain.cpp
#i nclude "myfirst.hpp"
// use of the template
int main()
{
double ice = 3.0;
print_typeof(ice); // call function template for type double
}
大部分C++編譯器(Compiler)很可能會接受這個程序,沒有任何問題,但是鏈接器(Linker)大概會報告一個錯誤,指出缺少函數print_typeof()的定義。
這個錯誤的原因在于,模板函數print_typeof()的定義還沒有被具現化(instantiate)。為了具現化一個模板,編譯器必須知道 哪一個定義應該被具現化,以及使用什么樣的模板參數來具現化。不幸的是,在前面的例子中,這兩組信息存在于分開編譯的不同文件中。因此,當我們的編譯器看 到對print_typeof()的調用,但是沒有看到此函數為double類型具現化的定義時,它只是假設這樣的定義在別處提供,并且創建一個那個定義 的引用(鏈接器使用此引用解析)。另一方面,當編譯器處理myfirst.cpp時,該文件并沒有任何指示表明它必須為它所包含的特殊參數具現化模板定 義。
頭文件中的模板
解決上面這個問題的通用解法是,采用與我們使用宏或者內聯函數相同的方法:我們將模板的定義包含進聲明模板的頭文件中。對于我們的例子,我們可以通 過將#i nclude "myfirst.cpp"添加到myfirst.hpp文件尾部,或者在每一個使用我們的模板的點C文件中包含myfirst.cpp文件,來達到目 的。當然,還有第三種方法,就是刪掉myfirst.cpp文件,并重寫myfirst.hpp文件,使它包含所有的模板聲明與定義:
// basics/myfirst2.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
#i nclude <iostream>
#i nclude <typeinfo>
// declaration of template
template <typename T>
void print_typeof (T const&);
// implementation/definition of template
template <typename T>
void print_typeof (T const& x)
{
std::cout << typeid(x).name() << std::endl;
}
#endif // MYFIRST_HPP
這種組織模板代碼的方式就稱作包含模式。經過這樣的調整,你會發現我們的程序已經能夠正確編譯、鏈接、執行了。
從這個方法中我們可以得到一些觀察結果。最值得注意的一點是,這個方法在相當程度上增加了包含myfirst.hpp的開銷。在這個例子中,這種開 銷并不是由模板定義自身的尺寸引起的,而是由這樣一個事實引起的,即我們必須包含我們的模板用到的頭文件,在這個例子中 是<iostream>和<typeinfo>。你會發現這最終導致了成千上萬行的代碼,因為諸 如<iostream>這樣的頭文件也包含了和我們類似的模板定義。
這在實踐中確實是一個問題,因為它增加了編譯器在編譯一個實際程序時所需的時間。我們因此會在以后的章節中驗證其他一些可能的方法來解決這個問題。但無論如何,現實世界中的程序花一小時來編譯鏈接已經是快的了(我們曾經遇到過花費數天時間來從源碼編譯的程序)。
拋開編譯時間不談,我們強烈建議如果可能盡量按照包含模式組織模板代碼。
另一個觀察結果是,非內聯模板函數與內聯函數和宏的最重要的不同在于:它并不會在調用端展開。相反,當模板函數被具現化時,會產生此函數的一個新的
拷貝。由于這是一個自動的過程,編譯器也許會在不同的文件中產生兩個相同的拷貝,從而引起鏈接器報告一個錯誤。理論上,我們并不關心這一點:這是編譯器設
計者應當關心的事情。實際上,大多數時候一切都運轉正常,我們根本就不用處理這種狀況。然而,對于那些需要創建自己的庫的大型項目,這個問題偶爾會顯現出
來。
最后,需要指出的是,在我們的例子中,應用于普通模板函數的方法同樣適用于模板類的成員函數和靜態數據成員,以及模板成員函數。
Definition:
A class is a pure interface if it meets the following requirements:
= 0
") methods
and static methods (but see below for destructor).
Interface
suffix.
An interface class can never be directly instantiated because of the pure virtual method(s) it declares. To make sure all implementations of the interface can be destroyed correctly, they must also declare a virtual destructor (in an exception to the first rule, this should not be pure). See Stroustrup, The C++ Programming Language, 3rd edition, section 12.4 for details.
Pros:
Tagging a class with the Interface
suffix lets
others know that they must not add implemented methods or non
static data members. This is particularly important in the case of
multiple inheritance.
Additionally, the interface concept is already well-understood by
Java programmers.
Cons:
The Interface
suffix lengthens the class name, which
can make it harder to read and understand. Also, the interface
property may be considered an implementation detail that shouldn't
be exposed to clients.
Decision:
A class may end with Interface
only if it meets the
above requirements. We do not require the converse, however:
classes that meet the above requirements are not required to end
with Interface
.
void placement() {
char *buf = new char[1000]; //pre-allocated buffer
string *p = new (buf) string("hi"); //placement new
string *q = new string("hi"); //ordinary heap allocation
cout<
<
c_str()
<
<c_str();
}
:
placement new 表達式只是定位,不存在與其相對應的delete,如果delete則選擇
delete[] buf。
operator new實際上總是以標準
extern void* operator new( size_t size )
{
if( size == 0 )
size = 1; // 這里保證像 new T[0] 這樣得語句也是可行的
void *last_alloc;
while( !(last_alloc = malloc( size )) )
{
if( _new_handler )
( *_new_handler )();
else
return 0;
}
return last_alloc;
}