本文通過鏈表操作實例,直觀演示懸空指針的形成過程:指針非空但指向已釋放/無效內存地址。這是 C/C++ 開發中最隱蔽、破壞力最強的內存錯誤之一。
一個指針不為 NULL,但指向一個已釋放、已銷毀或超出作用域的對象地址,稱為懸空指針。解引用該指針屬於未定義行為。
#include#include typedef struct listnode mynode; struct listnode { mynode *next; int number; int age; }; mynode *addnode(mynode *prevnd, int number, int age) { mynode *ndtemp = (mynode*)malloc(sizeof(mynode)); prevnd->next = ndtemp; ndtemp->number = number; ndtemp->age = age; ndtemp->next = NULL; return ndtemp; } mynode *initlist() { mynode *temp = (mynode*)malloc(sizeof(mynode)); temp->number = 0; temp->age = 0; temp->next = NULL; return temp; } int main() { // 第一階段:正常構建、輸出、完整銷毀 mynode *mylist = initlist(); mynode *mytempnd = mylist; for (int i = 0; i < 10; i++) mytempnd = addnode(mytempnd, i, 20+i); for (mytempnd = mylist->next; mytempnd; mytempnd = mytempnd->next) printf("id:%d, age:%d\n", mytempnd->number, mytempnd->age); mynode *oldtmpnd; for (mytempnd = mylist->next; mytempnd; ) { printf("delete id:%d\n", mytempnd->number); oldtmpnd = mytempnd; mytempnd = mytempnd->next; free(oldtmpnd); } free(mylist); // 第二階段:重新構建,故意製造懸空指針 mylist = initlist(); mytempnd = mylist; for (int i = 0; i < 10; i++) mytempnd = addnode(mytempnd, i, 20+i); for (mytempnd = mylist->next; mytempnd; mytempnd = mytempnd->next) printf("id:%d, age:%d\n", mytempnd->number, mytempnd->age); printf("-------------------------\n"); int j = 0; for (mytempnd = mylist->next; mytempnd; ) { oldtmpnd = mytempnd; mytempnd = mytempnd->next; if (++j > 6) { // 刪除第 7~10 節點 printf("delete id:%d\n", oldtmpnd->number); free(oldtmpnd); } } // 此時第 6 個節點的 next 仍指向已被 free 的第 7 個節點地址 → 懸空指針 return 0; }
id:0, age:20 ... id:9, age:29 delete id:0 ... delete id:9 id:0, age:20 ... id:9, age:29 ------------------------- delete id:6 delete id:7 delete id:8 delete id:9
next 原指向第 7 節點,第 7 節點被 free 後,第 6 節點的 next 未置 NULL,成為懸空指針。free(p) 僅釋放 p 指向的內存,不會修改 p 本身的值,也不会自動更新其他指向同一地址的指針。free(ptr); ptr = NULL; 形成肌肉記憶。next 指向被刪節點的 next(或 NULL)。懸空指針比野指針(未初始化指針)更隱蔽,因為它持有「看起來合法」的非零地址。嚴格遵循「釋放即置空」、鏈表操作同步更新前驅指針、引入所有權語義,是 C 語言工程師避免此類災難性錯誤的基本功。