堆棧的理解

7. Memory : Stack vs Heap


Stack vs Heap

So far we have seen how to declare basic type variables such as intdouble, etc, and complex types such as arrays and structs. The way we have been declaring them so far, with a syntax that is like other languages such as MATLAB, Python, etc, puts these variables on the stack in C.

The Stack

What is the stack? It's a special region of your computer's memory that stores temporary variables created by each function (including the main() function). The stack is a "LIFO" (last in, first out) data structure, that is managed and optimized by the CPU quite closely. Every time a function declares a new variable, it is "pushed" onto the stack. Then every time a function exits, all of the variables pushed onto the stack by that function, are freed (that is to say, they are deleted). Once a stack variable is freed, that region of memory becomes available for other stack variables.

The advantage of using the stack to store variables, is that memory is managed for you. You don't have to allocate memory by hand, or free it once you don't need it any more. What's more, because the CPU organizes stack memory so efficiently, reading from and writing to stack variables is very fast.

A key to understanding the stack is the notion that when a function exits, all of its variables are popped off of the stack (and hence lost forever). Thus stack variables are local in nature. This is related to a concept we saw earlier known as variable scope, or local vs global variables. A common bug in C programming is attempting to access a variable that was created on the stack inside some function, from a place in your program outside of that function (i.e. after that function has exited).

Another feature of the stack to keep in mind, is that there is a limit (varies with OS) on the size of variables that can be store on the stack. This is not the case for variables allocated on the heap.

To summarize the stack:

  • the stack grows and shrinks as functions push and pop local variables
  • there is no need to manage the memory yourself, variables are allocated and freed automatically
  • the stack has size limits
  • stack variables only exist while the function that created them, is running

The Heap

The heap is a region of your computer's memory that is not managed automatically for you, and is not as tightly managed by the CPU. It is a more free-floating region of memory (and is larger). To allocate memory on the heap, you must use malloc() or calloc(), which are built-in C functions. Once you have allocated memory on the heap, you are responsible for using free() to deallocate that memory once you don't need it any more. If you fail to do this, your program will have what is known as a memory leak. That is, memory on the heap will still be set aside (and won't be available to other processes). As we will see in the debugging section, there is a tool called valgrind that can help you detect memory leaks.

Unlike the stack, the heap does not have size restrictions on variable size (apart from the obvious physical limitations of your computer). Heap memory is slightly slower to be read from and written to, because one has to use pointers to access memory on the heap. We will talk about pointers shortly.

Unlike the stack, variables created on the heap are accessible by any function, anywhere in your program. Heap variables are essentially global in scope.

Stack vs Heap Pros and Cons

Stack

  • very fast access
  • don't have to explicitly de-allocate variables
  • space is managed efficiently by CPU, memory will not become fragmented
  • local variables only
  • limit on stack size (OS-dependent)
  • variables cannot be resized

Heap

  • variables can be accessed globally
  • no limit on memory size
  • (relatively) slower access
  • no guaranteed efficient use of space, memory may become fragmented over time as blocks of memory are allocated, then freed
  • you must manage memory (you're in charge of allocating and freeing variables)
  • variables can be resized using realloc()

Examples

Here is a short program that creates its variables on the stack. It looks like the other programs we have seen so far.

#include <stdio.h>

double multiplyByTwo (double input) {
  double twice = input * 2.0;
  return twice;
}

int main (int argc, char *argv[])
{
  int age = 30;
  double salary = 12345.67;
  double myList[3] = {1.2, 2.3, 3.4};

  printf("double your salary is %.3f\n", multiplyByTwo(salary));

  return 0;
}
double your salary is 24691.340

On lines 10, 11 and 12 we declare variables: an int, a double, and an array of three doubles. These three variables are pushed onto the stack as soon as the main()function allocates them. When the main() function exits (and the program stops) these variables are popped off of the stack. Similarly, in the function multiplyByTwo(), the twice variable, which is a double, is pushed onto the stack as soon as the multiplyByTwo() function allocates it. As soon as the multiplyByTwo() function exits, the twice variable is popped off of the stack, and is gone forever.

As a side note, there is a way to tell C to keep a stack variable around, even after its creator function exits, and that is to use the static keyword when declaring the variable. A variable declared with the static keyword thus becomes something like a global variable, but one that is only visible inside the function that created it. It's a strange construction, one that you probably won't need except under very specific circumstances.

Here is another version of this program that allocates all of its variables on the heap instead of the stack:

#include <stdio.h>
#include <stdlib.h>

double *multiplyByTwo (double *input) {
  double *twice = malloc(sizeof(double));
  *twice = *input * 2.0;
  return twice;
}

int main (int argc, char *argv[])
{
  int *age = malloc(sizeof(int));
  *age = 30;
  double *salary = malloc(sizeof(double));
  *salary = 12345.67;
  double *myList = malloc(3 * sizeof(double));
  myList[0] = 1.2;
  myList[1] = 2.3;
  myList[2] = 3.4;

  double *twiceSalary = multiplyByTwo(salary);

  printf("double your salary is %.3f\n", *twiceSalary);

  free(age);
  free(salary);
  free(myList);
  free(twiceSalary);

  return 0;
}

As you can see, using malloc() to allocate memory on the heap and then using free() to deallocate it, is no big deal, but is a bit cumbersome. The other thing to notice is that there are a bunch of star symbols * all over the place now. What are those? The answer is, they are pointers. The malloc() (and calloc() and free()) functions deal with pointers not actual values. We will talk more about pointers shortly. The bottom line though: pointers are a special data type in C that store addresses in memory instead of storing actual values. Thus on line 5 above, the twice variable is not a double, but is a pointer to a double. It's an address in memory where the double is stored.

When to use the Heap?

When should you use the heap, and when should you use the stack? If you need to allocate a large block of memory (e.g. a large array, or a big struct), and you need to keep that variable around a long time (like a global), then you should allocate it on the heap. If you are dealing with realtively small variables that only need to persist as long as the function using them is alive, then you should use the stack, it's easier and faster. If you need variables like arrays and structs that can change size dynamically (e.g. arrays that can grow or shrink as needed) then you will likely need to allocate them on the heap, and use dynamic memory allocation functions like malloc()calloc()realloc() and free() to manage that memory "by hand". We will talk about dynamically allocated data structures after we talk about pointers.

Links


什麼是堆和棧,它們在哪兒?

問題描述

編程語言書籍中經常解釋值類型被創建在棧上,引用類型被創建在堆上,但是並沒有本質上解釋這堆和棧是什麼。我僅有高級語言編程經驗,沒有看過對此更清晰的解釋。我的意思是我理解什麼是棧,但是它們到底是什麼,在哪兒呢(站在實際的計算機物理內存的角度上看)? 
1、在通常情況下由操作系統(OS)和語言的運行時(runtime)控制嗎? 
2、它們的作用範圍是什麼? 
3、它們的大小由什麼決定? 
4、哪個更快?

答案一

棧是爲執行線程留出的內存空間。當函數被調用的時候,棧頂爲局部變量和一些 bookkeeping 數據預留塊。當函數執行完畢,塊就沒有用了,可能在下次的函數調用的時候再被使用。棧通常用後進先出(LIFO)的方式預留空間;因此最近的保留塊(reserved block)通常最先被釋放。這麼做可以使跟蹤堆棧變的簡單;從棧中釋放塊(free block)只不過是指針的偏移而已。

堆(heap)是爲動態分配預留的內存空間。和棧不一樣,從堆上分配和重新分配塊沒有固定模式;你可以在任何時候分配和釋放它。這樣使得跟蹤哪部分堆已經被分配和被釋放變的異常複雜;有許多定製的堆分配策略用來爲不同的使用模式下調整堆的性能。

每一個線程都有一個棧,但是每一個應用程序通常都只有一個堆(儘管爲不同類型分配內存使用多個堆的情況也是有的)。

直接回答你的問題:1.當線程創建的時候,操作系統(OS)爲每一個系統級(system-level)的線程分配棧。通常情況下,操作系統通過調用語言的運行時(runtime)去爲應用程序分配堆。2.棧附屬於線程,因此當線程結束時棧被回收。堆通常通過運行時在應用程序啓動時被分配,當應用程序(進程)退出時被回收。3.當線程被創建的時候,設置棧的大小。在應用程序啓動的時候,設置堆的大小,但是可以在需要的時候擴展(分配器向操作系統申請更多的內存)。4.棧比堆要快,因爲它存取模式使它可以輕鬆的分配和重新分配內存(指針/整型只是進行簡單的遞增或者遞減運算),然而堆在分配和釋放的時候有更多的複雜的 bookkeeping 參與。另外,在棧上的每個字節頻繁的被複用也就意味着它可能映射到處理器緩存中,所以很快(譯者注:局部性原理)。

答案二

Stack:

1、和堆一樣存儲在計算機 RAM 中。 
2、在棧上創建變量的時候會擴展,並且會自動回收。 
3、相比堆而言在棧上分配要快的多。 
4、用數據結構中的棧實現。 
5、存儲局部數據,返回地址,用做參數傳遞。

6、當用棧過多時可導致棧溢出(無窮次(大量的)的遞歸調用,或者大量的內存分配)。 
7、在棧上的數據可以直接訪問(不是非要使用指針訪問)。 
8、如果你在編譯之前精確的知道你需要分配數據的大小並且不是太大的時候,可以使用棧。 
9、當你程序啓動時決定棧的容量上限。

Heap:

1、和棧一樣存儲在計算機RAM。 
2、在堆上的變量必須要手動釋放,不存在作用域的問題。數據可用 delete, delete[] 或者 free 來釋放。 
3、相比在棧上分配內存要慢。 
4、通過程序按需分配。 
5、大量的分配和釋放可造成內存碎片。

6、在 C++ 中,在堆上創建數的據使用指針訪問,用 new 或者 malloc 分配內存。 
7、如果申請的緩衝區過大的話,可能申請失敗。 
8、在運行期間你不知道會需要多大的數據或者你需要分配大量的內存的時候,建議你使用堆。 
9、可能造成內存泄露。

舉例:

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
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

答案三

堆和棧是兩種內存分配的兩個統稱。可能有很多種不同的實現方式,但是實現要符合幾個基本的概念:

1、對棧而言,棧中的新加數據項放在其他數據的頂部,移除時你也只能移除最頂部的數據(不能越位獲取)。

這裏寫圖片描述

2.對堆而言,數據項位置沒有固定的順序。你可以以任何順序插入和刪除,因爲他們沒有“頂部”數據這一概念。

這裏寫圖片描述

上面上個圖片很好的描述了堆和棧分配內存的方式。

在通常情況下由操作系統(OS)和語言的運行時(runtime)控制嗎?

如前所述,堆和棧是一個統稱,可以有很多的實現方式。計算機程序通常有一個棧叫做調用棧,用來存儲當前函數調用相關的信息(比如:主調函數的地址,局部變量),因爲函數調用之後需要返回給主調函數。棧通過擴展和收縮來承載信息。實際上,程序不是由運行時來控制的,它由編程語言、操作系統甚至是系統架構來決定。

堆是在任何內存中動態和隨機分配的(內存的)統稱;也就是無序的。內存通常由操作系統分配,通過應用程序調用 API 接口去實現分配。在管理動態分配內存上會有一些額外的開銷,不過這由操作系統來處理。 
它們的作用範圍是什麼?

調用棧是一個低層次的概念,就程序而言,它和“作用範圍”沒什麼關係。如果你反彙編一些代碼,你就會看到指針引用堆棧部分。就高級語言而言,語言有它自己的範圍規則。一旦函數返回,函數中的局部變量會直接直接釋放。你的編程語言就是依據這個工作的。

在堆中,也很難去定義。作用範圍是由操作系統限定的,但是你的編程語言可能增加它自己的一些規則,去限定堆在應用程序中的範圍。體系架構和操作系統是使用虛擬地址的,然後由處理器翻譯到實際的物理地址中,還有頁面錯誤等等。它們記錄那個頁面屬於那個應用程序。不過你不用關心這些,因爲你僅僅在你的編程語言中分配和釋放內存,和一些錯誤檢查(出現分配失敗和釋放失敗的原因)。

它們的大小由什麼決定?

依舊,依賴於語言,編譯器,操作系統和架構。棧通常提前分配好了,因爲棧必須是連續的內存塊。語言的編譯器或者操作系統決定它的大小。不要在棧上存儲大塊數據,這樣可以保證有足夠的空間不會溢出,除非出現了無限遞歸的情況(額,棧溢出了)或者其它不常見了編程決議。

堆是任何可以動態分配的內存的統稱。這要看你怎麼看待它了,它的大小是變動的。在現代處理器中和操作系統的工作方式是高度抽象的,因此你在正常情況下不需要擔心它實際的大小,除非你必須要使用你還沒有分配的內存或者已經釋放了的內存。

哪個更快一些?

棧更快因爲所有的空閒內存都是連續的,因此不需要對空閒內存塊通過列表來維護。只是一個簡單的指向當前棧頂的指針。編譯器通常用一個專門的、快速的寄存器來實現。更重要的一點事是,隨後的棧上操作通常集中在一個內存塊的附近,這樣的話有利於處理器的高速訪問(譯者注:局部性原理)。

答案四

你問題的答案是依賴於實現的,根據不同的編譯器和處理器架構而不同。下面簡單的解釋一下:

1、棧和堆都是用來從底層操作系統中獲取內存的。 
2、在多線程環境下每一個線程都可以有他自己完全的獨立的棧,但是他們共享堆。並行存取被堆控制而不是棧。

堆:

1、堆包含一個鏈表來維護已用和空閒的內存塊。在堆上新分配(用 new 或者 malloc)內存是從空閒的內存塊中找到一些滿足要求的合適塊。這個操作會更新堆中的塊鏈表。這些元信息也存儲在堆上,經常在每個塊的頭部一個很小區域。 
2、堆的增加新快通常從地地址向高地址擴展。因此你可以認爲堆隨着內存分配而不斷的增加大小。如果申請的內存大小很小的話,通常從底層操作系統中得到比申請大小要多的內存。

3、申請和釋放許多小的塊可能會產生如下狀態:在已用塊之間存在很多小的空閒塊。進而申請大塊內存失敗,雖然空閒塊的總和足夠,但是空閒的小塊是零散的,不能滿足申請的大小,。這叫做“堆碎片”。 
4、當旁邊有空閒塊的已用塊被釋放時,新的空閒塊可能會與相鄰的空閒塊合併爲一個大的空閒塊,這樣可以有效的減少“堆碎片”的產生。

這裏寫圖片描述

棧:

1、棧經常與 sp 寄存器(譯者注:”stack pointer”,瞭解彙編的朋友應該都知道)一起工作,最初 sp 指向棧頂(棧的高地址)。 
2、CPU 用 push 指令來將數據壓棧,用 pop 指令來彈棧。當用 push 壓棧時,sp 值減少(向低地址擴展)。當用 pop 彈棧時,sp 值增大。存儲和獲取數據都是 CPU 寄存器的值。 
3、當函數被調用時,CPU使用特定的指令把當前的 IP (譯者注:“instruction pointer”,是一個寄存器,用來記錄 CPU 指令的位置)壓棧。即執行代碼的地址。CPU 接下來將調用函數地址賦給 IP ,進行調用。當函數返回時,舊的 IP 被彈棧,CPU 繼續去函數調用之前的代碼。

4、當進入函數時,sp 向下擴展,擴展到確保爲函數的局部變量留足夠大小的空間。如果函數中有一個 32-bit 的局部變量會在棧中留夠四字節的空間。當函數返回時,sp 通過返回原來的位置來釋放空間。 
5、如果函數有參數的話,在函數調用之前,會將參數壓棧。函數中的代碼通過 sp 的當前位置來定位參數並訪問它們。

6、函數嵌套調用和使用魔法一樣,每一次新調用的函數都會分配函數參數,返回值地址、局部變量空間、嵌套調用的活動記錄都要被壓入棧中。函數返回時,按照正確方式的撤銷。

7、棧要受到內存塊的限制,不斷的函數嵌套/爲局部變量分配太多的空間,可能會導致棧溢出。當棧中的內存區域都已經被使用完之後繼續向下寫(低地址),會觸發一個 CPU 異常。這個異常接下會通過語言的運行時轉成各種類型的棧溢出異常。(譯者注:“不同語言的異常提示不同,因此通過語言運行時來轉換”我想他表達的是這個含義)

這裏寫圖片描述

*函數的分配可以用堆來代替棧嗎?

不可以的,函數的活動記錄(即局部或者自動變量)被分配在棧上, 這樣做不但存儲了這些變量,而且可以用來嵌套函數的追蹤。

堆的管理依賴於運行時環境,C 使用 malloc ,C++ 使用 new ,但是很多語言有垃圾回收機制。

棧是更低層次的特性與處理器架構緊密的結合到一起。當堆不夠時可以擴展空間,這不難做到,因爲可以有庫函數可以調用。但是,擴展棧通常來說是不可能的,因爲在棧溢出的時候,執行線程就被操作系統關閉了,這已經太晚了。

譯者注

關於堆棧的這個帖子,對我來說,收穫非常多。我之前看過一些資料,自己寫代碼的時候也常常思考。就這方面,也和祥子(我的大學舍友,現在北京郵電讀研,技術牛人)探討過多次了。但是終究是一個一個的知識點,這個帖子看完之後,豁然開朗,把知識點終於連接成了一個網。這種感覺,經歷過的一定懂得,期間的興奮不言而喻。

這個帖子跟帖者不少,我選了評分最高的四個。這四個之間也有一些是重複的觀點。個人鍾愛第四個回答者,我看的時候,瞬間高潮了,有木有?不過需要一些彙編語言、操作系統、計算機組成原理的的基礎,知道那幾個寄存器是幹什麼的,要知道計算機的流水線指令工作機制,保護/恢復現場等概念。三個回覆者都涉及到了操作系統中虛擬內存;在比較速度的時候,大家一定要在腦中對“局部性原理”和計算機高速緩存有一個概念。

如果你把這篇文章看懂了,我相信你收穫的不只是堆和棧,你會理解的更多!

興奮之餘,有幾點還是要強調的,翻譯沒有逐字逐詞翻譯,大部分是通過我個人的知識積累和對回帖者的意圖揣測而來的。請大家不要咬文嚼字,逐個推敲,我們的目的在於技術交流,不是麼?達到這一目的就夠了。

下面是一些不確定點:

1、我沒有聽過 bookkeeping data 這種說法,故沒有翻譯。從上下文理解來看,可以想成是用來寄存器值?函數參數?返回地址?如果有了解具體含義的朋友,煩請告知。

2、棧和堆棧是一回事,英文表達是 stack,堆是 heap。

3、調用棧的概念,我是第一次聽說,不太熟悉。大家可以去查查資料研究一下。

來自: 獨酌逸醉 
鏈接:http://www.perfect-is-shit.com/what-and-where-are-the-stack-and-heap.html 
原文:http://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap

0

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