# 3 控制流 ## 3.1 完整概念讲解 控制流语句决定了程序的执行路径。C 语言提供选择结构(if-else、switch)、循环结构(for、while、do-while)和跳转语句(break、continue、goto),组合起来可以表达任意复杂的逻辑。 ### if-else / else if ```c if (condition1) { /* ... */ } else if (condition2) { /* ... */ } else { /* ... */ } ``` - 条件为非零值时进入对应分支 - `else if` 可以无限链式延伸 - `else` 捕获所有未匹配的情况 ### switch-case-break-default ```c switch (expression) { case constant1: /* ... */ break; case constant2: /* ... */ break; default: /* ... */ break; } ``` - `expression` 必须是整型(`int`、`char`、`enum` 等) - `case` 后必须是**编译时常量**(不能是变量) - 不写 `break` 会导致 fall-through(穿透到下一个 case) - `default` 可选,建议总是写上 - 多个 `case` 可共享同一段代码 ### for 循环 ```c for (init; condition; update) { /* ... */ } ``` - `init` 只在循环开始前执行一次 - `condition` 每次迭代前检查,为 0 时退出 - `update` 每次迭代后执行 - 三个表达式均可省略(死循环:`for (;;)`) ### while 循环 ```c while (condition) { /* ... */ } ``` 先判断条件,再执行循环体。条件为 0 时退出。 ### do-while 循环 ```c do { /* ... */ } while (condition); ``` 先执行循环体,再判断条件。**至少执行一次**。常用于菜单交互、输入校验等必须先执行一次的场景。 ### break / continue / goto - `break`:跳出当前循环或 switch - `continue`:跳过本次循环的剩余部分,进入下一次迭代 - `goto`:无条件跳转到标签位置 ### goto 在嵌入式错误处理中的合理使用 在内核和嵌入式代码中,`goto` 常用于**统一错误处理路径**(资源清理): ```c int func(void) { int *buf1 = NULL, *buf2 = NULL; buf1 = malloc(1024); if (!buf1) goto err_buf1; buf2 = malloc(1024); if (!buf2) goto err_buf2; /* 正常逻辑 */ free(buf2); free(buf1); return 0; err_buf2: free(buf1); err_buf1: return -1; } ``` 这种模式在 Linux 内核中大量使用(`goto` 错误处理是内核编码规范的一部分)。 ### 循环嵌套与性能 - 内层循环变量变化频繁,对 cache 不友好 - 循环展开(loop unrolling)可减少分支开销 - 避免在循环内做不必要的内存分配或系统调用 - 注意循环变量的类型,避免溢出 --- ## 3.2 核心 API/语法 ### switch 的 fall-through 特性 ```c switch (val) { case 1: case 2: case 3: printf("1, 2, or 3\n"); break; case 4: printf("4\n"); break; } ``` ### C99 for 循环的声明式初始化 ```c for (int i = 0; i < 10; i++) { /* C99: 变量声明在 for 内 */ /* ... */ } ``` --- ## 3.3 代码示例 ### 示例 1:if-else 与 switch-case ```c #include const char *grade_str(int score) { switch (score / 10) { case 10: case 9: return "A (优秀)"; case 8: return "B (良好)"; case 7: return "C (中等)"; case 6: return "D (及格)"; default: return "F (不及格)"; } } int main(void) { /* if-else 链 */ int temp = 25; printf("温度 %d°C: ", temp); if (temp < 0) { printf("结冰\n"); } else if (temp < 15) { printf("寒冷\n"); } else if (temp < 25) { printf("凉爽\n"); } else if (temp < 35) { printf("温暖\n"); } else { printf("炎热\n"); } /* switch-case */ int score = 85; printf("成绩 %d 分: %s\n", score, grade_str(score)); /* 多 case 共享代码 */ int day = 3; switch (day) { case 1: case 2: case 3: case 4: case 5: printf("星期%d: 工作日\n", day); break; case 6: case 7: printf("星期%d: 周末\n", day); break; } return 0; } ``` 编译命令: ```bash gcc -std=c99 -Wall -Wextra -o control1 control1.c ./control1 ``` ### 示例 2:三种循环对比 ```c #include int main(void) { /* for 循环:已知迭代次数 */ printf("=== for 循环 ===\n"); int sum = 0; for (int i = 1; i <= 100; i++) { sum += i; } printf("1+2+...+100 = %d\n", sum); /* while 循环:条件驱动 */ printf("\n=== while 循环 ===\n"); int n = 1024; int count = 0; while (n > 0) { n >>= 1; count++; } printf("1024 需要右移 %d 次变为 0\n", count); /* do-while:至少执行一次 */ printf("\n=== do-while 循环 ===\n"); int value; do { printf("请输入正数 (0 退出): "); scanf("%d", &value); if (value > 0) { printf("你输入了 %d\n", value); } } while (value != 0); /* break 与 continue */ printf("\n=== break 与 continue ===\n"); printf("1-20 中不能被 3 整除的数: "); for (int i = 1; i <= 20; i++) { if (i % 3 == 0) { continue; /* 跳过 3 的倍数 */ } printf("%d ", i); } printf("\n"); printf("寻找第一个大于 100 的完全平方数: "); for (int i = 1; ; i++) { int sq = i * i; if (sq > 100) { printf("%d (i=%d)\n", sq, i); break; } } return 0; } ``` 编译命令: ```bash gcc -std=c99 -Wall -Wextra -o control2 control2.c ./control2 ``` ### 示例 3:goto 错误处理模式 ```c #include #include #include typedef struct { char *name; int *data; size_t size; } Resource; int init_resource(Resource *r, const char *name, size_t size) { r->name = NULL; r->data = NULL; r->size = 0; r->name = malloc(strlen(name) + 1); if (!r->name) { fprintf(stderr, "Failed to allocate name\n"); goto err_name; } strcpy(r->name, name); r->data = malloc(size * sizeof(int)); if (!r->data) { fprintf(stderr, "Failed to allocate data\n"); goto err_data; } r->size = size; memset(r->data, 0, size * sizeof(int)); printf("Resource '%s' initialized (size=%zu)\n", r->name, r->size); return 0; err_data: free(r->name); r->name = NULL; err_name: return -1; } void destroy_resource(Resource *r) { free(r->data); free(r->name); r->data = NULL; r->name = NULL; r->size = 0; printf("Resource destroyed\n"); } int main(void) { Resource res; if (init_resource(&res, "sensor_buf", 256) != 0) { fprintf(stderr, "Initialization failed\n"); return 1; } /* 使用资源 */ res.data[0] = 42; printf("res.data[0] = %d\n", res.data[0]); destroy_resource(&res); return 0; } ``` 编译命令: ```bash gcc -std=c99 -Wall -Wextra -o goto_err goto_err.c ./goto_err ``` --- ## 3.4 注意事项与易错点 1. **if 后面多余分号**: ```c if (x == 5); // 空语句,if 什么也没做 { printf("ok"); } // 这个块总是执行 ``` 2. **switch 漏写 break**:忘记 break 导致 fall-through 是最常见的 switch bug。C17 引入 `[[fallthrough]]` 属性(C23 为标准关键字)可显式标注意图。 3. **for 循环的悬挂分号**: ```c for (i = 0; i < 10; i++); // 空循环体 { sum += i; } // 这个块只执行一次 ``` 4. **do-while 的分号**:`do { ... } while (condition);` 末尾必须有分号,这是语法要求。 5. **goto 标签作用域**:标签的作用域是整个函数,不同函数可以有同名标签,但同一函数内不能重复。 6. **循环变量溢出**:`unsigned char i` 从 0 到 255,`i++` 后回绕到 0,条件 `i < 256` 永远为真,造成死循环。 7. **switch 不支持浮点数和字符串**:`switch` 的表达式必须是整型或枚举类型。 --- ## 3.5 面试要点 **Q1:while(1) 和 for(;;) 有区别吗?** A:在功能上没有区别,都是死循环。但 `for(;;)` 在某些编译器上可能生成略微不同的代码(`while(1)` 需要对常量 1 求值,`for(;;)` 完全省略条件检查)。实际中两者均可,风格偏好问题。更专业的写法是 `for (;;) { /* ... */ }`。 **Q2:goto 为什么在嵌入式/内核代码中仍然被使用?** A:在内核和嵌入式开发中,`goto` 用于统一的错误清理路径。当函数需要按顺序分配多个资源时,使用 `goto` 可以避免多层嵌套的 if-else 或重复的清理代码。Linux 内核编码风格明确允许并推荐这种用法。但不应滥用 `goto` 做任意跳转。 **Q3:for 循环的三个表达式可以是任意表达式吗?** A:是的。init、condition、update 都可以是任意表达式,包括函数调用、赋值、逗号表达式等。甚至可以修改循环变量本身。但为了可读性,通常只做简单操作。 **Q4:switch-case 中 case 后面能用变量吗?** A:不能。`case` 后必须是**编译时常量表达式**(整型常量表达式),不能是 `const` 变量(C 语言的 `const` 不是真正意义上的常量)、变量或字符串。这是 C 语言 `switch` 的硬性限制。 **Q5:如何避免循环嵌套中的性能问题?** A:(1) 减少内层循环的计算量,将不变的计算提到外层;(2) 注意数据局部性,按行优先(C 语言默认)遍历二维数组;(3) 循环展开减少分支预测失败;(4) 避免在循环内做内存分配/释放;(5) 利用缓存行大小优化步长。