內存分配方式和控制內存分配

    內存管理是C++最令人切齒痛恨的問題,也是C++最有爭議的問題,C++高手從中獲得了更好的性能,更大的自由,C++菜鳥的收穫則是一遍一遍的檢查代碼和對C++的痛恨,但內存管理在C++中無處不在,內存泄漏幾乎在每個C++程序中都會發生,因此要想成爲C++高手,內存管理一關是必須要過的,除非放棄C++,轉到Java或者.NET,他們的內存管理基本是自動的,當然你也放棄了自由和對內存的支配權,還放棄了C++超絕的性能。

 

一、內存分配方式

 

1、簡介

 

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

  • <font size=4>:在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限。
  • <font size=4>:就是那些由 new分配的內存塊,他們的釋放編譯器不去管,由我們的應用程序去控制,一般一個new就要對應一個 delete。如果程序員沒有釋放掉,那麼在程序結束後,操作系統會自動回收。
  • <font size=4>自由存儲區:就是那些由malloc等分配的內存塊,他和堆是十分相似的,不過它是用free來結束自己的生命的。
  • <font size=4>全局/靜態存儲區全局變量和靜態變量被分配到同一塊內存中,在以前的C語言中,全局變量又分爲初始化的和未初始化的,在C++裏面沒有這個區分了,他們共同佔用同一塊內存區。
  • <font size=4>常量存儲區:這是一塊比較特殊的存儲區,他們裏面存放的是常量,不允許修改。


2、明確區分堆與棧

 

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

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

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

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

堆和棧究竟有什麼區別

    好了,我們回到我們的主題:堆和棧究竟有什麼區別?
 主要的區別由以下幾點:
 (1). 管理方式不同
 (2). 空間大小不同
 (3). 能否產生碎片不同
 (4). 生長方向不同
 (5). 分配方式不同
 (6). 分配效率不同
 

    管理方式對於來講,是由編譯器自動管理,無需我們手工控制;對於來說,釋放工作由程序員控制,容易產生memory leak。
 

    空間大小:一般來講在32位系統下,堆內存可以達到4G的空間,從這個角度來看堆內存幾乎是沒有什麼限制的。但是對於棧來講,一般都是有一定的空間大小的,例如,在VC6下面,默認的棧空間大小是1M(好像是,記不清楚了)。當然,我們可以修改:
 打開工程,依次操作菜單如下:Project->Setting->Link,在Category 中選中Output,然後在Reserve中設定堆棧的最大值和commit。
 注意:reserve最小值爲4Byte;commit是保留在虛擬內存的頁文件裏面,它設置的較大會使棧開闢較大的值,可能增加內存的開銷和啓動時間。

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

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

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

 分配效率:棧是機器系統提供的數據結構,計算機會在底層對棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高堆則是C/C++函數庫提供的它的機制是很複雜的,例如爲了分配一塊內存,庫函數會按照一定的算法(具體的算法可以參考數據結構/操作系統)在堆內存中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由於內存碎片太多),就有可能調用系統功能去增加程序數據段的內存空間,這樣就有機會分到足夠大小的內存,然後進行返回。顯然,堆的效率比棧要低得多。
 從這裏我們可以看到,堆和棧相比,由於大量new/delete的使用,容易造成大量的內存碎片;由於沒有專門的系統支持,效率很低;由於可能引發用戶態和核心態的切換,內存的申請,代價變得更加昂貴。所以棧在程序中是應用最廣泛的,就算是函數的調用也利用棧去完成,函數調用過程中的參數,返回地址,EBP和局部變量都採用棧的方式存放。所以,我們推薦大家儘量用棧,而不是用堆。
 雖然棧有如此衆多的好處,但是由於和堆相比不是那麼靈活,有時候分配大量的內存空間,還是用堆好一些。
 無論是堆還是棧,都要防止越界現象的發生(除非你是故意使其越界),因爲越界的結果要麼是程序崩潰,要麼是摧毀程序的堆、棧結構,產生以想不到的結果,就算是在你的程序運行過程中,沒有發生上面的問題,你還是要小心,說不定什麼時候就崩掉,那時候debug可是相當困難的。

 

二、控制內存分配

 

    在嵌入式系統中使用C++的一個常見問題是內存分配,即對new 和 delete 操作符的失控。
 具有諷刺意味的是,問題的根源卻是C++對內存的管理非常的容易而且安全。具體地說,當一個對象被消除時,它的析構函數能夠安全的釋放所分配的內存。
 這當然是個好事情,但是這種使用的簡單性使得程序員們過度使用new 和 delete,而不注意在嵌入式C++環境中的因果關係。並且,在嵌入式系統中,由於內存的限制,頻繁的動態分配不定大小的內存會引起很大的問題以及堆破碎的風險。
 作爲忠告,保守的使用內存分配是嵌入式環境中的第一原則
 但當你必須要使用new和delete時,你不得不控制C++中的內存分配。你需要用一個全局的new 和delete來代替系統的內存分配符,並且一個類一個類的重載new和delete。
 一個防止堆破碎的通用方法是從不同固定大小的內存持中分配不同類型的對象。對每個類重載new 和delete就提供了這樣的控制。

 

1、重載全局的new和delete操作符


 可以很容易地重載new 和 delete 操作符,如下所示:

 

  1. void * operator new(size_t size){
  2. void *p = malloc(size);
  3. return (p);
  4. }
  5. void operator delete(void *p){
  6. free(p);
  7. }

    這段代碼可以代替默認的操作符來滿足內存分配的請求。出於解釋C++的目的,我們也可以直接調用malloc() 和free()。
 也可以對單個類的new 和 delete操作符重載。這是你能靈活的控制對象的內存分配。

 

  1. #include<iostream>
  2. #include<malloc.h>
  3. using namespace std;
  4. class TestClass
  5. {
  6. public:
  7. void * operator new(size_t size);//size_t爲無符號整形
  8. void operator delete(void *p);
  9. void * operator new [] (size_t size);
  10. void operator delete [] (void *p);
  11. };
  12. void *TestClass::operator new(size_t size)
  13. {
  14. void *p=malloc(size);
  15. return p;
  16. }
  17. void TestClass::operator delete(void *p)
  18. {
  19. cout << (int)p << endl;
  20. free(p);
  21. }
  22. void *TestClass::operator new [] (size_t size)
  23. {
  24. void *p = malloc(size);
  25. return (p);
  26. }
  27. void TestClass::operator delete [] (void *p)
  28. {
  29. cout << (int)p << endl;
  30. free(p);
  31. }
  32. int main(void)
  33. {
  34. TestClass *p1 = new TestClass;
  35. delete p1;
  36. TestClass *p2 = new TestClass[10];
  37. delete [] p2;
  38. system("pause");
  39. }

    但是注意:對於多數C++的實現,new[]操作符中的個數參數是數組的大小加上額外的存儲對象數目的一些字節。在你的內存分配機制重要考慮的這一點。你應該儘量避免分配對象數組,從而使你的內存分配策略簡單。
 

三、關於內存碎片

 

    首先看碎片,32位系統的內存是按“頁”管理的,一頁內存爲64K,只有當前在使用的頁面纔會在內存中,其他頁面不一定總是在內存。因此,分配連續的內存時,當請求少於64K,系統會盡量將其分配在一個內存頁面中,當大於64K時,會分配在連續的頁面中。以實例說明,例如首先請求30K內存,那麼會在第一頁分配,第二次請求50K,就必須在第二頁面分配。於是第一次返回的地址爲0,第二次爲64K,30K~64K之間的內存就是所謂的“碎片”。可以將“碎片”簡單理解爲兩塊已分配內存之間的空間。當然,“碎片”也可能被利用,但是考慮一種極端的情況:如果一臺電腦擁有4G內存,但是每個頁面都只分配了32K,那麼你將不能申請一片大於32K的內存,即使目前你的物理內存還有2G沒有使用。這種情況是很常見的,當程序比較大時如果你沒作好這些管理,你會發現new個3~5百MB內存會經常失敗。
    系統中的new會實施一些算法或者策略,防止內存碎片過快產生,這些算法類似於數據結構中的“堆”,所以new被稱之爲“堆分配”。但是,系統的堆管理策略是宏觀的,通用的。你只要使用它,一定會產生內存碎片。同時,隨着堆的規模的加大,會有很多時間浪費在頁面在主存與虛擬內存的交換中,這是因爲一般情況下,系統返回給你的內存指針是不能改變的,試想你的程序中new了一個新指針,可這片內存不知什麼時候被系統換到其他地方了,那麼你的程序離奔潰就不遠了。這說明在C++中,出現內存碎片後系統是無法執行“碎片整理”的,然而在底層的windows接口中,你能使用“內存句柄”代替指針,內存句柄代表的內存是操作系統管理的而不是地址本身,因此這種情況下操作系統能幫你完成“碎片整理”。只是,內存句柄即使在微軟自己的平臺上也不及指針通用,各種內庫的接口中,絕大部分只認指針,因此你可能在“內存句柄”與指針間不停轉換消耗掉程序的時間。
    重寫內存管理要根據實際需要,這沒有統一的方法。最簡單的做法是讓new返回一個全局數組的地址。全局數組的內存空間在程序啓動時就初始化好了,因此你立即就能獲取到地址並且這個分配一定是成功的(要是內存耗盡,程序會在啓動時掛掉)。全局數組好處在於它的內存一定是連續的,32位系統上能保證2G左右的長度,因此對於操作系統而言可以做到消除碎片,至於如何高效使用這些內存就是程序員的事情了。最後,全局數組的方法也要注意64K對其,否則程序性能會因爲內存交換收到影響,特別是當內存使用量很大的時候。

參考:《c++內存管理技術內幕》

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