位移過多與修改字串字面值
兩個看起來日常無害的小操作,其實是未定義行為,而兩者都會讓人意外,因為它們「感覺」沒事。第一個是把一個數字的位元位移太多位。第二個是去更動你直接用引號寫出來的字串裡的字母。每個看起來都無辜,甚至可能看似能跑,這正是值得把它們放在一起點名的原因。
先看位移。運算式 x << n 把 x 的位元往左移 n 位,x >> n 往右移。C 要求 n 至少為 0、且「嚴格小於」x 的(提升後)型別位元數。對 32 位元的值,n 必須是 0..31;寫 x << 32 或 x << 40,或任何負的位移數,都是未定義行為——它沒有被定義成給出 0,而且在許多 CPU 上硬體實際只用 n 的低位元,所以 x << 32 可能表現得像 x << 0、悄悄把 x 原封不動還給你。把負的有號值往左移也是未定義。再看字串字面值。當你寫 char *s = "hello"; 時,文字 "hello" 存在整支程式共用的唯讀記憶體裡,而 s 指「進」它。寫 s[0] = 'H'; 是試圖修改那段唯讀資料,是未定義行為——通常會當掉,但不保證。修正方式是把字面值複製進可寫的陣列:char s[] = "hello"; 給你自己一份可修改的副本。
為什麼重要:這兩個錯誤都躲在編譯時毫無怨言、有時跑起來也看不出傷害的程式碼後面,然後在不同 CPU、不同編譯器、或較高最佳化等級上爆掉。位移陷阱常出現在位元操作與序列化程式碼裡,那裡的位移量來自一個可能達到型別寬度的變數。字串字面值陷阱則出現在每當一個來自字面值的 'char *' 被當成可編輯緩衝區使用時。把字面值指標宣告為 'const char *' 能讓編譯器在編譯期抓到那次寫入,而把位移數對型別寬度做驗證能讓位移留在範圍內。
uint32_t x = 1; uint32_t y = x << 32; /* UB:位移數必須是 0..31,不能是 32 */ char *s = "hello"; /* 指向唯讀記憶體 */ s[0] = 'H'; /* UB:修改字串字面值 */ char buf[] = "hello"; /* OK:堆疊上一份可寫的副本 */ buf[0] = 'H'; /* 沒問題 */
位移數等於型別寬度是 UB;透過字面值指標寫入是 UB。請讓位移數留在範圍內,並把字面值複製進 char 陣列再去編輯。
對 32 位元型別做 x << 32 並未被定義為 0;許多 CPU 會遮罩位移數、結果給你 x << 0。而字面值指標應宣告為 'const char *',讓編譯器標記任何試圖透過它寫入的行為。