經典面試題:int i=3; int b=i+++*i++; 為什麼結果可能是 9?本文結合 C 標準、編譯器實現差異、序列點規則,徹底講清表達式求值順序未定義行為的本質,並提供跨編譯器對比實測數據。
在兩個序列點之間,對同一對象修改超過一次,行為未定義(Undefined Behavior, UB)。不同編譯器、優化等級、架構下結果完全不可預期。
;、&& || ?: , 運算符、函數調用進入/返回。i+++*i++詞法分析(最大吞噬原則):i++ + *i++
i++ → 讀取 i (3),承諾稍後 +1*i++ → i++ 讀取 i (?) ,解引用,承諾稍後 +1;)i 在同一序列點間被修改了兩次(兩個后置++),且右操作數還讀取了 i 的值(非為計算自身新值)測試代碼(12 種變體):
int i[12] = {3,3,3,3,3,3,3,3,3,3,3,3};
int b[12];
b[0] = i[0]++ + *i[0]++; // 原題
b[1] = ++i[1] * ++i[1]; // 兩前置++
b[2] = ++i[2] * i[2]++; // 前置 * 後置
b[3] = i[3]++ * ++i[3]; // 後置 * 前置
b[4] = i[4]++ * i[4]++ * i[4]++; // 三後置
b[5] = i[5]++ * i[5]++ * ++i[5];
b[6] = ++i[6] * ++i[6] * ++i[6]; // 三前置
b[7] = ++i[7] * ++i[7] * i[7]++;
b[8] = ++i[8] * i[8]++ * i[8]++;
b[9] = ++i[9] * i[9]++ * ++i[9];
b[10]= i[10]++ * ++i[10] * ++i[10];
b[11]= i[11]++ * ++i[11] * i[11]++;
| 表達式 | GCC 4.8 (Code::Blocks) | MSVC 2010 | Clang 3.4 |
|---|---|---|---|
i++ + *i++ | 9 | 12 | 9 |
++i * ++i | 16 | 16 | 16 |
++i * i++ | 12 | 12 | 12 |
i++ * ++i | 12 | 12 | 12 |
i++ * i++ * i++ | 27 | 64 | 27 |
i++ * i++ * ++i | 36 | 48 | 36 |
++i * ++i * ++i | 64 | 64 | 64 |
++i * ++i * i++ | 48 | 48 | 48 |
++i * i++ * i++ | 36 | 36 | 36 |
++i * i++ * ++i | 48 | 48 | 48 |
i++ * ++i * ++i | 48 | 48 | 48 |
i++ * ++i * i++ | 36 | 36 | 36 |
關鍵觀察:
i+++*i++ 在 GCC/Clang 下為 9(3*3),MSVC 為 12(3*4 或 4*3)GCC/Clang 典型策略:先使用舊值計算整個表達式,再在序列點前批量應用所有副作用。
i = 3
讀取左 i++ → 3
讀取右 *i++ → *3 = 3
計算 3 + 3 = 9
序列點前:i += 2 → i = 5
結果 b = 9
int t1 = i++; // 序列點1 int t2 = i++; // 序列點2 b = t1 + *t2; // 序列點3
-Wall -Wextra -Wsequence-point (GCC/Clang) 或 /Wall (MSVC) 靜態檢測a() + b() * c() 中三個函數調用順序完全未指定序列點規則是 C 語言「可移植性」的基石。任何依賴未定義求值順序/副作用時機的代碼,都是定時炸彈。現代編譯器優化越來越激進(如利用 UB 刪除死代碼),此類代碼在新版本編譯器下極易崩潰或產生安全漏洞。請堅守「一語句一修改、讀寫分離」原則,寫出確定性、可維護的 C 代碼。