1. 程式人生 > 實用技巧 >C 的動態記憶體管理

C 的動態記憶體管理

2.1 動態記憶體分配

  malloc函式的引數指定要分配的位元組數。如果成功,它會返回從堆上分配的記憶體的指標。如果失敗則會返回空指標。

sizeof操作符使應用程式更容易移植,還能確定在宿主系統中應該分配的正確位元組數。

在釋放用struct關鍵字建立的結構體時也可能發生記憶體洩漏。如果結構體包含指向動態記憶體分配的記憶體指標,那麼可能需要在釋放結構體之前先釋放這些指標。

2.2 動態記憶體分配函式

2.2.1 使用malloc函式

  malloc函式從堆上分配一塊記憶體,所分配的位元組數由該函式唯一的引數指定,返回值是void指標,如果記憶體不足,就會返回NULL。此函式不會清空或者修改記憶體,所以我們認為新分配的記憶體包含垃圾資料

  函式原型:void * malloc(size_t);

  這個函式只有一個引數,型別是size_t,如果引數是負數就會引發問題。在有些系統中,引數是負數會返回NULL。如果malloc的引數是0,其行為是實現相關的:可能返回NULL指標,也可能返回一個指向分配了0位元組區域的指標。如果malloc函式的引數是NULL,那麼一般會生成一個警告然後返回0位元組。

因為當malloc無法分配記憶體時會返回NULL,在使用它返回的指標之前先檢查NULL是不錯的做法。

1. 要不要進行強制型別轉換

  C引入void指標之前,在兩種互不相容的指標型別之間賦值需要對malloc使用顯示型別轉換以避免產生警告。因為可以將void指標賦值給其它任意型別指標,所以就不再需要顯示型別轉換了。但有些開發者認為顯示型別轉換是不錯的做法,因為:

  • 這樣可以說明malloc函式的用意。
  • 程式碼可以和C++(或早期的C編譯器)相容,後兩者需要顯示型別轉換。

2. 分配記憶體失敗

  如果宣告一個指標但沒有在使用它之前為它指向的地址分配記憶體,那麼記憶體通常會包含垃圾值,這往往會導致一個無效記憶體引用錯誤。

  char *name;

  scanf("%s",name); 這裡使用的是name所引用的記憶體,實際這塊記憶體還未分配。報錯:使用未初始化的區域性變數。

3. 為資料型別分配指定位元組數時儘量用sizeof操作符。

5. 靜態、全域性指標和malloc

  全域性變數的初始化要在 main 函式執行前完成。初始化靜態或全域性變數時不能呼叫函式。

  static int *pi = malloc(sizeof(int));   這樣會產生一個編譯時錯誤資訊,全域性變數也一樣。

對於靜態變數,可以通過後面用一個單獨的語句給變數分配記憶體來避免這個問題。

  static int *pi;
pi = malloc(sizeof(int));

但是全域性變數不能用單獨地賦值語句,因為全域性變數是在函式和可制行程式碼外部宣告的,賦值語句這類程式碼必須出現在函式中。

在編譯器看來,作為初始化操作符的 = 和作為賦值操作符的 = 不一樣。

2.2.2 使用calloc函式

  calloc會在分配的同時清空記憶體。該函式的原型如下:  

    void * calloc(size_t numElements,size_t elementSize);  numElements:元素數目  elementSize:元素大小

  清空記憶體的意思是將其內容置為二進位制0。函式calloc()會將所分配的記憶體空間中的每一位都初始化為零,也就是說,如果你是為字元型別或整數型別的元素分配記憶體
那麼這些元素將保證會被初始化為0;如果你是為指標型別的元素分配記憶體,那麼這些元素通常會被初始化為空指標;如果你為實型資料分配記憶體,則這些元素會被初始化為浮點型的零。

  calloc函式會根據numElementselementSize兩個引數的乘積來分配記憶體,並返回一個指向記憶體的第一個位元組的指標,如果乘積為0,那麼calloc可能返回空指標。如果不能分配記憶體,則會返回NULL

  下例為pi分配了20位元組,全部包含0:

  int *pi = calloc(5, sizeof(int));

不用calloc的話,用malloc函式和memset函式可以得到同樣的結果:

  int *pi = malloc(5 * sizeof(int));
memset(pi, 0, 5 * sizeof(int));

如果記憶體需要清零可以使用calloc,不過執行calloc可能比執行malloc慢。

2.2.3 使用realloc函式

  realloc函式會重新分配記憶體,函式原型如下:

  void *realloc(void *ptr, size_t size);  

第一個引數為原記憶體的指標,第二個引數為請求的大小,返回值為新申請記憶體的指標。具體情況總結如下:

2.3 用free函式釋放記憶體

  函式原型:

    void free(void *ptr);

指標引數應該指向由malloc類函式分配的記憶體地址,這塊記憶體會被返還給堆。

如果傳遞給free函式的引數是空指標,通常他什麼都不做。如果不是分配的記憶體則行為將是未定義的。

應該在同一層管理記憶體的分配和釋放。比如說,如果是在函式內分配的記憶體,那麼就應該在同一個函式內釋放它。

2.3.1 將已釋放的指標置為NULL

 如果試圖解引用一個已釋放的指標,其行為將是未定義的。呼叫free後給指標賦值NULL表示該指標無效,後續再使用這種指標會造成執行時異常。這種技術目的是解決迷途指標問題。

2.3.2 重複釋放

  重複釋放是指兩次釋放同一塊記憶體。重複釋放同一塊記憶體會造成執行時異常。

這兩種情況都是重複釋放,第二種更為隱蔽一些:

  int *pi = (int *) malloc(sizeof(int));
  *pi = 5;
  free(pi);
  ...
  free(pi);

  int *p1 = (int*) malloc(sizeof(int));
  int *p2 = p1;
  free(p1);
  ...
  free(p2);

2.3.3 堆和系統記憶體

  堆的大小可能在程式建立後就規定不變了,也可能可以增長。不過堆管理器不一定會在呼叫free函式時將記憶體返還給作業系統。釋放的記憶體只是可供應用程式後續使用。所以,如果程式先分配記憶體然後釋放,從作業系統的角度看,釋放的記憶體通常不會反映在應用程式的記憶體使用上。

如果記憶體已經釋放,而指標還在引用原始記憶體,這樣的指標就被稱為迷途指標。迷途指標沒有指向有效物件,有時也稱為過早釋放。

使用迷途指標會造成一系列問題,包括:

  • 如果訪問記憶體,則行為不可預期;
  • 如果記憶體不可訪問,則是段錯誤;
  • 潛在的安全隱患;

此類迷途指標更難察覺:一個以上的指標引用同一記憶體區域而其中一個指標被釋放。

memset函式

  函式原型:原型:extern void *memset(void *ptr, int value, size_t num)
  函式功能:將ptr所指的記憶體區域的前num個位元組都設定為value的ASCII值,然後返回指向ptr的指標。
  函式說明:引數value雖宣告為int,但必須是unsigned char,該函式使用unsigned char(一個位元組8位)轉換填充記憶體塊,所以範圍在0 到255 之間。