內存分配(new/delete,malloc/free,allocator,內存池)

以下來源http://www.cnblogs.com/JCSU/articles/1051826.html

程序員們經常編寫內存管理程序,往往提心吊膽。如果不想觸雷,唯一的解決辦法就是發現所有潛伏的地雷並且排除它們,躲是躲不了的。本文的內容比一般教科書的要深入得多,讀者需細心閱讀,做到真正地通曉內存管理。

     內存分配方式 

   (1)從靜態存儲區域分配。內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。例如全局變量,static變量。
   (2)在棧上創建。在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限。
   (3) 從堆上分配,亦稱動態內存分配。程序在運行的時候用malloc或new申請任意多少的內存,程序員自己負責在何時用free或delete釋放內存。動態內存的生存期由我們決定,使用非常靈活,但問題也最多。
 
     常見的內存錯誤及其對策 

     發生內存錯誤是件非常麻煩的事情。編譯器不能自動發現這些錯誤,通常是在程序運行時才能捕捉到。而這些錯誤大多沒有明顯的症狀,時隱時現,增加了改錯的難度。有時用戶怒氣衝衝地把你找來,程序卻沒有發生任何問題,你一走,錯誤又發作了。 常見的內存錯誤及其對策如下: 

     * 內存分配未成功,卻使用了它

編程新手常犯這種錯誤,因爲他們沒有意識到內存分配會不成功。常用解決辦法是,在使用內存之前檢查指針是否爲NULL。如果指針p是函數的參數,那麼在函數的入口處用assert(p!=NULL)進行檢查。如果是用malloc或new來申請內存,應該用if(p==NULL) 或if(p!=NULL)進行防錯處理。

* 內存分配雖然成功,但是尚未初始化就引用它

犯這種錯誤主要有兩個起因:一是沒有初始化的觀念;二是誤以爲內存的缺省初值全爲零,導致引用初值錯誤(例如數組)。內存的缺省初值究竟是什麼並沒有統一的標準,儘管有些時候爲零值,我們寧可信其無不可信其有。所以無論用何種方式創建數組,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。

* 內存分配成功並且已經初始化,但操作越過了內存的邊界

例如在使用數組時經常發生下標“多1”或者“少1”的操作。特別是在for循環語句中,循環次數很容易搞錯,導致數組操作越界。

* 忘記了釋放內存,造成內存泄露

含有這種錯誤的函數每被調用一次就丟失一塊內存。剛開始時系統的內存充足,你看不到錯誤。終有一次程序突然死掉,系統出現提示:內存耗盡。

動態內存的申請與釋放必須配對,程序中malloc與free的使用次數一定要相同,否則肯定有錯誤(new/delete同理)。

* 釋放了內存卻繼續使用它

有三種情況:

(1)程序中的對象調用關係過於複雜,實在難以搞清楚某個對象究竟是否已經釋放了內存,此時應該重新設計數據結構,從根本上解決對象管理的混亂局面。

(2)函數的return語句寫錯了,注意不要返回指向“棧內存”的“指針”或者“引用”,因爲該內存在函數體結束時被自動銷燬。

(3)使用free或delete釋放了內存後,沒有將指針設置爲NULL。導致產生“野指針”。

【規則1】用malloc或new申請內存之後,應該立即檢查指針值是否爲NULL。防止使用指針值爲NULL的內存。

【規則2】不要忘記爲數組和動態內存賦初值。防止將未被初始化的內存作爲右值使用。

【規則3】避免數組或指針的下標越界,特別要當心發生“多1”或者“少1”操作。

【規則4】動態內存的申請與釋放必須配對,防止內存泄漏。

【規則5】用free或delete釋放了內存之後,立即將指針設置爲NULL,防止產生“野指針”。

指針與數組的對比

C++/C程序中,指針和數組在不少地方可以相互替換着用,讓人產生一種錯覺,以爲兩者是等價的。

數組要麼在靜態存儲區被創建(如全局數組),要麼在棧上被創建。數組名對應着(而不是指向)一塊內存,其地址與容量在生命期內保持不變,只有數組的內容可以改變。

指針可以隨時指向任意類型的內存塊,它的特徵是“可變”,所以我們常用指針來操作動態內存。指針遠比數組靈活,但也更危險。

下面以字符串爲例比較指針與數組的特性

1 修改內容

下例中,字符數組a的容量是6個字符,其內容爲hello。a的內容可以改變,如a[0]= 'x'。指針p指向常量字符串"world"(位於靜態存儲區,內容爲world),常量字符串的內容是不可以被修改的。從語法上看,編譯器並不覺得語句 p[0]= 'x'有什麼不妥,但是該語句企圖修改常量字符串的內容而導致運行錯誤。

#include<iostream.h>

void main()
{
    
char a[] = "hello";
    a[
0= 'x';
    cout 
<< a << endl;
    
char *= "world"// 注意p指向常量字符串
    p[0= 'x'// 編譯器不能發現該錯誤
    cout << p << endl;
}

2 內容複製與比較

不能對數組名進行直接複製與比較。下例中,若想把數組a的內容複製給數組b,不能用語句 b = a ,否則將產生編譯錯誤。應該用標準庫函數strcpy進行復制。同理,比較b和a的內容是否相同,不能用if(b==a) 來判斷,應該用標準庫函數strcmp進行比較。

語句p = a 並不能把a的內容複製指針p,而是把a的地址賦給了p。要想複製a的內容,可以先用庫函數malloc爲p申請一塊容量爲strlen(a)+1個字符的內存,再用strcpy進行字符串複製。同理,語句if(p==a) 比較的不是內容而是地址,應該用庫函數strcmp來比較。

// 數組…
char a[] = "hello";
char b[10];
strcpy(b, a); 
// 不能用 b = a;
if(strcmp(b, a) == 0// 不能用 if (b == a)

// 指針…
int len = strlen(a);
char *= (char *)malloc(sizeof(char)*(len+1));
strcpy(p,a); 
// 不要用 p = a;
if(strcmp(p, a) == 0// 不要用 if (p == a)

 3 計算內存容量

用運算符sizeof可以計算出數組的容量(字節數)。下例(a)中,sizeof(a)的值是12(注意別忘了' ')。指針p指向a,但是 sizeof(p)的值卻是4。這是因爲sizeof(p)得到的是一個指針變量的字節數,相當於sizeof(char*),而不是p所指的內存容量。C++/C語言沒有辦法知道指針所指的內存容量,除非在申請內存時記住它。注意當數組作爲函數的參數進行傳遞時,該數組自動退化爲同類型的指針。下例(b)中,不論數組a的容量是多少,sizeof(a)始終等於sizeof(char *)。

示例(a)
char a[] = "hello world";
char *= a;
cout
<< sizeof(a) << endl; // 12字節
cout<< sizeof(p) << endl; // 4字節

示例(b)
void Func(char a[100])
{
 cout
<< sizeof(a) << endl; // 4字節而不是100字節
}

 

 

來源http://www.cnblogs.com/growup/archive/2011/06/27/2091101.html

new與malloc

(收集整理from web)

1.malloc與free是C++/C語言的標準庫函數,new/delete是C++的運算符。它們都可用於申請動態內存和釋放內存

2.對於非內部數據類型的對象而言,光用maloc/free無法滿足動態對象的要求。對象在創建的同時要自動執行構造函數,對象在消亡之前要自動執行析構函數。由malloc/free是庫函數而不是運算符,不在編譯器控制權限之內,不能夠把執行構造函數和析構函數的任務強加於malloc/free。

3.因此C++語言需要一個能完成動態內存分配和初始化工作的運算符new,以一個能完成清理與釋放內存工作的運算符delete。注意new/delete不是庫函數。
4.C++程序經常要調用C函數,而C程序只能用malloc/free管理動態內存。
5.new可以認爲是malloc加構造函數的執行。new出來的指針是直接帶類型信息的。而malloc返回的都是void*指針。

new delete在實現上其實調用了malloc,free函數

6.new建立的對象你可以把它當成一個普通的對象,用成員函數訪問,不要直接訪問它的地址空間;malloc分配的是一塊內存區域,就用指針訪問好了,而且還可以在裏面移動指針.

7.new 建立的是一個對象;alloc分配的是一塊內存.

***************************************

相同點:都可用於申請動態內存和釋放內存

不同點
(1)操作對象有所不同
malloc與free是C++/C 語言的標準庫函數,new/delete 是C++的運算符。對於非內部數據類的對象而言,光用maloc/free 無法滿足動態對象的要求。對象在創建的同時要自動執行構造函數, 對象消亡之前要自動執行析構函數。由於malloc/free 是庫函數而不是運算符,不在編譯器控制權限之內,不能夠把執行構造函數和析構函數的任務強加malloc/free。

(2)在用法上也有所不同
函數malloc 的原型如下:
void * malloc(size_t size);
用malloc 申請一塊長度爲length 的整數類型的內存,程序如下:
int *p = (int *) malloc(sizeof(int) * length);
我們應當把注意力集中在兩個要素上:“類型轉換”和“sizeof”。
malloc 返回值的類型是void *,所以在調用malloc 時要顯式地進行類型轉換,將void * 轉換成所需要的指針類型。
malloc 函數本身並不識別要申請的內存是什麼類型,它只關心內存的總字節數。

函數free 的原型如下:
void free( void * memblock );
爲什麼free 函數不象malloc 函數那樣複雜呢?這是因爲指針p 的類型以及它所指的內存的容量事先都是知道的,語句free(p)能正確地釋放內存。如果p 是NULL 指針,那麼free

對p 無論操作多少次都不會出問題。如果p 不是NULL 指針,那麼free 對p連續操作兩次就會導致程序運行錯誤。

new/delete 的使用要點
運算符new 使用起來要比函數malloc 簡單得多,例如:
int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];
這是因爲new 內置了sizeof、類型轉換和類型安全檢查功能。對於非內部數據類型的對象而言,new 在創建動態對象的同時完成了初始化工作。如果對象有多個構造函數,那麼new 的語句也可以有多種形式。

如果用new 創建對象數組,那麼只能使用對象的無參數構造函數。例如
Obj *objects = new Obj[100]; // 創建100 個動態對象
不能寫成
Obj *objects = new Obj[100](1);// 創建100 個動態對象的同時賦初值1
在用delete 釋放對象數組時,留意不要丟了符號‘[]’。例如
delete []objects; // 正確的用法
delete objects; // 錯誤的用法
後者相當於delete objects[0],漏掉了另外99 個對象。

***************************************

1  new自動計算需要分配的空間,而malloc需要手工計算字節數
2  new是類型安全的,而malloc不是,比如:
int* p = new float[2]; // 編譯時指出錯誤
int* p = malloc(2*sizeof(float)); // 編譯時無法指出錯誤
new operator 由兩步構成,分別是 operator new 和 construct
3  operator new對應於malloc,但operator new可以重載,可以自定義內存分配策略,甚至不做內存分配,甚至分配到非內存設備上。而malloc無能爲力
4  new將調用constructor,而malloc不能;delete將調用destructor,而free不能。
5  malloc/free要庫文件支持,new/delete則不要。

 

以下來源http://www.cnblogs.com/xinyuyuanm/archive/2013/05/09/3069918.html

 

內存分配C/C++堆、棧及靜態數據區詳解

本文純屬個人見解,是對面前學習的總結,如有描述不正確的地方還請高手指正~

    五大內存分區

    C++中,內存成分5個區,他們分別是堆、棧、自在存儲區、全局/態靜存儲區和常量存儲區。

    棧,就是那些由編譯器在須要的時候分配,在不須要的時候主動清晰的變量的存儲區。面裏的變量通常是局部變量、函數數參等。

    堆,就是那些由new分配的內存塊,他們的釋放編譯器不去管,由我們的用應序程去制控,一般一個new就要對應一個delete。如果序程員沒有釋放掉,那麼在序程束結後,操縱系統會主動回收。

    自在存儲區,就是那些由malloc等分配的內存塊,他和堆是十分相似的,不過它是用free來束結自己的性命的。

    全局/態靜存儲區,全局變量和態靜變量被分配到統一塊內存中,在之前的C語言中,全局變量又分爲初始化的和未初始化的,在C++面裏沒有這個分區了,他們獨特佔用統一塊內存區。

    常量存儲區,這是一塊比擬殊特的存儲區,他們面裏放存的是常量,不答應改修(當然,你要通過非合法段手也可以改修,而且方法很多)

    確明分區堆與棧

    bbs上,堆與棧的分區問題,似乎是一個恆永的話題,由此可見,初學者對此往往是淆混不清的,所以我定決拿他第一個開刀。

    首先,我們舉一個例子:

    void f() { int* p=new int[5]; }

    這條短短的一句話就包含了堆與棧,看到new,我們首先就應該想到,我們分配了一塊堆內存,那麼指針p呢?他分配的是一塊棧內存,所以這句話的意思就是:在棧內存中放存了一個指向一塊堆內存的指針p。在序程會先確定在堆中分配內存的鉅細,然後調用operator new分配內存,然後返回這塊內存的首址地,放入棧中,他在VC6下的彙編代碼如下:

    00401028 push 14h

    0040102A call operator new (00401060)

    0040102F add esp,4

    00401032 mov dword ptr [ebp-8],eax

    00401035 mov eax,dword ptr [ebp-8]

    00401038 mov dword ptr [ebp-4],eax

    這裏,我們爲了單簡並沒有釋放內存,那麼該怎麼去釋放呢?是delete p麼?澳,錯了,應該是delete []p,這是爲了訴告編譯器:我除刪的是一個組數,VC6就會根據應相的Cookie信息去行進釋放內存的作工。

    好了,我們回到我們的主題:堆和棧究竟有什麼區別?

    要主的區別由以下幾點:

    1、管理方法不同;

    2、空間鉅細不同;

    3、是否生產碎片不同;

    4、成長方向不同;

    5、分配方法不同;

    6、分配效率不同;

    管理方法:對於棧來講,是由編譯器主動管理,無需我們手工制控;對於堆來講,釋放作工由序程員制控,易容生產memory leak

    空間鉅細:一般來講在32位系統下,堆內存可以到達4G的空間,從這個度角來看堆內存幾乎是沒有什麼制約的。但是對於棧來講,一般都是有必定的空間鉅細的,例如,在VC6面下,默許的棧空間鉅細是1M(好像是,記不清晰了)。當然,我們可以改修:

    打開工程,次依操縱菜單如下:Project->Setting->Link,在Category 中中選Output,然後在Reserve中設定堆棧的最大值和commit

    注意:reserve最小值爲4Bytecommit是保留在虛擬內存的頁件文面裏,它設置的較大會使棧闢開較大的值,可能加增內存的開銷和啓動間時。

    碎片問題:對於堆來講,繁頻的new/delete勢必會形成內存空間的不續連,從而形成量大的碎片,使序程效率下降。對於棧來講,則不會存在這個問題,因爲棧是先進後出的列隊,他們是如此的一一對應,以至於永久都可不能有一個內存塊從棧間中彈出,在他彈出之前,在他面下的進後的棧內容經已被彈出,詳細的可以考參數據結構,這裏我們就不再一一探討了。

    成長方向:對於堆來講,成長方向是向上的,也就是向着內存址地加增的方向;對於棧來講,它的成長方向是向下的,是向着內存址地減小的方向長增。

    分配方法:堆都是動態分配的,沒有態靜分配的堆。棧有2種分配方法:態靜分配和動態分配。態靜分配是編譯器實現的,比如局部變量的分配。動態分配由alloca函數行進分配,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器行進釋放,無需我們手工現實。

    分配效率:棧是器機系統供給的數據結構,計算會機在底層對棧供給支撐:分配專門的寄存器放存棧的址地,壓棧出棧都有專門的指令執行,這就定決了棧的效率比擬高。堆則是C/C++函數庫供給的,它的機制是很雜復的,例如爲了分配一塊內存,庫函數會按照必定的法算(詳細的法算可以考參數據結構/操縱系統)在堆內存中索搜可用的夠足鉅細的空間,如果沒有夠足鉅細的空間(是能可由於內存碎片太多),就有可能調用系統功能去加增序程數據段的內存空間,這樣就有會機分到夠足鉅細的內存,然後行進返回。然顯,堆的效率比棧要低很多。

    從這裏我們可以看到,堆和棧比相,由於量大new/delete的用使,易容形成量大的內存碎片;由於沒有專門的系統支撐,效率很低;由於可能發引用戶態和核態心的換切,內存的請求,價值變得更加昂貴。所以棧在序程中是用應最普遍的,就算是函數的調用也利用棧去實現,函數調用過程當中的數參,返回址地,EBP和局部變量都採取棧的方法放存。所以,我們推薦大家儘量用棧,而不是用堆。

    每日一道理
一個安靜的夜晚,我獨自一人,有些空虛,有些淒涼。坐在星空下,擡頭仰望美麗天空,感覺真實卻由虛幻,閃閃爍爍,似乎看來還有些跳動。美的一切總在瞬間,如同“海市蜃樓”般,也只是剎那間的一閃而過,當天空變得明亮,而這星星也早已一同退去……

    雖然棧有如此多衆的利益,但是由於和堆比相不是那麼活靈,有時候分配量大的內存空間,還是用堆好一些。

    無論是堆還是棧,都要止防越界象現的產生(除非你是故意使其越界),因爲越界的結果要麼是序程崩潰,要麼是搗毀序程的堆、棧結構,生產以想不到的結果,就算是在你的序程行運過程當中,沒有產生面下的問題,你還是要當心,說不定什麼時候就崩掉,那時候debug可是相稱難題的:)

    對了,還有一件事,如果有人把堆棧合起來講,那它的意思是棧,可不是堆,呵呵,清晰了?

    static用來制控變量的存儲方法和可見性

    函數外部定義的變量,在序程執行到它的定義處時,編譯器爲它在棧上分配空間,函數在棧上分配的空間在此函數執行束結時會釋放掉,這樣就生產了一個問題如果想將函數中此變量的值存保至下一次調用時,如何現實? 最易容想到的方法是定義一個全局的變量,但定義爲一個全局變量有許多點缺,最顯明的點缺是壞破了此變量的問訪範圍(使得在此函數中定義的變量,不僅僅受此函數制控)。

    須要一個數據象對爲整個類而非某個象對服務,同時又力圖不壞破類的裝封性,即要求此成員隱藏在類的外部,對外可不見。

    static的外部機制:

    態靜數據成員要在序程一開始行運時就必須存在。因爲函數在序程行運中被調用,所以態靜數據成員不能在任何函數內分配空間和初始化。

    這樣,它的空間分配有三個可能的地方,一是作爲類的外部接口的頭件文,那裏有類聲明;二是類定義的外部現實,那裏有類的成員函數定義;三是用應序程的main()函數前的全局數據聲明和定義處。

    態靜數據成員要現實地分配空間,故不能在類的聲明中定義(只能聲明數據成員)。類聲明只聲明一個類的“尺寸和規格”,不併行進現實的內存分配,所以在類聲明中寫成定義是錯誤的。它也不能在頭件文中類聲明的外部定義,因爲那會形成在多個用使該類的源件文中,對其重複定義。

    static被引入以知告編譯器,將變量存儲在序程的態靜存儲區而非棧上空間,態靜

    數據成員按定義現出的先後序順次依初始化,注意態靜成員套嵌時,要保障所套嵌的成員經已初始化了。清除時的序順是初始化的反序順。

    static的優勢:

    可以節儉內存,因爲它是全部象對所私有的,因此,對多個象對來講,態靜數據成員只存儲一處,供全部象對共用。態靜數據成員的值對每一個象對都是一樣,但它的值是可以更新的。只要對態靜數據成員的值更新一次,保障全部象對存取更新後的同相的值,這樣可以進步間時效率。

    引用態靜數據成員時,採取如下格式:

    <類名>::<態靜成員名>

    如果態靜數據成員的問訪權限答應的話(public的成員),可在序程中,按上述格式

    來引用態靜數據成員。

    PS:

    (1)類的態靜成員函數是屬於整個類而非類的象對,所以它沒有this指針,這就致導

    了它僅能問訪類的態靜數據和態靜成員函數。

    (2)不能將態靜成員函數定義爲虛函數。

    (3)由於態靜成員聲明於類中,操縱於其外,所以對其取址地操縱,就多少有些殊特

    ,變量址地是指向其數據類型的指針,函數址地類型是一個“nonmember函數指針”。

    (4)由於態靜成員函數沒有this指針,所以就差多不等同於nonmember函數,結果就

    生產了一個意想不到的利益:成爲一個callback函數,使得我們得以將C++C-based X W

    indow系統結合,同時也勝利的用應於線程函數身上。

    (5)static並沒有加增序程的時空開銷,相反她還縮短了類子對類父態靜成員的問訪

    間時,節儉了類子的內存空間。

    (6)態靜數據成員在<定義或說明>時面前加關鍵字static

    (7)態靜數據成員是態靜存儲的,所以必須對它行進初始化。

    (8)態靜成員初始化與一般數據成員初始化不同:

    初始化在類體外行進,而面前不加static,以免與一般態靜變量或象對相淆混;

    初始化時不加該成員的問訪權制約控符privatepublic等;

    初始化時用使作用域運算符來明標它所屬類;

    所以我們得出態靜數據成員初始化的格式:

    <數據類型><類名>::<態靜數據成員名>=<>

    (9)爲了止防類父的影響,可以在類子定義一個與類父同相的態靜變量,以屏蔽類父的影響。這裏有一點須要注意:我們說態靜成員爲類父和類子享共,但我們有重複定義了態靜成員,這會不會發引錯誤呢?不會,我們的編譯器採取了一種絕妙的法手:name-mangling 用以成生獨一的標記。

 

 

內存池技術

http://kulong0105.blog.163.com/blog/static/1744061912011872057574/

以下來源http://blog.csdn.net/realxie/article/details/7228197

C++內存池的實現實

本節分析在某個大型應用程序實際應用到的一個內存池實現,並詳細講解其使用方法與工作原理。這是一個應用於單線程環境且分配單元大小固定的內存池,一般用來爲執行時會動態頻繁地創建且可能會被多次創建的類對象或者結構體分配內存。

本節首先講解該內存池的數據結構聲明及圖示,接着描述其原理及行爲特徵。然後逐一講解實現細節,最後介紹如何在實際程序中應用此內存池,並與使用普通內存函數申請內存的程序性能作比較。

內存池類MemoryPool的聲明如下:


 

MemoryBlock爲內存池中附着在真正用來爲內存請求分配內存的內存塊頭部的結構體,它描述了與之聯繫的內存塊的使用信息:


 

此內存池的數據結構如圖6-2所示。



圖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創建時,並沒有立刻創建真正用來滿足內存申請的內存塊,即內存塊鏈表剛開始時爲空。

②處和③處分別設置"第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提出內存請求時:


 

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的內部狀態。



圖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所示。



圖6-4  MemoryPool的結構 

可以看到,原來的第1個可供分配的單元(m編號處)已經顯示爲被分配的狀態。而pBlock的nFirst已經指向原來m單元下一個自由分配單元的編號,即k。

(3)MemoryPool回收內存時:


 

如前所述,回收分配單元時,可能會將整個內存塊返回給進程堆,也可能將被回收分配單元所屬的內存塊移至內存池的內存塊鏈表的頭部。這兩個操作都需要修改鏈表結構。這時需要知道該內存塊在鏈表中前一個位置的內存塊。

①處遍歷內存池的內存塊鏈表,確定該待回收分配單元(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-5  分配後的內存池狀態 

處理至⑥處之前,其狀態如圖6-6所示。



圖6-6  處理至⑥處之前的內存池狀態 

這裏需要注意的是,雖然pFree被"回收",但是pFree仍然指向m編號的單元,這個單元在回收過程中,其頭兩個字節被覆寫,但其他部分的內容並沒有改變。而且從整個進程的內存使用角度來看,這個m編號的單元的狀態仍然是"有效的"。因爲這裏的"回收"只是回收給了內存池,而並沒有回收給進程堆,因此程序仍然可以通過pFree訪問此單元。但是這是一個很危險的操作,因爲首先該單元在回收過程中頭兩個字節已被覆寫,並且該單元可能很快就會被內存池重新分配。因此回收後通過pFree指針對這個單元的訪問都是錯誤的,讀操作會讀到錯誤的數據,寫操作則可能會破壞程序中其他地方的數據,因此需要格外小心。

接着,需要判斷該內存塊的內部使用情況,及其在內存塊鏈表中的位置。如果該內存塊中省略號"……"所表示的其他部分中還有被分配的單元,即nFree乘以nUnitSize不等於nSize。因爲此內存塊不在鏈表頭,因此還需要將其移到鏈表頭部,如圖6-7所示。



圖6-7  因回收引起的MemoryBlock移動 

如果該內存塊中省略號"……"表示的其他部分中全部都是自由分配單元,即nFree乘以nUnitSize等於nSize。因爲此內存塊不在鏈表頭,所以此時需要將此內存塊整個回收給進程堆,回收後內存池的結構如圖6-8所示。



圖6-8  回收後內存池的結構 

一個內存塊在申請後會初始化,主要是爲了建立最初的自由分配單元鏈表,下面是其詳細代碼:


 

這裏可以看到,①處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所示爲一個內存塊初始化後的狀態。



圖6-9  一個內存塊初始化後的狀態 

當內存池析構時,需要將內存池的所有內存塊返回給進程堆:


 

分析內存池的內部原理後,本節說明如何使用它。從上面的分析可以看到,該內存池主要有兩個對外接口函數,即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章中已經討論過,因爲內存池技術使得同類型的對象分佈在相鄰的內存區域,而程序會經常對同一類型的對象進行遍歷操作。因此在程序運行過程中發生的缺頁應該會相應少一些,但這個一般只能在真實的複雜應用環境中進行驗證。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章