動態記憶體核心觀念:為什麼需要 malloc、如何正確使用、常見陷阱
從靜態陣列限制出發,理解動態分配必要性,掌握 malloc/free 配對、跨函數記憶體傳遞、堆疊與堆的差異。
靜態陣列的五大限制
- 大小必須編譯期常數,不可用變數
- 無法手動釋放,佔用記憶體直到函數結束
- 大小固定,執行期無法擴縮
- 生命週期綁定作用域,跨函數共享困難
- 大型陣列易造成堆疊溢位
malloc/free 基礎範例
#include <stdio.h>
#include <stdlib.h>
void modify(int *p) {
*p = 200; // 修改動態記憶體內容
// free(p); // 錯誤!釋放後主程式仍持有指標
}
int main() {
int *p = (int*)malloc(sizeof(int)); // 分配 4 bytes
if (!p) { perror("malloc 失敗"); return 1; }
*p = 10;
printf("修改前: %d\n", *p); // 10
modify(p); // 傳遞指標,共享同一塊記憶體
printf("修改後: %d\n", *p); // 200
free(p); // 歸還給堆
// printf("%d\n", *p); // 錯誤!使用已釋放記憶體 (UAF)
p = NULL; // 防野指標
return 0;
}
堆疊 vs 堆 對照表
| 特性 | 堆疊 | 堆 |
| 管理方式 | 編譯器自動 | 程式設計師手動 |
| 分配速度 | 極快 (指標移動) | 較慢 (搜尋/分割/鎖) |
| 大小限制 | 小 (通常 1-8MB) | 大 (虛擬記憶體上限) |
| 生命週期 | 作用域結束自動釋放 | malloc 到 free 為止 |
| 碎片化 | 無 | 有 (外部/內部) |
| 適用場景 | 區域變數、函數呼叫、小物件 | 大資料、動態大小、跨作用域共享 |
跨函數共享記憶體:正確模式
// 配置端負責 malloc,使用端負責 free (所有權轉移)
int *createBuffer(int size) {
return (int*)malloc(size * sizeof(int));
}
void processBuffer(int *buf, int size) {
for (int i = 0; i < size; i++) buf[i] = i * i;
}
void destroyBuffer(int *buf) {
free(buf);
}
int main() {
int *data = createBuffer(100);
processBuffer(data, 100);
destroyBuffer(data); // 明確釋放
return 0;
}
常見錯誤與後果
| 錯誤 | 現象 | 嚴重度 |
| 未檢查 malloc 回傳值 | 解引用 NULL → 崩潰 | 致命 |
| free 後繼續使用 (UAF) | 資料損毀、安全漏洞 | 致命 |
| 重複 free (Double Free) | 堆破壞、崩潰 | 致命 |
| free 非 malloc 指標 | 未定義行為、崩潰 | 致命 |
| 記憶體洩漏 (忘記 free) | 記憶體耗盡、效能下降 | 嚴重 |
| 緩衝區溢位 | 覆蓋相鄰資料、安全漏洞 | 致命 |
進階主題
- realloc:調整大小,可能移動記憶體,必須用暫存變數接收
- calloc:分配並歸零,防洩漏敏感資料
- aligned_alloc / posix_memalign:指定對齊 (SIMD、硬體暫存器)
- 記憶體池/物件池:高頻分配小物件的效能優化
- Valgrind/ASan:動態偵測記憶體錯誤的必備工具
嵌入式系統特別提醒
- 避免動態分配 (無 MMU、碎片風險、確定性需求)
- 改用靜態陣列、記憶體池、連結清單靜態節點陣列
- 若必須動態,實作專用分配器 (TLSF、bitmap 分配器)