1. 程式人生 > >C++中的記憶體區域——堆和棧的比較

C++中的記憶體區域——堆和棧的比較

參考連結:https://blog.csdn.net/sdfgh2046/article/details/5830807

                  https://blog.csdn.net/xtzmm1215/article/details/46810597

                  https://blog.csdn.net/ltag0110rtag/article/details/7315040

1. 5大記憶體區域

C++記憶體區域分為5個區域,分別是堆,棧,自由儲存區,全域性/靜態儲存區和常量儲存區。

棧:

由編譯器在需要的時候分配,在不需要的時候自動清除的變數儲存區。裡面通常是區域性變數,函式引數等。

堆:

由new分配的記憶體塊,他們的釋放編譯器不去管,由我們的應用程式去控制,一般一個new對應一個delete。如果程式設計師沒有釋放掉,那麼在程式結束後,作業系統會自動回收。注意它與資料結構中的堆是兩回事,分配方式倒是類似於連結串列

自由儲存區:

由malloc等分配的記憶體塊,和堆十分相似,不過它使用free來結束自己的生命。

全域性/靜態儲存區:

全域性變數和靜態變數被分配到同一塊記憶體中,在以前的c語言中。全域性變數又分為初始化的和未初始化的,在c++裡面沒有這個區分了,他們共同佔用同一塊記憶體。

常量儲存區:

這是一塊比較特殊的儲存區,裡面存放的是常量,不允許修改。

 

補充:C語言中記憶體區域

  1. 棧區 :   由編譯器自動分配釋放

  2. 堆區 :   一般由程式設計師分配釋放,若程式設計師不釋放,程式結束時可能由OS回收

  3. 靜態區:全域性變數和靜態變數的儲存是放在一塊的,初始化的全域性變數和靜態變數在一塊區域,未初始化的全域性變數和未初始化的靜態變數在相鄰的另一塊區域。- 程式結束釋放

  4. 文字常量區:另外還有一個專門放字串常量的地方。- 程式結束釋放

  5. 程式程式碼區:存放2進位制程式碼。

  在函式體中定義的變數通常是在棧上,用malloc, calloc, realloc等分配記憶體的函式分配得到的就是在堆上。這種將記憶體分配由C程式控制,是很多錯誤產生的根源,忘記釋放空間會導致“記憶體洩漏”,它會逐漸耗盡大量記憶體以至於作業系統可能崩潰。過早的釋放指標會造成“懸擺指標”,會造成指標指向程式不想訪問的位置。Java使用自動的記憶體分配和無用單元回收機制來防止類似的錯誤發生。在所有函式體外定義的是全域性量,加了static修飾符後不管在哪裡都存放在全域性區(靜態區),在所有函式體外定義的static變量表示在該檔案中有效,不能extern到別的檔案用,在函式體內定義的static表示只在該函式體內有效。另外,函式中的“adgfdf”這樣的字串存放在常量區。

2.示例程式

int a = 0; //全域性初始化區 
char *p1; //全域性未初始化區 
int main() 
{ 
    int b;//棧 
    char s[] = "abc";//棧 
    char *p2; //棧 
    char *p3 = "123456"; //123456/0在常量區,p3在棧上。 
    static int c =0; //全域性(靜態)初始化區 
    p1 = (char *)malloc(10); 
    p2 = (char *)malloc(20); 
    //分配得來得10和20位元組的區域就在堆區。 
    strcpy(p1, "123456"); //123456/0放在常量區,編譯器可能會將它與p3所指向的"123456"優化成一個地方。 
} 

3.堆疊理論知識


(1)申請方式 


棧:
由系統自動分配。 例如,宣告在函式中一個區域性變數 int b; 系統自動在棧中為b開闢空間 


堆:
需要程式設計師自己申請,並指明大小,在c中malloc函式 
如p1 = (char *)malloc(10); 
在C++中用new運算子 
如p2 = (char *)malloc(10); 
但是注意p1、p2本身是在棧中的。 


(2)申請後系統的響應 


棧:只要棧的剩餘空間大於所申請空間,系統將為程式提供記憶體,否則將報異常提示棧溢位。 


堆:首先應該知道作業系統有一個記錄空閒記憶體地址的連結串列,當系統收到程式的申請時, 
會 遍歷該連結串列,尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閒結點連結串列中刪除,並將該結點的空間分配給程式,另外,對於大多數系統,會在這塊內 存空間中的首地址處記錄本次分配的大小,這樣,程式碼中的delete語句才能正確的釋放本記憶體空間。另外,由於找到的堆結點的大小不一定正好等於申請的大 小,系統會自動的將多餘的那部分重新放入空閒連結串列中。 

(3)申請大小的限制 


棧:在Windows下,棧是向低地址擴充套件的資料結 構,是一塊連續的記憶體的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在WINDOWS下,棧的大小是2M(也有的說是1M,總之是 一個編譯時就確定的常數),如果申請的空間超過棧的剩餘空間時,將提示overflow。因此,能從棧獲得的空間較小。 


堆:堆是向高地址擴充套件的資料結構,是不連續的記憶體區域。這是由於系統是用連結串列來儲存的空閒記憶體地址的,自然是不連續的,而連結串列的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬記憶體。由此可見,堆獲得的空間比較靈活,也比較大。 


(4)申請效率的比較: 


棧由系統自動分配,速度較快。但程式設計師是無法控制的。

 
堆是由new分配的記憶體,一般速度比較慢,而且容易產生記憶體碎片,不過用起來最方便. 
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配記憶體,他不是在堆,也不是在棧是直接在程序的地址空間中保留一快記憶體,雖然用起來最不方便。但是速度快,也最靈活。 

(5)堆和棧中的儲存內容 


棧: 在函式呼叫時,第一個進棧的是主函式中後的下一條指令(函式呼叫語句的下一條可執行語句)的地址,然後是函式的各個引數,在大多數的C編譯器中,引數是由右往左入棧的,然後是函式中的區域性變數。注意靜態變數是不入棧的。 
當本次函式呼叫結束後,區域性變數先出棧,然後是引數,最後棧頂指標指向最開始存的地址,也就是主函式中的下一條指令,程式由該點繼續執行。 


堆:一般是在堆的頭部用一個位元組存放堆的大小。堆中的具體內容有程式設計師安排。 

(6)存取效率的比較 

char s1[] = "aaaaaaaaaaaaaaa"; 
char *s2 = "bbbbbbbbbbbbbbbbb"; 
aaaaaaaaaaa是在執行時刻賦值的; 
而bbbbbbbbbbb是在編譯時就確定的; 
但是,在以後的存取中,在棧上的陣列比指標所指向的字串(例如堆)快。 
比如: 

void main() 
{ 
    char a = 1; 
    char c[] = "1234567890"; 
    char *p ="1234567890"; 
    a = c[1]; 
    a = p[1]; 
    return; 
} 


對應的彙編程式碼 

10: a = c[1]; 
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh] 
0040106A 88 4D FC mov byte ptr [ebp-4],cl 
11: a = p[1]; 
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h] 
00401070 8A 42 01 mov al,byte ptr [edx+1] 
00401073 88 45 FC mov byte ptr [ebp-4],al 


第一種在讀取時直接就把字串中的元素讀到暫存器cl中,而第二種則要先把指標值讀到edx中,在根據edx讀取字元,顯然慢了。 


(7)小結:


堆和棧的區別可以用如下的比喻來看出: 
使用棧就象我們去飯館裡吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。 
使用堆就象是自己動手做喜歡吃的菜餚,比較麻煩,但是比較符合自己的口味,而且自由度大。 

4.C++記憶體區域中堆和棧的區別:

 具體地說,現代計算機(序列執行機制),都直接在程式碼底層支援棧的資料結構。這體現在,有專門的暫存器指向棧所在的地址,有專門的機器指令完成資料入棧出棧的操作。這種機制的特點是效率高,支援的資料有限,一般是整數,指標,浮點數等系統直接支援的資料型別,並不直接支援其他的資料結構。因為棧的這種特點,對棧的使用在程式中是非常頻繁的。對子程式的呼叫就是直接利用棧完成的。機器的call指令裡隱含了把返回地址推入棧,然後跳轉至子程式地址的操作,而子程式中的ret指令則隱含從堆疊中彈出返回地址並跳轉之的操作。C/C++中的自動變數是直接利用棧的例子,這也就是為什麼當函式返回時,該函式的自動變數自動失效的原因。

  和棧不同,堆的資料結構並不是由系統(無論是機器系統還是作業系統)支援的,而是由函式庫提供的。基本的malloc/realloc/free 函式維護了一套內部的堆資料結構。當程式使用這些函式去獲得新的記憶體空間時,這套函式首先試圖從內部堆中尋找可用的記憶體空間,如果沒有可以使用的記憶體空間,則試圖利用系統呼叫來動態增加程式資料段的記憶體大小,新分配得到的空間首先被組織進內部堆中去,然後再以適當的形式返回給呼叫者。當程式釋放分配的記憶體空間時,這片記憶體空間被返回內部堆結構中,可能會被適當的處理(比如和其他空閒空間合併成更大的空閒空間),以更適合下一次記憶體分配申請。這套複雜的分配機制實際上相當於一個記憶體分配的緩衝池(Cache),使用這套機制有如下若干原因:

  1. 系統呼叫可能不支援任意大小的記憶體分配。有些系統的系統呼叫只支援固定大小及其倍數的記憶體請求(按頁分配);這樣的話對於大量的小記憶體分類來說會造成浪費。

  2. 系統呼叫申請記憶體可能是代價昂貴的。系統呼叫可能涉及使用者態和核心態的轉換。

  3. 沒有管理的記憶體分配在大量複雜記憶體的分配釋放操作下很容易造成記憶體碎片。

從以上知識可知,棧是系統提供的功能,特點是快速高效,缺點是有限制,資料不靈活;而棧是函式庫提供的功能,特點是靈活方便,資料適應面廣泛,但是效率有一定降低。棧是系統資料結構,對於程序/執行緒是唯一的;堆是函式庫內部資料結構,不一定唯一。不同堆分配的記憶體無法互相操作。棧空間分靜態分配和動態分配兩種。靜態分配是編譯器完成的,比如自動變數(auto)的分配。動態分配由alloca函式完成。棧的動態分配無需釋放(是自動的),也就沒有釋放函式。為可移植的程式起見,棧的動態分配操作是不被鼓勵的!堆空間的分配總是動態的,雖然程式結束時所有的資料空間都會被釋放回系統,但是精確的申請記憶體/ 釋放記憶體匹配是良好程式的基本要素。

管理方式不同

棧是由編譯器自動管理,無需我們手工控制;對於堆來說,釋放由程式設計師完成,容易產生記憶體洩漏。

空間大小不同:

一般來講,在32為系統下面,堆記憶體可達到4G的空間,從這個角度來看堆記憶體幾乎是沒有什麼限制的。但是對於棧來講,一般都是有一定空間大小的,例如,在vc6下面,預設的棧大小好像是1M。當然,也可以自己修改:開啟工程。 project-->setting-->link,在category中選中output,然後再reserve中設定堆疊的最大值和 commit。

能否產生碎片:

對於堆來講,頻繁的new/delete勢必會造成記憶體空間的不連續,從而造成大量的碎片,使程式效率降低。對於棧來講,則不會存在這個問題。

生長方向不同:

對於堆來講,生長方向是向上的,也就是向著記憶體地址增加的方向;對於棧來講,它的生長方式是向下的,是向著記憶體地址減小的方向增長。

分配方式不同:

堆都是動態分配的;棧有靜態和動態兩種分配方式。靜態分配由編譯器完成,比如區域性變數的分配。動態分配由alloca函式進行、但棧的動態分配和堆是不同的,它的動態分配由編譯器進行釋放,無需我們手工實現。

分配效率不同:

棧是機器系統提供的資料結構,計算機會在底層對棧提供支援:分配專門的暫存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。堆則是c/c++庫函式提供的,機制很複雜。庫函式會按照一定的演算法進行分配。顯然,堆的效率比棧要低得多。

 

程序記憶體中的映像,主要有程式碼區,堆(動態儲存區,new/delete的動態資料),棧,靜態儲存區

記憶體區域地址從低到高的方向:程式碼區,靜態儲存區,堆,棧

堆”和“棧”是獨立的概念平常說的“堆疊”實際上是兩個概念:“堆”和“棧”。在英文中,堆是heap,棧是stack,不知道什麼時候,什麼原因,在中文裡,這兩個不同的概念硬是被搞在一起了,所以,圍繞這個混合詞所發生的誤解和爭執這幾年就沒有斷過。 

“棧”一般是由硬體(CPU)實現的,CPU用棧來儲存呼叫子程式(函式)時的返回地址,高階語言有時也用它作為區域性變數的儲存空間。 

“堆”是個實實在在的軟體概念,使用與否完全由程式設計者“顯示地(explicitly)”決定,如malloc。 

程式經過編譯連線生成執行程式後,堆和棧的起始地址就已經確定了(具體說,是通過“連線程式”),在一個具有反向增長的棧的CPU上,資料空間可表示如下: 

低    ->|-----------------| 
      | 全域性量(所有已初始化量 .data, | 
      | 未初始化量 .bss )       | 
  堆起始->|-----------------| 
      |    堆向高地址增長      | 
      |                 | 
      |                 | 
      |     自由空間        | 
      |                 | 
      |                 | 
      |    棧向低地址增長      | 
高 棧起始->|-----------------| 

程式示例說明:


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

     這條短短的一句話就包含了堆與棧,看到new,我們首先就應該想到,我們分配了一塊堆記憶體,那麼指標p呢?他分配的是一塊棧記憶體,所以這句話的意思就是:在棧記憶體中存放了一個指向一塊堆記憶體的指標p.在程式會先確定在堆中分配記憶體的大小,然後呼叫operator new分配記憶體,然後返回這塊記憶體的首地址,放入棧中,他在VC6下的彙編程式碼如下:  這條短短的一句話就包含了堆與棧,看到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麼?這是為了告訴編譯器:我刪除的是一個數組,VC6就會根據相應的Cookie資訊去進行釋放記憶體的工作。


5.堆疊衝突


在記憶體中,“堆”和“棧”共用全部的自由空間,只不過各自的起始地址和增長方向不同,它們之間並沒有一個固定的界限,如果在執行時,“堆”和 “棧”增長到發生了相互覆蓋時,稱為“棧堆衝突”,系統肯定垮臺。由於開銷方面的原因,各種編譯在實現中都沒有考慮解決這個問題,只有靠設計者自己解決,比如增加記憶體等。