隨著代碼重構、單元測試普及,調試技能雖弱化,但內存錯誤與並發問題仍是難以定位的 Bug 兩大來源。內存錯誤更是普遍且隱蔽,即使資深工程師也難免中招。本文系統梳理 10 類高發內存錯誤,助你編碼時預防、排查時定位。
成因:堆內存分配後未釋放。後果:輕則長期運行耗盡內存導致分配失敗崩潰,重則嵌入式/長運行服務致命。對策:編碼時嚴格配對 malloc/free、new/delete;RAII 思想管理資源;工具輔助檢測(Valgrind、ASan)。
分類:讀越界(讀到隨機數據導致邏輯錯誤或崩潰)、寫越界/緩衝區溢出(破壞他數據,後果隨機、難定位)。對策:邊界檢查、安全函數(strncpy 替代 strcpy)、編譯器加固(-fstack-protector)、ASan 運行時檢測。
本質:free(p) 釋放內存但 p 值不變,指向已失效內存。後續訪問導致不可預測後果。對策:釋放後立即置 NULL;注意函數參數傳遞指針置 NULL 不影響外層實參(需傳二級指針或引用)。
機制:現代 OS 保留低地址頁(通常 4KB),訪問觸發異常。嵌入式無 MMU 可能不崩潰但邏輯錯誤。對策:解引用前檢查 if (p);嵌入式同樣適用邏輯判斷。
現象:棧/堆變量內容隨機(MSVC 初始化為 0xCC)。對策:定義即初始化;重視編譯器警告(-Wuninitialized)。
核心:p + n == (char*)p + n * sizeof(*p),非字節偏移。對策:不確定時寫測試代碼驗證;掌握指針與數組的等價關係。
陷阱:struct s {int l; char* p;} s1 = {4, "abcd"}; 依賴內存佈局。第三方庫調整成員順序導致邏輯錯誤且編譯無警告。對策:指定初始化器 {.l=4, .p="abcd"}(C99)或 {l:4, p:"abcd"}(GNU 擴展)。
場景:基類結構體在 DLL 中,派生類在應用中。基類新增成員導致內存佈局重疊,邏輯錯亂。對策:重新編譯所有相關代碼;接口設計避免跨 DLL 繼承結構體(參考 COM 規範)。
隱形坑:同為 free 但鏈接不同運行時庫(調試/發布、單線程/多線程),內存管理器不同導致崩潰。對策:統一項目運行時庫設置(/MT, /MD 等);模塊邊界由分配者負責釋放。
錯誤:char* get_str() { char str[]="abcd"; return str; } 返回局部數組地址。正確:char* get_str() { char* str="abcd"; return str; } 字符串字面量存儲在只讀數據區,生命周期全局。對策:嚴禁返回局部變量地址;需動態內存則由調用者釋放。
內存安全是 C/C++ 程序員的基本功,養成良好習慣勝過事後諸葛亮。