跳轉到內容

C 程式設計/記憶體管理

來自華夏公益教科書
前文:指標和陣列 C 程式設計 後文:錯誤處理

在 C 中,您已經考慮過建立變數供程式使用。您建立了一些用於的陣列,但您可能已經注意到了一些限制

  • 陣列的大小必須事先知道
  • 陣列的大小在程式執行期間不能更改

C 中的動態記憶體分配是一種規避這些問題的方法。

malloc 函式

[編輯 | 編輯原始碼]
#include <stdlib.h>
void *calloc(size_t nmemb, size_t size);
void free(void *ptr);
void *malloc(size_t size);
void *realloc(void *ptr, size_t size);

標準 C 函式 malloc 是實現動態記憶體分配的方法。它在 stdlib.h 或 malloc.h 中定義,具體取決於您可能使用的作業系統。Malloc.h 僅包含記憶體分配函式的定義,而不包含 stdlib.h 中定義的其他函式。通常,您不需要在程式中如此具體,如果兩者都支援,您應該使用 <stdlib.h>,因為這是 ANSI C,也是我們將在這裡使用的。

釋放分配的記憶體回作業系統的對應呼叫是 free

當不再需要動態分配的記憶體時,應呼叫 free 將其釋放回記憶體池。覆蓋指向動態分配記憶體的指標會導致該資料變得不可訪問。如果這種情況經常發生,最終作業系統將無法再為程序分配更多記憶體。一旦程序退出,作業系統就可以釋放與程序相關的所有動態分配記憶體。

讓我們看看如何將動態記憶體分配用於陣列。

通常,當我們想要建立一個數組時,我們會使用以下宣告

int array[10];

回想一下,array 可以被視為一個指標,我們將其用作陣列。我們指定此陣列的長度為 10 個 int。在 array[0] 之後,還有九個其他整數可以連續儲存。

有時在編寫程式時不知道某些資料需要多少記憶體;例如,當它取決於使用者輸入時。在這種情況下,我們希望在程式開始執行後動態分配所需的記憶體。為此,我們只需要宣告一個指標,並在我們希望為陣列中的元素騰出空間時呼叫 malloc或者,我們可以在我們首次初始化陣列時告訴 malloc 騰出空間。無論哪種方式都是可以接受的,也很有用。

我們還需要知道一個 int 在記憶體中佔多少空間,以便為它騰出空間;幸運的是,這並不難,我們可以使用 C 的內建 sizeof 運算子。例如,如果 sizeof(int) 返回 4,則一個 int 佔用 4 個位元組。自然,2*sizeof(int) 是我們為 2 個 int 需要多少記憶體,依此類推。

那麼我們如何 malloc 一個與之前類似的包含十個 int 的陣列呢?如果我們希望一次宣告並騰出空間,我們可以簡單地說

int *array = malloc(10*sizeof(int));

我們只需要宣告指標;malloc 為我們提供了一些空間來儲存 10 個 int,並返回指向第一個元素的指標,該指標被分配給該指標。

重要說明! malloc 不會 初始化陣列;這意味著陣列可能包含隨機或意外的值!就像建立沒有動態分配的陣列一樣,程式設計師必須在使用陣列之前用合理的值對其進行初始化。確保你也這樣做。(稍後看看函式 memset 以獲得一種簡單的方法。

在宣告用於分配記憶體的指標後,不必立即呼叫 malloc。通常,在宣告和呼叫 malloc 之間存在許多語句,如下所示

int *array = NULL;
printf("Hello World!!!");
/* more statements */
array = malloc(10*sizeof(int)); /* delayed allocation */
/* use the array */

動態記憶體分配的更實用的示例如下

給定一個包含 10 個整數的陣列,從陣列中刪除所有重複元素,並建立一個不包含重複元素的新陣列(一個集合)。

一種簡單的演算法來刪除重複元素

    int arrl = 10; // Length of the initial array
    int arr[10] = {1, 2, 2, 3, 4, 4, 5, 6, 5, 7}; // A sample array, containing several duplicate elements
    
    for (int x = 0; x < arrl; x++)
    {
        for (int y = x + 1; y < arrl; y++)
        {
            if (arr[x] == arr[y])
            {
                for (int s = y; s < arrl; s++)
                {
                    if (!(s + 1 == arrl))
                        arr[s] = arr[s + 1];
                }

                arrl--; 
                y--;
            }
        }
    }

由於我們新陣列的長度取決於輸入,因此它必須是動態分配的

int *newArray = malloc(arrl*sizeof(int));

上面的陣列目前將包含意外的值,因此我們必須使用 memcpy 將我們的動態分配記憶體塊設定為新值

memcpy(newArray, arr, arrl*sizeof(int));

一些安全研究人員建議始終使用 calloc(x,y) 而不是 malloc(x*y),原因有兩個

  • calloc() 的許多實現都會仔細檢查 x 和 y 引數,並在 "x*y" 可能溢位時返回 NULL。使用 malloc(x*y),乘法 "x*y" 可能溢位為 0 或其他太小的數字,通常會導致緩衝區溢位。[1][2][3][4]
  • calloc 確保緩衝區完全沒有敏感資訊,避免某些型別的安全漏洞[5](但不幸的是,這並不能阻止 Heartbleed 漏洞)。

錯誤檢查

[編輯 | 編輯原始碼]

當我們想要使用 malloc 時,我們必須注意程式設計師可用的記憶體池是有限的。即使現代 PC 至少具有 1GB 的記憶體,但仍然有可能並且可以想象用完它!在這種情況下,malloc 將返回 NULL。為了防止程式因沒有更多可用的記憶體而崩潰,在嘗試使用記憶體之前,應始終檢查 malloc 是否沒有返回 NULL;我們可以透過以下方式做到這一點

int *pt = malloc(3 * sizeof(int));
if(pt == NULL)
{
   fprintf(stderr, "Out of memory, exiting\n");
   exit(1);
}

當然,像上面示例那樣突然退出並不總是合適的,並且取決於您試圖解決的問題以及您正在為其程式設計的架構。例如,如果程式是一個執行在桌面上的小型、非關鍵應用程式,則退出可能是合適的。但是,如果程式是執行在桌面上的某種型別的編輯器,您可能希望讓操作員選擇儲存他們辛苦輸入的資訊,而不是僅僅退出程式。嵌入式處理器(例如洗衣機中的嵌入式處理器)中的記憶體分配失敗可能會導致機器自動重置。出於這個原因,許多嵌入式系統設計人員完全避免使用動態記憶體分配。

calloc 函式

[編輯 | 編輯原始碼]

calloc 函式為一個專案陣列分配空間,並將記憶體初始化為零。呼叫 mArray = calloc( count, sizeof(struct V)) 分配 count 個物件,每個物件的尺寸足以包含結構 struct V 的例項。該空間被初始化為所有位為零。該函式返回指向已分配記憶體的指標,或者如果分配失敗,則返回 NULL

realloc 函式

[編輯 | 編輯原始碼]
 void * realloc ( void * ptr, size_t size );

realloc 函式將 ptr 指向的物件的大小更改為 size 指定的大小。物件的原始內容將保持不變,直到新大小和舊大小中較小的一個。如果新大小更大,則新分配的物件部分的值是不確定的。如果 ptr 是一個空指標,則 realloc 函式的行為類似於 malloc 函式,對於指定的大小。否則,如果 ptr 與之前由 callocmallocrealloc 函式返回的指標不匹配,或者如果空間已被 freerealloc 函式呼叫解除分配,則行為是未定義的。如果無法分配空間,則 ptr 指向的物件保持不變。如果 size 為零且 ptr 不是空指標,則指向的物件將被釋放。realloc 函式返回一個空指標或指向可能已移動的已分配物件的指標。

free 函式

[編輯 | 編輯原始碼]

使用 mallocrealloccalloc 分配的記憶體必須在不再需要時釋放回系統記憶體池。這樣做是為了避免永久分配越來越多的記憶體,這會導致最終的記憶體分配失敗。但是,未用 free 釋放的記憶體將在大多數作業系統上當前程式終止時釋放。對 free 的呼叫如下例所示。

int *myStuff = malloc( 20 * sizeof(int)); 
if (myStuff != NULL) 
{
   /* more statements here */
   /* time to release myStuff */
   free( myStuff );
}

free 與遞迴資料結構

[編輯 | 編輯原始碼]

需要注意的是,free 既不智慧也不遞迴。以下程式碼依賴於對struct 內部變數的 free 的遞迴應用,但不起作用。

typedef struct BSTNode 
{
   int value; 
   struct BSTNode* left;
   struct BSTNode* right;
} BSTNode;

// Later: ... 

BSTNode* temp = calloc(1, sizeof(BSTNode));
temp->left = calloc(1, sizeof(BSTNode));

// Later: ... 

free(temp); // WRONG! don't do this!

語句 "free(temp);" 不會 釋放 temp->left,從而導致記憶體洩漏。正確的方法是定義一個函式,該函式釋放資料結構中的每個節點。

void BSTFree(BSTNode* node){
    if (node != NULL) {
        BSTFree(node->left);
        BSTFree(node->right);
        free(node);
    }
}

由於 C 沒有垃圾回收器,因此 C 程式設計師有責任確保對於每個 malloc() 都有一個 free()。如果樹是逐個節點分配的,那麼它也需要逐個節點釋放。

不要釋放未定義的指標

[編輯 | 編輯原始碼]

此外,當所討論的指標最初從未分配時使用 free 通常會導致崩潰或在後續導致神秘的錯誤。

為了避免此問題,請始終在宣告指標時初始化它們。要麼在宣告時使用 calloc(如本章中的大多數示例所示),要麼在宣告時將它們設定為 NULL(如本章中的“延遲分配”示例所示)。[6]

編寫建構函式/解構函式

[編輯 | 編輯原始碼]

獲得正確的記憶體初始化和銷燬的一種方法是模仿面向物件的程式設計。在這種正規化中,物件在為它們分配原始記憶體後被構造,然後生存,當它們需要被銷燬時,一個名為解構函式的特殊函式會在物件本身被銷燬之前銷燬物件的內部結構。

例如

#include <stdlib.h> /* need malloc and friends */

/* this is the type of object we have, with a single int member */
typedef struct WIDGET_T {
  int member;
} WIDGET_T;

/* functions that deal with WIDGET_T */

/* constructor function */
void
WIDGETctor (WIDGET_T *this, int x)
{
  this->member = x;
}

/* destructor function */
void
WIDGETdtor (WIDGET_T *this)
{
  /* In this case, I really don't have to do anything, but
     if WIDGET_T had internal pointers, the objects they point to
     would be destroyed here.  */
  this->member = 0;
}

/* create function - this function returns a new WIDGET_T */
WIDGET_T *
WIDGETcreate (int m)
{
  WIDGET_T *x = 0;

  x = malloc (sizeof (WIDGET_T));
  if (x == 0)
    abort (); /* no memory */
  WIDGETctor (x, m);
  return x;
}

/* destroy function - calls the destructor, then frees the object */
void
WIDGETdestroy (WIDGET_T *this)
{
  WIDGETdtor (this);
  free (this);
}

/* END OF CODE */
前文:指標和陣列 C 程式設計 後文:錯誤處理

參考資料

[編輯 | 編輯原始碼]
華夏公益教科書