這篇筆記源於同學的一個疑問,讓我重新審視 C 語言中自增運算符(++)在複雜表達式中的行為。雖然 C 語言設計優雅,但缺乏標準約束的表達式在不同編譯器下可能產生截然不同的結果,這正是本文要探討的核心問題。
考慮這段極簡的程式碼:
#include <stdio.h>
#include <stdlib.h>
int main() {
int a = 1, b = 3, c = 0;
a = (++b) + (++b) + (++b);
printf("a=%d\nb=%d\n", a, b);
return 0;
}
這是一段故意設計的「糟糕代碼」,雖然看似能提升微小效率,但因編譯器解釋不一,極不可取。
為什麼同樣的代碼會產生 15、16、18 三種結果?關鍵在於編譯器如何處理「副作用」與「序列點」。
透過 GCC 編譯後的 AT&T 語法組合語言分析,關鍵指令序列如下:
addl $0x1, 0x1c(%esp):b 自增第一次(b=4)addl $0x1, 0x1c(%esp):b 自增第二次(b=5)mov 0x1c(%esp), %eax:將 b(=5) 載入 eaxadd %eax, %eax:eax = 5+5 = 10addl $0x1, 0x1c(%esp):b 自增第三次(b=6)add 0x1c(%esp), %eax:eax = 10+6 = 16mov %eax, 0x1c(%esp):存入 a可見 GCC 先將 b 連續自增兩次至 5,進行第一次加法(5+5),再自增一次至 6,完成最後加法(10+6),最終 a=16。
TC 的組合語言顯示:先連續三次 inc si 將 b 從 3 遞增至 6,然後執行兩次 add di, si(6+6+6=18)。這說明 TC 採用「先完成所有副作用,再計算表達式」的策略。
C 標準規定:在同一序列點內,對同一變量進行多次修改,或修改後又讀取其值(非為了計算新值),屬於未定義行為。編譯器可自由選擇求值順序,導致結果差異。
表達式 (++b)+(++b)+(++b) 中,b 被修改三次,且值被多次讀取參與加法,違反序列點規則。
| 表達式 | GCC 結果 | 說明 |
|---|---|---|
(++b)+(b++)+(b++) | a=12, b=6 | 前綴自增先執行,後綴自增延後 |
c=++b; a=c+(++b)+(++b) | a=15, b=6 | 引入臨時變量避免歧義,符合直覺 |
(b++)+(b++)+(b++) | a=3, b=6 | 三次後綴自增均在加法後執行 |
(++b)+(b++)+(++b)+(++b) | a=19, b=7 | 混合運算,最後 b++ 延後執行 |
-Wsequence-point)這個問題本質上反映了編譯器對未定義行為的不同處理選擇。理解組合語言層面的執行流程,有助於我們編寫更健壯、可移植的 C 代碼。正如文中所言:「程序還有個可讀性要求,畢竟你寫的代碼以後要維護。」