堆和棧的區別(轉)

幾天前被人問到這個問題,一時半會組織不上來,人家還追問,win32可以申請的棧的最大空間是多少?爲什麼是這麼大?什麼時候最常出現棧溢出等等。所以今天把這個帖子摘出來,供大家穩固和學習。

原帖地址:http://bbs.cup.edu.cn/cupbbs/ReplyList.aspx?tid=86696&fid=54

鋪墊:鏈表與數組的區別


A 從邏輯結構來看
A-1. 數組必須事先定義固定的長度(元素個數),不能適應數據動態地增減的情況。當數據增加時,可能超出原先定義的元素個數;當數據減少時,造成內存浪費。

A-2. 鏈表動態地進行存儲分配,可以適應數據動態地增減的情況,且可以方便地插入、刪除數據項。(數組中插入、刪除數據項時,需要移動其它數據項)


B 從內存存儲來看
B-1. (靜態)數組從棧中分配空間, 對於程序員方便快速,但是自由度小
B-2. 鏈表從堆中分配空間, 自由度大但是申請管理比較麻煩

堆和棧的區別

一、預備知識—程序的內存分配
一個由c/C++編譯的程序佔用的內存分爲以下幾個部分
1、棧區(stack)—   由編譯器(Compiler)自動分配釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。
2、堆區(heap) —   一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。注意它與數據結構中的堆是兩回事,分配方式倒是類似於鏈表,呵呵。
3、全局區(靜態區)(static)—,全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。 - 程序結束後有系統釋放 
4、文字常量區  — 常量字符串就是放在這裏的。 程序結束後由系統釋放
5、程序代碼區— 存放函數體的二進制代碼。



二、例子程序 
這是一個前輩寫的,非常詳細 
//main.cpp 
int a = 0; 全局初始化區 
char *p1; 全局未初始化區 
main() 

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



二、堆和棧的理論知識 
2.1申請方式 
stack: 
由系統自動分配。 例如,聲明在函數中一個局部變量 int b; 系統自動在棧中爲b開闢空間 
heap: 
需要程序員自己申請,並指明大小,在c中malloc函數 
如p1 = (char *)malloc(10); 
在C++中用new運算符 
如p2 = (char *)malloc(10); 
但是注意p1、p2本身是在棧中的。 


2.2 申請後系統的響應 
棧:只要棧的剩餘空間大於所申請空間,系統將爲程序提供內存,否則將報異常提示棧溢出。 
堆:首先應該知道操作系統有一個記錄空閒內存地址的鏈表,當系統收到程序的申請時, 
會遍歷該鏈表,尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閒結點鏈表中刪除,並將該結點的空間分配給程序,另外,對於大多數系統,會在這塊內存空間中的首地址處記錄本次分配的大小,這樣,代碼中的delete語句才能正確的釋放本內存空間。另外,由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多餘的那部分重新放入空閒鏈表中。 

2.3申請大小的限制 
棧:在Windows下, 棧是向低地址擴展的數據結構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩餘空間時,將提示overflow。因此,能從棧獲得的空間較小。 
堆:堆是向高地址擴展的數據結構,是不連續的內存區域。這是由於系統是用鏈表來存儲的空閒內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。 


2.4申請效率的比較: 
棧由系統自動分配,速度較快。但程序員是無法控制的。 
堆是由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便. 
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配內存,他不是在堆,也不是在棧是直接在進程的地址空間中保留一快內存,雖然用起來最不方便。但是速度快,也最靈活。 

2.5堆和棧中的存儲內容 
棧: 在函數調用時,(1) 第一個進棧的是主函數中後的下一條指令(函數調用語句的下一條可執行語句)的地址,(2) 然後是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的,(3) 然後是函數中的局部變量。 注意: 靜態變量是不入棧的。 
當本次函數調用結束後,(1) 局部變量先出棧,(2) 然後是參數,(3) 最後棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。
堆:一般是在堆的頭部用一個字節存放堆的大小。堆中的具體內容有程序員安排。 

2.6存取效率的比較 
char s1[] = "aaaaaaaaaaaaaaa"; 
char *s2 = "bbbbbbbbbbbbbbbbb"; 
aaaaaaaaaaa是在運行時刻賦值的; 
而bbbbbbbbbbb是在編譯時就確定的; 
但是,在以後的存取中,在棧上的數組比指針所指向的字符串(例如堆)快。 
比如: 
#include 
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讀取字符,顯然慢了。 


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

 

 

 

堆和棧(2) 
  堆(heap)和棧(stack)是C/C++編程不可避免會碰到的兩個基本概念。首先,這兩個概念都可以在講數據結構的書中找到,他們都是基本的數據結構,雖然棧更爲簡單一些。在具體的C/C++編程框架中,這兩個概念並不是並行的。對底層機器代碼的研究可以揭示,棧是機器系統提供的數據結構,而堆則是C/C++函數庫提供的。

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

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

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

  2. 系統調用申請內存可能是代價昂貴的。系統調用可能涉及用戶態和核心態的轉換。

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

堆和棧的對比

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

 

 

進程在內存中的影像.  
  我們假設現在有一個程序, 它的函數調用順序如下.  
  main(...) -> func_1(...) -> func_2(...) -> func_3(...)  
  即: 主函數main調用函數func_1; 函數func_1調用函數func_2; 函數func_2調用函數func_3  

  當程序被操作系統調入內存運行, 其相對應的進程在內存中的影像如下圖所示.  

  (內存高址)  
  +--------------------------------------+  
  |             ......                   |  ... 省略了一些我們不需要關心的區  
  +--------------------------------------+  
  |  env strings (環境變量字串)          | /  
  +--------------------------------------+  /  
  |  argv strings (命令行字串)           |   /  
  +--------------------------------------+    /  
  |  env pointers (環境變量指針)         |    SHELL的環境變量和命令行參數保存區  
  +--------------------------------------+    /  
  |  argv pointers (命令行參數指針)      |   /  
  +--------------------------------------+  /  
  |  argc (命令行參數個數)               | /  
  +--------------------------------------+  
  |            main 函數的棧幀           | /  
  +--------------------------------------+  /  
  |            func_1 函數的棧幀         |   /  
  +--------------------------------------+    /  
  |            func_2 函數的棧幀         |     /  
  +--------------------------------------+      /  
  |            func_3 函數的棧幀         |      Stack (棧)  
  +......................................+      /  
  |                                      |     /  
  ......                        /  
  |                                      |   /  
  +......................................+  /  
  |            Heap (堆)                 | /  
  +--------------------------------------+  
  |        Uninitialised (BSS) data      |  非初始化數據(BSS)區  
  +--------------------------------------+  
  |        Initialised data              |  初始化數據區  
  +--------------------------------------+  
  |        Text                          |  文本區  
  +--------------------------------------+  
  (內存低址)  

  這裏需要說明的是:  
  i)   隨着函數調用層數的增加, 函數棧幀是一塊塊地向內存低地址方向延伸的.  
  隨着進程中函數調用層數的減少, 即各函數調用的返回, 棧幀會一塊塊地  
  被遺棄而向內存的高址方向回縮.  
  各函數的棧幀大小隨着函數的性質的不同而不等, 由函數的局部變量的數目決定.  
  ii)  進程對內存的動態申請是發生在Heap(堆)裏的. 也就是說, 隨着系統動態分  
  配給進程的內存數量的增加, Heap(堆)有可能向高址或低址延伸, 依賴於不  
  同CPU的實現. 但一般來說是向內存的高地址方向增長的.  
  iii) 在BSS數據或者Stack(棧)的增長耗盡了系統分配給進程的自由內存的情況下,  
  進程將會被阻塞, 重新被操作系統用更大的內存模塊來調度運行.  
  (雖然和exploit沒有關係, 但是知道一下還是有好處的)  
  iv)  函數的棧幀裏包含了函數的參數(至於被調用函數的參數是放在調用函數的棧  
  幀還是被調用函數棧幀, 則依賴於不同系統的實現),  
  它的局部變量以及恢復調用該函數的函數的棧幀(也就是前一個棧幀)所需要的  
  數據, 其中包含了調用函數的下一條執行指令的地址.  
  v)   非初始化數據(BSS)區用於存放程序的靜態變量, 這部分內存都是被初始化爲零的.  
  初始化數據區用於存放可執行文件裏的初始化數據.  
  這兩個區統稱爲數據區.  
  vi)  Text(文本區)是個只讀區, 任何嘗試對該區的寫操作會導致段違法出錯. 文本區  
  是被多個運行該可執行文件的進程所共享的. 文本區存放了程序的代碼.  

  2) 函數的棧幀.  
  函數調用時所建立的棧幀包含了下面的信息:  
  i)   函數的返回地址. 返回地址是存放在調用函數的棧幀還是被調用函數的棧幀裏,  
  取決於不同系統的實現.  
  ii)  調用函數的棧幀信息, 即棧頂和棧底.  
  iii) 爲函數的局部變量分配的空間  
  iv)  爲被調用函數的參數分配的空間--取決於不同系統的實現. 
 

發佈了102 篇原創文章 · 獲贊 22 · 訪問量 24萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章