從一條直線到分岔的道路
上一篇裡,你的第一支真正的程式由上往下執行:宣告一個變數、印出來、結束。那是一條直線。但世界上有趣的程式都會提問——這個數是偶數嗎?、檔案打開了嗎?、密碼對不對?——然後依答案轉向不同的方向。讓程式得以轉向的機制,稱為流程控制,而它所提的問題,是由運算子建構出來的。這篇就是 C 從照本宣科走向自行抉擇的轉捩點。
這裡的一切都建立在一個關於 C 的安靜事實之上:一個決定其實就是一個數字。C 底層沒有另外藏一種「真假」的物質——當它要問某件事是否為真,它就計算出一個值,規則很單純:零代表假,任何非零的值都代表真。所以 `if (3)` 永遠會執行,`if (0)` 永遠不會,`if (x - x)` 也永遠不會,因為 x - x 等於 0。一旦你把這點內化,下面的運算子就不再是神祕的標點符號,而成了普通的算術,只是它的答案恰好用來掌舵。
運算子:構成問題的動詞
運算子分成幾個家族。算術類(+、-、*、/、%)負責計算;要注意兩個整數相除時,/ 會截斷(7 / 2 是 3,不是 3.5),而 % 取餘數(7 % 2 是 1)。關係類(<、<=、>、>=)與相等類(== 與 !=)比較兩個值,回傳 1 表真、0 表假——它們正是把「x 比較大嗎」化成數字的方式。邏輯類(&&、||、!)則把那些答案黏起來:&& 是「且」,|| 是「或」,! 把真假對調。
有兩個細節很重要。第一,優先順序決定誰先結合,就像數學裡 * 先於 +:`2 + 3 * 4` 是 14,而 `x < 10 && y > 0` 會讀作 `(x < 10) && (y > 0)`。你不必背下整張表——拿不定主意時就加上括號;清楚勝過聰明。第二,&& 與 || 會短路:在 `a && b` 中,若 a 為假,b 根本不會被求值,因為答案已成定局。這不是要忍受的怪癖,而是可善用的工具——`if (p != NULL && p->ready)` 能在碰到 p 所指之物前先安全地檢查 p。
if、else,以及底下的真假值
你最先伸手取用的分支就是 `if`。它接受括號裡的一個值,若該值非零,就執行隨後的區塊;可選的 `else` 接住為零的情況。你可以串接 `else if`,由上而下依序測試多個條件,採用第一個成立的。因為測試「只是一個數字」,你可以寫 `if (count)` 來表示「若 count 非零」——這很常見也很道地,不過 `if (count != 0)` 把同樣的意思說得更大聲,而大聲往往對下一位讀者更友善。
不過真假值裡藏著一個安靜的陷阱,值得誠實點名。C 的 `int` 真假對整數運作良好,但浮點數的比較可能讓你吃驚:在機器的數值系統裡,0.1 + 0.2 並不剛好等於 0.3,所以對浮點數寫 `if (a == b)`,即使數學上相等也可能為假。解法是在一個小容差範圍內比較,而不是要求完全相等。另外 C99 確實加入了真正的布林型別——引入 stdbool.h,你就有 `bool`、`true`、`false`——讓意圖更清晰,儘管底層仍是同一個 0 或 1 的整數。
迴圈,以及三元簡寫
分支讓程式在一次抉擇中走向兩條路之一;迴圈則讓它把某件事做許多次。C 提供三種。`while (cond) { ... }` 先檢查再重複,只要條件成立就繼續——適合可能執行零次的情況。`do { ... } while (cond);` 會在檢查之前先執行一次主體——適合至少必須執行一次的情況。而 `for (init; cond; step) { ... }` 把計數的三道手續——起始、測試、前進——打包在一行上,這正是它成為「做 N 次」的自然選擇的原因。三者都倚賴本篇開頭那條「非零即真」的規則。
for (int i = 0; i < 3; i++) {
printf("%d\n", i);
}
// 1. init: i = 0
// 2. test: i < 3 ? -> if false, stop
// 3. body: printf
// 4. step: i++ -> back to test
// prints 0, 1, 2 (NOT 3 -- the test fails at i == 3)有兩個關鍵字能從迴圈內部掌舵:`break` 直接跳出整個迴圈,`continue` 跳過本輪剩下的部分、直接進入下一次疊代。兩者都很適合提早離開——「一找到就停」——但塞滿這兩者的迴圈會變得難以追蹤,所以要審慎取用。C 還有 `switch`,是對單一整數值做的整潔多向分支;只要記得每個 `case` 若不以 `break` 結束,就會落穿(fall through)到下一個——而這本身就是一個經典的錯誤來源。
當抉擇小到能塞進一個運算式裡時,三元運算子 `cond ? a : b` 就值回票價:它求 cond,若真則給出 a,若假則給出 b。所以 `int max = (x > y) ? x : y;` 讀起來幾乎像英文,也省下四行的 `if`/`else`。請用它來選一個值;別濫用它在帶有副作用的動作之間做選擇,那種情形用單純的 `if` 更清楚。
誠實的險地:轉換與未定義行為
運算子看似無害,但它們坐落在 C 處理型別的方式之上,而那正是決定悄悄出錯之處。當你在比較或算術裡混用型別,C 會先執行隱式轉換。最鋒利的一刃:把有號 int 拿去和無號值比較時,會把有號的那個提升為無號,於是 `-1 < 1u` 其實是假,因為 -1 變成了一個極大的正數。寫成 `for (unsigned i = n; i >= 0; i--)` 的迴圈永遠不會結束,因為無號值永遠 >= 0。這些並不冷僻——而是天天發生的失手,也是你該把自己的條件多讀兩遍的原因。
更深的險地是未定義行為(UB),它值得現在就被直白點明,而不是日後才讓你大吃一驚。某些運算在 C 裡沒有定義的意義:有號整數溢位、除以零,或位移超過型別寬度的量。最關鍵的一點——也是初學者常被誤導的部分——是 UB 並不代表「平台碰巧做的任何事」。標準允許最佳化器假設 UB 從不發生,並由此推理。這正是為什麼一個臭蟲能在 `gcc -O0` 下藏身,卻在 `gcc -O2` 下毀壞你的資料:最佳化器深信溢位不可能發生,於是刪掉了你寫來捕捉它的那道檢查。
這一切都不該嚇退你——它該讓你變得整潔。把警告打開(`gcc -Wall -Wextra`)、為每個分支加括號、不確定時就用括號標明、測試裡寧用 == 而非 =,並且只在你真正意指「不可能為負」時才選用無號型別。養成這些習慣,流程控制就會成為程式裡最可靠的一部分。而同樣的是非思維會在最頂層再度回歸:一支 C 程式以一個數字交還給作業系統作為它的結束狀態——0 代表成功、非零代表失敗——所以連你程式最後的那個決定,也再一次,只是一個數字。