C語言基礎-內存管理

內存管理

在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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章