#include <stdio.h>int main() { int score = 85; // 形式1: 单独 if if (score >= 90) { printf("优秀\n"); } // 形式2: if / else if (score >= 60) { printf("及格\n"); } else { printf("不及格\n"); } // 形式3: if / else if / else 链 if (score >= 90) { printf("A 等级\n"); } else if (score >= 80) { printf("B 等级\n"); } else if (score >= 70) { printf("C 等级\n"); } else if (score >= 60) { printf("D 等级\n"); } else { printf("F 等级(不及格)\n"); } // 注意:条件会按顺序检查,首个匹配的被执行 return 0;}
1.2 单语句 if 与花括号
#include <stdio.h>int main() { int x = 10; // 如果 if 体只有一条语句,花括号可以省略 if (x > 0) printf("正数\n"); // 但这是常见的 bug 来源! if (x > 0) printf("第一行\n"); printf("第二行\n"); // 这行不属于 if!始终会被执行! // 推荐做法:始终使用花括号 if (x > 0) { printf("始终用花括号\n"); printf("避免 bug\n"); } // 悬空 else 问题 int a = 5, b = 10; // 这个 else 属于哪个 if? if (a > 0) if (b > 0) printf("a 和 b 都是正数\n"); else printf("a 不是正数\n"); // 注意!这个 else 属于内层 if (b > 0) // C 语言规定:else 与最近的未匹配 if 配对 // 用花括号消除歧义 if (a > 0) { if (b > 0) { printf("都是正数\n"); } } else { printf("a 不是正数\n"); } return 0;}
1.3 条件表达式中的陷阱
#include <stdio.h>int main() { int x = 0; // 陷阱1:赋值 vs 比较(C 语言最著名的 bug) if (x = 10) { // 本意是 x == 10 printf("x = %d(条件为真,因为赋值返回 10,10 非 0)\n", x); } // 防御措施:常量放左边 if (10 == x) { // 如果写成 10 = x,编译错误 printf("安全写法\n"); } // 陷阱2:浮点比较 double d = 0.1 + 0.2; if (d == 0.3) { // 可能为假! printf("0.1+0.2 == 0.3\n"); } // 浮点比较应使用 epsilon if ((d - 0.3) < 0.000001 && (0.3 - d) < 0.000001) { printf("约等于 0.3\n"); } // 陷阱3:signed/unsigned 比较 int s = -1; unsigned int u = 1; if (s < u) { // 通常为假!(-1 → 4294967295 unsigned) printf("s < u\n"); } else { printf("s > u(因为 -1 变为很大的 unsigned)\n"); } return 0;}
1.4 汇编视角:if 如何变成跳转
// C 代码if (x >= 10) { y = 1;} else { y = 0;}
对应的 x86-64 汇编(AT&T 语法,简化):
cmpl $9, -4(%rbp) ; 比较 x 和 9(注意:>= 10 等价于 > 9) jle .L_else ; 如果 x <= 9 就跳转到 else movl $1, -8(%rbp) ; y = 1 jmp .L_end.L_else: movl $0, -8(%rbp) ; y = 0.L_end:
cmp 指令本质是执行减法但不保存结果,只设置标志寄存器。jle(Jump if Less or Equal)检查标志位决定跳转。汇编层面:条件就是跳转。详见 。
小节练习
选择题 1
以下代码的 else 属于哪个 if?
if (a) if (b) c=1; else d=2;
A. 外层 if (a)
B. 内层 if (b)
C. 编译错误
D. 取决于编译器
点击查看答案 解析: C 语言规定 else 与最近的未匹配的 if 配对。"悬空 else"问题——代码的缩进与实际语义不一致时的经典陷阱。
正确答案: B
判断题 1
C 语言的 if 条件必须是一个布尔类型 (_Bool) 的表达式。 ( )
正确
错误
点击查看答案 解析: C 语言 if 的条件可以是任何标量表达式——值为 0 表示假,非 0 表示真。这是 C 灵活性的体现,也是很多隐性 bug 的来源(如 if (x = 10) 始终为真)。
答案: 错误
第二节:switch/case 语句
2.1 基本 switch 语法
#include <stdio.h>int main() { int day = 3; switch (day) { case 1: printf("星期一\n"); break; case 2: printf("星期二\n"); break; case 3: printf("星期三\n"); break; case 4: printf("星期四\n"); break; case 5: printf("星期五\n"); break; case 6: case 7: printf("周末!\n"); break; default: printf("无效的天数\n"); break; } return 0;}
2.2 Fall-through(贯穿)特性
#include <stdio.h>int main() { // switch 的 case 没有自动 break // 执行完一个 case 后会"贯穿"到下一个 case int num = 2; switch (num) { case 1: printf("1 "); // 没有 break!会继续执行 case 2 case 2: printf("2 "); // 继续贯穿 case 3: printf("3\n"); break; // 终于 break case 4: printf("4\n"); break; } // 输出:2 3 // 利用 fall-through 实现多个值共享代码 int month = 6; switch (month) { case 12: case 1: case 2: printf("冬季\n"); break; case 3: case 4: case 5: printf("春季\n"); break; case 6: case 7: case 8: printf("夏季\n"); break; case 9: case 10: case 11: printf("秋季\n"); break; default: printf("无效月份\n"); break; } return 0;}
2.3 switch 与 if/else 的底层对比
#include <stdio.h>int main() { int x = 3; // switch 适用于"等值比较"场景 // 编译器可能将其优化为跳转表(jump table) switch (x) { case 1: printf("一"); break; case 2: printf("二"); break; case 3: printf("三"); break; case 4: printf("四"); break; case 5: printf("五"); break; default: printf("?"); } printf("\n"); // 等价的 if/else 链(更灵活,但可能更慢) if (x == 1) { printf("一"); } else if (x == 2) { printf("二"); } else if (x == 3) { printf("三"); } else if (x == 4) { printf("四"); } else if (x == 5) { printf("五"); } else { printf("?"); } printf("\n"); return 0;}
底层原理:当 case 值连续且密集时(如 1,2,3,4,5),编译器将 switch 编译为跳转表:
graph TD
A[switch x] --> B{x 在范围内?}
B -->|是| C[跳转表 jt x]
B -->|否| H[default]
C --> D[jt#1# → 分支1]
C --> E[jt#2# → 分支2]
C --> F[jt#3# → 分支3]
C --> G[...]
跳转表是一个地址数组,编译器直接用 x 作为索引查找目标地址,O(1) 跳转。这是 switch 相比多层 if/else 链(O(n))在性能上的核心优势。
2.4 switch 的限制和注意事项
#include <stdio.h>int main() { // 限制1:switch 的表达式必须是整数类型 int x = 10; // 以下不行: // double d = 3.14; // switch (d) { ... } // 编译错误!浮点数不行 // char s[] = "hello"; // switch (s) { ... } // 编译错误!字符串不行 // 限制2:case 值必须是编译时常量 const int a = 1; switch (x) { case 1: // OK:字面量 break; // case a: // 编译错误!a 不是编译时常量表达式(C 语言中) // break; case 5 + 3: // OK:常量表达式 break; case 'A': // OK:字符常量是整数 break; } // 限制3:case 值不能重复 // case 1: // 编译错误!与上面的 case 1 重复 // 限制4:可以在 switch 中定义变量,但需用花括号 switch (x) { case 1: { int local = 42; // OK:在花括号内 printf("%d\n", local); break; } case 2: // int local = 100; // 编译错误!需要花括号 break; } return 0;}
#include <stdio.h>#include <stddef.h>int main() { // 安全检查模式1:先检查指针再解引用 int arr[] = {1, 2, 3}; int *p = arr; if (p != NULL && *p > 0) { printf("*p = %d 是正数\n", *p); } // 如果把 *p > 0 放在前面,p 为 NULL 时会崩溃 // if (*p > 0 && p != NULL) ← 危险!段错误 // 安全检查模式2:先检查索引再访问数组 int i = 5; if (i >= 0 && i < 3 && arr[i] > 0) { // i=5 超范围 printf("arr[%d] > 0\n", i); } else { printf("索引无效或值非正\n"); } // 安全检查模式3:先检查除数 int a = 10, b = 0; if (b != 0 && a / b > 1) { printf("商大于 1\n"); } else { printf("除数为零,跳过除法\n"); } return 0;}
3.3 条件表达式作为值
#include <stdio.h>int main() { // 条件表达式的结果可以直接用于赋值 int a = 10, b = 20; int max = (a > b) ? a : b; printf("max = %d\n", max); // 可以在数组下标中使用 int arr[] = {100, 200}; int idx = (a > b); // 0 或 1 printf("arr[idx] = %d\n", arr[idx]); // 可以在计算中使用 int discount = (a > 100) ? 20 : 0; printf("折扣: %d%%\n", discount); return 0;}
#include <stdio.h>int main() { int a, b, c; printf("输入三边长度: "); scanf("%d %d %d", &a, &b, &c); // 三角形存在条件:任意两边之和大于第三边 if (a + b <= c || a + c <= b || b + c <= a) { printf("不能构成三角形\n"); } else if (a == b && b == c) { printf("等边三角形\n"); } else if (a == b || b == c || a == c) { if (a * a + b * b == c * c || a * a + c * c == b * b || b * b + c * c == a * a) { printf("等腰直角三角形\n"); } else { printf("等腰三角形\n"); } } else if (a * a + b * b == c * c || a * a + c * c == b * b || b * b + c * c == a * a) { printf("直角三角形\n"); } else { printf("普通三角形\n"); } return 0;}
4.4 条件运算符的链式使用
#include <stdio.h>int main() { int num = 42; // 链式三元(可读性差,不推荐嵌套过多) const char *type = num > 0 ? "正数" : num < 0 ? "负数" : "零"; printf("分类: %s\n", type); // 三元运算符作为函数参数 printf("%d 是%s\n", num, num % 2 == 0 ? "偶数" : "奇数"); // 注意:三元运算符的两个分支类型必须兼容 int a = 10; double result = (a > 5) ? 1.5 : a; // 返回 double(a 被提升) printf("result = %f\n", result); return 0;}
// C 代码int max(int a, int b) { if (a > b) { return a; } else { return b; }}
对应的 x86-64 汇编(简化版):
max: cmpl %esi, %edi ; 比较 a (%edi) 和 b (%esi) jle .L_return_b ; 如果 a <= b,跳转 movl %edi, %eax ; eax = a (return a) ret.L_return_b: movl %esi, %eax ; eax = b (return b) ret
编译器优化:编译器有时使用条件传送(cmov)指令避免分支,提高效率:
; 优化版本(使用 cmovge — Conditional MOVe if Greater or Equal)max_optimized: movl %edi, %eax ; eax = a (先假设 a 是答案) cmpl %edi, %esi ; 比较 b 和 a cmovg %esi, %eax ; 如果 b > a,eax = b ret
分支预测失败是 CPU 性能的重大瓶颈。条件传送指令(cmov)消除了分支,现代编译器会尝试将简单的 if/else 优化为条件传送。详见 。
5.2 switch 的跳转表
// C 代码int get_priority(char c) { switch (c) { case 'A': return 1; case 'B': return 2; case 'C': return 3; default: return 0; }}