面試經典題:int j = 7, q = (++j) + (++j) + (++j); 請問 `q` 與 `j` 的值?
許多初學者會直覺回答:`j` 變 10,`q = 8+9+10 = 27`。但在 C 標準下,這段代碼屬於未定義行為。不同編譯器、不同優化等級、甚至同編譯器不同版本,產生的結果可能完全不同。本文深入剖析根本原因、彙編層面差異、標準規定與避坑指南。
(++j) + (++j) + (++j) 是單一完整表達式,**僅結尾有一個序列點**。其中對 `j` 有三次修改 (`++j` 三次),且每次修改的值參與了加法運算 (非單純決定儲存值) → **嚴重違規 → 未定義行為 (UB)**。| 環境 | 編譯器/選項 | j 最終值 | q 值 | 備註 |
|---|---|---|---|---|
| Windows 7 | GCC (Dev-C++ / VC++6.0 兼容模式) | 10 | 22 | 依賴寄存器分配順序 |
| Windows 7 | VC++ 2008 (MSVC) | 10 | 24 | 不同寄存器分配策略 |
| Linux x86_64 | GCC 9.3 -O0 | 10 | 24 | 無優化 |
| Linux x86_64 | GCC 9.3 -O2 | 10 | 30 | 優化器直接常數摺疊 |
| Linux x86_64 | Clang 10 -O0 | 10 | 24 | Clang 行為近似 GCC |
; 假設 j 在 [rbp-4]
mov eax, DWORD PTR [rbp-4] ; eax = j (7)
add eax, 1 ; eax = 8 (++j 第1次)
mov DWORD PTR [rbp-4], eax ; j = 8
mov ecx, DWORD PTR [rbp-4] ; ecx = j (8)
add ecx, 1 ; ecx = 9 (++j 第2次)
mov DWORD PTR [rbp-4], ecx ; j = 9
mov edx, DWORD PTR [rbp-4] ; edx = j (9)
add edx, 1 ; edx = 10 (++j 第3次)
mov DWORD PTR [rbp-4], edx ; j = 10
add eax, ecx ; eax = 8+9 = 17
add eax, edx ; eax = 17+10 = 27? 實際常因寄存器重用順序不同導致 22/24/30
關鍵點:編譯器**不保證**三個 `++j` 的求值順序,也不保證中間值是寫回記憶體還是停留在寄存器。優化器可能直接常數傳播、重排指令、甚至刪除看似無用的存取。
i = i++ + ++i; // UB: 多次修改 i
a[i] = i++; // UB: 修改 i 並用於陣列索引
printf("%d %d", i++, i++); // UB: 引數求值順序未定 (但序列點在函數呼叫邊界)
i = (i, ++i, ++i); // OK: 逗號運算子引入序列點,依序求值
明確意圖,分步寫,引入序列點:
// 寫法 1: 明確序列
int j = 7;
int q = 0;
q += ++j; // j=8, q=8
q += ++j; // j=9, q=17
q += ++j; // j=10, q=27
// 寫法 2: 使用逗號運算子 (單一表達式,但有序列點)
int j = 7, q;
q = (++j, ++j, ++j, j + (j-1) + (j-2)); // 晦澀,不推薦
// 寫法 3: 函數封裝 (最清晰)
int add_and_inc(int *p) { return ++(*p); }
int j = 7, q = add_and_inc(&j) + add_and_inc(&j) + add_and_inc(&j); // 明確順序
gcc -fsanitize=undefined -g test.c -o test
./test
// 輸出: runtime error: multiple unsequenced modifications to 'j'
q = (++j) + (++j) + (++j) 是**教科書級未定義行為**,**絕不可用於生產代碼**。理解 UB 不是為了「利用它」,而是為了徹底避開它。這正是 C 程式設計師從「能跑通」進化為「寫正確、可移植、可維護代碼」的關鍵認知躍升。