為什麼需要動態內存?
| 靜態/自動數組缺陷 | 動態內存解決 |
| 長度必須編譯期常量 | 運行時根據輸入決定大小 |
| 生命週期綁定作用域/程序 | 手動控制分配/釋放時機 |
| 無法跨函數持久保存 | 堆內存進程全局可見,任意傳遞 |
| 無法擴縮容 | realloc 靈活調整 |
核心 API 速查
#include <stdlib.h>
void *malloc(size_t size); // 分配 size 字節,未初始化,失敗返回 NULL
void *calloc(size_t n, size_t size); // 分配 n*size 字節,歸零初始化
void *realloc(void *ptr, size_t size); // 調整大小,可能移動內存,舊數據保留
void free(void *ptr); // 釋放 ptr 指向的塊,ptr 必須來自 malloc 系
/* 釋放後 ptr 成為野指針,必須立即置 NULL */
經典錯誤與修正
| 錯誤代碼 | 後果 | 修正 |
int *p = malloc(4); *p = 10; free(p); printf("%d", *p); | Use-after-free (UB) | 釋放後不再訪問,p = NULL; |
int *p = malloc(100); p = malloc(200); // 未 free 首塊 | 內存洩漏 | 重新分配前務必 free(p); |
void foo(int *p) { free(p); } int main() { int *p=malloc(4); foo(p); *p=1; } | 雙重釋放/使用已釋放 | 所有權清晰:誰分配誰釋放,或文檔標註轉移所有權 |
int *p = realloc(p, 200); // 失敗返回 NULL 導致原指針丟失 | 內存洩漏 | void *tmp = realloc(p, 200); if(tmp) p = tmp; else handle_error(); |
struct S { int *buf; }; struct S s; s.buf = malloc(100); // 結構體拷貝導致雙重 free | Double free | 實現深拷貝/引用計數/禁用拷貝 (C++ 用 RAII) |
堆內存佈局與分配器原理 (簡述)
malloc 向作業系統申請大塊 (brk/mmap),內部維護空閒鏈表/樹 (bins) 管理不同尺寸塊。
- 小塊 (
< 128KB) 用 brk 擴展堆頂;大塊用 mmap 匿名映射。
- 分配策略:首次適配/最佳適配/分離適配 (glibc ptmalloc 用分離適配 + thread arena)。
- 釋放時嘗試合併相鄰空閒塊,歸還給 OS (munmap/縮減 brk) 滿足閾值時觸發。
跨函數使用動態內存模式
/* 模式 1:調用者分配,被調用者填充 */
void fill_buffer(int *buf, size_t len) { for(size_t i=0;i<len;i++) buf[i]=i; }
int main() { int *p = malloc(100*sizeof(int)); fill_buffer(p,100); free(p); }
/* 模式 2:被調用者分配,返回指針,調用者釋放 (需文檔明確) */
int* create_array(size_t len) { return calloc(len, sizeof(int)); }
int main() { int *p = create_array(100); /* use */ free(p); }
/* 模式 3:輸出參數傳遞指針地址 (雙重指針) */
int alloc_array(int **out, size_t len) {
*out = malloc(len * sizeof(int));
return *out ? 0 : -1;
}
int main() { int *p; if(alloc_array(&p, 100)==0) { /* use */ free(p); } }
進階技巧
- 柔性數組成員 (C99):
struct { size_t len; char data[]; } 實現變長結構體單次分配。
- 對齊分配 (C11):
aligned_alloc(alignment, size) 滿足 SIMD/硬件對齊需求。
- 自定義分配器:嵌入式/實時系統實現
my_malloc/my_free 替換默認 (覆寫 __malloc_hook 或鏈接時替換)。
- 檢測工具:
valgrind --leak-check=full、AddressSanitizer (-fsanitize=address)、Electric Fence 必須集成 CI。
總結:動態內存鐵律
- 每個
malloc/calloc/realloc 必有對應 free (且僅一次)。
- 檢查返回值 必須 判斷
NULL。
- 釋放後指針 立即置 NULL。
- 所有權界限清晰:誰分配誰釋放,跨模塊傳遞必須文檔化。
- 嚴禁訪問已釋放/未初始化/越界內存。