內存管理
在C語言中,關於內存管理的知識點比較多,如函數、變量、作用域、指針等,在探究C語言內存管理機制時,先簡單複習下這幾個基本概念:
1.變量不解釋。但需要搞清楚這幾種變量類型:
- 全局變量(外部變量):出現在代碼塊{}之外的變量就是全局變量。
- 局部變量(自動變量):一般情況下,代碼塊{}內部定義的變量就是自動變量,也可使用auto顯示定義。
- 靜態變量:是指內存位置在程序執行期間一直不改變的變量,用關鍵字static修飾。代碼塊內部的靜態變量只能被這個代碼塊內部訪問,代碼塊外部的靜態變量只能被定義這個變量的文件訪問。
- 注意:extern修飾變量時,根據具體情況,既可以看作是定義也可以看作是聲明;但extern修飾函數時只能是定義,沒有二義性。
2.作用域:
通常指的是變量的作用域,廣義上講,也有函數作用域及文件作用域等。我理解的作用域就是指某個事物能夠存在的區域或範圍,比如一滴水只有在0-100攝氏度之間才能存在,超出這個範圍,廣義上講的“水”就不存在了,它就變成了冰或氣體。
3.函數:不解釋。
注意:C語言中函數默認都是全局的,可以使用static關鍵字將函數聲明爲靜態函數(只能被定義這個函數的文件訪問的函數)。
二、內存四區
計算機中的內存是分區來管理的,程序和程序之間的內存是獨立的,不能互相訪問,比如QQ和瀏覽器分別所佔的內存區域是不能相互訪問的。而每個程序的內存也是分區管理的,一個應用程序所佔的內存可以分爲很多個區域,我們需要了解的主要有四個區域,通常叫內存四區,如下圖:
1.代碼區
程序被操作系統加載到內存的時候,所有的可執行代碼(程序代碼指令、常量字符串等)都加載到代碼區,這塊內存在程序運行期間是不變的。代碼區是平行的,裏面裝的就是一堆指令,在程序運行期間是不能改變的。函數也是代碼的一部分,故函數都被放在代碼區,包括main函數。
注意:"int a = 0;"語句可拆分成"int a;“和"a = 0”,定義變量a的"int a;"語句並不是代碼,它在程序編譯時就執行了,並沒有放到代碼區,放到代碼區的只有"a = 0"這句。
2.靜態區
靜態區存放程序中所有的全局變量和靜態變量。
3.棧區
棧(stack)是一種先進後出的內存結構,所有的自動變量、函數形參都存儲在棧中,這個動作由編譯器自動完成,我們寫程序時不需要考慮。棧區在程序運行期間是可以隨時修改的。當一個自動變量超出其作用域時,自動從棧中彈出。
-
每個線程都有自己專屬的棧;
-
棧的最大尺寸固定,超出則引起棧溢出;
-
變量離開作用域後棧上的內存會自動釋放。
實驗一:觀察代碼區、靜態區、棧區的內存地址#include "stdafx.h" int n = 0; void test(int a, int b) { printf("形式參數a的地址是:%d\n形式參數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靜態變量m的地址是:%d\n", &n, &m); test(a, b); printf("_tmain函數的地址是:%d", &_tmain); getchar(); }
運行結果如下:
結果分析:
- 自動變量a和b依次被定義和賦值,都在棧區存放,內存地址只相差12,需要注意的是a的地址比b要大,這是因爲棧是一種先進後出的數據存儲結構,先存放的a,後存放的b,形象化表示如上圖(注意地址編號順序)。一旦超出作用域,那麼變量b將先於變量a被銷燬。這很像往箱子裏放衣服,最先放的最後才能被拿出,最後放的最先被拿出。
實驗二:棧變量與作用域
#include "stdafx.h"
//函數的返回值是一個指針,儘管這樣可以運行程序,但這樣做是不合法的,因爲
//非要這樣做需在x變量前加static關鍵字修飾,即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();
}
結果分析
-
這段代碼沒有任何語法錯誤,也能得到預期的結果:20。但是這麼寫是有問題的:因爲int p =
getx()中變量x的作用域爲getx()函數體內部,這裏得到一個臨時棧變量x的地址,getx()函數調用結束後這個地址就無效了,但是後面的p
= 20仍然在對其進行訪問並修改,結果可能對也可能錯,實際工作中應避免這種做法,不然怎麼死的都不知道。不能將一個棧變量的地址通過函數的返回值返回,切記! -
另外,棧不會很大,一般都是以K爲單位。如果在程序中直接將較大的數組保存在函數內的棧變量中,很可能會內存溢出,導致程序崩潰(如下實驗三),嚴格來說應該叫棧溢出(當棧空間以滿,但還往棧內存壓變量,這個就叫棧溢出)。
實驗三:看看什麼是棧溢出
int _tmain(int argc, _TCHAR* argv[])
{
char array_char[1024*1024*1024] = {0};
array_char[0] = 'a';
printf("%s", array_char);
getchar();
}
怎麼辦?這個時候就該堆出場了。
4.堆區
堆(heap)和棧一樣,也是一種在程序運行過程中可以隨時修改的內存區域,但沒有棧那樣先進後出的順序。更重要的是堆是一個大容器,它的容量要遠遠大於棧,這可以解決上面實驗三造成的內存溢出困難。一般比較複雜的數據類型都是放在堆中。但是在C語言中,堆內存空間的申請和釋放需要手動通過代碼來完成。對於一個32位操作系統,最大管理管理4G內存,其中1G是給操作系統自己用的,剩下的3G都是給用戶程序,一個用戶程序理論上可以使用3G的內存空間。堆上的內存必須手動釋放(C/C++),除非語言執行環境支持GC(如C#在.NET上運行就有垃圾回收機制)。那堆內存如何使用?
接下來看堆內存的分配和釋放:
malloc與free
malloc函數用來在堆中分配指定大小的內存,單位爲字節(Byte),函數返回void *指針;free負責在堆中釋放malloc分配的內存。malloc與free一定成對使用。看下面的例子:
實驗四:解決棧溢出的問題
#include "stdafx.h"
#include "stdlib.h"
#include "string.h"
void print_array(char *p, char n)
{
int i = 0;
for (i = 0; i < n; i++)
{
printf("p[%d] = %d\n", i, p[i]);
}
}
int _tmain(int argc, _TCHAR* argv[])
{
char *p = (char *)malloc(1024*1024*1024);//在堆中申請了內存
memset(p, 'a', sizeof(int) * 10);//初始化內存
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = i + 65;
}
print_array(p, 10);
free(p);//釋放申請的堆內存
getchar();
}
程序可以正常運行,這樣就解決了剛纔實驗三的棧溢出問題。堆的容量有多大?理論上講,它可以使用除了系統佔用內存空間之外的所有空間。實際上比這要小些,比如我們平時會打開諸如QQ、瀏覽器之類的軟件,但這在一般情況下足夠用了。實驗二中說到,不能將一個棧變量的地址通過函數的返回值返回,如果我們需要返回一個函數內定義的變量的地址該怎麼辦?可以這樣做:
//實驗五:
#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);
}
這樣寫是沒有問題的,可以通過函數返回一個堆地址,但記得一定用通過free函數釋放申請的堆內存空間。"int p = (int)malloc(sizeof(int));"換成"static int a =0"也是合法的。因爲靜態區的內存在程序運行的整個期間都有效,但是後面的free函數就不能用了!
關於內存管理的幾個函數
malloc:
作用:從空閒內存池中分配內存
函數原型:void *malloc(size_t bytes);
返回值:申請成功返回內存首地址
失敗返回NULL
備註:申請的內存不會自動初始化
free:**
作用:釋放內存
函數原型:void free(void *p);
備註:動態分配的內存一定要通過free()函數來釋放,否則會造成內存泄露
內存泄露:
memset:
作用:內存初始化(按順序)
函數原型:void *memset(void *buffer, char ch,size_t n);
buffer:需要初始化的內存首地址
ch:初始化內容
n:初始化內存字節數
返回值:返回buffer
calloc:
作用:和malloc一樣,但可以初始化內存空間爲0
函數原型:void *calloc(size_t num, size_t bytes);
返回值:內存空間首地址,否則返回NULL
realloc:
作用:重新內存分配,並且會把原內存裏的內容拷貝到新內存
函數原型:void *realloc(void *ptr,size_t bytes);
備註:
1、如果新分配內存大小 按照指定內存大小分配
2、如果分配內存小於原大小,只拷貝指定個數的內存
3、如果新分配內存爲0,則釋放原內存,並返回NULL