一、預備知識—程序的內存分配
一個由c/c++編譯的程序佔用的內存分爲以下幾個部分:
1、棧區(stack):由編譯器自動分配釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。
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堆和棧中的存儲內容
棧: 在函數調用時,第一個進棧的是主函數中後的下一條指令(函數調用語句的下一條可執行語句)的地址,然後是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的,然後是函數中的局部變量。注意靜態變量是不入棧的。
當本次函數調用結束後,局部變量先出棧,然後是參數,最後棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。
堆:一般是在堆的頭部用一個字節存放堆的大小。堆中的具體內容有程序員安排。
2.6存取效率的比較
char s1[]
= "aaaaaaaaaaaaaaa";
char
*s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在運行時刻賦值的;
而bbbbbbbbbbb是在編譯時就確定的;
但是,在以後的存取中,在棧上的數組比指針所指向的字符串(例如堆)快。
比如:
#include
<stdio.h>
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個元素有最高的優先權;棧實際上就是滿足先進後出的性質的數學或數據結構。
雖然堆棧,堆棧的說法是連起來叫,但是他們還是有很大區別的,連着叫只是由於歷史的原因。
深入探討
1.性能效率
由於棧內存由系統管理,分配和回收都是使用指令,速度比較快
堆內存用戶通過系統調用來分配,因此會造成運行時間延長,效率、性能下降。另一個大問題就是會造成缺頁和內存碎片
2.使用問題
棧內存由系統管理,自動分配和回收,用戶不需要了解內存的管理情況,只是需要注意不能返回局部對象。
堆內存是用戶自己管理的內存,需要自己分配,自己釋放,否則會造成段錯或者內存泄漏問題。
鏈接:http://www.cnblogs.com/liangxiufei/archive/2009/03/23/1419960.html
1) 在棧上創建。在執行函數時,函數內局部變量的存儲單元都在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,一般使用寄存器來存取,效率很高,但是分配的內存容量有限。
2) 從堆上分配,亦稱動態內存分配。程序在運行的時候用malloc或new申請任意多少的內存,程序員自己負責在何時用free或delete來釋放內存。動態內存的生存期由程序員自己決定,使用非常靈活。
3) 從靜態存儲區域分配。內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。例如全局變量,static變量。
4) 文字常量分配在文字常量區,程序結束後由系統釋放。
5)程序代碼區。
經典實例:(代碼來自網絡高手,沒有找到原作者)
#i nclude <string>
int a=0; //全局初始化區
char *p1; //全局未初始化區
void 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"優化成一個地方。
}
二 三種內存對象的比較
棧對象的優勢是在適當的時候自動生成,又在適當的時候自動銷燬,不需要程序員操心;而且棧對象的創建速度一般較堆對象快,因爲分配堆對象時,會調用operator new操作,operator new會採用某種內存空間搜索算法,而該搜索過程可能是很費時間的,產生棧對象則沒有這麼麻煩,它僅僅需要移動棧頂指針就可以了。但是要注意的是,通常棧空間容量比較小,一般是1MB~2MB,所以體積比較大的對象不適合在棧中分配。特別要注意遞歸函數中最好不要使用棧對象,因爲隨着遞歸調用深度的增加,所需的棧空間也會線性增加,當所需棧空間不夠時,便會導致棧溢出,這樣就會產生運行時錯誤。
堆對象創建和銷燬都要由程序員負責,所以,如果處理不好,就會發生內存問題。如果分配了堆對象,卻忘記了釋放,就會產生內存泄漏;而如 果已釋放了對象,卻沒有將相應的指針置爲NULL,該指針就是所謂的“懸掛指針”,再度使用此指針時,就會出現非法訪問,嚴重時就導致程序崩潰。但是高效的使用堆對象也可以大大的提高代碼質量。比如,我們需要創建一個大對象,且需要被多個函數所訪問,那麼這個時候創建一個堆對象無疑是良好的選擇,因爲我們通過在各個函數之間傳遞這個堆對象的指針,便可以實現對該對象的共享,相比整個對象的傳遞,大大的降低了對象的拷貝時間。另外,相比於棧空間,堆的容量要大得多。實際上,當物理內存不夠時,如果這時還需要生成新的堆對象,通常不會產生運行時錯誤,而是系統會使用虛擬內存來擴展實際的物理內存。
靜態存儲區。所有的靜態對象、全局對象都於靜態存儲區分配。關於全局對象,是在main()函數執行前就分配好了的。其實,在main()函數中的顯示代 碼執行之前,會調用一個由編譯器生成的_main()函數,而_main()函數會進行所有全局對象的的構造及初始化工作。而在main()函數結束之 前,會調用由編譯器生成的exit函數,來釋放所有的全局對象。比如下面的代碼:
void main(void) |
實際上,被轉化成這樣:
void main(void) |
除了全局靜態對象,還有局部靜態對象通和class的靜態成員,局部靜態對象是在函數中定義的,就像棧對象一樣,只不過,其前面多了個static關鍵字。局部靜態對象的生命期是從其所在函數第一次被調用,更確切地說,是當第一次執行到該靜態對象的聲明代碼時,產生該靜態局部對象,直到整個程序結束時,才銷燬該對象。class的靜態成員的生命週期是該class的第一次調用到程序的結束。
三 函數調用與堆棧
1)編譯器一般使用棧來存放函數的參數,局部變量等來實現函數調用。有時候函數有嵌套調用,這個時候棧中會有多個函數的信息,每個函數佔用一個連續的區域。一個函數佔用的區域被稱作幀()。同時棧是線程獨立的,每個線程都有自己的棧。例如下面簡單的函數調用:
另外函數堆棧的清理方式決定了當函數調用結束時由調用函數或被調用函數來清理函數幀,在VC中對函數棧的清理方式由兩種:
參數傳遞順序 | 誰負責清理參數佔用的堆棧 | |
__stdcall | 從右到左 | 被調函數 |
__cdecl | 從右到左 | 調用者 |
2) 有了上面的知識爲鋪墊,我們下面細看一個函數的調用時堆棧的變化:
代碼如下:
int Add(int x, int y)
{
return x + y;
}
void main()
{
int *pi = new int(10);
int *pj = new int(20);
int result = 0;
result = Add(*pi,*pj);
delete pi;
delete pj;
}
對上面的代碼,我們分爲四步,當然我們只畫出了我們的代碼對堆棧的影響,其他的我們假設它們不存在,哈哈!
第一,int *pi = new int(10); int *pj = new int(20); int result = 0; 堆棧變化如下:
第二,Add(*pi,*pj);堆棧如下:
第三,將Add的結果給result,堆棧如下:
第四,delete pi; delete pj; 堆棧如下:
第五,當main()退出後,堆棧如下,等同於main執行前,哈哈!