C 語言中 ++/-- 的副作用(修改變數值)與值運算(取得舊/新值)的相對時序,在單一運算式中多次出現同一變數時會導致**未定義行為 (UB)**。本文透過 12 組實測案例,揭示巨集重複展開、邏輯運算子短路、關係運算子序列點、算術運算子無序列點的差異,並給出避坑原則。
|| && ?: , 運算子的左運算元評估後、函數呼叫進入前/返回後。| 代碼片段 | 分類 | 行為 | 原因 |
|---|---|---|---|
BAND1(++i) | 巨集+前置遞增 | 定義良好 | 巨集參數僅展開一次,單一副作用 |
BAND1(i++) | 巨集+後置遞增 | 定義良好 | 同前,副作用在序列點前完成 |
BAND2(++i) ((i)>=5)&&((i)<=5) | 巨集重複展開 | UB | 參數 ++i 展開兩次 → 兩次修改無序列點 |
a = (i++) + (i++) + (i++) | 算術運算子 | UB | + 無序列點,三次修改競爭 |
a = (i++ >= 5) + (i++ >= 5) | 關係+算術 | UB | 兩子式各有副作用,+ 不保證順序 |
a = (i++ >= 4) && (i++ >= 5) | 邏輯 AND | 定義良好 | && 左側為序列點,保證左先右後 |
a = (i--) && (i) | 邏輯 AND+後置遞減 | 定義良好 | 左側副作用在序列點前完成 |
a = (i) && (i--) | 邏輯 AND | 定義良好 | 左側為 1 才評估右側,右側副作用在序列點後 |
a = (i--) + (i) | 算術 | UB | + 無序列點,修改與讀取順序未定 |
do{...}while(0) 區塊。&& || ?: , 的序列點特性刻意控制順序,但仍避免同變數雙改。-Wall -Wextra -Wsequence-point (GCC/Clang) 可偵測多數 UB。// 錯誤:a = (i++) + (i++);
// 正確:
int t1 = i++; int t2 = i++; a = t1 + t2;
// 錯誤:#define SQ(x) ((x)*(x)) 配合 SQ(i++)
// 正確:static inline int sq(int x) { return x*x; }
理解序列點與副作用規則,是從「能編譯跑出結果」進階到「寫出可攜、可維護、符合標準 C 代碼」的關鍵門檻。