一、五大記憶體區域職責
| 區域 | 管理方式 | 存放內容 | 生命週期 |
| 棧區 | 編譯器自動分配/釋放 | 區域變數、函數參數、返回位址、暫存器溢出 | 函數呼叫期間 |
| 堆區 | 程式設計師手動 malloc/free | 動態分配物件、大型緩衝區 | 手動釋放前持續存在 |
| 靜態/全域區 | 連結器配置,程式啟動初始化 | static 變數、全域變數、字串常值 | 程式整個執行期間 |
| 常數區 | 唯讀記憶體段 | 字串字面值、const 全域變數 | 程式執行期間,不可修改 |
| 程式碼區 | 唯讀 | CPU 指令、函數機器碼 | 程式執行期間 |
二、動態記憶體分配核心 API
malloc:原始位元組分配
void *malloc(size_t size);
// 回傳對齊後的位址,內容未初始化(隨機值)
int *p = malloc(10 * sizeof(int)); // 分配 10 個 int 空間
calloc:歸零分配
void *calloc(size_t nmemb, size_t size);
// 分配 nmemb * size 位元組並全部設為 0
int *p = calloc(10, sizeof(int)); // 等效 malloc + memset 0
realloc:調整大小
void *realloc(void *ptr, size_t new_size);
// 1. 原地擴展(位址不變)若後方有足夠空閒空間
// 2. 新分配 + 複製舊資料 + 釋放舊塊(位址改變)
// 3. new_size=0 等效 free(ptr)
int *p = malloc(4 * sizeof(int));
p = realloc(p, 100 * sizeof(int)); // 必須用回傳值更新指標!
三、記憶體操作函數族
| 函數 | 用途 | 關鍵差異 |
memset(ptr, val, n) | 將 n 位元組設為 val(以 unsigned char 逐位元組填充) | 逐位元組填充,memset(p, 1, 4) 使每個 int 變 0x01010101 ≠ 1 |
memcpy(dst, src, n) | 複製 n 位元組,來源與目的不可重疊 | 高效、不檢查重疊 |
memmove(dst, src, n) | 複製 n 位元組,安全處理重疊 | 內部判斷方向複製,稍慢於 memcpy |
memcmp(s1, s2, n) | 比較前 n 位元組,回傳 <0, 0, >0 | 按 unsigned char 比較 |
四、完整實戰範例:動態字串陣列管理
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define INIT_CAP 4
#define GROW_FACTOR 2
typedef struct {
char **data;
size_t len;
size_t cap;
} StrArray;
StrArray *sa_create() {
StrArray *sa = malloc(sizeof(StrArray));
sa->data = malloc(INIT_CAP * sizeof(char*));
sa->len = 0; sa->cap = INIT_CAP;
return sa;
}
void sa_push(StrArray *sa, const char *str) {
if (sa->len == sa->cap) {
sa->cap *= GROW_FACTOR;
sa->data = realloc(sa->data, sa->cap * sizeof(char*));
}
sa->data[sa->len] = strdup(str); // strdup = malloc + strcpy
sa->len++;
}
void sa_destroy(StrArray *sa) {
for (size_t i = 0; i < sa->len; i++) free(sa->data[i]);
free(sa->data);
free(sa);
}
int main() {
StrArray *sa = sa_create();
sa_push(sa, "Hello");
sa_push(sa, "World");
sa_push(sa, "C Language");
for (size_t i = 0; i < sa->len; i++)
puts(sa->data[i]);
sa_destroy(sa);
return 0;
}
五、避坑指南:十大記憶體錯誤
- 記憶體洩漏:
malloc 無對應 free,長期運行耗盡記憶體。
- 重複釋放:
free(p); free(p); → 未定義行為。釋放後立即 p = NULL。
- 使用已釋放記憶體:
free(p); *p = 1; → Use-After-Free,安全性風險極高。
- 緩衝區溢位:寫入超過分配大小,破壞堆元資料導致後續
malloc/free 崩潰。
- 未檢查回傳值:分配失敗回傳
NULL,直接解參考導致 Segfault。
- 型別大小計算錯誤:
malloc(n * sizeof(int*)) 應為 sizeof(int)。
- 結構體柔性數組分配:
malloc(sizeof(Struct) + len) 忘記加資料長度。
memset 誤用於非 0/-1 整數:逐位元組填充導致整數值非預期。
realloc 未保存回傳值:位址可能改變,舊指標失效。
- 跨模組釋放:DLL 邊界傳遞指標並在另一模組
free,堆管理器不共用導致崩潰。
六、現代最佳實踐
- 使用
calloc 分配結構體陣列,自動歸零指標成員。
- 封裝分配/釋放為
create/destroy 配對函數,隱藏細節。
- 嚴格遵循「誰分配誰釋放」所有權原則。
- 啟用 AddressSanitizer (
-fsanitize=address) 開發期偵測記憶體錯誤。
- 大型專案考慮記憶體池分配器減少系統呼叫開銷。