某技術社團幹部面試遇到的經典記憶體分配題目:兩段外觀相似的程式碼,執行結果截然不同。核心差異在於 allocmem 函數內部採用區域陣列還是 malloc 分配記憶體。
void allocmem(char **p) {
char r[10] = {1}; // 區域陣列,存放於棧
*p = r; // 將棧位址賦給輸出指標
}
int main() {
char *m = NULL;
allocmem(&m);
if (m) {
for (int i = 0; i < 10; i++)
printf("m[%d] = %d\n", i, m[i]); // 輸出亂碼或崩潰
}
}
執行結果:印出的值非預期的 1,而是隨機亂碼(見圖片)。
void allocmem(char **p) {
*p = malloc(10); // 堆上分配
memset(*p, 1, 10); // 初始化為 1
}
int main() {
char *m = NULL;
allocmem(&m);
if (m) {
for (int i = 0; i < 10; i++)
printf("m[%d] = %d\n", i, m[i]); // 正確輸出十個 1
}
free(m); // 記得釋放!
}
執行結果:正確印出十個 1(見圖片)。
| 特性 | 版本 A:區域陣列 | 版本 B:malloc |
|---|---|---|
| 儲存區域 | 棧 | 堆 |
| 生命週期 | 函數返回即銷毀 | 手動 free 前持續有效 |
| 位址有效性 | 函數結束後成「懸空指標」 | 跨函數有效 |
| 記憶體殘留 | 棧幀被後續呼叫覆寫 | 資料完整保留 |
版本 A 執行流程:
main() 呼叫 allocmem()
├─ allocmem() 棧幀建立
│ ├─ r[10] 分配於棧頂
│ └─ *p = &r[0] ← 回傳棧位址
└─ allocmem() 返回,棧幀銷毀,r[10] 記憶體標記為可重用
main() 繼續執行,m 指向已失效位址 → 未定義行為
版本 B 執行流程:
main() 呼叫 allocmem()
└─ malloc(10) 向作業系統申請堆記憶體
└─ 位址存放於 *p,函數返回後堆區塊仍屬進程所有
main() 透過 m 正常存取 → 正確結果
void func(char *buf, size_t len)malloc,呼叫端 free(本文版本 B)valgrind、AddressSanitizer (`-fsanitize=address`) 能精準捕捉 Use-after-free、Stack-use-after-scope 等錯誤。std::vector、std::string、std::unique_ptr 等 RAII 容器,從語言層面杜絕手動記憶體管理錯誤。這道題目揭示了 C 語言最核心也最危險的特性:手動記憶體管理與作用域生命週期。理解棧/堆差異、識別懸空指標,是從「能寫出跑得動的程式」進階為「寫出健壯、可維護系統程式」的必經之路。