C語言 內存管理

在計算機系統,特別是嵌入式系統中,內存資源是非常有限的。尤其對於移動端開發者來說,硬件資源的限制使得其在程序設計中首要考慮的問題就是如何有效地管理內存資源。本文是作者在學習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)是一種先進後出的內存結構,所有的自動變量、函數形參都存儲在棧中,這個動作由編譯器自動完成,我們寫程序時不需要考慮。棧區在程序運行期間是可以隨時修改的。當一個自動變量超出其作用域時,自動從棧中彈出。

  • 每個線程都有自己專屬的棧;
  • 棧的最大尺寸固定,超出則引起棧溢出;
  • 變量離開作用域後棧上的內存會自動釋放。

  Talk is cheap, show you the code:

複製代碼

//實驗一:觀察代碼區、靜態區、棧區的內存地址

#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函數就不能用了!

  用來在堆中申請內存空間的函數還有calloc和realloc,用法與malloc類似。

 三、案例分析

案例一

 部分分析如下:

  main函數和UpdateCounter爲代碼的一部分,故存放在代碼區

  數組a默認爲全局變量,故存放在靜態區

  main函數中的"char *b = NULL"定義了自動變量b(variable),故其存放在棧區

  接着"b = (char *)malloc(1024*sizeof(char));"向堆申請了部分內存空間,故這段空間在堆區

案例二

  需要注意以下幾點:

  • 棧是從高地址向低地址方向增長;
  • 在C語言中,函數參數的入棧順序是從右到左,因此UpdateCounter函數的3個參數入棧順序是a1、c、b
  • C語言中形參和實參之間是值傳遞,UpdateCounter函數裏的參數a[1]、c、b與靜態區的a[1]、c、b不是同一個

  "char *b = NULL"定義一個指針變量b,b的地址是0xFFF8,值爲空-->運行到"b = (char*)malloc(1024*sizeof(char))"時纔在堆中申請了一塊內存(假設這塊內存地址爲0x77a0080)給了b,此時b的地址並沒有變化,但其值變爲了0x77a0080,這個值指向了一個堆空間的地址(棧變量的值指向了堆空間),這個過程b的內存變化如下:

                                      ---------->

四、學習內存管理的目的

  學習內存管理就是爲了知道日後怎麼樣在合適的時候管理我們的內存。那麼問題來了?什麼時候用堆什麼時候用棧呢?一般遵循以下三個原則:

  • 如果明確知道數據佔用多少內存,那麼數據量較小時用棧,較大時用堆;
  • 如果不知道數據量大小(可能需要佔用較大內存),最好用堆(因爲這樣保險些);
  • 如果需要動態創建數組,則用堆。
複製代碼
//實驗六:動態創建數組
int _tmain(int argc, _TCHAR* argv[])
{
    int i;
    scanf("%d", &i);
    int *array = (int *)malloc(sizeof(int) * i);
    //...//這裏對動態創建的數組做其他操作
    free(array);
}
複製代碼

最後的最後 

  操作系統在管理內存時,最小單位不是字節,而是內存頁(32位操作系統的內存頁一般是4K)。比如,初次申請1K內存,操作系統會分配1個內存頁,也就是4K內存。4K是一個折中的選擇,因爲:內存頁越大,內存浪費越多,但操作系統內存調度效率高,不用頻繁分配和釋放內存;內存頁越小,內存浪費越少,但操作系統內存調度效率低,需要頻繁分配和釋放內存。嵌入式系統的內存內存資源很稀缺,其內存頁會更小,因此在嵌入式開發當中需要特別注意。

轉:https://www.cnblogs.com/yif1991/p/5049638.html

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