堆與棧是 C 程序內存管理的兩大基石。理解二者差異,是寫出無洩漏、無野指針、無棧溢出代碼的前提。
一、 核心對比
| 維度 | 棧 | 堆 |
| 管理方式 | 編譯器自動管理(幀指針 EBP/RBP) | 程序員手動管理 |
| 分配函數 | 函數調用隱式分配 | malloc/calloc/realloc |
| 釋放函數 | 函數返回隱式釋放 | free |
| 大小限制 | 較小(典型 1-8MB) | 大(受限於虛擬記憶體) |
| 分配速度 | 極快(指針遞增/遞減) | 較慢(查找空閒塊、分裂/合併) |
| 碎片化 | 無 | 有(外部碎片) |
| 適用對象 | 局部變量、函數參數、返回地址、保存的寄存器 | 動態大小、長生命週期、大對象、跨函數共享 |
二、 生命週期與作用域
- 棧上對象:生命週期 = 作用域 = 所在塊/函數執行期間。離開作用域自動銷毀,調用析構函數(C++)。
- 堆上對象:生命週期 = 從 malloc 到 free,可跨函數、跨線程、跨模塊。需程序員精確掌控。
三、 三大經典陷阱
- 棧上地址逃逸:
int* foo() { int x = 10; return &x; } // 錯誤:返回棧地址
// 調用者拿到野指針,後續棧幀復用導致數據損壞
修正:返回 malloc 內存、靜態變量、或輸出參數填充調用者提供的緩衝區。
- 棧溢出:
void foo() { char big[1024*1024]; foo(); } // 遞歸 + 大局部數組 → Stack Overflow
// 修正:大數組放靜態/堆、遞歸改迭代、增大棧大小(ulimit -s / linker 選項)
- 堆內存洩漏/雙重釋放/使用後釋放:
- 洩漏:malloc 無對應 free。
- 雙重 free:free 同一指針兩次 → heap corruption。
- Use-after-free:free 後繼續訪問 → 未定義行為。
修正:所有權語義明確(獨佔/共享)、RAII/智能指針、Valgrind/ASan 檢測。
四、 進階主題
- 棧幀佈局:參數 → 返回地址 → 舊 EBP → 局部變量(編譯器優化可能重排)。
- 調用約定:cdecl(調用者清棧)、stdcall(被調用者清棧)、fastcall(寄存器傳參)。
- 緩衝區溢出攻擊:覆蓋返回地址劫持控制流,防禦:棧保護金絲雀、ASLR、DEP/NX、安全函數。
- 自定義分配器:對象池、區域分配器、bump pointer,提升特定場景性能。
五、 最佳實踐清單
- 優先使用棧(自動管理、無洩漏、缓存友好)。
- 大對象(>1KB)、可變大小、需跨作用域 → 堆。
- malloc 立即檢查 NULL,free 後置 NULL。
- Valgrind/AddressSanitizer 納入 CI 流程。
- 嵌入式/實時系統:禁用堆,靜態池分配。
六、 總結
棚是「自動駕駛」,堆是「手動擋」。新手依賴自動駕駛,高手駕馭手動擋。記住:**每個 malloc 都要有對應的 free,每個指針都要知曉其所有權,每個棧地址都不準逃出作用域**。