C語言-內存管理基礎

前言

C語言作為一門應用途廣泛、功能強大、使用靈活的面向過程式編程語言。既可用于編寫應用軟件,又能用于編寫系統軟件。所以深入理解C語言的內存管理能夠加深我們對程序的理解,有助于開發出更高質量的應用。本文就是筆者在學習C語言過程中對其內存管理的學習總結,分享出來希望能給正在學習C、Objective-C、C++等語言的小伙伴們一點啟發。

內存定義

  • 硬件角度:內存是計算機必不可少的一個組成部分,是于CPU溝通的橋梁,計算機中所有的程序都是運行在內存中的。
  • 邏輯角度:內存是一塊具備隨機訪問能力,支持讀、寫操作,用來存放程序及程序運行中產生的數據的區域。
  • 聯想理解:以現實生活中的建筑物來類比最合適不過,計算機中可用的內存對應著一棟大樓,內存中的每一個單元格的地址對應著房間的門牌號。內存中存儲的內容就是住在房間里面的人或物。我們根據內存單元格的地址就能找到對應內存中存儲的數據。

內存單位和編址

  • 位 :( bit ) 是電子計算機中最小的數據單位。每一位的狀態只能是0或1。
  • 字節:1 Byte = 8 bit ,是內存基本的計量單位,
  • 字:"字" 由若干個字節構成,字的位數叫做字長,不同檔次的機器有不同的字長。
  • KB :1KB = 1024 Byte。也就是1024個字節。
  • MB : 1MB = 1024 KB。類似的還有GB、TB。
  • 內存編址計算機中的內存按字節編址,每個地址的存儲單元可以存放一個字節(8個bit)的數據,CPU通過內存地址獲取指令和數據,并不關心這個地址所代表的空間具體在什么位置、怎么分布,因為硬件的設計保證一個地址對應著一個固定的空間,所以說:內存地址和地址指向的空間共同構成了一個內存單元。
  • 內存地址內存地址通常用十六進制的數據表示,例如通常在C或者Objective-C中輸出一個變量的地址可能為:0x7fff5fbff79c,這就是一個用十六進制的數表示的地址。
    某塊內存的存儲單元分配可能如下:
    內存單元分配示意圖

    由上面的圖可以看出:內存是由字節為單位組成的,字節在內存中是連續分配的,一個字節單元的地址一般由十六進制的數表示。
  • 十六進制:計算機中數據的一種表示方法。由0-9,A-F組成,字母不區分大小寫。C、C++規定,16進制數必須以 0x 開頭。比如 0x1 表示一個16進制數。而 1 則通常表示一個十進制數。另外如:0xff、0xFF、0X102A等等。其中的x也不區分大小寫。(注意:0x 中的0是數字0,而不是字母O)。使用十六進制表示一個內存地址是因為:1,二進制、十進制、十六進制之間相互轉換比較方便;2,一位十六進制數可以表示4個二進制位數,更大的數使用十六進制數表示更加精短。3,計算機硬件設計需要。例如下圖的整數100在三種進制中的表示:
    二進制、十進制、十六進制表示

內存組成

對于一個由C語言編寫的程序而言,內存主要可以分為以下5個部分組成:

C語言程序內存組成圖

其中需要注意的是:代碼段、數據段、BSS段在程序編譯期間由編譯器分配空間,在程序啟動時加載,由于未初始化的全局變量存放在BSS段,已初始化的全局變量存放在數據段,所以程序中應該盡量少的使用全局變量以節省程序編譯和啟動時間;棧和堆在程序運行中由系統分配空間。下面簡單介紹一下各個組成部分具體含義,重點介紹堆、棧。

  • 棧(stack)
    首先棧應該作為數據結構中的一種線性結構被介紹。其具有先進后出的特點(簡稱FILO)
    棧的結構示意圖

    類似于手槍中的彈夾(別告訴我你沒打過手槍...),最先放入的子彈最后彈出,最后放入的子彈最先彈出,通過不斷移動棧頂指針實現子彈的裝載和發射(對應棧的操作是入棧和出棧)。與棧對應的還有一種數據結構叫做 “隊列”,具有先進先出(FIFO)的特點,這里就不再贅述了,有興趣的同學可以看看關于“數據結構”方面的書了解更多。
    其次棧作為內存中存儲結構,通常存放程序臨時創建的局部變量,即函數括大括號 “{ }” 中定義的變量,其中還包括函數調用時其形參,調用后的返回值等。 棧是由到高地址向低地址擴展的數據結構。即依次定義兩個局部變量,首先定義的變量的地址是高地址,其次變量的地址是低地址。例如
#include <stdio.h>
int main(int argc, const char * argv[]) {
    int a = 100;
    int b = 100;
    printf("%p \n",&a); // 0x7fff5fbff79c
    printf("%p \n",&b); // 0x7fff5fbff798
    return 0;
}   
// a 變量的地址 0x7fff5fbff79c 比 b 變量的地址 0x7fff5fbff798 要大 

最后棧還具有“小內存、自動化、可能會溢出”的特點。棧頂的地址和棧的最大容量一般是系統預先規定好的,通常不會太大。由于棧中主要存放的是局部變量,而局部變量的占用的內存空間是其所在的代碼段或函數段結束時由系統回收重新利用,所以棧的空間是循環利用自動管理的,一般不需要人為操作。如果某次局部變量申請的空間超過棧的剩余空間時就有可能出現 “棧的溢出”,進而導致意想不到的后果。所以一般不宜在棧中申請過大的空間,比如長度很大的數組、遞歸調用重復次數很多的函數等等。

  • 堆(heap)
    通常存放程序運行中動態分配的存儲空間。堆是低地址向高地址擴展的數據結構,是一塊不連續的內存區域。在標準C語言上,使用malloc等內存分配函數是從堆中分配內存的,在Objective-C中,使用new創建的對象也是從堆中分配內存的。
    堆具有“大內存、手工分配管理、申請大小隨意、可能會泄露”的特點,堆內存是操作系統劃分給堆管理器來管理的,管理器向使用者(用戶進程)提供API(malloc和free等)來使用堆內存。需要程序員手動分配釋放,如果程序員在使用完申請后的堆內存卻沒有及時把它釋放掉,那么這塊內存就丟失了(進程自身認為該內存沒被使用,但是在堆內存記錄中該內存仍然屬于這個進程,所以當需要分配空間時又會重新去申請新的內存而不是重復利用這塊內存),就是我們常說的-內存泄漏,所以內存泄漏指的是堆內存被泄露了。下面是一個很典型關于各種變量在內存中分配位置的例子(示例代碼來源于網絡):
#include <stdio.h>
int a = 0;                  // 全局初始化區
char p1;                    // 全局未初始化區
int main(int argc, const char * argv[]) {
      int b ;                 // 棧
      char s[] = "abc";       // 棧
      char p2 ;               // 棧
      char p3 = "123456";     // 123456在常量區,p3在棧上。
      static int c = 0 ;      // 全局(靜態)初始化區
      p1 = (char )malloc(10); // 分配的10字節的區域就在堆區
      p2 = (char )malloc(20); // 分配的20字節的區域就在堆區
      printf("%p\n",p1);      // 0xffffffb0
      printf("%p\n",p2);      // 0xffffffc0
      return 0;                
//p1 變量的地址 0xffffffb0 比 p2 變量的地址 0xffffffc0 要小
}
  • BSS段
    Block Started by Symbol的簡稱,通常是指用來存放程序中未初始化的全局變量和靜態變量。
  • 數據段
    通常是指用來存放程序中已初始化的全局變量和靜態變量以及字符串常量
  • 代碼段
    通常是指用來存放程序執行代碼的一塊內存區域。這部分區域的大小在程序運行前就已經確定。

C語言操作堆內存的函數

C語言中申請和釋放堆內存有四個函數,分別是:

  • malloc
    【函數原型】void *malloc(size_t __size)
    【參數說明】size 需要分配的內存空間的大小,單位是字節。
    【返回值類型】void * 表示未確定類型的指針,分配成功返回指向該內存的地址,失敗則返回NULL。C、C++規定,void * 類型可以強制轉換為任何其它類型的指針。
    【函數功能】 表示向系統申請分配指定 size 個字節的內存空間。例如:
    int *a = malloc(4);  //申請4個字節的空間用于存放一個int類型的值
    char *b = malloc(2);  //申請2個字節的空間用于存放一個char類型的值
  • calloc
    【函數原型】void *calloc(size_t __count, size_t __size)
    【參數說明】count 表示個數,size 單位個需要分配的內存空間的大小,單位是字節。
    【返回值類型】void * 表示未確定類型的指針。例如:
    【函數功能】 表示向系統申請分配 count 個長度為 size 一共為 count 乘以 size 個字節長度的連續內存空間,并將每一個字節都初始化為 0。
    int *c = calloc(10, sizeof(int)); 申請10個sizeof(int) 字節的空間
    char *d = calloc(2, sizeof(char)); 申請10個sizeof(char) 字節的空間
  • realloc
    【函數原型】void *realloc(void *__ptr, size_t __size)
    【參數說明】ptr 表示需要修改的內存空間的地址,size 表示需要重寫分配的內存空間的大小,單位是字節。
    【返回值類型】void * 表示未確定類型的指針。
    【函數功能】 表示更改已經配置好的內存空間到指定的大小。例如:
    char *d = calloc(2, sizeof(char));  //申請2個sizeof(char) 字節的空間
    char *f = realloc(d, 5 * sizeof(char));  //將原來變量d指向的2個sizeof(char) 字節的空間更改到5個sizeof(char) 字節的空間并由變量f指向。
  • free
    【函數原型】void free(void *)
    【參數說明】void * 表示需要釋放的內存空間對應的內存地址。
    【返回值類型】返回值為空。
    【函數功能】 表示用來釋放已經動態分配的內存空間。free() 可以釋放由 malloc()calloc()realloc() 分配的內存空間,以便其他程序再次使用。需要注意的是:free() 不會改變 傳入的指針的值,調用 free() 后它仍然會指向相同的內存空間,但是此時該內存已無效,不能被使用。所以建議將釋放完的指針設置為 NULL。例如:
    char *g = malloc(sizeof(char)); //申請sizeof(char)大小內存空間
    free(g);      //釋放掉g指針指向的內存空間
    g = NULL;     //將g指針指向NULL

free()函數只能釋放動態分配的內存,并不能釋放任意分配的內存,比如:

    int h[10];    //在棧上申請的10乘以sizeof(int)大小的內存空間
    free(h);      //此處報錯:不能釋放棧上分配的空間

void 指針類型

在系統提供的內存分配的4個庫函數malloccallocrecollocfree中,除了free的返回值為空外,其他三個函數的返回值均為void *類型,由于指針不是本文的重點,所以這里僅僅簡單介紹一下關于void指針類型(即void *類型)。
《C語言程序設計》一書中對void指針類型是這樣解釋的:

C99(C語言的官方標準第二版)允許使用基類型為void的指針類型,可以定義一個基類型為void的指針變量,它不指向任何類型的數據。請注意:不要把 “指向void類型” 理解為能指向 “任何的類型” 的數據,而應該理解為 “指向空類型” 或者 “不指向確定” 的類型的數據。在將它的值賦給另一個指針變量時由系統對它進行類型轉換,使之適合被賦值變量的類型。

例如下面的代碼:

int main(int argc, const char * argv[]) {
    int a = 3;                   //定義a為整型變量
    int *p1 = &a;                //p1指向 int 型變量
    char *p2;                    //p2指向 char 型變量
    void *p3;                    //p3為無類型指針變量
    p3 = (void *)p1;             //將p1的值轉換為void *類型,然后賦值給p3
    p2 = (char *)p3;             //將p3的值轉換為char *類型,然后賦值給p2
    printf("%d\n", *p1);         //輸出a的值 3
    p3 = &a;                    
    printf("%d", *p3);           //此處報錯,p3無指向,不能指向a 
    return 0;
}

C99標準把上述三個函數的基類型定義為void類型,這種指針稱之為無類型指針,即不指向哪一種具體的類型數據,只表示用來指向一個抽象類型的數據,僅僅提供一個純地址,不能指向任何具體的對象。

變量的存儲方式

  • 變量的作用域
    局部變量:在函數內定義的變量,只在該函數內有效
    全局變量:在函數外定義的變量,從定義開始到文件結束都有效
    全局變量的作用范圍

resultAresultB都是全局變量,但是作用范圍不同,在add()sub()main()函數中都可以使用resultA,但是add()不能使用resultB變量。

  • 變量的存儲規律
    變量在內存中以二進制形式存儲,一個變量占用的存儲空間,不僅和變量類型有關,還和編譯環境有關,同一種類型的變量在不同編譯環境下占用的存儲空間不一樣。比如開發中常用的基本數據類型charint等在不同編譯環境下就會占用不同大小的空間。

    占用空間和編譯環境示意圖

    類似下面的代碼在64位編譯器中其內存中地址和分配的內存空間應該如下圖:

int main(int argc, const char * argv[]) {
    int a = 1;
    int b = 2;
    printf("%p\n",&a);  //0x7fff5fbff79b
    printf("%p\n",&b);  //0x7fff5fbff797
    return 0;
}

int型變量內存分配

局部變量存儲細節:由于是a、b是臨時變量,因此他們的內存空間分配在棧上,棧中內存尋址由高到低,所以 a 變量的地址比 b 變量的地址要大,其次由于是在64位編譯環境中,int 型變量占據4個字節的空間,每一個字節由低到高依次對應著8位二進制數,四個8位二進制數就是十進制中的 1 或 2,而變量a、b的地址就是四個字節中最小值的內存地址。
全局變量存儲細節:關于全局變量存儲在前面介紹內存組成已經說明,這里不再贅述。
變量的存儲類別:C的存儲類別包括4種:auto(自動的)static(靜態的)register(寄存器的)extern(外部的)。根據變量的存儲類別可以得知其作用域和生命周期。

數組的存儲

C語言中數組的存儲和普通的變量不太一樣,數值中存儲的元素,是從所占用的低地址開始存儲的。例如:

int main(int argc, const char * argv[]) {
    char chars[4] = {'l','o','v','e'};
    printf("chars[0] = %p\n",&chars[0]); //0x7fff5fbff79b
    printf("chars[1] = %p\n",&chars[1]); //0x7fff5fbff79c
    printf("chars[2] = %p\n",&chars[2]); //0x7fff5fbff79d
    printf("chars[3] = %p\n",&chars[3]); //0x7fff5fbff79e
    return 0;
}
字符數組的存儲示意圖

僅僅通過上面的字符數組例子還不能完全說明數組在內存中關于其元素存儲和元素中值的存儲關系。如果換用一個整型數組就能看出一些差別。

int main(int argc, const char * argv[]) {
    int nums[2] = {5, 6};
    printf("nums[0] = %p\n",&nums[0]); // 0x7fff5fbff7a0
    printf("nums[1] = %p\n",&nums[1]); // 0x7fff5fbff7a4
    return 0;
}

整型數組內存分配

通過字符數組和整型數組內存分配示意圖可以看出:數組中的元素按照存放順序依次從低地址到高地址存放,但是每個元素中的內容又是按高地址向低地址方向存儲
數組在使用過程中遇到的最多的問題可能就是下標越界,下面的代碼就是越界訪問數組示例:

int main(int argc, const char * argv[]) {
    char charsOne[2] = {'a', 'b'};
    char charsTwo[3] = {'c', 'd', 'e'};
    charsTwo[3] = 'f';
    printf("charsOne[0] = %p\n",&charsOne[0]); // 0x7fff5fbff79e
    printf("charsTwo[0] = %p\n",&charsTwo[0]); // 0x7fff5fbff79b
    printf("charsOne[0] = %c\n",charsOne[0]); // f
    return 0;
}

數組下標越界示意圖

結合下標越界示意圖看上面的的代碼會發現,由于越界設置charsTwo[3]元素的值,導致變相更改了charsOne[0]的值。

int main(int argc, const char * argv[]) {
    char charsOne[2] = {'a', 'b'};
    char charsTwo[3] = {'c', 'd', 'e'};
    charsOne[-1] = 'g';
    printf("charsTwo[2] = %c\n",charsTwo[2]); // g
    return 0;
}

下標越界示意圖

同樣的道理,由于越界訪問charsOne[-1]元素,但是charsOne并不存在這個元素,編譯器會向上尋找到charsTwo [2]的位置,導致變相更改了charsTwo[2]的值。

通過數組在內存中的分配圖能夠更好幫助我們理解數組越界問題的嚴重性,在開發中使用數組首先應該考慮的就是數組長度問題,盡量不要出現類似低級錯誤。

總結

以上就是筆者對于C語言內存管理基本概念的學習心得,非常的基礎和入門,在接下來的文章中還可能會深入探討關于“一般函數調用”、“遞歸函數調用”、“結構體和指針”等在C語言中內存的分配。

如果文中有任何紕漏或錯誤歡迎在評論區留言指出,本人將在第一時間修改過來;喜歡我的文章,可以關注我以此促進交流學習; 如果覺得此文戳中了你的G點請隨手點贊;轉載請注明出處,謝謝支持。

下一篇:C語言-內存管理深入

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

  • C語言中內存分配 在任何程序設計環境及語言中,內存管理都十分重要。在目前的計算機系統或嵌入式系統中,內存資源仍然是...
    一生信仰閱讀 1,175評論 0 2
  • iOS面試小貼士 ———————————————回答好下面的足夠了------------------------...
    不言不愛閱讀 1,996評論 0 7
  • __block和__weak修飾符的區別其實是挺明顯的:1.__block不管是ARC還是MRC模式下都可以使用,...
    LZM輪回閱讀 3,350評論 0 6
  • ———————————————回答好下面的足夠了---------------------------------...
    恒愛DE問候閱讀 1,742評論 0 4
  • 全縣“五城同創”工作動員大會召開后,織金縣委統戰部迅速召開會議,傳達學習動員會議精神,明確責任人員,安排部署了相關...
    e75ce0a8161b閱讀 496評論 0 0