動態記憶體分配,C語言動態記憶體分配詳解

2020-07-16 10:04:21
動態記憶體是相對靜態記憶體而言的。所謂動態和靜態就是指記憶體的分配方式。動態記憶體是指在堆上分配的記憶體,而靜態記憶體是指在棧上分配的記憶體。

前面所寫的程式大多數都是在棧上分配的,比如區域性變數、形參、函數呼叫等。棧上分配的記憶體是由系統分配和釋放的,空間有限,在複合語句或函數執行結束後就會被系統自動釋放。而堆上分配的記憶體是由程式設計師通過程式設計自己手動分配和釋放的,空間很大,儲存自由。堆和棧後面還會專門講,這裡先了解一下。

傳統陣列的缺點

“傳統陣列”就是前面所使用的陣列,與動態記憶體分配相比,傳統陣列主要有以下幾個缺點:

1) 陣列的長度必須事先指定,而且只能是常數,不能是變數。比如像下面這麼寫就是對的:
int a[5];
而像下面這麼寫就是錯的:
int length = 5;
int a[length];  //錯誤
2) 因為陣列長度只能是常數,所以它的長度不能在函數執行的過程當中動態地擴充和縮小。

3) 對於陣列所占記憶體空間程式設計師無法手動程式設計釋放,只能在函數執行結束後由系統自動釋放,所以在一個函數中定義的陣列只能在該函數執行期間被其他函數使用。

而動態記憶體就不存在這個問題,因為動態記憶體是由程式設計師手動程式設計釋的,所以想什麼時候釋放就甚麼時候釋放。只要程式設計師不手動程式設計釋放,就算函數執行結束,動態分配的記憶體空間也不會被釋放,其他函數仍可繼續使用它。除非是整個程式執行結束,這時系統為該程式分配的所有記憶體空間都會被釋放。

所謂“傳統陣列”的問題,實際上就是靜態記憶體的問題。我們講傳統陣列的缺陷實際上就是以傳統陣列為例講靜態記憶體的缺陷。本質上講的是以前所有的記憶體分配的缺陷。正因為它有這麼多缺陷,所以動態記憶體就變得很重要。動態陣列能很好地解決傳統陣列的這幾個缺陷。

malloc函數的使用

那麼動態記憶體是怎麼造出來的?在講如何動態地把一個陣列造出來之前,我們必須要先介紹 malloc 函數的使用。

malloc 是一個系統函數,它是 memory allocate 的縮寫。其中memory是“記憶體”的意思,allocate是“分配”的意思。顧名思義 malloc 函數的功能就是“分配記憶體”。要呼叫它必須要包含標頭檔案<stdlib.h>。它的原型為:

# include <stdlib.h>
void *malloc(unsigned long size);

malloc 函數只有一個形參,並且是整型。該函數的功能是在記憶體的動態儲存空間即堆中分配一個長度為size的連續空間。函數的返回值是一個指向所分配記憶體空間起始地址的指標,型別為 void*型。

簡單的理解,malloc 函數的返回值是一個地址,這個地址就是動態分配的記憶體空間的起始地址。如果此函數未能成功地執行,如記憶體空間不足,則返回空指標 NULL。

“int i=5;”表示分配了 4 位元組的“靜態記憶體”。這裡需要強調的是:“靜態記憶體”和“靜態變數”雖然都有“靜態”兩個字,但是它們沒有任何關係。不要以為“靜態”變數的記憶體就是“靜態記憶體”。靜態變數的關鍵字是 static,它與全域性變數一樣,都是在“靜態儲存區”中分配的。這塊記憶體在程式編譯的時候就已經分配好了,而且在程式的整個執行期間都存在;而靜態記憶體是在棧中分配的,比如區域性變數。

那麼,如何判斷一個記憶體是靜態記憶體還是動態記憶體呢?凡是動態分配的記憶體都有一個標誌:都是用一個系統的動態分配函數來實現的,如 malloc 或 calloc。

calloc 和 malloc 的功能很相似,我們一般都用 malloc。calloc 用得很少,這裡不做講解,有興趣的話可自行查閱。

如何用 malloc 動態分配記憶體呢?比如:
int *p = (int *)malloc(4);
它的意思是:請求系統分配 4 位元組的記憶體空間,並返回第一位元組的地址,然後賦給指標變數 p。當用 malloc 分配動態記憶體之後,上面這個指標變數 p 就被初始化了。

需要注意的是,函數 malloc 的返回值型別為 void* 型,而指標變數 p 的型別是 int* 型,即兩個型別不一樣,那麼可以相互賦值嗎?

上面語句是將 void* 型“強制型別轉換”成 int*型,但事實上可以不用轉換。C 語言中,void* 型可以不經轉換(系統自動轉換)地直接賦給任何型別的指標變數(函數指標變數除外)。

所以“int*p=(int*)malloc(4);”就可以寫成“int*p=malloc(4);”。此句執行完之後指標變數 p 就指向動態分配記憶體的首地址了。

void和void*

可能有人會問,void 不是不會有返回值嗎?為什麼 malloc 還會有返回值?需要注意的是,malloc 函數的返回值型別是 void*,而不是 void。void*和void是有區別的。

void* 是定義一個無型別的指標變數,它可以指向任何型別的資料。任何型別的指標變數都可以直接賦給 void* 型的指標變數,無需進行強制型別轉換。本教學後面很多函數的引數都是 void* 型的,表示它們可以接收任何型別的資料。

同樣,根據我們上面所講的,void* 型也可以直接賦給任何型別的指標變數,而無需進行強制型別轉換,但前提是必須在C語言中。

注意,不能對 void* 型的指標變數進行運算操作,如指標的運算、指標的移動等。原因很簡單,前面講int*型的指標變數加 1 就是移動 4 個單元,因為 int* 型的指標變數指向的是 int 型資料;但是 void* 型可以指向任何型別的資料,所以無法知道“1”所表示的是幾個記憶體單元。

另外,在“int*p=malloc(4);”中,指標變數 p 是靜態分配的。前面介紹過,動態分配的記憶體空間都有一個標誌,即都是用一個系統的動態分配函數實現的。而指標變數 p 是用傳統的方式定義的,所以是靜態分配的記憶體空間。而 p 所指向的記憶體是動態分配的。

那麼,動態分配和靜態分配到底有什麼區別呢?稍後你就明白了。

我們在前面講過,程式設計的時間長了就會發現程式設計中百分之八九十的問題都屬於記憶體的問題,如記憶體什麼時候分配、什麼時候釋放、由誰分配、由誰釋放、怎麼分配、怎麼釋放、哪塊記憶體可以用、哪塊記憶體不能用、哪塊記憶體可以讀、哪塊記憶體可讀可寫、哪塊記憶體不能讀也不能寫。這些問題形成了計算機語言的語法規則,如 C 語言語法、C++ 語法、Java 語法,它們本質上都是記憶體的問題。包括區域性變數、靜態變數等都一樣。所以記憶體是很關鍵的問題。

下面利用“int*p=malloc(4);”語句給大家寫一個很有意思的程式:
# include <stdio.h>
# include <stdlib.h>  //malloc()的標頭檔案
int main(void)
{
    while (1)
    {
        int *p = malloc(1000);   
    }
    return 0;
}
這個程式是非常簡單的一個木馬病毒。只要執行一會兒,你的計算機就宕機了。宕機速度的快慢取決於 malloc 後面括號中數位的大小。數位越大,“死”得越快。我們可以試驗一下:按“Ctrl+Alt+Delete”鍵開啟 Windows 工作管理員,然後選擇“效能”,如圖 1 所示。


圖 1