學習目標

  • 熟練使用算術、關係、邏輯運算子,理解各自的運算元型別與結果型別。
  • 理解隱式型別轉換(integer promotion、usual arithmetic conversions)與強制轉型(cast)。
  • 分清楚 i++ 與 ++i 的差別,知道什麼時候該用哪個、什麼寫法是未定義行為。
  • 會使用指定運算子(+=、-=、*=…)與條件運算子 ?:。
  • 熟悉運算子優先順序表,並理解「優先順序」與「求值順序」是兩回事。

對照 K&R 章節

  • K&R §2.5 Arithmetic Operators
  • K&R §2.6 Relational and Logical Operators
  • K&R §2.7 Type Conversions、§2.8 Increment and Decrement Operators
  • K&R §2.9 Bitwise Operators(本單元僅預告)
  • K&R §2.10 Assignment Operators and Expressions
  • K&R §2.11 Conditional Expressions、§2.12 Precedence and Order of Evaluation

觀念講解

1. 算術運算子

int a = 7, b = 2;
a + b;   /* 9  加 */
a - b;   /* 5  減 */
a * b;   /* 14 乘 */
a / b;   /* 3  整數除法:截斷小數 */
a % b;   /* 1  取餘數:只能用於整數 */

重點:

  • 整數除法向零截斷:7 / 2 == 3、-7 / 2 == -3(C99 起明確定義)。
  • % 的結果符號跟隨被除數:-7 % 2 == -1。
  • % 不能用在浮點數;浮點取餘要用 <math.h> 的 fmod。

2. 關係與邏輯運算子

類別 運算子 說明
關係 > >= < <= 結果是 int:真為 1、假為 0
相等 == != 注意 == 不是 =!
邏輯 &&(且)\|\|(或)!(非) 短路求值

C 沒有布林字面值的傳統:任何非零值都是「真」,零是「假」(C99 之後有 <stdbool.h>)。

短路求值(short-circuit):&& 左邊為假就不看右邊;|| 左邊為真就不看右邊。 這是保證的行為,常用來防呆:

if (n != 0 && total / n > 10) { ... }   /* n 為 0 時不會執行除法 */

3. 型別轉換

隱式轉換發生在混合型別運算時,規則簡述:「窄的往寬的靠」:

int i = 5;
double d = 2.0;
double r = i / d;      /* i 先轉成 5.0,做浮點除法 → 2.5 */
  • char 與 short 參與運算時先升為 int(integer promotion)。
  • int 與 double 混算 → int 轉 double。
  • 有號與無號混算 → 有號轉無號(危險!見陷阱)。
  • 把寬型別指定給窄型別會「截斷」,-Wall 之下 double → int 通常需要明說。

強制轉型(cast):(型別)運算式,明確要求轉換:

int total = 7, count = 2;
double avg = (double)total / count;   /* 7.0 / 2 = 3.5,而不是 3 */
int truncated = (int)3.99;            /* 3:截斷不是四捨五入 */

4. 遞增遞減:i++ 與 ++i

int i = 5;
int a = i++;    /* 後置:a = 5(先用舊值),之後 i 變 6 */

int j = 5;
int b = ++j;    /* 前置:j 先變 6,b = 6 */
  • 單獨成一行時(i++; vs ++i;)兩者效果完全相同,選一種風格即可。
  • 絕對禁止在同一運算式對同一變數又讀又寫:i = i++;、a[i] = i++;、 printf("%d %d", i, i++); 都是未定義行為。

5. 指定運算子

x += 5;     /* x = x + 5 */
x -= 2;     /* x = x - 2 */
x *= 3;     /* x = x * 3 */
x /= 4;     /* x = x / 4 */
x %= 7;     /* x = x % 7 */

好處:更短、變數名只寫一次(不易打錯)、對複雜的左值只求值一次。 注意 x *= y + 1 是 x = x * (y + 1),右邊整體是一個運算元。

指定本身也是運算式,有值(指定後的值),所以 K&R 風格常見:

while ((c = getchar()) != EOF) { ... }

6. 條件運算子 ?:

三元運算子 條件 ? 值A : 值B——條件成立取 A、否則取 B:

int max = (a > b) ? a : b;
printf("You have %d item%s\n", n, n == 1 ? "" : "s");

適合「二選一的值」;若兩個分支是複雜動作,仍應用 if/else 保持可讀。

7. 位元運算子(預告)

C 提供 &(AND)、|(OR)、^(XOR)、~(NOT)、<<、>> 六個位元運算子, 用於旗標、遮罩、硬體暫存器操作。高階單元會深入,此處只需知道:

unsigned flags = 0x0F & 0x03;   /* 位元 AND → 0x03 */

小心 &/|(位元)與 &&/||(邏輯)是完全不同的運算子。

8. 優先順序與求值順序

常用優先順序表(由高到低,同列同級):

優先 運算子 結合性
1 () [] -> . 後置++ -- 左→右
2 ! ~ 前置++ -- 一元+ - * & (cast) sizeof 右→左
3 * / % 左→右
4 + - 左→右
5 << >> 左→右
6 < <= > >= 左→右
7 == != 左→右
8 & 左→右
9 ^ 左→右
10 \| 左→右
11 && 左→右
12 \|\| 左→右
13 ?: 右→左
14 = += -= 等 右→左
15 , 左→右

三個特別容易中招的地方:

  1. 關係運算子優先於位元運算子:x & 1 == 0 其實是 x & (1 == 0)!
  2. 優先順序 ≠ 求值順序:f() + g() 中誰先被呼叫沒有規定。
  3. 除了 &&、||、?:、, 之外,運算元的求值順序都是未指定的。

範例程式

檔案 內容 編譯與執行
01_arithmetic_relational.c 算術/關係/邏輯運算子總覽、整數除法與取餘、短路求值示範 cc -std=c11 -Wall -Wextra -o 01_arithmetic_relational 01_arithmetic_relational.c && ./01_arithmetic_relational
02_conversion_cast.c 隱式轉換、cast、截斷、char 升 int、有號無號混用示範 cc -std=c11 -Wall -Wextra -o 02_conversion_cast 02_conversion_cast.c && ./02_conversion_cast
03_incdec_assign_ternary.c i++/++i 差異、複合指定、?:、指定運算式的值 cc -std=c11 -Wall -Wextra -o 03_incdec_assign_ternary 03_incdec_assign_ternary.c && ./03_incdec_assign_ternary
04_precedence_pitfalls.c 優先順序陷阱實測:== vs =、& vs &&、括號的重要性 cc -std=c11 -Wall -Wextra -o 04_precedence_pitfalls 04_precedence_pitfalls.c && ./04_precedence_pitfalls

常見錯誤與陷阱

陷阱 1:把 == 寫成 =

if (x = 5) { ... }      /* 恆真!x 被指定成 5,運算式值為 5(非零) */

修正:if (x == 5)。開啟 -Wall 會警告;防禦性寫法可把常數放左邊 if (5 == x)。

陷阱 2:整數除法混進浮點公式

double avg = (a + b) / 2;      /* a、b 是 int:先整數除法再轉 double */

修正:double avg = (a + b) / 2.0; 或 (double)(a + b) / 2;。

陷阱 3:同一運算式對同一變數又讀又寫

i = i++ + 1;            /* 未定義行為 */
a[i] = i++;             /* 未定義行為 */

修正:拆成兩行,一行只改一次:int old = i; i++; a[old] = old;(依實際語意調整)。

陷阱 4:位元運算與比較的優先順序

if (flags & MASK == 0) { ... }   /* 實際是 flags & (MASK == 0) */

修正:if ((flags & MASK) == 0)。位元運算永遠加括號是好習慣。

陷阱 5:對負數取餘的符號

int wrap = -1 % 12;      /* 是 -1,不是 11! */

需要「永遠非負」的餘數(例如環形索引)時:((x % n) + n) % n。

練習題

  1. 基本題:宣告 int a = 17, b = 5;,印出 a+b、a-b、a*b、a/b、a%b 五個結果,並手算驗證。
  2. 閏年判斷:寫一個運算式判斷 year 是否為閏年(能被 4 整除但不能被 100 整除,或能被 400 整除),用 &&、|| 組合,對 1900、2000、2024、2025 四個年份印出結果。
  3. 平均值:三個 int 成績 88、73、95,分別用「整數除法」與「正確的浮點除法」計算平均並印出,觀察差異。
  4. 三元運算子:用 ?: 寫出:(a) 兩數取大值;(b) 判斷奇偶印出 "odd"/"even";(c) 巢狀 ?: 三數取最大值(然後想想是否 if/else 更可讀)。
  5. 優先順序偵探:不執行程式,先手算 2 + 3 * 4 % 5、10 > 9 > 8、1 << 2 + 1 的值,再寫程式驗證。特別解釋 10 > 9 > 8 為什麼是 1 而不是你以為的答案。
  6. 挑戰題(安全遞增):寫程式示範 i++ 與 ++i 在 (a) 單獨敘述、(b) 指定右邊、(c) 函式引數中的行為差異;並舉出一個未定義行為的寫法(用註解說明,不要真的執行依賴其結果)。

延伸閱讀

  • K&R 2/e §2.12 表 2-1(完整優先順序表,建議影印貼在螢幕旁)
  • C11 標準 §6.5(運算式)——「sequence point / sequenced before」的正式定義
  • 《Expert C Programming》第 2 章:優先順序設計的歷史包袱
  • comp.lang.c FAQ 3.x 節:關於求值順序的經典問答
  • 下一單元預告:控制流程——if/else 與 switch