C語言探索之旅 | 第二部分第八課:動態分配
作者 謝恩銘,慕課網精英講師 Oscar老師。
內容簡介
- 前言
- 變數的大小
- 記憶體的動態分配
- 動態分配一個陣列
- 總結
- 第二部分第九課預告
1. 前言
上一課是 。
經歷了第二部分的一些難點課程,我們終於來到了這一課,一個聽起來有點酷酷的名字:動態分配。
“萬水千山總是情,分配也由系統定”。
到目前為止,我們建立的變數都是系統的編譯器為我們自動構建的,這是簡單的方式。
其實還有一種更偏手動的建立變數的方式,我們稱為“動態分配”(Dynamic Allocation)。dynamic 表示“動態的”,allocation 表示“分配”。
動態分配的一個主要好處就是可以在記憶體中“預置”一定空間大小,在編譯時還不知道到底會用多少。
使用這個技術,我們可以建立大小可變的陣列。到目前為止我們所建立的陣列都是大小固定不可變的。而學完這一課後我們就會建立所謂“動態陣列”了。
我們知道當我們建立一個變數時,在記憶體中要為其分配一定大小的空間。例如:
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 octetsn", sizeof(char));
printf("int : %d octetsn", sizeof(int));
printf("long : %d octetsn", sizeof(long));
printf("double : %d octetsn", 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:表示“解放,釋放,自由的”。意味著“釋放那塊記憶體空間”。告訴作業系統我們不再需要這塊已經分配的空間了,這塊記憶體空間會被釋放,另一個程式就可以使用這塊空間了。
當我們手動分配記憶體時,須要按照以下三步順序來:
-
呼叫 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 萬個朋友的年齡。”
但是問題是:可能我們使用到的只是這個大陣列的很小一部分,豈不是浪費記憶體嘛。
最恰當的方式是詢問使用者他有多少朋友,然後建立對應大小的陣列。
而這樣,我們的陣列大小就只有在執行時才能知道了。
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("nn您的朋友的年齡如下 :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. 第二部分第九課預告
今天的課就到這裡,一起加油吧!
我是 ,慕課網精英講師 ,終生學習者。
熱愛生活,喜歡游泳,略懂烹飪。
人生格言:「向著標杆直跑」
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4369/viewspace-2825656/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Web 探索之旅 | 第二部分第二課:伺服器語言Web伺服器
- C語言探索之旅 | 第二部分第十課: 實戰"懸掛小人&quoC語言
- C語言-記憶體分配C語言記憶體
- Linux 探索之旅 | 第二部分第二課:命令列,世界盡在掌握Linux命令列
- Web 探索之旅 | 第二部分第四課:資料庫Web資料庫
- Web探索之旅 | 第二部分第四課:資料庫Web資料庫
- C語言的記憶體分配C語言記憶體
- Web探索之旅 | 第三部分第二課:IP地址和域名Web
- c語言野指標與結構體指標動態記憶體分配小解C語言指標結構體記憶體
- 小凱15天快速講完c語言-簡單學習第八課C語言
- Linux 探索之旅 | 第四部分第二課:SSH 連線,安全快捷Linux
- Web 探索之旅 | 第二部分第五課:響應式網站和移動應用Web網站
- C++動態記憶體分配C++記憶體
- C語言動態陣列小作業C語言陣列
- C#語言————第二章 C#語言快速熱身C#
- 第二章 C語言概述C語言
- Python 語言特性:編譯+解釋、動態型別語言、動態語言Python編譯型別
- [C語言] 第一章|C語言入門第一課C語言
- Linux 探索之旅 | 第二部分測試題Linux
- C語言I博課作業04C語言
- C++ 指標動態記憶體分配C++指標記憶體
- C語言作業|第二次C語言
- 【C/C++】C語言基礎知識【第二版】C++C語言
- Linux 探索之旅 | 第二部分第三課:檔案和目錄,組織不會虧待你Linux
- Linux 探索之旅 | 第二部分第五課:使用者和許可權,有權就任性Linux
- SystemVerilog 語言部分(二)
- 計算機語言:編譯型/解釋型、動態語言/靜態語言、強型別語言/弱型別語言計算機編譯型別
- 鵬哥C語言初識課程總結C語言
- C語言 二維陣列課題程式碼C語言陣列
- 機器學習進階 第二節 第八課機器學習
- C語言記憶體管理,分配、使用、釋放以及安全性C語言記憶體
- Web探索之旅 | 第三部分第一課:伺服器Web伺服器
- C語言動態庫libxxx.so的幾種使用方法C語言
- 高階語言程式設計課程第八次個人作業程式設計
- C語言--靜態區域性變數C語言變數
- 算數表示式求值--c語言課程設計C語言
- C語言課程訓練系統題-字串cquptC語言字串
- C++ 動態記憶體分配與名稱空間C++記憶體