struct A { int a; char b; };
struct B { struct A *n; };
struct B *new_B() {
struct A t = { 100, 'c' }; // 局部變量,棧上分配
struct B *p = malloc(sizeof(struct B));
p->n = &t; // 保存局部變量地址!
return p;
}
int main() {
struct B *p = new_B();
printf("%d, %c\n", p->n->a, p->n->b); // 偶爾輸出正確
free(p);
return 0;
}
觀察到輸出正確 ≠ 代碼正確。這是典型的未定義行為(Undefined Behavior, UB):
t 是 new_B 的局部變量,存儲在棧幀中p->n 指向一塊已失效的棧內存(懸垂指針)malloc 就「能跑通」?
// 有 malloc
struct B *p = malloc(sizeof(struct B)); // p 在堆上,有效
p->n = &t; // p->n 指向棧上已死的 t
// 無 malloc
struct B *p; // p 未初始化,野指針
p->n = &t; // 寫入隨機地址 → 立即崩潰
malloc 只保證 p 本身合法,無法延長 t 的生命周期。兩種情況本質都是 UB,只是崩潰時機不同。
| 方案 | 代碼 | 優點 | 缺點 |
|---|---|---|---|
| 1. 動態分配結構體 A | p->n = malloc(sizeof(struct A)); *p->n = t; | 生命周期可控,需手動 free(p->n) | 兩次分配,兩次釋放 |
| 2. 結構體 B 內嵌結構體 A | struct B { struct A n; }; | 單次分配,無指針,自動釋放 | 結構體變大,不靈活 |
| 3. 傳入輸出參數 | void new_B(struct B *out) { out->n = malloc(...); } | 調用者控制內存 | 接口稍複雜 |
struct B *new_B_fixed() {
struct B *p = malloc(sizeof(struct B));
if (!p) return NULL;
p->n = malloc(sizeof(struct A));
if (!p->n) { free(p); return NULL; }
p->n->a = 100;
p->n->b = 'c';
return p;
}
void free_B(struct B *p) {
if (p) {
free(p->n); // 先釋放內嵌指針
free(p); // 再釋放外層
}
}
struct B {
struct A n; // 直接內嵌,非指針
};
struct B *new_B_simple() {
struct B *p = malloc(sizeof(struct B));
if (p) {
p->n.a = 100;
p->n.b = 'c';
}
return p;
}
// 釋放只需 free(p)
鐵律:絕不返回局部變量地址(包括數組、結構體、字面量地址)。編譯器(GCC -Wreturn-local-addr、Clang、MSVC /W4)會發出警告,務必開啟並修復。
-fsanitize=address 運行時精準報錯 heap-use-after-free / stack-use-after-scope--tool=memcheck 檢測非法內存訪問__attribute__((cleanup)) (GCC) 自動釋放本例揭示了 C 語言指針最核心的陷阱:**指針有效性 ≠ 指向內存有效性**。malloc 分配的是指針變量本身的存儲空間,而非它指向的數據。理解棧幀生命周期、所有權語義、RAII 思想(C++ 智能指針/C 手動配對),是寫出無內存錯誤 C 代碼的基石。