C語言內(nèi)存精講,圖文分析內(nèi)存四區(qū)
鏈接:https://www.cnblogs.com/yif1991/p/5049638.html
在計算機(jī)系統(tǒng),特別是嵌入式系統(tǒng)中,內(nèi)存資源是非常有限的。尤其對于移動端開發(fā)者來說,硬件資源的限制使得其在程序設(shè)計中首要考慮的問題就是如何有效地管理內(nèi)存資源。本文是作者在學(xué)習(xí)C語言內(nèi)存管理的過程中做的一個總結(jié),如有不妥之處,望讀者不吝指正。
一、幾個基本概念
在C語言中,關(guān)于內(nèi)存管理的知識點比較多,如函數(shù)、變量、作用域、指針等,在探究C語言內(nèi)存管理機(jī)制時,先簡單復(fù)習(xí)下這幾個基本概念:
1.變量:不解釋。但需要搞清楚這幾種變量類型:
全局變量(外部變量):出現(xiàn)在代碼塊{}之外的變量就是全局變量。局部變量(自動變量):一般情況下,代碼塊{}內(nèi)部定義的變量就是自動變量,也可使用auto顯示定義。靜態(tài)變量:是指內(nèi)存位置在程序執(zhí)行期間一直不改變的變量,用關(guān)鍵字static修飾。代碼塊內(nèi)部的靜態(tài)變量只能被這個代碼塊內(nèi)部訪問,代碼塊外部的靜態(tài)變量只能被定義這個變量的文件訪問。注意:extern修飾變量時,根據(jù)具體情況,既可以看作是定義也可以看作是聲明;但extern修飾函數(shù)時只能是定義,沒有二義性。
2.作用域:通常指的是變量的作用域,廣義上講,也有函數(shù)作用域及文件作用域等。我理解的作用域就是指某個事物能夠存在的區(qū)域或范圍,比如一滴水只有在0-100攝氏度之間才能存在,超出這個范圍,廣義上講的“水”就不存在了,它就變成了冰或氣體。
3.函數(shù):不解釋。
注意:C語言中函數(shù)默認(rèn)都是全局的,可以使用static關(guān)鍵字將函數(shù)聲明為靜態(tài)函數(shù)(只能被定義這個函數(shù)的文件訪問的函數(shù))。
二、內(nèi)存四區(qū)
計算機(jī)中的內(nèi)存是分區(qū)來管理的,程序和程序之間的內(nèi)存是獨立的,不能互相訪問,比如QQ和瀏覽器分別所占的內(nèi)存區(qū)域是不能相互訪問的。而每個程序的內(nèi)存也是分區(qū)管理的,一個應(yīng)用程序所占的內(nèi)存可以分為很多個區(qū)域,我們需要了解的主要有四個區(qū)域,通常叫內(nèi)存四區(qū),如下圖:

1.代碼區(qū)
程序被操作系統(tǒng)加載到內(nèi)存的時候,所有的可執(zhí)行代碼(程序代碼指令、常量字符串等)都加載到代碼區(qū),這塊內(nèi)存在程序運行期間是不變的。代碼區(qū)是平行的,里面裝的就是一堆指令,在程序運行期間是不能改變的。函數(shù)也是代碼的一部分,故函數(shù)都被放在代碼區(qū),包括main函數(shù)。
注意:"int a = 0;"語句可拆分成"int a;"和"a = 0",定義變量a的"int a;"語句并不是代碼,它在程序編譯時就執(zhí)行了,并沒有放到代碼區(qū),放到代碼區(qū)的只有"a = 0"這句。
2.靜態(tài)區(qū)
靜態(tài)區(qū)存放程序中所有的全局變量和靜態(tài)變量。
3.棧區(qū)
棧(stack)是一種先進(jìn)后出的內(nèi)存結(jié)構(gòu),所有的自動變量、函數(shù)形參都存儲在棧中,這個動作由編譯器自動完成,我們寫程序時不需要考慮。棧區(qū)在程序運行期間是可以隨時修改的。當(dāng)一個自動變量超出其作用域時,自動從棧中彈出。
每個線程都有自己專屬的棧;棧的最大尺寸固定,超出則引起棧溢出;變量離開作用域后棧上的內(nèi)存會自動釋放?! alk is cheap, show you the code:
//實驗一:觀察代碼區(qū)、靜態(tài)區(qū)、棧區(qū)的內(nèi)存地址
#include?"stdafx.h"
int?n?=?0;
void?test(int?a,?int?b)
{
printf("形式參數(shù)a的地址是:%d\n形式參數(shù)b的地址是:%d\n",&a,?&b);
}
int?_tmain(int?argc,?_TCHAR*?argv[])
{
static?int?m?=?0;
int?a?=?0;
int?b?=?0;
printf("自動變量a的地址是:%d\n自動變量b的地址是:%d\n",?&a,?&b);
printf("全局變量n的地址是:%d\n靜態(tài)變量m的地址是:%d\n",?&n,?&m);
test(a,?b);
printf("_tmain函數(shù)的地址是:%d",?&_tmain);
getchar();
}
運行結(jié)果如下:

結(jié)果分析:自動變量a和b依次被定義和賦值,都在棧區(qū)存放,內(nèi)存地址只相差12,需要注意的是a的地址比b要大,這是因為棧是一種先進(jìn)后出的數(shù)據(jù)存儲結(jié)構(gòu),先存放的a,后存放的b,形象化表示如上圖(注意地址編號順序)。一旦超出作用域,那么變量b將先于變量a被銷毀。這很像往箱子里放衣服,最先放的最后才能被拿出,最后放的最先被拿出。
關(guān)注公眾號:C語言中文社區(qū),免費領(lǐng)取300G編程資料
//實驗二:棧變量與作用域
#include?"stdafx.h"
//函數(shù)的返回值是一個指針,盡管這樣可以運行程序,但這樣做是不合法的,因為
//非要這樣做需在x變量前加static關(guān)鍵字修飾,即static?int?a?=?0;
int?*getx()
{
????int?x?=?10;
????return?&x;
}
int?_tmain(int?argc,?_TCHAR*?argv[])
{
????int?*p?=?getx();
????*p?=?20;
????printf("%d",?*p);
????getchar();
}
這段代碼沒有任何語法錯誤,也能得到預(yù)期的結(jié)果:20。但是這么寫是有問題的:因為int p = getx()中變量x的作用域為getx()函數(shù)體內(nèi)部,這里得到一個臨時棧變量x的地址,getx()函數(shù)調(diào)用結(jié)束后這個地址就無效了,但是后面的p = 20仍然在對其進(jìn)行訪問并修改,結(jié)果可能對也可能錯,實際工作中應(yīng)避免這種做法,不然怎么死的都不知道。不能將一個棧變量的地址通過函數(shù)的返回值返回,切記!
另外,棧不會很大,一般都是以K為單位。如果在程序中直接將較大的數(shù)組保存在函數(shù)內(nèi)的棧變量中,很可能會內(nèi)存溢出,導(dǎo)致程序崩潰(如下實驗三),嚴(yán)格來說應(yīng)該叫棧溢出(當(dāng)??臻g以滿,但還往棧內(nèi)存壓變量,這個就叫棧溢出)。
//實驗三:看看什么是棧溢出
int?_tmain(int?argc,?_TCHAR*?argv[])
{
????char?array_char[1024*1024*1024]?=?{0};
????array_char[0]?=?'a';
????printf("%s",?array_char);
????getchar();
}

怎么辦?這個時候就該堆出場了。
4.堆區(qū)
堆(heap)和棧一樣,也是一種在程序運行過程中可以隨時修改的內(nèi)存區(qū)域,但沒有棧那樣先進(jìn)后出的順序。更重要的是堆是一個大容器,它的容量要遠(yuǎn)遠(yuǎn)大于棧,這可以解決上面實驗三造成的內(nèi)存溢出困難。一般比較復(fù)雜的數(shù)據(jù)類型都是放在堆中。但是在C語言中,堆內(nèi)存空間的申請和釋放需要手動通過代碼來完成。對于一個32位操作系統(tǒng),最大管理管理4G內(nèi)存,其中1G是給操作系統(tǒng)自己用的,剩下的3G都是給用戶程序,一個用戶程序理論上可以使用3G的內(nèi)存空間。堆上的內(nèi)存必須手動釋放(C/C++),除非語言執(zhí)行環(huán)境支持GC(如C#在.NET上運行就有垃圾回收機(jī)制)。那堆內(nèi)存如何使用?
接下來看堆內(nèi)存的分配和釋放:
malloc與free
malloc函數(shù)用來在堆中分配指定大小的內(nèi)存,單位為字節(jié)(Byte),函數(shù)返回void *指針;free負(fù)責(zé)在堆中釋放malloc分配的內(nèi)存。malloc與free一定成對使用??聪旅娴睦樱?/p>
//實驗四:解決棧溢出的問題
#include?"stdafx.h"
#include?"stdlib.h"
#include?"string.h"
void?print_array(char?*p,?char?n)
{
????int?i?=?0;
????for?(i?=?0;?i?????{
????????printf("p[%d]?=?%d\n",?i,?p[i]);
????}
}
int?_tmain(int?argc,?_TCHAR*?argv[])
{
????char?*p?=?(char?*)malloc(1024*1024*1024);//在堆中申請了內(nèi)存
????memset(p,?'a',?sizeof(int)?*?10);//初始化內(nèi)存
????int?i?=?0;
????for?(i?=?0;?i?10;?i++)
????{
????????p[i]?=?i?+?65;
????}
????print_array(p,?10);
????free(p);//釋放申請的堆內(nèi)存
????getchar();
}
運行結(jié)果為:

程序可以正常運行,這樣就解決了剛才實驗三的棧溢出問題。堆的容量有多大?理論上講,它可以使用除了系統(tǒng)占用內(nèi)存空間之外的所有空間。實際上比這要小些,比如我們平時會打開諸如QQ、瀏覽器之類的軟件,但這在一般情況下足夠用了。實驗二中說到,不能將一個棧變量的地址通過函數(shù)的返回值返回,如果我們需要返回一個函數(shù)內(nèi)定義的變量的地址該怎么辦?可以這樣做:
//實驗五:
#include?"stdafx.h"
#include?"stdlib.h"
int?*getx()
{
????int?*p?=?(int?*)malloc(sizeof(int));//申請了一個堆空間
????return?p;
}
int?_tmain(int?argc,?_TCHAR*?argv[])
{
????int?*pp?=?getx();
????*pp?=?10;
????free(pp);
}
這樣寫是沒有問題的,可以通過函數(shù)返回一個堆地址,但記得一定用通過free函數(shù)釋放申請的堆內(nèi)存空間。"int *p = (int *)malloc(sizeof(int));"換成"static int a = 0"也是合法的。因為靜態(tài)區(qū)的內(nèi)存在程序運行的整個期間都有效,但是后面的free函數(shù)就不能用了!
用來在堆中申請內(nèi)存空間的函數(shù)還有calloc和realloc,用法與malloc類似。
三、案例分析 案例一

部分分析如下:
main函數(shù)和UpdateCounter為代碼的一部分,故存放在代碼區(qū)
數(shù)組a默認(rèn)為全局變量,故存放在靜態(tài)區(qū)
main函數(shù)中的"char *b = NULL"定義了自動變量b(variable),故其存放在棧區(qū)
接著"b = (char?)malloc(1024sizeof(char));"向堆申請了部分內(nèi)存空間,故這段空間在堆區(qū)
案例二

需要注意以下幾點:
棧是從高地址向低地址方向增長; 在C語言中,函數(shù)參數(shù)的入棧順序是從右到左,因此UpdateCounter函數(shù)的3個參數(shù)入棧順序是a1、c、b; C語言中形參和實參之間是值傳遞,UpdateCounter函數(shù)里的參數(shù)a[1]、c、b與靜態(tài)區(qū)的a[1]、c、b不是同一個;
"char *b = NULL"定義一個指針變量b,b的地址是0xFFF8,值為空-->運行到"b = (char*)malloc(1024*sizeof(char))"時才在堆中申請了一塊內(nèi)存(假設(shè)這塊內(nèi)存地址為0x77a0080)給了b,此時b的地址并沒有變化,但其值變?yōu)榱?x77a0080,這個值指向了一個堆空間的地址(棧變量的值指向了堆空間),這個過程b的內(nèi)存變化如下:

四、學(xué)習(xí)內(nèi)存管理的目的
學(xué)習(xí)內(nèi)存管理就是為了知道日后怎么樣在合適的時候管理我們的內(nèi)存。那么問題來了?什么時候用堆什么時候用棧呢?一般遵循以下三個原則:
如果明確知道數(shù)據(jù)占用多少內(nèi)存,那么數(shù)據(jù)量較小時用棧,較大時用堆; 如果不知道數(shù)據(jù)量大小(可能需要占用較大內(nèi)存),最好用堆(因為這樣保險些); 如果需要動態(tài)創(chuàng)建數(shù)組,則用堆。
//實驗六:動態(tài)創(chuàng)建數(shù)組
int?_tmain(int?argc,?_TCHAR*?argv[])
{
????int?i;
????scanf("%d",?&i);
????int?*array?=?(int?*)malloc(sizeof(int)?*?i);
????//...//這里對動態(tài)創(chuàng)建的數(shù)組做其他操作
????free(array);
}
最后的最后
操作系統(tǒng)在管理內(nèi)存時,最小單位不是字節(jié),而是內(nèi)存頁(32位操作系統(tǒng)的內(nèi)存頁一般是4K)。比如,初次申請1K內(nèi)存,操作系統(tǒng)會分配1個內(nèi)存頁,也就是4K內(nèi)存。4K是一個折中的選擇,因為:內(nèi)存頁越大,內(nèi)存浪費越多,但操作系統(tǒng)內(nèi)存調(diào)度效率高,不用頻繁分配和釋放內(nèi)存;內(nèi)存頁越小,內(nèi)存浪費越少,但操作系統(tǒng)內(nèi)存調(diào)度效率低,需要頻繁分配和釋放內(nèi)存。嵌入式系統(tǒng)的內(nèi)存內(nèi)存資源很稀缺,其內(nèi)存頁會更小,因此在嵌入式開發(fā)當(dāng)中需要特別注意。
好文章點贊、在看和分享一條龍吧??
