Week 03: 控制流语句
掌握 C 语言的条件语句、循环结构、跳转语句与控制流的底层实现.
1. 条件语句
1.1 if 语句
int age = 18;
if (age >= 18) {
printf("成年人\n");
}
// 多分支
if (age < 12) {
printf("儿童\n");
} else if (age < 18) {
printf("青少年\n");
} else {
printf("成年人\n");
}1.2 单语句与代码块
// 单语句可以省略花括号
if (x > 0)
printf("正数\n");
// 但推荐始终使用花括号
if (x > 0) {
printf("正数\n");
}
// 避免悬空 else 问题
if (a)
if (b)
printf("a && b\n");
else
printf("这个 else 属于谁?\n"); // 属于最近的 if (b)
// 推荐写法
if (a) {
if (b) {
printf("a && b\n");
}
} else {
printf("!a\n");
}1.3 条件表达式 (三元运算符)
int max = (a > b) ? a : b;
// 等价于
int max;
if (a > b) {
max = a;
} else {
max = b;
}
// 嵌套 (可读性差, 谨慎使用)
int sign = (x > 0) ? 1 : (x < 0) ? -1 : 0;1.4 switch 语句
int day = 3;
switch (day) {
case 1:
printf("星期一\n");
break;
case 2:
printf("星期二\n");
break;
case 3:
printf("星期三\n");
break;
default:
printf("其他\n");
break;
}1.5 switch 的 fall-through
int month = 2;
int days;
switch (month) {
case 1: case 3: case 5: case 7:
case 8: case 10: case 12:
days = 31;
break;
case 4: case 6: case 9: case 11:
days = 30;
break;
case 2:
days = 28; // 简化, 未考虑闰年
break;
default:
days = -1;
break;
}
// 有意的 fall-through 应该注释
switch (state) {
case STATE_A:
do_a();
// fall through
case STATE_B:
do_b();
break;
}1.6 switch 的限制
// case 必须是整数常量表达式
switch (x) {
case 1: // OK
case 1 + 1: // OK (编译期计算)
// case y: // 错误: 不能是变量
// case 1.5: // 错误: 不能是浮点数
}
// 不能在 case 中声明变量 (除非用代码块)
switch (x) {
case 1: {
int temp = 10; // OK: 在代码块中
printf("%d\n", temp);
break;
}
}2. 循环结构
2.1 while 循环
int i = 0;
while (i < 5) {
printf("%d\n", i);
i++;
}2.2 do-while 循环
int i = 0;
do {
printf("%d\n", i);
i++;
} while (i < 5);
// 至少执行一次
int x = 10;
do {
printf("执行一次\n");
} while (x < 0); // 条件为假, 但已执行一次2.3 for 循环
for (int i = 0; i < 5; i++) {
printf("%d\n", i);
}
// 等价的 while
int i = 0;
while (i < 5) {
printf("%d\n", i);
i++;
}2.4 for 循环变体
// 省略部分
int i = 0;
for (; i < 5; i++) { } // 省略初始化
for (int i = 0; ; i++) { // 省略条件 (无限循环)
if (i >= 5) break;
}
for (int i = 0; i < 5; ) { // 省略更新
i++;
}
// 无限循环
for (;;) {
// ...
}
while (1) {
// ...
}
// 多变量
for (int i = 0, j = 10; i < j; i++, j--) {
printf("i=%d, j=%d\n", i, j);
}2.5 循环计数器
// 推荐: 使用 size_t 遍历数组
size_t len = sizeof(arr) / sizeof(arr[0]);
for (size_t i = 0; i < len; i++) {
printf("%d\n", arr[i]);
}
// 倒序遍历注意事项
size_t len = 5;
// 错误: size_t 是无符号的, i >= 0 永远为真
// for (size_t i = len - 1; i >= 0; i--) { }
// 正确写法
for (size_t i = len; i > 0; i--) {
printf("%zu\n", i - 1); // 实际索引是 i - 1
}
// 或者使用 ssize_t
for (ssize_t i = (ssize_t)len - 1; i >= 0; i--) {
printf("%zd\n", i);
}3. 跳转语句
3.1 break
// 跳出最近的循环或 switch
for (int i = 0; i < 10; i++) {
if (i == 5) {
break; // 跳出 for 循环
}
printf("%d\n", i); // 输出 0-4
}
// 嵌套循环中只跳出一层
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (j == 1) {
break; // 只跳出内层循环
}
printf("(%d, %d)\n", i, j);
}
}3.2 continue
// 跳过本次迭代, 继续下一次
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
continue; // 跳过偶数
}
printf("%d\n", i); // 输出 1, 3, 5, 7, 9
}3.3 goto
// 跳转到标签
if (error) {
goto cleanup;
}
// 正常代码
// ...
cleanup:
free(ptr);
close(fd);
return result;3.4 goto 的合理使用
goto 在错误处理中的价值:
int process_file(const char *filename) {
FILE *file = NULL;
char *buffer = NULL;
int result = -1;
file = fopen(filename, "r");
if (file == NULL) {
goto cleanup;
}
buffer = malloc(1024);
if (buffer == NULL) {
goto cleanup;
}
if (fread(buffer, 1, 1024, file) == 0) {
goto cleanup;
}
// 处理数据
result = 0;
cleanup:
if (buffer) free(buffer);
if (file) fclose(file);
return result;
}Linux 内核常用此模式进行错误处理.
3.5 goto 的限制
// 不能跳过变量初始化 (C99 VLA)
// goto label; // 错误
// int arr[n];
// label:
// 不能跳入代码块
// goto inside;
// {
// int x = 10;
// inside: // 错误: x 未初始化
// printf("%d\n", x);
// }4. 控制流的底层实现
4.1 if 的汇编实现
int x = 5;
if (x > 0) {
printf("positive\n");
}对应的伪汇编:
cmp x, 0 ; 比较 x 和 0
jle skip ; 如果 x <= 0, 跳过
call printf ; 调用 printf
skip:
; 继续执行4.2 for 的汇编实现
for (int i = 0; i < 10; i++) {
printf("%d\n", i);
}对应的伪汇编:
mov i, 0 ; 初始化 i = 0
loop_start:
cmp i, 10 ; 比较 i 和 10
jge loop_end ; 如果 i >= 10, 跳出
call printf ; 循环体
inc i ; i++
jmp loop_start ; 无条件跳转到循环开始
loop_end:
; 继续执行4.3 switch 的跳转表
对于连续的 case 值, 编译器可能使用跳转表:
switch (x) {
case 0: do_0(); break;
case 1: do_1(); break;
case 2: do_2(); break;
case 3: do_3(); break;
}编译器生成:
; 跳转表
jump_table:
.quad case_0
.quad case_1
.quad case_2
.quad case_3
; 使用跳转表
cmp x, 3
ja default ; 如果 x > 3, 跳到 default
jmp [jump_table + x*8] ; 直接跳转跳转表使得 O(n) 的 if-else 链变成 O(1) 的直接跳转.
5. 真值与布尔表达式
5.1 C 的真值规则
- 0 为假
- 非 0 为真 (包括负数)
if (0) {
// 不执行
}
if (1) {
// 执行
}
if (-1) {
// 执行! 负数也是真
}
if (3.14) {
// 执行! 非零浮点数也是真
}
if (NULL) {
// 不执行 (NULL 定义为 0)
}5.2 比较运算的结果
int a = (5 > 3); // 1
int b = (5 < 3); // 0
int c = (5 == 5); // 1
// 注意: 结果是 int, 不是 bool
printf("%d\n", (5 > 3)); // 15.3 常见陷阱
// 陷阱 1: 赋值 vs 比较
int x = 5;
if (x = 0) { // 赋值! x 变为 0, 条件为假
// 不执行
}
// 推荐: 常量放左边 (Yoda 条件)
if (0 == x) { } // 如果写成 = 会编译错误
// 陷阱 2: 浮点比较
if (0.1 + 0.2 == 0.3) { // 可能为假!
// ...
}
// 陷阱 3: 字符串比较
char *s1 = "hello";
char *s2 = "hello";
if (s1 == s2) { // 比较指针, 不是内容!
// 可能为真 (字符串字面量可能共享)
}
// 正确: 使用 strcmp
if (strcmp(s1, s2) == 0) { }6. 循环优化
6.1 循环展开
// 原始循环
for (int i = 0; i < 100; i++) {
arr[i] = i * 2;
}
// 手动展开 (4 倍)
for (int i = 0; i < 100; i += 4) {
arr[i] = i * 2;
arr[i + 1] = (i + 1) * 2;
arr[i + 2] = (i + 2) * 2;
arr[i + 3] = (i + 3) * 2;
}编译器通常会自动进行循环展开 (使用 -O2 或更高).
6.2 循环不变代码外提
// 优化前
for (int i = 0; i < n; i++) {
arr[i] = strlen(s); // strlen 每次都计算
}
// 优化后
int len = strlen(s);
for (int i = 0; i < n; i++) {
arr[i] = len;
}6.3 减少循环开销
// 优化前: 每次计算边界
for (int i = 0; i < strlen(s); i++) { } // O(n^2)!
// 优化后: 缓存边界
int len = strlen(s);
for (int i = 0; i < len; i++) { } // O(n)7. 练习
7.1 FizzBuzz
从 1 到 100, 3 的倍数输出 "Fizz", 5 的倍数输出 "Buzz", 15 的倍数输出 "FizzBuzz".
7.2 素数判断
编写函数判断一个数是否为素数.
7.3 错误处理
使用 goto 实现一个多步骤资源分配函数的错误处理.
8. 思考题
- switch 比 if-else 链快在哪里?
- 为什么不建议在循环中使用 goto?
- continue 在 for 和 while 中的行为有区别吗?
- 如何跳出多层嵌套循环?
- 编译器如何优化循环?
9. 本周小结
- 条件语句: if/else, switch/case, 条件表达式.
- 循环结构: while, do-while, for.
- 跳转语句: break, continue, goto.
- 真值规则: 0 为假, 非 0 为真.
- 底层实现: 条件跳转, 跳转表.
- 循环优化: 展开, 外提, 缓存边界.
控制流是程序的骨架. 理解其底层实现, 有助于写出更高效的代码.