C++堆疊詳解

天涯問路人發表於2016-09-27

C++中堆和棧的完全解析

記憶體分配方面:

堆:作業系統有一個記錄空閒記憶體地址的連結串列,當系統收到程式的申請時,會遍歷該連結串列,尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閒結點連結串列中刪除,並將該結點的空間分配給程式,另外,對於大多數系統,會在這塊記憶體空間中的首地址處記錄本次分配的大小,這樣程式碼 中的delete語句才能正確的釋放本記憶體空間。我們常說的記憶體洩露,最常見的就是堆洩露(還有資源洩露),它是指程式在執行中出現洩露,如果程式被關閉掉的話,作業系統會幫助釋放洩露的記憶體。

棧:在函式呼叫時第一個進棧的主函式中的下一條指令(函式呼叫語句的下一條可執行語句)的地址然後是函式 的各個引數,在大多數的C編譯器中,引數是由右往左入棧,然後是函式中的區域性變數。

 

一、預備知識—程式的記憶體分配

一個由c/C++編譯的程式佔用的記憶體分為以下幾個部分

1、棧區(stack)— 由編譯器自動分配釋放 ,存放函式的引數值,區域性變數的值等。其操作方式類似於資料結構中的棧。

2、堆區(heap) — 一般由程式設計師分配釋放, 若程式設計師不釋放,程式結束時可能由OS回收 。注意它與資料結構中的堆是兩回事,分配方式倒是類似於連結串列,呵呵。

3、全域性區(靜態區)(static)—,全域性變數和靜態變數的儲存是放在一塊的,初始化的全域性變數和靜態變數在一塊區域, 未初始化的全域性變數和未初始化的靜態變數在相鄰的另一塊區域。 - 程式結束後有系統釋放

4、文字常量區 —常量字串就是放在這裡的。 程式結束後由系統釋放

5、程式程式碼區—存放函式體的二進位制程式碼。

 

有些說法,把3,4合在一起,也有的把3分成自由儲存區(malloc/free)和全域性/靜態儲存區。

這與編譯器和作業系統有關。

 

二、例子程式

這是一個前輩寫的,非常詳細

//main.cpp

int a = 0; 全域性初始化區

char *p1; 全域性未初始化區

main()

{

int b; 棧

char s[] = "abc"; 棧 //更正: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"優化成一個地方。

}

二、堆和棧的理論知識

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堆和棧中的儲存內容

棧: 在函式呼叫時,第一個進棧的是主函式的下一條指令(函式呼叫語句的下一條可執行語句)的地址,然後是函式的各個引數,在大多數的C編譯器中,引數是由右往左入棧的,然後是函式中的區域性變數。注意靜態變數是不入棧的。

當本次函式呼叫結束後,區域性變數先出棧,然後是引數,最後棧頂指標指向最開始存的地址,也就是主函式中的下一條指令,程式由該點繼續執行。

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

2.6存取效率的比較

 

char s1[] = "aaaaaaaaaaaaaaa";

char *s2 = "bbbbbbbbbbbbbbbbb";

aaaaaaaaaaa是在執行時刻賦值的;

而bbbbbbbbbbb是在編譯時就確定的;

但是,在以後的存取中,在棧上的陣列比指標所指向的字串(例如堆)快。

比如:

#i nclude

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小結:

堆和棧的區別可以用如下的比喻來看出:

使用棧就象我們去飯館裡吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。

使用堆就象是自己動手做喜歡吃的菜餚,比較麻煩,但是比較符合自己的口味,而且自由度大。

 

堆和棧的區別主要分:

作業系統方面的堆和棧,如上面說的那些,不多說了。

還有就是資料結構方面的堆和棧,這些都是不同的概念。這裡的堆實際上指的就是(滿足堆性質的)優先佇列的一種資料結構,第1個元素有最高的優先權;棧實際上就是滿足先進後出的性質的數學或資料結構。

雖然堆疊,堆疊的說法是連起來叫,但是他們還是有很大區別的,連著叫只是由於歷史的原因。

 

2.8 補充知識: 

堆(heap)和棧(stack)是C/C++程式設計不可避免會碰到的兩個基本概念。首先,這兩個概念都可以在講資料結構的書中找到,他們都是基本的資料結構,雖然棧更為簡單一些。

在具體的C/C++程式設計框架中,這兩個概念並不是並行的。對底層機器程式碼的研究可以揭示,棧是機器系統提供的資料結構,而堆則是C/C++函式庫提供的。

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

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

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

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

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

堆和棧的對比

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

堆和棧究竟有什麼區別?

    主要的區別由以下幾點:

    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可是相當困難的:)

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

棧到底是什麼東西

MSDN中關於Thread   Stack   Size有這麼一段話:        

Platform   SDK:   DLLs,   Processes,   and   Threads    

Thread   Stack   Size     

Each   new   thread   receives   its   own   stack   space,   consisting   of   both   committed   and   reserved  memory.   The   system   will   commit   one   page   blocks   from   the   reserved   stack   memory   as   needed,  until   the   stack   cannot   grow   any   farther.  

The   default   size   for   committed   and   reserved   memory   is   specified   in   the   executable   file   header.  The   default   reserved   memory   is   one   megabyte.   To   specify   a   different   default   stack   size,   use  the   STACKSIZE   statement   in   the   module   definition   (.DEF)   file.   Your   linker   may   also   support   a  command-line   option   for   setting   the   stack   size.   For   more   information,   see   the   documentation  included   with   your   linker.    

Threads   that   call   the   C   run-time   library   or   the   Windows   API   must   allow   sufficient   stack   space  for   use   of   these   functions.   Do   not   lower   the   reserved   stack   size   below   64   KB.      

To   increase   the   amount   of   stack   space   which   is   to   be   initially   committed   for   a   thread,   specify  the   value   in   the   dwStackSize   parameter   of   the   CreateThread   or   CreateRemoteThread   function.  This   value   is   rounded   to   the   nearest   page.   The   call   to   create   the   thread   fails   if   there   is   not  enough   memory   to   commit   or   reserve   the   number   of   bytes   requested.   If   dwStackSize   is   smaller  than   the   default   reserve   size,   the   new   thread   uses   the   default   reserve   size.   If   dwStackSize   is  larger   than   the   default   reserve   size,   the   reserve   size   is   rounded   up   to   the   nearest   multiple   of  1   MB.  

Windows   Server   2003   and   Windows   XP:     If   the   dwCreationFlags   parameter   of   CreateThread   or  CreateRemoteThread   is   STACK_SIZE_PARAM_IS_A_RESERVATION,   the   dwStackSize   parameter   specifies  the   amount   of   stack   space   which   is   to   be   initially   reserved   for   the   thread.    

The   stack   is   freed   when   the   thread   terminates.

 

Reference:

http://blog.csdn.net/fondiyass007/archive/2007/06/13/1650683.aspx

http://blog.csdn.net/szs1860/archive/2007/05/12/1606192.aspx

相關文章