一:C語言中的內存機制
在C語言中,內存主要分為如下5個存儲區:
(1)棧(Stack):位於函數內的局部變量(包括函數實參),由編譯器負責分配釋放,函數結束,棧變量失效。
(2)堆(Heap):由程序員用malloc/calloc/realloc分配,free釋放。如果程序員忘記free了,則會造成內存泄露,程序結束時該片內存會由OS回收。
(3)全局區/靜態區(Global Static Area): 全局變量和靜態變量存放區,程序一經編譯好,該區域便存在。並且在C語言中初始化的全局變量和靜態變量和未初始化的放在相鄰的兩個區域(在C++中,由於全局變量和靜態變量編譯器會給這些變量自動初始化賦值,所以沒有區分了)。由於全局變量一直占據內存空間且不易維護,推薦少用。程序結束時釋放。
(4)C風格字符串常量存儲區: 專門存放字符串常量的地方,程序結束時釋放。
(5)程序代碼區:存放程序二進制代碼的區域。
二:C++中的內存機制
在C++語言中,與C類似,不過也有所不同,內存主要分為如下5個存儲區:
(1)棧(Stack):位於函數內的局部變量(包括函數實參),由編譯器負責分配釋放,函數結束,棧變量失效。
(2)堆(Heap):這里與C不同的是,該堆是由new申請的內存,由delete或delete[]負責釋放
(3)自由存儲區(Free Storage):由程序員用malloc/calloc/realloc分配,free釋放。如果程序員忘記free了,則會造成內存泄露,程序結束時該片內存會由OS回收。
(4)全局區/靜態區(Global Static Area): 全局變量和靜態變量存放區,程序一經編譯好,該區域便存在。在C++中,由於全局變量和靜態變量編譯器會給這些變量自動初始化賦值,所以沒有區分了初始化變量和未初始化變量了。由於全局變量一直占據內存空間且不易維護,推薦少用。程序結束時釋放。
(5)常量存儲區: 這是一塊比較特殊的存儲區,專門存儲不能修改的常量(如果采用非正常手段更改當然也是可以的了)。
三:堆和棧的區別
3.1 棧(Stack)
具體的講,現代計算機(馮諾依曼串行執行機制),都直接在代碼低層支持棧的數據結構。這體現在有專門的寄存器指向棧所在的地址(SS,堆棧段寄存器,存放堆棧段地址);有專門的機器指令完成數據入棧出棧的操作(匯編中有PUSH和POP指令)。
這種機制的特點是效率高,但支持數據的數據有限,一般是整數、指針、浮點數等系統直接支持的數據類型,並不直接支持其他的數據結構(可以自定義棧結構支持多種數據類型)。因為棧的這種特點,對棧的使用在程序中是非常頻繁的 。對子程序的調用就是直接利用棧完成的。機器的call指令里隱含了把返回地址入棧,然后跳轉至子程序地址的操作,而子程序的ret指令則隱含從堆棧中彈出返回地址並跳轉之的操作。
C/C++中的函數自動變量就是直接使用棧的例子,這也就是為什么當函數返回時,該函數的自動變量自動失效的原因,因而要避免返回棧內存和棧引用,以免內存泄露。
3.2 堆(Heap)
和棧不同的是,堆得數據結構並不是由系統(無論是機器硬件系統還是操作系統)支持的,而是由函數庫提供的。基本的malloc/calloc/realloc/free函數維護了一套內部的堆數據結構(在C++中則增加了new/delete維護)。
當程序用這些函數去獲得新的內存空間時,這套函數首先試圖從內部堆中尋找可用的內存空間(常見內存分配算法有:首次適應算法、循環首次適應算法、最佳適應算法和最差適應算法等。os的基本內容!!)。如果沒有可用的內存空間,則試圖利用系統調用來動態增加程序數據段的內存大小,新分配得到的空間首先被組織進內部堆中去,然后再以適當的形式返回給調用者。當程序釋放分配的內存空間時,這片內存空間被返回到內部堆結構中,可能會被適當的處理(比如空閑空間合並成更大的空閑空間),以更適合下一次內存分配申請。 這套復雜的分配機制實際上相當於一個內存分配的緩沖池(Cache),使用這套機制有如下幾個原因:
(1)系統調用可能不支持任意大小的內存分配。有些系統的系統調用只支持固定大小及其倍數的內存請求(按頁分配);這樣的話對於大量的小內存分配來說會造成浪費。
(2)系統調用申請內存可能是代價昂貴的。 系統調用可能涉及到用戶態和核心態的轉換。
(3)沒有管理的內存分配在大量復雜內存的分配釋放操作下很容易造成內存碎片。
3.3 棧和堆的對比
從以上介紹中,它們有如下區別:
(1)棧是系統提供的功能,特點是快速高效,缺點是由限制,數據不靈活;
堆是函數庫提供的功能,特點是靈活方便,數據適應面廣,但是效率有一定降低。
(2)棧是系統數據結構,對於進程/線程是唯一的;
堆是函數庫內部數據結構,不一定唯一,不同堆分配的內存無法互相操作。
(3)棧空間分靜態分配和動態分配,一般由編譯器完成靜態分配,自動釋放,棧的動態分配是不被鼓勵的;
堆得分配總是動態的,雖然程序結束時所有的數據空間都會被釋放回系統,但是精確的申請內存/釋放內存匹配是良好程序的基本要素。
(4)碎片問題
對於堆來講,頻繁的new/delete等操作勢必會造成內存空間的不連續,從而造成大量的碎片,使程序的效率降低;對於棧來講,則不會存在這個問題,因為棧是后進先出(LIFO)的隊列。
(5)生長方向
堆的生長方向是向上的,也就是向這內存地址增加的方向;對於棧來講,生長方向卻是向下的,是向着內存地址減少的方向增長。
(6)分配方式
堆都是動態分配的,沒有靜態分配的堆;
棧有兩種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,比如局部變量的分配。動態分配則由alloca函數進行分配,但是棧的動態分配和堆不同,它的動態分配是由編譯器進行釋放,無需我們手工實現。
(7)分配效率
棧是機器系統提供的數據結構,計算機在底層提供支持,分配有專門的堆棧段寄存器,入棧出棧有專門的機器指令,這些都決定了棧的高效率執行。
堆是由C/C++函數庫提供的,機制比較復雜,有不同的分配算法,易產生內存碎片,需要對內存進行各種管理,效率比棧要低很多。
四:具體實例分析
例子(一)
看下面的一小段C程序,仔細體會各種內存分配機制。
int a = 0; //全局初始化區,a的值為0 char *p1; //全局未初始化區(C++中則初始化為NULL) int main() { int b; //b分配在棧上,整型 char s[] = "abc"; //s分配在棧上,char *類型;"abc\0"分配在棧上,運行時賦值,函數結束銷毀 char *p2; //p2分配在棧上,未初始化 char *p3 = "123456"; //p3指向"123456"分配在字符串常量存儲區的地址,編譯時確定 static int c = 0; //c在全局(靜態)初始化區,可以多次跨函數調用而保持原值 p1 = (char *)malloc(10); //p1在全局未初始化區,指向分配得來得10字節的堆區地址 p2 = (char *)malloc(20); //p2指向分配得來得20字節的堆區地址 strcpy(p1, "123456"); //"123456"放在字符串常量存儲區,編譯器可能會將它與p3所指向的"123456"優化成一塊 return 0; }
例子(二)
看下面的一小段代碼,體會堆與棧的區別:
int foo() { //其余代碼 int *p = new int[5]; //其余代碼 return 0; }
其中的語句int *p = new int[5];就包含了堆與棧。其中new關鍵字分配了一塊堆內存,而指針p本身所占得內存為棧內存(一般4個字節表示地址)。這句話的意思是在棧內存中存放了一個指向一塊堆內存的指針p。在程序中先確定在堆中分配內存的大小,然后調用new關鍵字分配內存,最后返回這塊內存首址,放入棧中。匯編代碼為:
int foo() { 008C1520 push ebp 008C1521 mov ebp,esp 008C1523 sub esp,0D8h 008C1529 push ebx 008C152A push esi 008C152B push edi 008C152C lea edi,[ebp-0D8h] 008C1532 mov ecx,36h 008C1537 mov eax,0CCCCCCCCh 008C153C rep stos dword ptr es:[edi] int *p = new int[5]; 008C153E push 14h 008C1540 call operator new[] (8C1258h) 008C1545 add esp,4 008C1548 mov dword ptr [ebp-0D4h],eax 008C154E mov eax,dword ptr [ebp-0D4h] 008C1554 mov dword ptr [p],eax return 0; 008C1557 xor eax,eax } 008C1559 pop edi 008C155A pop esi 008C155B pop ebx 008C155C add esp,0D8h 008C1562 cmp ebp,esp 008C1564 call @ILT+395(__RTC_CheckEsp) (8C1190h) 008C1569 mov esp,ebp 008C156B pop ebp 008C156C ret
如果需要釋放內存,這里我們需要使用delete[] p,告訴編譯器,我要刪除的是一個數組。
例子(三)
看下面的一小段代碼,試着找出其中的錯誤:
#include <iostream>
using namespace std; int main() { char a[] = "Hello"; // 分配在棧上 a[0] = 'X'; cout << a << endl; char *p = "World"; // 分配在字符串常量存儲區的地址 p[0] = 'X'; cout << p << endl; return 0; }
發現問題了嗎?是的,字符數組a的容量是6個字符,其內容為"hello\0"。a的內容時可以改變的,比如a[0]='X',因為其是在棧上分配的,也就是在運行時確定的內容。但是指針p指向的字符串"world"分配在字符串常量存儲區,內容為"world\0",常量字符串的內容時不可以修改的。從語法上來說,編譯器並不覺得語句p[0]='X'有什么問題,但是在運行時則會出現"access violation"非法內存訪問的問題。
以下幾個函數的變化要看清楚了:
char *GetString1(void) { char p[] = "hello,world"; //結果:h。由於數組指針指向第一元素的地址,所以調用之后是h return p; } char *GetString2(void) { char *p = "hello,world"; //結果:hello,world。由於p指向“hello,world”字符串常量區域地址 return p; } char *GetString3(void) { char *p = (char *)malloc(20); // 指向p所分配的堆上的內存空間。 return p; } char *GetString4(void) { char *p = new char[20]; // 指向p所分配的內存空間,p本身在棧上的,p所指向的空間是堆上的。 return p; }
附錄:內存管理注意事項
【規則1】用malloc或new申請內存之后,應該立即檢查指針值是否為NULL,防止使用指針值為NULL的內存,可以在函數入口處斷言檢測。
【規則2】不要忘記為數組或動態內存賦初值(比如calloc比malloc就要好),指針初始化為NULL(c++中為0)。
【規則3】避免數組或指針下標越界,特別當心發生“多1”或者"少1"
的操作。
【規則4】動態內存的申請和釋放必須配對,防止內存泄露,具體為malloc/calloc/realloc和free配對,new和delete以及delete[]配對。
【規則5】用free或者delete釋放內存后,應立即將指針設置為NULL(C++中為0),防止產生“野指針”、"懸垂指針"。
【規則6】遇到不懂得問題及時debug,一般的蟲子debug一下就灰飛煙滅了,一切bug都是浮雲而已