學習目標
- 熟練使用算術、關係、邏輯運算子,理解各自的運算元型別與結果型別。
- 理解隱式型別轉換(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 | , |
左→右 |
三個特別容易中招的地方:
- 關係運算子優先於位元運算子:
x & 1 == 0其實是x & (1 == 0)! - 優先順序 ≠ 求值順序:
f() + g()中誰先被呼叫沒有規定。 - 除了
&&、||、?:、,之外,運算元的求值順序都是未指定的。
範例程式
| 檔案 | 內容 | 編譯與執行 |
|---|---|---|
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。
練習題
- 基本題:宣告
int a = 17, b = 5;,印出a+b、a-b、a*b、a/b、a%b五個結果,並手算驗證。 - 閏年判斷:寫一個運算式判斷
year是否為閏年(能被 4 整除但不能被 100 整除,或能被 400 整除),用&&、||組合,對 1900、2000、2024、2025 四個年份印出結果。 - 平均值:三個
int成績 88、73、95,分別用「整數除法」與「正確的浮點除法」計算平均並印出,觀察差異。 - 三元運算子:用
?:寫出:(a) 兩數取大值;(b) 判斷奇偶印出"odd"/"even";(c) 巢狀?:三數取最大值(然後想想是否if/else更可讀)。 - 優先順序偵探:不執行程式,先手算
2 + 3 * 4 % 5、10 > 9 > 8、1 << 2 + 1的值,再寫程式驗證。特別解釋10 > 9 > 8為什麼是 1 而不是你以為的答案。 - 挑戰題(安全遞增):寫程式示範
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