內存管理是 C 語言的核心竞争力,也是最大坑源。本文系統整理 sizeof 語義、結構體對齊規則、三大分配區(靜態/棧/堆)、malloc/calloc/realloc/alloca 區別與陷阱、內存洩漏成因與檢測工具,構建完整知識體系。
一、sizeof:編譯期常量,非函數
- 返回類型:
size_t(無符號整數)。
- 操作數:類型名(需括號)或表達式(不求值,僅推導類型)。
- 不適用:函數類型、不完全類型(未知大小陣列、前向聲明結構體、void)、位域。
常見類型大小(x86-64 Linux/GCC 典型值)
| 類型 | 大小 | 備註 |
| char / signed char / unsigned char | 1 | 定義為 1 字節 |
| short | 2 | |
| int | 4 | |
| long | 8 | Windows 為 4 |
| long long | 8 | |
| float | 4 | |
| double | 8 | |
| 指針 | 8 | 64 位系統 |
二、結構體對齊與填充
struct MyStruct {
char a; // offset 0, size 1
// 7 bytes padding
double b; // offset 8 (align 8), size 8
int c; // offset 16 (align 4), size 4
// 4 bytes padding (struct align = max member align = 8)
}; // total = 24
- 每個成員按
alignof(type) 對齊,編譯器自動插入填充字節。
- 結構體整體大小為最大成員對齊數的倍數(尾部填充)。
#pragma pack(n) 可壓縮對齊,但可能導致非對齊訪問性能下降或硬件異常(ARM)。
offsetof 宏獲取成員偏移,_Alignof (C11) 獲取對齊要求。
三、三大內存區與分配函數
| 區域 | 分配方式 | 生命週期 | 典型大小 | 速度 | 用例 |
| 靜態/全局 | 編譯期固定 | 程序全周期 | 受限於數據段 | 最快 | 全局變量、static 變量、字串字面量 |
| 棧 | 編譯器自動(alloca 手動) | 函數調用周期 | ~MB 級(線程棧) | 極快(指針移動) | 局部變量、臨時緩衝、遞歸 |
| 堆 | 手動 malloc/free | 程序員控制 | 虛擬地址空間 | 較慢(系統調用、鎖、碎片整理) | 動態數據結構、大塊緩衝、跨函數生命週期 |
四、堆分配函數族詳細對比
void *malloc(size_t size); // 未初始化,內容不定
void *calloc(size_t nmemb, size_t size); // 全 0 初始化(位模式全 0)
void *realloc(void *ptr, size_t size); // 調整大小,可能移動內存
void free(void *ptr); // 釋放,ptr 必須來自 malloc/calloc/realloc
void *alloca(size_t size); // 棧上分配,函數返回自動釋放(非標準,慎用)
關鍵區別
- malloc vs calloc:
calloc 零初始化,對指針類型不保證產生 NULL(位模式全 0 ≠ 空指針表示),對浮點數不保證產生 0.0。malloc 需手動 memset。
- calloc 內部優化:OS 可直接映射零頁,大塊分配比
malloc+memset 快。
- realloc 遷移:可能返回新地址,舊指針失效,必須用返回值更新指針。
- alloca 危險:棧溢出無保護、不支持可變長度數組(C99 VLA 已棄用)、異常處理/長跳轉時不釋放。
五、分配器內部機制簡析
- 用戶調用
malloc → 分配器從空閒鏈表/紅黑樹尋找合適塊 → 分割/合並 → 返回用戶指針(通常指向用戶數據區,元數據在前)。
- 空閒內存不足 → 調用
brk/mmap 向 OS 申請大塊(批發)→ 再切分(零售)。
free 不立即歸還 OS,放入空閒池供下次複用(緩衝機制)。
- 內存洩漏:
malloc 無對應 free,長期運行進程可用內存單調下降,最終 OOM。
六、避坑檢查表
| 陷阱 | 後果 | 對策 |
| malloc 未檢查 NULL | 解引用空指針崩潰 | 永遠檢查返回值 |
| free 後繼續使用指針 | Use-after-free,未定義行為 | free 後置 NULL |
| 重複 free | 堆損壞,崩潰 | free 後置 NULL,邏輯保證單次 |
| 釋放非堆指針 | 堆損壞 | 只 free malloc 系返回指針 |
| 柔性數組結構體 malloc 尺寸錯誤 | 越界寫入 | malloc(sizeof(Struct) + n * sizeof(Elem)) |
| realloc 失敗未保存舊指針 | 內存洩漏 | void *tmp = realloc(p, sz); if (!tmp) { /* handle */ } p = tmp; |
七、檢測工具
- Valgrind Memcheck:
valgrind --leak-check=full ./prog,檢測洩漏、越界、使用未初始化值。
- AddressSanitizer (ASan):
gcc -fsanitize=address -g,編譯期插桿,運行時檢測,速度快於 Valgrind。
- Malloc 棧保護:
MALLOC_CHECK_=3 環境變量啟用 glibc 內部一致性檢查。
總結
掌握 sizeof/對齊、三大區特性、分配函數語義、分配器工作原理、常見陷阱與檢測工具,是寫出高效、穩健、無洩漏 C 代碼的必經之路。原則:能靜態/棧分配就不上堆;上堆必成對 free;大塊優先 calloc;工具常掛 CI。