作者 謝恩銘,公眾號「程序員聯盟」(微信號:coderhub)。
轉載請注明出處。
原文:http://www.lxweimin.com/p/bbce8f04faf1
《C語言探索之旅》全系列
內容簡介
- 前言
- 變量的大小
- 內存的動態分配
- 動態分配一個數組
- 總結
- 第二部分第九課預告
1. 前言
上一課是 C語言探索之旅 | 第二部分第七課:文件讀寫 。
經歷了第二部分的一些難點課程,我們終于來到了這一課,一個聽起來有點酷酷的名字:動態分配。
“萬水千山總是情,分配也由系統定”。
到目前為止,我們創建的變量都是系統的編譯器為我們自動構建的,這是簡單的方式。
其實還有一種更偏手動的創建變量的方式,我們稱為“動態分配”(Dynamic Allocation)。dynamic 表示“動態的”,allocation 表示“分配”。
動態分配的一個主要好處就是可以在內存中“預置”一定空間大小,在編譯時還不知道到底會用多少。
使用這個技術,我們可以創建大小可變的數組。到目前為止我們所創建的數組都是大小固定不可變的。而學完這一課后我們就會創建所謂“動態數組”了。
學習這一章需要對指針有一定了解,如果指針的概念你還沒掌握好,可以回去復習 C語言探索之旅 | 第二部分第二課:進擊的指針,C語言的王牌! 那一課。
我們知道當我們創建一個變量時,在內存中要為其分配一定大小的空間。例如:
int number = 2;
當程序運行到這一行代碼時,會發生幾件事情:
應用程序詢問操作系統(Operating System,簡稱 OS。例如Windows,Linux,macOS,Android,iOS,等)是否可以使用一小塊內存空間。
操作系統回復我們的程序,告訴它可以將這個變量存儲在內存中哪個地方(給出分配的內存地址)。
當函數結束后,你的變量會自動從內存中被刪除。你的程序對操作系統說:“我已經不需要內存中的這塊地址了,謝謝!” (當然,實際上你的程序不可能對操作系統說一聲“謝謝”,但是確實是操作系統在掌管一切,包括內存,所以對它還是客氣一點比較好...)。
可以看到,以上的過程都是自動的。當我們創建一個變量,操作系統就會自動被程序這樣調用。
那么什么是手動的方式呢?說實在的,沒人喜歡把事情復雜化,如果自動方式可行,何必要大費周章來使用什么手動方式呢?但是要知道,很多時候我們是不得不使用手動方式。
這一課中,我們將會:
探究內存的機制(是的,雖然以前的課研究過,但是還是要繼續深入),了解不同變量類型所占用的內存大小。
接著,探究這一課的主題,來學習如何向操作系統動態請求內存。也就是所謂的“動態內存分配”。
最后,通過學習如何創建一個在編譯時還不知道其大小(只有在程序運行時才知道)的數組來了解動態內存分配的好處。
準備好了嗎?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 個字節
對于內存的全新視角
之前,我們在繪制內存圖示時,還是比較不精準的?,F在,我們知道了每個變量所占用的大小,我們的內存圖示就可以變得更加精準了。
假如我定義一個 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:表示“解放,釋放,自由的”。意味著“釋放那塊內存空間”。告訴操作系統我們不再需要這塊已經分配的空間了,這塊內存空間會被釋放,另一個程序就可以使用這塊空間了。
當我們手動分配內存時,須要按照以下三步順序來:
調用 malloc 函數來申請內存空間。
檢測 malloc 函數的返回值,以得知操作系統是否成功為我們的程序分配了這塊內存空間。
一旦使用完這塊內存,不再需要時,必須用 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 的值來判斷申請內存是否成功了:
如果為 NULL,則說明 malloc 調用沒有成功。
否則,就說明成功了。
一般來說內存分配不會失敗,但是也有極端情況:
你的內存(堆內存)已經不夠了。
你申請的內存值大得離譜(比如你申請 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];
很簡單對嗎?那問題不就解決了?
但是以上方式有兩個缺陷:
你怎么知道這個用戶只有 18 個朋友呢?可能他有更多朋友呢。
你說:“那好,我就創建一個數組:
int ageFriends[10000];
足夠儲存 1 萬個朋友的年齡?!?/p>
但是問題是:可能我們使用到的只是這個大數組的很小一部分,豈不是浪費內存嘛。
最恰當的方式是詢問用戶他有多少朋友,然后創建對應大小的數組。
而這樣,我們的數組大小就只有在運行時才能知道了。
Voila,這就是動態分配的優勢了:
可以在運行時才確定申請的內存空間大小。
不多不少剛剛好,要多少就申請多少,不怕不夠或過多。
所以借著動態分配,我們就可以在運行時詢問用戶他到底有多少朋友。
如果他說有 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. 總結
不同類型的變量在內存中所占的大小不盡相同。
借助 sizeof 這個關鍵字(也是運算符)可以知道一個類型所占的字節數。
動態分配就是在內存中手動地預留一塊空間給一個變量或者數組。
動態分配的常用函數是 malloc(當然,還有 calloc,realloc,可以查閱使用方法,和 malloc 是類似的),但是在不需要這塊內存之后,千萬不要忘了使用 free 函數來釋放。而且,malloc 和 free 要一一對應,不能一個 malloc 對應兩個 free,會出錯;或者兩個 malloc 對應一個 free,會內存泄露!
動態分配使得我們可以創建動態數組,就是它的大小在運行時才能確定。
6. 第二部分第九課預告
今天的課就到這里,一起加油吧!
下一課: C語言探索之旅 | 第二部分第九課: 實戰"懸掛小人"游戲
我是 謝恩銘,公眾號「程序員聯盟」(微信號:coderhub)運營者,慕課網精英講師 Oscar 老師,終生學習者。
熱愛生活,喜歡游泳,略懂烹飪。
人生格言:「向著標桿直跑」