在 C 語言程式設計中,記憶體管理是核心技能之一。靜態記憶體分配雖然簡單,但缺乏彈性;動態記憶體管理則能讓程式在執行期根據實際需求申請、調整與釋放記憶體,是編寫高效、健壯系統軟體的關鍵。本文將系統整理 malloc、calloc、realloc、free 的用法細節、常見陷阱與最佳實踐,並輔以 typedef 與位欄位等進階型別技巧,助你建立完整的記憶體管理知識體系。
typedef 並非創建新型別,而是為既有型別建立別名,提升程式碼可讀性與可移植性。語法為:typedef 原型別名 新型別名;
// 基本型別別名
typedef int Integer;
Integer a, b; // 等同於 int a, b;
// 結構體別名:省去重複輸入 struct 關鍵字
typedef struct {
int month;
int day;
int year;
} Date;
Date birthday; // 直接使用 Date 做型別
Date *pDate = &birthday;
// 陣列別名
typedef int IntArray[100];
IntArray scores; // 等同於 int scores[100];
// 指標別名
typedef char *String;
String name, *names[10];
// 函數指標別名
typedef int (*FuncPtr)(void);
FuncPtr handler1, handler2;
typedef 集中放在共用標頭檔(如 types.h),便於跨平台移植。uint32_t、int16_t)務必使用 typedef,避免直接依賴 int、long 等實作定義大小。位欄位允許在結構體中指定成員佔用的位元數,適用於旗標、狀態碼、通訊協定封包等場景。
struct BitFlags {
unsigned int flagA : 4; // 佔 4 bits
unsigned int : 0; // 強制對齊到下一個儲存單元邊界
unsigned int flagB : 4;
unsigned int flagC : 4;
};
關鍵限制: 位欄位長度不可超過其基礎型別位元寬度;無名位欄位可用於填充或對齊;位欄位不支援取址操作(&)。
假設 unsigned int 為 16 bits:
| flagA (4) | padding (4) | flagB (4) | flagC (4) | <- 共 16 bits
| 區域 | 特性 | 典型內容 | 管理方式 |
|---|---|---|---|
| Stack(棧) | 自動分配/釋放,LIFO,大小有限(~1-8MB) | 區域變數、函數參數、返回位址 | 編譯器自動 |
| Heap(堆) | 手動分配/釋放,空間大(32-bit 近 4GB),易產生碎片 | 動態配置物件、大型緩衝區 | 程式設計師(malloc/free) |
| Data/BSS | 靜態儲存,程式啟動時分配,結束時釋放 | 全域/靜態變數 | 系統自動 |
| Code/ROData | 唯讀,存放機器碼與常數字串 | 函數本體、字串常值 | 系統自動 |
void *malloc(size_t size);
// 成功:返回新分配區塊起始位址(void*,需強制轉型)
// 失敗:返回 NULL
範例:
int *arr = (int *)malloc(10 * sizeof(int));
if (arr == NULL) {
perror("malloc failed");
exit(EXIT_FAILURE);
}
// 使用 arr[0] ~ arr[9]
free(arr);
arr = NULL; // 防止野指標
void *calloc(size_t nmemb, size_t size);
// 相當於 malloc(nmemb * size) + memset(ptr, 0, nmemb * size)
適用場景: 需要初始化為 0 的陣列、結構體陣列。
void *realloc(void *ptr, size_t new_size);
// 成功:返回新區塊位址(可能與原位址不同)
// 失敗:返回 NULL,**原區塊保持不變**
⚠️ 經典錯誤與正確寫法:
// ❌ 錯誤:若 realloc 失敗,ptr 變 NULL,原記憶體洩漏
ptr = realloc(ptr, new_size);
// ✅ 正確:使用暫存指標
void *tmp = realloc(ptr, new_size);
if (tmp == NULL) {
// 處理錯誤,ptr 仍指向舊有效區塊
} else {
ptr = tmp;
}
void free(void *ptr);
// ptr 必須是 malloc/calloc/realloc 返回的位址
// 釋放後 ptr 變為野指標,務必立即設為 NULL
鐵律:
malloc/calloc/realloc 成功調用必須配對一個 free。ptr = NULL;,避免二次釋放或誤用。free。++ptr、ptr + 5)會改變指標值,導致無法正確 free,必須保留原始基址。typedef struct {
char name[32];
int id;
float gpa;
} Student;
Student *allocate_students(size_t n) {
Student *p = (Student *)calloc(n, sizeof(Student));
if (!p) return NULL;
return p;
}
int expand_students(Student **pp, size_t old_n, size_t new_n) {
Student *tmp = (Student *)realloc(*pp, new_n * sizeof(Student));
if (!tmp) return -1;
// 新增區塊歸零
memset(tmp + old_n, 0, (new_n - old_n) * sizeof(Student));
*pp = tmp;
return 0;
}
void free_students(Student **pp) {
free(*pp);
*pp = NULL;
}
| 錯誤類型 | 徵狀 | 防範對策 |
|---|---|---|
| 記憶體洩漏 | 程式執行久記憶體持續增長 | 嚴格配對 malloc/free;使用 Valgrind/AddressSanitizer 檢測 |
| 重複釋放 | 崩潰、heap corruption | free 後設 NULL;引入參考計數或智慧指標封裝 |
| 緩衝區溢位 | 莫名其妙的資料損毀 | 分配足夠大小;使用 strncpy、snprintf 等安全函式 |
| 使用已釋放記憶體 | 隨機崩潰、資料錯誤 | free 後設 NULL;工具偵測 use-after-free |
| realloc 失敗未處理 | 原資料丟失 | 務必用暫存指標接收返回值 |
動態記憶體管理是 C 語言力量的來源,也是 Bug 溫床。掌握 malloc、calloc、realloc、free 的語義細節、失敗處理模式,結合 typedef 提升型別抽象層次,並養成「分配即檢查、釋放即歸零」的肌肉記憶,才能寫出高可靠度的系統級程式碼。建議在專案中引入統一的記憶體分配封裝層,集中處理錯誤碼、日誌與統計,進一步降低風險。