c/c++內存機制(一)(轉)

一:C語言中的內存機制ios

在C語言中,內存主要分爲以下5個存儲區:c++

(1)棧(Stack):位於函數內的局部變量(包括函數實參),由編譯器負責分配釋放,函數結束,棧變量失效。程序員

(2)堆(Heap):由程序員用malloc/calloc/realloc分配,free釋放。若是程序員忘記free了,則會形成內存泄露,程序結束時該片內存會由OS回收。算法

(3)全局區/靜態區(Global Static Area): 全局變量和靜態變量存放區,程序一經編譯好,該區域便存在。而且在C語言中初始化的全局變量和靜態變量和未初始化的放在相鄰的兩個區域(在C++中,因爲全局變量和靜態變量編譯器會給這些變量自動初始化賦值,因此沒有區分了)。因爲全局變量一直佔據內存空間且不易維護,推薦少用。程序結束時釋放。數組

(4)C風格字符串常量存儲區: 專門存放字符串常量的地方,程序結束時釋放。數據結構

(5)程序代碼區:存放程序二進制代碼的區域。app


二:C++中的內存機制函數

在C++語言中,與C相似,不過也有所不一樣,內存主要分爲以下5個存儲區:post

(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都是浮雲而已

相關文章
相關標籤/搜索