數據結構的棧和堆
首先在數據結構上要知道堆棧,盡管我們這么稱呼它,但實際上堆棧是兩種數據結構:堆和棧。
堆和棧都是一種數據項按序排列的數據結構。
棧就像裝數據的桶或箱子
我們先從大家比較熟悉的棧說起吧,它是一種具有后進先出性質的數據結構,也就是說后存放的先取,先存放的后取。
這就如同我們要取出放在箱子里面底下的東西(放入的比較早的物體),我們首先要移開壓在它上面的物體(放入的比較晚的物體)。
堆像一棵倒過來的樹
- 而堆就不同了,堆是一種經過排序的樹形數據結構,每個結點都有一個值。
- 通常我們所說的堆的數據結構,是指二叉堆。
- 堆的特點是根結點的值最小(或最大),且根結點的兩個子樹也是一個堆。
由於堆的這個特性,常用來實現優先隊列,堆的存取是隨意,這就如同我們在圖書館的書架上取書,雖然書的擺放是有順序的,但是我們想取任意一本時不必像棧一樣,先取出前面所有的書,書架這種機制不同於箱子,我們可以直接取出我們想要的書。
內存分配中的棧和堆
先看百度百科中的內存堆棧介紹:
堆棧空間分配
棧(操作系統):由操作系統自動分配釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。
堆(操作系統): 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收,分配方式倒是類似於鏈表。
堆棧緩存方式
棧使用的是一級緩存, 他們通常都是被調用時處於存儲空間中,調用完畢立即釋放。
堆則是存放在二級緩存中,生命周期由虛擬機的垃圾回收算法來決定(並不是一旦成為孤兒對象就能被回收)。所以調用這些對象的速度要相對來得低一些。
下面就說說C語言程序內存分配中的堆和棧,這里有必要把內存分配也提一下,大家不要嫌我啰嗦,一般情況下程序存放在Rom(只讀內存,比如硬盤)或Flash中,運行時需要拷到RAM(隨機存儲器RAM)中執行,RAM會分別存儲不同的信息,如下圖所示:
內存中的棧區處於相對較高的地址以地址的增長方向為上的話,棧地址是向下增長的。
棧中分配局部變量空間,堆區是向上增長的用於分配程序員申請的內存空間。另外還有靜態區是分配靜態變量,全局變量空間的;只讀區是分配常量和程序代碼空間的;以及其他一些分區。
來看一個網上很流行的經典例子:
main.cpp
int a = 0; //全局初始化區 char *p1; //全局未初始化區 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); //堆
0.申請方式和回收方式不同
不知道你是否有點明白了。
堆和棧的第一個區別就是申請方式不同:棧(英文名稱是stack)是系統自動分配空間的,例如我們定義一個 char a;系統會自動在棧上為其開辟空間。而堆(英文名稱是heap)則是程序員根據需要自己申請的空間,例如malloc(10);開辟十個字節的空間。
由於棧上的空間是自動分配自動回收的,所以棧上的數據的生存周期只是在函數的運行過程中,運行后就釋放掉,不可以再訪問。而堆上的數據只要程序員不釋放空間,就一直可以訪問到,不過缺點是一旦忘記釋放會造成內存泄露。還有其他的一些區別我認為網上的朋友總結的不錯這里轉述一下:
1.申請后系統的響應
棧:只要棧的剩余空間大於所申請空間,系統將為程序提供內存,否則將報異常提示棧溢出。
堆:首先應該知道操作系統有一個記錄空閑內存地址的鏈表,當系統收到程序的申請時,會遍歷該鏈表,尋找第一個空間大於所申請空間的堆結點,然后將該結點從空閑結點鏈表中刪除,並將該結點的空間分配給程序,另外,對於大多數系統,會在這塊內存空間中的首地址處記錄本次分配的大小,這樣,代碼中的 delete語句才能正確的釋放本內存空間。另外,由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多余的那部分重新放入空閑鏈表中。
也就是說堆會在申請后還要做一些后續的工作這就會引出申請效率的問題。
2.申請效率的比較
根據第0點和第1點可知。
棧:由系統自動分配,速度較快。但程序員是無法控制的。
堆:是由new分配的內存,一般速度比較慢,而且容易產生內存碎片不過用起來最方便。
3.申請大小的限制
棧:在Windows下棧是向低地址擴展的數據結構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在 WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩余空間時,將提示overflow。因此,能從棧獲得的空間較小。
堆:堆是向高地址擴展的數據結構,是不連續的內存區域。這是由於系統是用鏈表來存儲的空閑內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。
4.堆和棧中的存儲內容
由於棧的大小有限,所以用子函數還是有物理意義的,而不僅僅是邏輯意義。
棧: 在函數調用時,第一個進棧的是主函數中函數調用后的下一條指令(函數調用語句的下一條可執行語句)的地址,然后是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的,然后是函數中的局部變量。注意靜態變量是不入棧的。
當本次函數調用結束后,局部變量先出棧,然后是參數,最后棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。
堆:一般是在堆的頭部用一個字節存放堆的大小。堆中的具體內容有程序員安排。
5.存取效率的比較
<span stchar 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 clbyte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4]cl
11: a = p[1];
0040106D 8B 55 EC mov edxdword ptr [ebp-14h]
00401070 8A 42 01 mov albyte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4]al
關於堆和棧區別的比喻
堆和棧的區別可以引用一位前輩的比喻來看出:
使用棧就象我們去飯館里吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等准備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。
使用堆就象是自己動手做喜歡吃的菜餚,比較麻煩,但是比較符合自己的口味,而且自由度大。比喻很形象,說的很通俗易懂,不知道你是否有點收獲。
問題描述
編程語言書籍中經常解釋值類型被創建在棧上,引用類型被創建在堆上,但是並沒有本質上解釋這堆和棧是什么。什么是棧,但是它們到底是什么,在哪兒呢(站在實際的計算機物理內存的角度上看)?
- 在通常情況下由操作系統(OS)和語言的運行時(runtime)控制嗎?
- 它們的作用范圍是什么?
- 它們的大小由什么決定?
- 哪個更快?
答案一
棧是為執行線程留出的內存空間。當函數被調用的時候,棧頂為局部變量和一些 bookkeeping 數據預留塊。當函數執行完畢,塊就沒有用了,可能在下次的函數調用的時候再被使用。棧通常用后進先出(LIFO)的方式預留空間;因此最近的保留塊(reserved block)通常最先被釋放。這么做可以使跟蹤堆棧變的簡單;從棧中釋放塊(free block)只不過是指針的偏移而已。
堆(heap)是為動態分配預留的內存空間。和棧不一樣,從堆上分配和重新分配塊沒有固定模式;你可以在任何時候分配和釋放它。這樣使得跟蹤哪部分堆已經被分配和被釋放變的異常復雜;有許多定制的堆分配策略用來為不同的使用模式下調整堆的性能。
每一個線程都有一個棧,但是每一個應用程序通常都只有一個堆(盡管為不同類型分配內存使用多個堆的情況也是有的)。
直接回答你的問題: 1. 當線程創建的時候,操作系統(OS)為每一個系統級(system-level)的線程分配棧。通常情況下,操作系統通過調用語言的運行時(runtime)去為應用程序分配堆。 2. 棧附屬於線程,因此當線程結束時棧被回收。堆通常通過運行時在應用程序啟動時被分配,當應用程序(進程)退出時被回收。 3. 當線程被創建的時候,設置棧的大小。在應用程序啟動的時候,設置堆的大小,但是可以在需要的時候擴展(分配器向操作系統申請更多的內存)。 4. 棧比堆要快,因為它存取模式使它可以輕松的分配和重新分配內存(指針/整型只是進行簡單的遞增或者遞減運算),然而堆在分配和釋放的時候有更多的復雜的 bookkeeping 參與。另外,在棧上的每個字節頻繁的被復用也就意味着它可能映射到處理器緩存中,所以很快(譯者注:局部性原理)。
答案二
Stack:
- 和堆一樣存儲在計算機 RAM 中。
- 在棧上創建變量的時候會擴展,並且會自動回收。
- 相比堆而言在棧上分配要快的多。
- 用數據結構中的棧實現。
- 存儲局部數據,返回地址,用做參數傳遞。
- 當用棧過多時可導致棧溢出(無窮次(大量的)的遞歸調用,或者大量的內存分配)。
- 在棧上的數據可以直接訪問(不是非要使用指針訪問)。
- 如果你在編譯之前精確的知道你需要分配數據的大小並且不是太大的時候,可以使用棧。
- 當你程序啟動時決定棧的容量上限。
Heap:
- 和棧一樣存儲在計算機RAM。
- 在堆上的變量必須要手動釋放,不存在作用域的問題。數據可用 delete, delete[] 或者 free 來釋放。
- 相比在棧上分配內存要慢。
- 通過程序按需分配。
- 大量的分配和釋放可造成內存碎片。
- 在 C++ 中,在堆上創建數的據使用指針訪問,用 new 或者 malloc 分配內存。
- 如果申請的緩沖區過大的話,可能申請失敗。
- 在運行期間你不知道會需要多大的數據或者你需要分配大量的內存的時候,建議你使用堆。
- 可能造成內存泄露。
舉例:
int foo() { char *pBuffer; //<--nothing allocated yet (excluding the pointer itself, which is allocated here on the stack). bool b = true; // Allocated on the stack. if(b) { //Create 500 bytes on the stack char buffer[500]; //Create 500 bytes on the heap pBuffer = new char[500]; }//<-- buffer is deallocated here, pBuffer is not }//<--- oops there's a memory leak, I should have called delete[] pBuffer;
答案三
堆和棧是兩種內存分配的兩個統稱。可能有很多種不同的實現方式,但是實現要符合幾個基本的概念:
1.對棧而言,棧中的新加數據項放在其他數據的頂部,移除時你也只能移除最頂部的數據(不能越位獲取)。
2.對堆而言,數據項位置沒有固定的順序。你可以以任何順序插入和刪除,因為他們沒有“頂部”數據這一概念。
上面上個圖片很好的描述了堆和棧分配內存的方式。
在通常情況下由操作系統(OS)和語言的運行時(runtime)控制嗎?
如前所述,堆和棧是一個統稱,可以有很多的實現方式。計算機程序通常有一個棧叫做調用棧,用來存儲當前函數調用相關的信息(比如:主調函數的地址,局部變量),因為函數調用之后需要返回給主調函數。棧通過擴展和收縮來承載信息。實際上,程序不是由運行時來控制的,它由編程語言、操作系統甚至是系統架構來決定。
堆是在任何內存中動態和隨機分配的(內存的)統稱;也就是無序的。內存通常由操作系統分配,通過應用程序調用 API 接口去實現分配。在管理動態分配內存上會有一些額外的開銷,不過這由操作系統來處理。
它們的作用范圍是什么?
調用棧是一個低層次的概念,就程序而言,它和“作用范圍”沒什么關系。如果你反匯編一些代碼,你就會看到指針引用堆棧部分。就高級語言而言,語言有它自己的范圍規則。一旦函數返回,函數中的局部變量會直接直接釋放。你的編程語言就是依據這個工作的。
在堆中,也很難去定義。作用范圍是由操作系統限定的,但是你的編程語言可能增加它自己的一些規則,去限定堆在應用程序中的范圍。體系架構和操作系統是使用虛擬地址的,然后由處理器翻譯到實際的物理地址中,還有頁面錯誤等等。它們記錄那個頁面屬於那個應用程序。不過你不用關心這些,因為你僅僅在你的編程語言中分配和釋放內存,和一些錯誤檢查(出現分配失敗和釋放失敗的原因)。
它們的大小由什么決定?
依舊,依賴於語言,編譯器,操作系統和架構。棧通常提前分配好了,因為棧必須是連續的內存塊。語言的編譯器或者操作系統決定它的大小。不要在棧上存儲大塊數據,這樣可以保證有足夠的空間不會溢出,除非出現了無限遞歸的情況(額,棧溢出了)或者其它不常見了編程決議。
堆是任何可以動態分配的內存的統稱。這要看你怎么看待它了,它的大小是變動的。在現代處理器中和操作系統的工作方式是高度抽象的,因此你在正常情況下不需要擔心它實際的大小,除非你必須要使用你還沒有分配的內存或者已經釋放了的內存。
哪個更快一些?
棧更快因為所有的空閑內存都是連續的,因此不需要對空閑內存塊通過列表來維護。只是一個簡單的指向當前棧頂的指針。編譯器通常用一個專門的、快速的寄存器來實現。更重要的一點事是,隨后的棧上操作通常集中在一個內存塊的附近,這樣的話有利於處理器的高速訪問(譯者注:局部性原理)。
答案四
你問題的答案是依賴於實現的,根據不同的編譯器和處理器架構而不同。下面簡單的解釋一下:
- 棧和堆都是用來從底層操作系統中獲取內存的。
- 在多線程環境下每一個線程都可以有他自己完全的獨立的棧,但是他們共享堆。並行存取被堆控制而不是棧。
堆:
- 堆包含一個鏈表來維護已用和空閑的內存塊。在堆上新分配(用 new 或者 malloc)內存是從空閑的內存塊中找到一些滿足要求的合適塊。這個操作會更新堆中的塊鏈表。這些元信息也存儲在堆上,經常在每個塊的頭部一個很小區域。
- 堆的增加新快通常從地地址向高地址擴展。因此你可以認為堆隨着內存分配而不斷的增加大小。如果申請的內存大小很小的話,通常從底層操作系統中得到比申請大小要多的內存。
- 申請和釋放許多小的塊可能會產生如下狀態:在已用塊之間存在很多小的空閑塊。進而申請大塊內存失敗,雖然空閑塊的總和足夠,但是空閑的小塊是零散的,不能滿足申請的大小,。這叫做“堆碎片”。
- 當旁邊有空閑塊的已用塊被釋放時,新的空閑塊可能會與相鄰的空閑塊合並為一個大的空閑塊,這樣可以有效的減少“堆碎片”的產生。

棧:
- 棧經常與 sp 寄存器(譯者注:”stack pointer”,了解匯編的朋友應該都知道)一起工作,最初 sp 指向棧頂(棧的高地址)。
- CPU 用 push 指令來將數據壓棧,用 pop 指令來彈棧。當用 push 壓棧時,sp 值減少(向低地址擴展)。當用 pop 彈棧時,sp 值增大。存儲和獲取數據都是 CPU 寄存器的值。
- 當函數被調用時,CPU使用特定的指令把當前的 IP (譯者注:“instruction pointer”,是一個寄存器,用來記錄 CPU 指令的位置)壓棧。即執行代碼的地址。CPU 接下來將調用函數地址賦給 IP ,進行調用。當函數返回時,舊的 IP 被彈棧,CPU 繼續去函數調用之前的代碼。
- 當進入函數時,sp 向下擴展,擴展到確保為函數的局部變量留足夠大小的空間。如果函數中有一個 32-bit 的局部變量會在棧中留夠四字節的空間。當函數返回時,sp 通過返回原來的位置來釋放空間。
- 如果函數有參數的話,在函數調用之前,會將參數壓棧。函數中的代碼通過 sp 的當前位置來定位參數並訪問它們。
- 函數嵌套調用和使用魔法一樣,每一次新調用的函數都會分配函數參數,返回值地址、局部變量空間、嵌套調用的活動記錄都要被壓入棧中。函數返回時,按照正確方式的撤銷。
- 棧要受到內存塊的限制,不斷的函數嵌套/為局部變量分配太多的空間,可能會導致棧溢出。當棧中的內存區域都已經被使用完之后繼續向下寫(低地址),會觸發一個 CPU 異常。這個異常接下會通過語言的運行時轉成各種類型的棧溢出異常。(譯者注:“不同語言的異常提示不同,因此通過語言運行時來轉換”我想他表達的是這個含義)

*函數的分配可以用堆來代替棧嗎?
不可以的,函數的活動記錄(即局部或者自動變量)被分配在棧上, 這樣做不但存儲了這些變量,而且可以用來嵌套函數的追蹤。
堆的管理依賴於運行時環境,C 使用 malloc ,C++ 使用 new ,但是很多語言有垃圾回收機制。
棧是更低層次的特性與處理器架構緊密的結合到一起。當堆不夠時可以擴展空間,這不難做到,因為可以有庫函數可以調用。但是,擴展棧通常來說是不可能的,因為在棧溢出的時候,執行線程就被操作系統關閉了,這已經太晚了。
轉載自:https://blog.csdn.net/langb2014/article/details/79376349