C語言探索之旅 | 第二部分第八課:動態分配

做者 謝恩銘,公衆號「程序員聯盟」(微信號:coderhub)。 轉載請註明出處。 原文:www.jianshu.com/p/bbce8f04f…程序員

《C語言探索之旅》全系列編程

內容簡介


  1. 前言
  2. 變量的大小
  3. 內存的動態分配
  4. 動態分配一個數組
  5. 總結
  6. 第二部分第九課預告

1. 前言


上一課是 C語言探索之旅 | 第二部分第七課:文件讀寫數組

經歷了第二部分的一些難點課程,咱們終於來到了這一課,一個聽起來有點酷酷的名字:動態分配bash

「萬水千山老是情,分配也由系統定」。微信

到目前爲止,咱們建立的變量都是系統的編譯器爲咱們自動構建的,這是簡單的方式。函數

其實還有一種更偏手動的建立變量的方式,咱們稱爲「動態分配」(Dynamic Allocation)。dynamic 表示「動態的」,allocation 表示「分配」。學習

動態分配的一個主要好處就是能夠在內存中「預置」必定空間大小,在編譯時還不知道到底會用多少。測試

使用這個技術,咱們能夠建立大小可變的數組。到目前爲止咱們所建立的數組都是大小固定不可變的。而學完這一課後咱們就會建立所謂「動態數組」了。ui

學習這一章須要對指針有必定了解,若是指針的概念你還沒掌握好,能夠回去複習 C語言探索之旅 | 第二部分第二課:進擊的指針,C語言的王牌! 那一課。spa

咱們知道當咱們建立一個變量時,在內存中要爲其分配必定大小的空間。例如:

int number = 2;
複製代碼

當程序運行到這一行代碼時,會發生幾件事情:

  1. 應用程序詢問操做系統(Operating System,簡稱 OS。例如Windows,Linux,macOS,Android,iOS,等)是否可使用一小塊內存空間。

  2. 操做系統回覆咱們的程序,告訴它能夠將這個變量存儲在內存中哪一個地方(給出分配的內存地址)。

  3. 當函數結束後,你的變量會自動從內存中被刪除。你的程序對操做系統說:「我已經不須要內存中的這塊地址了,謝謝!」 (固然,實際上你的程序不可能對操做系統說一聲「謝謝」,可是確實是操做系統在掌管一切,包括內存,因此對它仍是客氣一點比較好...)。

能夠看到,以上的過程都是自動的。當咱們建立一個變量,操做系統就會自動被程序這樣調用。

那麼什麼是手動的方式呢?說實在的,沒人喜歡把事情複雜化,若是自動方式可行,何須要大費周章來使用什麼手動方式呢?可是要知道,不少時候咱們是不得不使用手動方式。

這一課中,咱們將會:

  1. 探究內存的機制(是的,雖然之前的課研究過,可是仍是要繼續深刻),瞭解不一樣變量類型所佔用的內存大小。

  2. 接着,探究這一課的主題,來學習如何向操做系統動態請求內存。也就是所謂的「動態內存分配」。

  3. 最後,經過學習如何建立一個在編譯時還不知道其大小(只有在程序運行時才知道)的數組來了解動態內存分配的好處。

準備好了嗎?Let's Go !

2. 變量的大小


根據咱們所要建立的變量的類型(char,int,double,等等),其所佔的內存空間大小是不同的。

事實上,爲了存儲一個大小在 -128 至 127 之間的數(char 類型),只須要佔用一個字節(8 個二進制位)的內存空間,是很小的。

然而,一個 int 類型的變量就要佔據 4 個字節了;一個 double 類型要佔據 8 個字節。

問題是:並不老是這樣。

什麼意思呢?

由於類型所佔內存的大小還與操做系統有關係。不一樣的操做系統可能就不同,32 位和 64 位的操做系統的類型大小通常會有區別。

這一節中咱們的目的是學習如何獲知變量所佔用的內存大小。

有一個很簡單的方法:使用 sizeof()

雖然看着有點像函數,但其實 sizeof 不是一個函數,而是一個 C語言的關鍵字,也算是一個運算符吧。

咱們只須要在 sizeof 的括號裏填入想要檢測的變量類型,sizeof 就會返回所佔用的字節數了。

例如,咱們要檢測 int 類型的大小,就能夠這樣寫:

sizeof(int)
複製代碼

在編譯時,sizeof(int) 就會被替換爲 int 類型所佔用的字節數了。

在個人電腦上,sizeof(int) 是 4,也就是說 int 類型在個人電腦的內存中佔據 4 個字節。在你的電腦上,也許是 4,但也多是其餘的值。

咱們用一個例子來測試一下吧:

// octet 是英語「字節」的意思,和 byte 相似
printf("char : %d octets\n", sizeof(char));
printf("int : %d octets\n", sizeof(int));
printf("long : %d octets\n", sizeof(long));
printf("double : %d octets\n", sizeof(double));
複製代碼

在個人電腦(64 位)運行,輸出:

char : 1 octets
int : 4 octets
long : 8 octets
double : 8 octets
複製代碼

咱們並無測試全部已知的變量類型,你也能夠課後本身去測試一下其餘的類型,例如:short,float。

曾幾什麼時候,當電腦的內存很小的年代,有這麼多不一樣大小的變量類型可供選擇是一件很好的事,由於咱們能夠選「夠用的最小的」那種變量類型,以節約內存。

如今,電腦的內存通常都很大,「有錢任性」麼。因此咱們在編程時也不必太「拘謹」。不過在嵌入式領域,內存大小通常是有限的,咱們就得斟酌着使用變量類型了。

既然 sizeof 這麼好用,咱們可不能夠用它來顯示咱們自定義的變量類型的大小呢?例如 struct,enum,union。

是能夠的。寫一個程序測試一下:

#include <stdio.h>

typedef struct Coordinate
{
    int x;
    int y;
} Coordinate;

int main(int argc, char *argv[])
{
    printf("Coordinate 結構體的大小是 : %d 個字節\n", sizeof(Coordinate));

    return 0;
}
複製代碼

運行輸出:

Coordinate 結構體的大小是 : 8 個字節
複製代碼

對於內存的全新視角


以前,咱們在繪製內存圖示時,仍是比較不精準的。如今,咱們知道了每一個變量所佔用的大小,咱們的內存圖示就能夠變得更加精準了。

假如我定義一個 int 類型的變量:

int age = 17;
複製代碼

咱們用 sizeof 測試後得知 int 的大小爲 4。假設咱們的變量 age 被分配到的內存地址起始是 1700,那麼咱們的內存圖示就以下所示:

咱們看到,咱們的 int 型變量 age 在內存中佔用 4 個字節,起始地址是 1700(它的內存地址),一直到 1703。

若是咱們對一個 char 型變量(大小是一個字節)一樣賦值:

char number = 17;
複製代碼

那麼,其內存圖示是這樣的:

假如是一個 int 型的數組:

int age[100];
複製代碼

用 sizeof() 測試一下,就能夠知道在內存中 age 數組佔用 400 個字節。4 * 100 = 400。

即便這個數組沒有賦初值,可是在內存中仍然佔據 400 個字節的空間。變量一聲明,在內存中就爲它分配必定大小的內存了。

那麼,若是咱們建立一個類型是 Coordinate 的數組呢?

Coordinate coordinate[100];
複製代碼

其大小就是 8 * 100 = 800 個字節了。

3. 內存的動態分配


好了,如今咱們就進入這一課的關鍵部分了,重提一次這一課的目的:學會如何手動申請內存空間。

咱們須要引入 stdlib.h 這個標準庫頭文件,由於接下來要使用的函數是定義在這個庫裏面。

這兩個函數是什麼呢?就是:

  • malloc:是 Memory Allocation 的縮寫,表示「內存分配」。詢問操做系統可否預支一塊內存空間來使用。

  • free:表示「解放,釋放,自由的」。意味着「釋放那塊內存空間」。告訴操做系統咱們再也不須要這塊已經分配的空間了,這塊內存空間會被釋放,另外一個程序就可使用這塊空間了。

當咱們手動分配內存時,需要按照如下三步順序來:

  1. 調用 malloc 函數來申請內存空間。

  2. 檢測 malloc 函數的返回值,以得知操做系統是否成功爲咱們的程序分配了這塊內存空間。

  3. 一旦使用完這塊內存,再也不須要時,必須用 free 函數來釋放佔用的內存,否則可能會形成內存泄漏。

以上三個步驟是否是讓咱們回憶起關於上一課「文件讀寫」的內容了?

這三個步驟和文件指針的操做有點相似,也是先申請內存,檢測是否成功,用完釋放。

malloc 函數:申請內存


malloc 分配的內存是在堆上,通常的局部變量(自動分配的)大可能是在棧上。

關於堆和棧的區別,還有內存的其餘區域,如靜態區等,你們能夠本身延伸閱讀。

以前「字符串」那一課裏已經給出過一張圖表了。再來回顧一下吧:

名稱 內容
代碼段 可執行代碼、字符串常量
數據段 已初始化全局變量、已初始化全局靜態變量、局部靜態變量、常量數據
BSS段 未初始化全局變量,未初始化全局靜態變量
局部變量、函數參數
動態內存分配

給出 malloc 函數的原型,你會發現有點滑稽:

void* malloc(size_t numOctetsToAllocate);
複製代碼

能夠看到,malloc 函數有一個參數 numOctetsToAllocate,就是須要申請的內存空間大小(用字節數表示),這裏的 size_t(以前的課程有提到過)其實和 int 是相似的,就是一個 define 宏定義,實際上不少時候就是 int。

對於咱們目前的演示程序,能夠將 sizeof(int) 置於 malloc 的括號中,表示要申請 int 類型的大小的空間。

真正引發咱們興趣的是 malloc 函數的返回值:

void*
複製代碼

若是你還記得咱們在函數那章所說的,void 表示「空」,咱們用 void 來表示函數沒有返回值。

因此說,這裏咱們的函數 malloc 會返回一個指向 void 的指針,一個指向「空」(void 表示「虛無,空」)的指針,有什麼意義呢?malloc 函數的做者不會搞錯了吧?

不要擔憂,這麼作確定是有理由的。

難道有人敢質疑老爺子 Dennis Ritchie(C語言的做者)的智商? 來人吶,拖出去... 罰寫 100 個 C語言小遊戲。

事實上,這個函數返回一個指針,指向操做系統分配的內存的首地址。

若是操做系統在 1700 這個地址爲你開闢了一塊內存的話,那麼函數就會返回一個包含 1700 這個值的指針。

可是,問題是:malloc 函數並不知道你要建立的變量是什麼類型的。

實際上,你只給它傳遞了一個參數: 在內存中你須要申請的字節數。

若是你申請 4 個字節,那麼有多是 int 類型,也有多是 long 類型。

正由於 malloc 不知道本身應該返回什麼變量類型(它也無所謂,只要分配了一塊內存就能夠了),因此它會返回 void* 這個類型。這是一個能夠表示任意指針類型的指針。

void* 與其餘類型的指針之間能夠經過強制轉換來相互轉換。例如:

int *i = (int *)p;  // p 是一個 void* 類型的指針

void *v = (void *)c;  // c 是一個 char* 類型的指針
複製代碼

實踐


若是我實際來用 malloc 函數分配一個 int 型指針:

int *memoryAllocated = NULL;  // 建立一個 int 型指針

memoryAllocated = malloc(sizeof(int));  // malloc 函數將分配的地址賦值給咱們的指針 memoryAllocated
複製代碼

通過上面的兩行代碼,咱們的 int 型指針 memoryAllocated 就包含了操做系統分配的那塊內存地址的首地址值。

假如咱們用以前咱們的圖示來舉例,這個值就是 1700。

檢測指針


既然上面咱們用兩行代碼使得 memoryAllocated 這個指針包含了分配到的地址的首地址值,那麼咱們就能夠經過檢測 memoryAllocated 的值來判斷申請內存是否成功了:

  1. 若是爲 NULL,則說明 malloc 調用沒有成功。

  2. 不然,就說明成功了。

通常來講內存分配不會失敗,可是也有極端狀況:

  1. 你的內存(堆內存)已經不夠了。

  2. 你申請的內存值大得離譜(好比你申請 64 GB 的內存空間,那我想大多數電腦都是不可能分配成功的)。

但願你們每次用 malloc 函數時都要作指針的檢測,萬一真的出現返回值爲 NULL 的狀況,那咱們須要當即中止程序,由於沒有足夠的內存,也不可能進行下面的操做了。

爲了中斷程序的運行,咱們來使用一個新的函數:

exit()
複製代碼

exit 函數定義在 stdlib.h 中,調用此函數會使程序當即中止。

這個函數也只有一個參數,就是返回值,這和 return 函數的參數是同樣原理的。實例:

int main(int argc, char *argv[])
{
    int *memoryAllocated = NULL;

    memoryAllocated = malloc(sizeof(int));

    if (memoryAllocated == NULL)  // 若是分配內存失敗
    {
        exit(0);  // 當即中止程序
    }

    // 若是指針不爲 NULL,那麼能夠繼續進行接下來的操做

    return 0;
}
複製代碼

另一個問題:用 malloc 函數申請 0 字節內存會返回 NULL 指針嗎?


能夠測試一下,也能夠去查找關於 malloc 函數的說明文檔。

申請 0 字節內存,函數並不返回 NULL,而是返回一個正常的內存地址。 可是你卻沒法使用這塊大小爲 0 的內存!

這就比如尺子上的某個刻度,刻度自己並無長度,只有某兩個刻度一塊兒才能量出長度。

對於這一點必定要當心,由於這時候 if(NULL != p) 語句校驗將不起做用。

free函數:釋放內存


記得上一課咱們使用 fclose 函數來關閉一個文件指針,也就是釋放佔用的內存。

free 函數的原理和 fclose 是相似的,咱們用它來釋放一塊咱們再也不須要的內存。原型:

void free(void* pointer);
複製代碼

free 函數只有一個目的:釋放 pointer 指針所指向的那塊內存。

實例程序:

int main(int argc, char *argv[])
{
    int* memoryAllocated = NULL;

    memoryAllocated = malloc(sizeof(int));

    if (memoryAllocated == NULL)  // 若是分配內存失敗
    {
        exit(0);  // 當即中止程序
    }

    // 此處添加使用這塊內存的代碼

    free(memoryAllocated);  // 咱們再也不須要這塊內存了,釋放之

    return 0;
}
複製代碼

綜合上面的三個步驟,咱們來寫一個完整的例子:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int* memoryAllocated = NULL;

    memoryAllocated = malloc(sizeof(int));  // 分配內存

    if (memoryAllocated == NULL)  // 檢測是否分配成功
    {
        exit(0);  // 不成功,結束程序
    }

    // 使用這塊內存
    printf("您幾歲了 ? ");

    scanf("%d", memoryAllocated);

    printf("您已經 %d 歲了\n", *memoryAllocated);

    free(memoryAllocated);  // 釋放這塊內存

    return 0;
}
複製代碼

運行輸出:

您幾歲了 ? 32
您已經 32 歲了
複製代碼

以上就是咱們用動態分配的方式來建立了一個 int 型變量,使用它,釋放它所佔用的內存。

可是,咱們也徹底能夠用之前的方式來實現,以下:

int main(int argc, char *argv[])
{
    int myAge = 0; // 分配內存 (自動)

    // 使用這塊內存
    printf("您幾歲了 ? ");

    scanf("%d", &myAge);

    printf("你已經 %d 歲了\n", myAge);

    return 0;
}  // 釋放內存 (在函數結束後自動釋放)
複製代碼

在這個簡單使用場景下,兩種方式(手動和自動)都是能完成任務的。

總結說來,建立一個變量(說到底也就是分配一塊內存空間)有兩種方式:自動和手動。

  • 自動:咱們熟知而且一直使用到如今的方式。

  • 手動(動態):這一課咱們學習的內容。

你可能會說:「我發現動態分配內存的方式既複雜又沒什麼用嘛!」

複雜麼?還行吧,確實相對自動的方式要考慮比較多的因素。

沒有用麼?毫不!

由於不少時候咱們不得不使用手動的方式來分配內存。

接下來咱們就來看一下手動方式的必要性。

4. 動態分配一個數組


暫時咱們只是用手動方式來建立了一個簡單的變量。

然而,通常說來,咱們的動態分配可不是這樣「大材小用」的。

若是隻是建立一個簡單的變量,咱們用自動的方式就夠了。

那你會問:「啥時候需要用動態分配啊?」

問得好。動態分配最常被用來建立在運行時才知道大小的變量,例如動態數組。

假設咱們要存儲一個用戶的朋友的年齡列表,按照咱們之前的方式(自動方式),咱們能夠建立一個 int 型的數組:

int ageFriends[18];
複製代碼

很簡單對嗎?那問題不就解決了?

可是以上方式有兩個缺陷:

  1. 你怎麼知道這個用戶只有 18 個朋友呢?可能他有更多朋友呢。

  2. 你說:「那好,我就建立一個數組:

int ageFriends[10000];
複製代碼

足夠儲存 1 萬個朋友的年齡。」

可是問題是:可能咱們使用到的只是這個大數組的很小一部分,豈不是浪費內存嘛。

最恰當的方式是詢問用戶他有多少朋友,而後建立對應大小的數組。

而這樣,咱們的數組大小就只有在運行時才能知道了。

Voila,這就是動態分配的優點了:

  1. 能夠在運行時才肯定申請的內存空間大小。

  2. 很少很多剛恰好,要多少就申請多少,不怕不夠或過多。

因此藉着動態分配,咱們就能夠在運行時詢問用戶他到底有多少朋友。

若是他說有 20 個,那咱們就申請 20 個 int 型的空間;若是他說有 50 個,那就申請 50 個。經濟又環保。

咱們以前說過,C語言中禁止用變量名來做爲數組大小,例如不能這樣:

int ageFriends[numFriends];  // numFriends 是一個變量
複製代碼

儘管有的 C編譯器可能容許這樣的聲明,可是咱們不推薦。

咱們來看看用動態分配的方式如何實現這個程序:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int numFriends = 0, i = 0;

    int *ageFriends= NULL;  // 這個指針用來指示朋友年齡的數組

    // 詢問用戶有多少個朋友
    printf("請問您有多少朋友 ? ");

    scanf("%d", &numFriends);

    if (numFriends > 0)  // 至少得有一個朋友吧,否則也太慘了 :P
    {
        ageFriends = malloc(numFriends * sizeof(int));  // 爲數組分配內存
        if (ageFriends== NULL)  // 檢測分配是否成功
        {
            exit(0); // 分配不成功,退出程序
        }

        // 逐個詢問朋友年齡
        for (i = 0 ; i < numFriends; i++)  {
            printf("第%d位朋友的年齡是 ? ", i + 1);
            scanf("%d", &ageFriends[i]);
        }

        // 逐個輸出朋友的年齡
        printf("\n\n您的朋友的年齡以下 :\n");
        for (i = 0 ; i < numFriends; i++) {
            printf("%d 歲\n", ageFriends[i]);
        }

        // 釋放 malloc 分配的內存空間,由於咱們再也不須要了
        free(ageFriends);
    }

    return 0;
}
複製代碼

運行輸出:

請問您有多少朋友 ? 7
第1位朋友的年齡是 ? 25
第2位朋友的年齡是 ? 21
第3位朋友的年齡是 ? 27
第4位朋友的年齡是 ? 18
第5位朋友的年齡是 ? 14
第6位朋友的年齡是 ? 32
第7位朋友的年齡是 ? 30

您的朋友的年齡以下 :
25歲
21歲
27歲
18歲
14歲
32歲
30歲
複製代碼

固然了,這個程序比較簡單,但我向你保證之後的課程會使用動態分配來作更有趣的事。

5. 總結


  1. 不一樣類型的變量在內存中所佔的大小不盡相同。

  2. 藉助 sizeof 這個關鍵字(也是運算符)能夠知道一個類型所佔的字節數。

  3. 動態分配就是在內存中手動地預留一塊空間給一個變量或者數組。

  4. 動態分配的經常使用函數是 malloc(固然,還有 calloc,realloc,能夠查閱使用方法,和 malloc 是相似的),可是在不須要這塊內存以後,千萬不要忘了使用 free 函數來釋放。並且,malloc 和 free 要一一對應,不能一個 malloc 對應兩個 free,會出錯;或者兩個 malloc 對應一個 free,會內存泄露!

  5. 動態分配使得咱們能夠建立動態數組,就是它的大小在運行時才能肯定。

6. 第二部分第九課預告


今天的課就到這裏,一塊兒加油吧!

下一課: C語言探索之旅 | 第二部分第九課: 實戰"懸掛小人"遊戲


我是 謝恩銘,公衆號「程序員聯盟」(微信號:coderhub)運營者,慕課網精英講師 Oscar 老師,終生學習者。 熱愛生活,喜歡游泳,略懂烹飪。 人生格言:「向着標杆直跑」

相關文章
相關標籤/搜索