深刻淺出C語言中的堆和棧

 

在談堆棧的時候,我在這有必要把計算機的內存結構給你們簡單的介紹下(高手們能夠直接飄過)程序員

1、 內存結構
 
每一個程序一啓動都有一個大小爲4GB的內存,這個內存叫虛擬內存,是概念上的,真正能用到的,只是很小一部分,通常也就是在幾百K到幾百M。咱們PC中內存,咱們稱之爲物理內存,也就是256M,512M等,虛擬內存和物理內存的如何轉換是由操做系統完成的,咱們不須要管它。咱們只須要管好咱們本身程序的那4GB內存就能夠了。
要管理4GB的虛擬內存,就必須給每一個字節分配一個號碼,以便程序與訪問到其中任何一個字節。這個號碼是從0開始順序遞增的,針對於這個號碼咱們就稱之爲地址,從0x00000000-0xFFFFFFFF,這樣,咱們理論上就能夠訪問其中內存中任何一個字節了。但有一點請注意,系統並不讓咱們所有均可以用。其中後面2GB的內容是留給系統用的,用戶是不能夠訪問的,並且在前面的2GB也有部分區段不能訪問,好比0x00000000就不能訪問。具體是哪些區段,沒必要關心。
注意:相似於0x12345678或12345678H是10進制數305419896的16進製表示法,他們是一回事,顯示16進制是爲了方便顯示及計算機計算。
程序都是用來作一些具體的事情,無論作什麼事,結構都是很類似。程序啓動,就有4GB的虛擬內存,經過CPU的計算,改變內存的內容,最後再複製內存的內容輸出,輸出的目的地能夠是:屏幕、文件、磁盤等外存、端口、網絡等。如何輸出呢,最後所有都是調用系統的API,由操做系統完成。(這段話,請仔細體會,並緊緊記住)
因此咱們的核心問題就是:如何控制內存,讓內存裏的值,變成咱們想要的結果。
注意:這裏的控制,指讀取或寫入某段內存的內容。
 
在虛擬內存中,咱們通常將其分爲如下幾個區域:
一、棧區(stack)— 由編譯器自動分配釋放 ,存放函數的參數值,局部變量的值等。其 
操做方式相似於數據結構中的棧。 
二、堆區(heap) — 通常由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回 
收 。注意它與數據結構中的堆是兩回事,分配方式卻是相似於鏈表,呵呵。 
三、全局區(靜態區)(static)—,全局變量和靜態變量的存儲是放在一塊的,初始化的 
全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另 
一塊區域。 - 程序結束後由系統釋放。 
四、文字常量區 —常量字符串就是放在這裏的。 程序結束後由系統釋放 
五、程序代碼區—存放函數體的二進制代碼。 
 
示例以下:
這是一個前輩寫的,很是詳細 
//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); 
分配得來得10和20字節的區域就在堆區。 
strcpy(p1, "123456"); 123456\0放在常量區,編譯器可能會將它與p3所指向的"123456" 
優化成一個地方。 
 
2、堆和棧的理論知識 
2.1申請方式 
stack: 
由系統自動分配。 例如,聲明在函數中一個局部變量 int b; 系統自動在棧中爲b開闢空 
間 
heap: 
須要程序員本身申請,並指明大小,在c中malloc函數 
如p1 = (char *)malloc(10); 
在C++中用new運算符 
如p2 = new char[10]; 
可是注意p一、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是在編譯時就肯定的; 
可是,在之後的存取中,在棧上的數組比指針所指向的字符串(例如堆)快。 
好比: 
#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 進一步的比較以下:
堆是相對於棧的,前面說到棧的大小大概爲1MB,而用戶能用到的內存大概有2GB,所以除了少許的數據區域和靜態區域,以及這2G中被小部分限制的區域處,剩餘都是堆的空間,其大小仍是接近2G。(接近2G,是個人理解,沒有在其它書中看到相似的結論,說錯了,你別笑話我。)
 
堆主要有兩個做用:
一、 欲分配內存空間的大小,或稱長度,能夠是變量,這意味着這個大小,在分配前,能夠隨着環境的改變而改變,不要求是定值。注意,這裏說的是「分配前」,在分配完了之後,這段內存的大小理論上仍是不能夠改大小的,除非釋放掉或用一些特別的方法。
二、 能夠分配比較大的空間。
 
堆分配內存,主要經過函數malloc(),釋放用free()。好比:
#include <stdlib.h> /* 要加上該頭文件 */
int size = 10*sizeof(int);
int* pInt = (int*)malloc(size);
表示在堆中,分配長度爲size的內存,將分配到的那段內存,標識成一系列int型數據,並將這段內存的地址,賦值給一個int型指針pInt,這樣,經過pInt就能夠控制這段內存了。
咱們知道在棧中,能夠經過數組,也能達上述的效果,以下:
int arr[10];
他們是有區別的:
一、 在效率上,前者(指pInt那段內存)是經過系統在整個堆空間中搜索到一段合適的內存,而後把這段內存分配給pInt,然後者只是在臨近的位置分配這樣大小的內存,這樣少了搜索過程,後者顯示效率高得多。
二、 在大小上,前者總共能分配的內存接近2GB,能夠說很大了,然後者,其棧總的大小才1MB,因此其分配的大小不可能超過1MB,確切來講,是不能超過1M-分配前棧的大小。換句話說,若是要分配的空間超過1M的話,只能選擇前者。
三、 在做用範圍上,前者的內存地址能夠用一個指針表示,假如這個指針是全局變量的話,則一直能夠控制這塊內存了,事實上,只要這塊內存不被釋放,那麼在程序任何地方,它只要知道該內存的地址,則能夠控制它。後者在退出函數或其做用域後,該段內存就被收回了。
四、 在「三、」中,彷佛感受堆很好,但隱藏了一個重要的麻煩,那就是內存釋放,由於堆的內存釋放是須要手工進行的。若是一不當心,用完後,我忘記釋放,那麼結果會怎樣呢?事實上這段內存則不會被再用到,直到程序結束,好比我申請了200M的內存,沒釋放,這種浪費仍是很可觀的。
 
針對於他們的區別,咱們能夠有一個結論:
同時知足:臨時性,小的(不超過1M),更快的內存分配用棧,(其實第3條「更快」因爲現代機器夠快,兩種方式都會知足),不然用堆。
 
這些是棧和堆的區別,在實際工做中很是重要,請充分理解,不理解的話就背下來,這在面試時會常常考到。
 
若是堆分配不成功的話,則返回NULL。
 
前面說過,針對於某段在堆中分配的內存,若是再也不須要使用了,則應該釋放。這個釋放用free();
以下:
int* pInt = (int*)malloc(size);
/* 針對於pInt作一些操做 */
free(pInt);
 
2.8小結: 堆和棧的區別能夠用以下的比喻來看出: 使用棧就象咱們去飯館裏吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就 走,沒必要理會切菜、洗菜等準備工做和洗碗、刷鍋等掃尾工做,他的好處是快捷,可是自 由度小。 使用堆就象是本身動手作喜歡吃的菜餚,比較麻煩,可是比較符合本身的口味,並且自由 度大。 (經典!) 
相關文章
相關標籤/搜索