C語言中文網 目錄
首頁 > 編程筆記 > C語言筆記 > 指針 閱讀:2,414

動態內存分配,C語言動態內存分配詳解

動態內存是相對靜態內存而言的。所謂動態和靜態就是指內存的分配方式。動態內存是指在堆上分配的內存,而靜態內存是指在棧上分配的內存。

前面所寫的程序大多數都是在棧上分配的,比如局部變量、形參、函數調用等。棧上分配的內存是由系統分配和釋放的,空間有限,在復合語句或函數運行結束后就會被系統自動釋放。而堆上分配的內存是由程序員通過編程自己手動分配和釋放的,空間很大,存儲自由。堆和棧后面還會專門講,這里先了解一下。

傳統數組的缺點

“傳統數組”就是前面所使用的數組,與動態內存分配相比,傳統數組主要有以下幾個缺點:

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
 
圖 1 中 CPU 使用的情況是 1%,內存的使用情況是使用了 1.64GB。現在我們運行上面那個程序看看有什么變化。單擊“編譯”→“鏈接”→“執行”,再看看 Windows 任務管理器有什么變化,如圖 2 所示。


圖 2
 
CPU 瞬間就使用了 27%,內存瞬間使用了 3.43GB,而且這個數值還在往上漲!當內存全部使用完后會啟動虛擬內存,就是把硬盤里的一塊區域當成內存來使用。如果虛擬內存也用完了,整個系統就死機了。我們只要把 VC++6.0 中執行時彈出的黑色窗口關掉,CPU 和內存的使用就會恢復正常了。因為把黑窗口關閉后程序就結束了,程序運行結束后系統為該程序分配的內存空間就都會被釋放。

這是最簡單的一個木馬程序,這個程序有一個專業的名稱,叫“內存泄漏”。什么是“內存泄漏”呢?每臺電腦都有內存,所有的程序都是先存放到內存里面才能運行。但是上面這個程序將內存空間都占滿了,那么其他程序就沒有地方存放了,所以內存就好像泄漏了一樣。

下面使用 malloc 函數寫一個程序,程序的功能是:調用被調函數,將主調函數中動態分配的內存中的數據放大 10 倍。
# include <stdio.h>
# include <stdlib.h>
void Decuple(int *i);  //函數聲明, decuple是10倍的意思
int main(void)
{
    int *p = malloc(4);
    *p = 10;
    Decuple(p);
    printf("*p = %d\n", *p);
    return 0;
}
void Decuple(int *i)
{
    *i = (*i) * 10;
    return;
}
輸出結果是:
*p = 100

當調用 Decuple 函數時,是把指針變量 p 中的地址傳遞給指針變量 i,此時 i 和 p 指向的是同一個內存單元。所以操作 i 所指向的內存空間就是操作 p 所指向的內存空間。

程序中,int*p=malloc(4); 中的 4 表示分配了 4 字節的動態內存。因為在前面用 sizeof 試過本人的計算機,其給 int 型變量分配的是 4 字節,所以這么寫沒有問題。但如果把這個程序移植到其他計算機中呢?系統給 int 型變量分配的就不一定是 4 字節(有可能是 8 字節),因此直接寫“4”的可移植性很差。最好的方式為 sizeof(int),即:
int *p = malloc(sizeof(int));
sizeof(int)的值是 int 型變量所占的字節數,這樣就能很好地表示當前計算機中 int 型變量占幾字節。這樣寫程序的可移植性就增強了。所以動態內存最好是需要多少就構建多少。多了雖然不會有問題,但是會浪費內存,而少了就可能出就可能出問題。

如果還想簡單一點的話,也可以像下面這樣寫:
int *p = malloc(sizeof*p);
前面講過,sizeof 的后面可以緊跟類型,也可以直接跟變量名。如果是變量名,那么就表示該變量在內存中所占的字節數。所以 *p 是 int 型的,那么 sizeof*p 就表示 int 型變量在內存中所占的字節數。

free函數的使用

前面定義了一個指向動態內存的指針變量 p:
int *p = malloc(sizeof*p);
前面講過,動態分配的內存空間是由程序員手動編程釋放的。那么怎么釋放呢?用 free 函數。

free 函數的原型是:

# include <stdlib.h>
void free(void *p);

free 函數無返回值,它的功能是釋放指針變量 p 所指向的內存單元。此時 p 所指向的那塊內存單元將會被釋放并還給操作系統,不再歸它使用。操作系統可以重新將它分配給其他變量使用。

需要注意的是,釋放并不是指清空內存空間,而是指將該內存空間標記為“可用”狀態,使操作系統在分配內存時可以將它重新分配給其他變量使用。

指針變量 p 被釋放之后,它仍然是指向那塊內存空間的,只是那塊內存空間已經不再屬于它了而已。下面寫一個程序看一下:
# include <stdio.h>
# include <stdlib.h>
int main(void)
{
    int *p = malloc(sizeof*p);
    *p = 10;
    printf("p = %p\n", p);
    free(p);
    printf("p = %p\n", p);
    return 0;
}
輸出結果是:
p = 002C2ED0
p = 002C2ED0

由該程序可見,釋放前后,p 所指向的內存空間是一樣的。所以釋放后 p 所指向的仍然是那塊內存空間。既然指向的仍然是那塊內存空間,那么就仍然可以往里面寫數據。可是釋放后該內存空間已經不屬于它了,該內存空間可能會被分配給其他變量使用。如果其他變量在里面存放了值,而你現在用 p 往里面寫入數據就會把那個值給覆蓋,這樣就會造成其他程序錯誤。所以當指針變量被釋放后,要立刻把它的指向改為 NULL。

那么,當指針變量被釋放后,它所指向的內存空間中的數據會怎樣呢?free 的標準行為只是表示這塊內存可以被再分配,至于它里面的數據是否被清空并沒有強制要求。不同的編譯器處理的方式可能不一樣。我們就看一下 VC++6.0 這個編譯器是怎么處理的:
# include <stdio.h>
# include <stdlib.h>
int main(void)
{
    int *p = malloc(sizeof*p);
    *p = 10;
    printf("p = %p\n", p);
    printf("*p = %d\n", *p);
    free(p);
    printf("p = %p\n", p);
    printf("*p = %d\n", *p);
    return 0;
}
輸出結果是:
p = 00842ED0
*p = 10
p = 00842ED0
*p = -572662307

可見在 VC++6.0 中,當指針變量被釋放后,雖然它仍然是指向那個內存空間的,但那個內存空間中的值將會被重新置一個非常小的負數。

動態創建的內存如果不用了必須要釋放。注意,一個動態內存只能釋放一次。如果釋放多次程序就會崩潰,因為已經釋放了,不能再釋放第二次。

綜上所述,malloc 和 free 一定要成對存在,一一對應。有 malloc 就一定要有 free,有幾個 malloc 就要有幾個 free,與此同時,每釋放一個指向動態內存的指針變量后要立刻把它指向 NULL。

最后需要強調的是,只有動態創建的內存才能用 free 把它釋放掉,靜態內存是不能用free釋放的。靜態內存只能由系統釋放。比如:
int a = 10;
int *p = &a;
如果試圖用 free(p) 把指針變量 p 所指向的內存空間釋放掉,那么編譯的時候不會出錯,但程序執行的時候立刻就出錯。

精美而實用的網站,提供C語言C++STLLinuxShellJavaGo語言等教程,以及socketGCCviSwing設計模式JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo