未定義行為與安全

違反嚴格別名

假設你和朋友各自記自己的帳本,並且都承諾絕不在對方帳本上塗寫。有了這個承諾,你們在每次改動後都不必再去重讀對方的帳本——可以信任自己手上的那份。C 的最佳化器對記憶體下了類似的賭注:它假設兩個「不同且不相容」型別的指標不會指向同一段位元組。這個假設就是嚴格別名規則,而悄悄違反它是一種微妙卻非常真實的未定義行為。

更精確地說,規則是:你只能透過物件「自己型別」的指標來存取那物件的位元組(外加少數被允許的例外,特別是 char 指標,它被允許和任何東西別名)。如果你存了一個 float,然後透過一個不相關的 int 指標去讀同一段位元組,你就違反了嚴格別名,結果是未定義行為——儘管天真地看,那些位元組「就好端端待在那裡」。最佳化器靠這個假設略過多餘的重新載入:如果它把某個值快取在暫存器裡,而你透過不同型別的指標寫入,它會假設那次寫入不可能碰到你的值,於是繼續用過時的暫存器副本。經典的壞慣用法是型別雙關(type-punning):寫 'int bits = *(int*)&my_float;' 去檢視 float 的位元樣式。誠實且良好定義的位元組重新詮釋方式,是用 memcpy 複製(編譯器會把它最佳化掉)或使用 union——兩者都繞過了這條規則。

為什麼重要:嚴格別名臭蟲是最難診斷的之一,因為程式碼看起來合理、編譯得乾乾淨淨,常常在 -O0 還能跑,到了 -O2 最佳化器真正利用那個假設時才出錯。實用的原則:不要靠在不相關指標型別之間轉型來重新詮釋記憶體;用 memcpy 或 union 把物件的位元組當作另一種型別來讀;真的需要逐位元組走訪原始位元組時就用 char*(或 unsigned char*);如果你為了舊程式碼非得關掉這條規則,gcc/clang 有 -fno-strict-aliasing 旗標,但倚賴它是拐杖,不是修正。

/* 壞掉:透過 int 指標讀 float 的位元(嚴格別名 UB) */ float f = 1.0f; int bad = *(int *)&f; /* 正確:複製位元組,沒有別名違反 */ int good; memcpy(&good, &f, sizeof good); /* 編譯器會把它最佳化成一次搬移 */

在不相關指標型別之間轉型來重新詮釋位元組是 UB。memcpy(或 union)以良好定義且零執行期成本的方式達成。

char*(或 unsigned char*)被允許和任何物件別名——這個例外正是 memcpy 能運作的原因。但例如在 int* 與 float* 之間轉型共用儲存空間是未定義的,而且這種損壞通常只在較高最佳化等級才現形。

又称
strict aliasing ruletype-based alias analysis嚴格別名規則型別別名