第3章 控制流
程序的逻辑结构
一、导读
1.1 本章学习目标
第3章是C语言的"逻辑引擎"。前两章我们学习了数据类型和运算符——这些是程序的"词汇"和"语法"。而控制流语句则赋予程序判断和重复的能力,让程序能够根据不同的条件执行不同的操作,或者反复执行某些操作直到满足终止条件。
本章的核心学习目标包括:
- 掌握条件语句:
if-else和switch的使用方法,以及嵌套条件语句的组织方式。 - 掌握循环语句:
while、do-while和for三种循环的特点和适用场景。 - 理解循环控制:
break和continue如何改变循环的执行流程。 - 掌握 `switch` 语句:多路分支选择的实现方式,以及
case穿透的特性。 - 理解 `goto` 语句:为什么它备受争议?在什么情况下可以合理使用?
- 学会设计清晰的程序逻辑:如何选择最合适的控制结构来表达算法。
1.2 为什么本章如此重要
控制流是算法的核心。一个程序的价值不在于它能存储多少数据,而在于它能做出多少判断、能执行多少次计算。没有控制流,程序就只是一个从上到下执行的线性脚本,无法处理任何复杂的逻辑。
K&R 在第三章的编排非常精妙:先讲 if-else 和 switch(条件判断),再讲 while、for、do-while(循环),最后讲 break、continue、goto(流程控制)。这种编排体现了结构化编程的核心思想:任何算法都可以用顺序、选择、循环三种基本结构来表达。
掌握控制流,你就能将任何算法转化为C代码。无论是排序、搜索、状态机、解析器,还是游戏逻辑,它们的本质都是控制流的组合。
1.3 核心问题引导
在阅读本章时,请带着以下问题思考:
if-else 和 switch 各自适合什么场景?它们可以互相替代吗?
while 和 do-while 的区别是什么?什么时候必须用 do-while?
for 循环的三个部分(初始化、条件、步进)可以省略吗?省略后等价于什么?
为什么 switch 的 case 后面通常需要 break?不写 break 会发生什么?
goto 语句为什么被认为有害?有没有完全不用 goto 就能实现的替代方案?
如何避免"嵌套地狱"——多层嵌套的 if-else 或循环?
二、核心概念详解
2.1 if-else 语句
if-else 是最基本的条件判断语句:
if (expression)
statement;
if (expression)
statement;
else
statement;
if (expression)
statement;
else if (expression)
statement;
else
statement;核心规则:if 后面的表达式,只要值非零就为"真",值为零就为"假"。在C语言中,没有专门的布尔类型(C99之前),任何非零值都被视为真。
if (5) // 真,因为5非零
if (0) // 假,因为0为零
if (-1) // 真,因为-1非零
if (NULL) // 假,因为NULL等价于0
if (ptr) // 检查指针是否非空2.1.1 悬空的 else(Dangling Else)
当 if-else 嵌套时,else 总是与最近的未匹配的 `if` 配对:
if (x > 0)
if (y > 0)
printf("both positive\n");
else
printf("x positive, y not\n"); // 这个else与if(y>0)配对
// 使用大括号消除歧义
if (x > 0) {
if (y > 0) {
printf("both positive\n");
}
} else {
printf("x not positive\n");
}最佳实践:始终使用大括号 {},即使只有一条语句。这可以避免很多因缩进误导而产生的bug。
2.1.2 条件表达式(三元运算符)
condition ? expression1 : expression2int max = (a > b) ? a : b;
// 等价于
int max;
if (a > b)
max = a;
else
max = b;三元运算符适合简单的条件赋值。对于复杂的逻辑,使用 if-else 更清晰。
2.2 switch 语句
switch 语句用于多路分支选择:
switch (expression) {
case constant1:
statements;
break;
case constant2:
statements;
break;
case constant3:
statements;
break;
default:
statements;
break;
}2.2.1 case 穿透(Fall-through)
switch 语句的一个独特特性是 case 穿透:如果没有 break,执行会"穿透"到下一个 case。
switch (c) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
printf("vowel\n");
break;
default:
printf("consonant\n");
break;
}这种穿透特性可以用于将多个 case 合并处理。但如果不注意忘记写 break,就会产生bug:
// 危险的穿透
switch (grade) {
case 'A': printf("Excellent\n"); // 忘记break,会穿透到B
case 'B': printf("Good\n"); // 也会穿透到C
case 'C': printf("Pass\n");
break;
default: printf("Fail\n");
}
// 如果 grade == 'A',会输出 "Excellent Good Pass"2.2.2 switch vs if-else
| 特性 | switch | if-else |
|---|---|---|
| 条件类型 | 只能是整型常量表达式 | 任意表达式 |
| 比较方式 | 只能判断相等 | 可以判断范围 |
| 性能 | 编译器可优化为跳转表,O(1) | 逐个判断,O(n) |
| 可读性 | case多时更清晰 | 条件复杂时更灵活 |
// switch适合:离散值的多路分支
switch (day) {
case 1: printf("Monday\n"); break;
case 2: printf("Tuesday\n"); break;
// ...
}
// if-else适合:范围判断或复杂条件
if (score >= 90) {
printf("A\n");
} else if (score >= 80) {
printf("B\n");
} else if (score >= 70) {
printf("C\n");
}2.3 while 循环
while (expression)
statement;while 循环先判断条件,再执行循环体。如果初始条件为假,循环体一次也不执行。
// 经典的计数循环
int i = 0;
while (i < 10) {
printf("%d ", i);
i++;
}
// 读取字符直到EOF
int c;
while ((c = getchar()) != EOF) {
putchar(c);
}
// 链表遍历
struct Node *p = head;
while (p != NULL) {
process(p->data);
p = p->next;
}2.4 do-while 循环
do
statement;
while (expression);do-while 先执行循环体,再判断条件。循环体至少执行一次。
// 菜单程序:至少显示一次菜单
int choice;
do {
printf("1. Add\n");
printf("2. Delete\n");
printf("3. Quit\n");
printf("Enter choice: ");
scanf("%d", &choice);
switch (choice) {
case 1: add_item(); break;
case 2: delete_item(); break;
case 3: break;
default: printf("Invalid choice\n"); break;
}
} while (choice != 3);`while` vs `do-while` 的选择:如果循环体需要在条件判断之前至少执行一次,使用 do-while。否则使用 while。
2.5 for 循环
for (initialization; condition; increment)
statement;for 循环将初始化、条件判断、步进三个部分集中在一起,是最常用的循环结构。
// 标准计数循环
for (int i = 0; i < 10; i++) {
printf("%d ", i);
}
// 等价于
int i = 0;
while (i < 10) {
printf("%d ", i);
i++;
}2.5.1 for 的灵活性
for 的三个部分都可以省略或替换:
// 省略初始化(变量已在外部初始化)
for (; i < 10; i++) { ... }
// 省略步进(在循环体内更新)
for (i = 0; i < 10; ) {
printf("%d ", i);
i += 2;
}
// 省略条件(无限循环)
for (i = 0; ; i++) {
if (i >= 10) break;
printf("%d ", i);
}
// 三个部分全部省略(无限循环)
for (;;) {
// 等价于 while(1)
}
// 多个变量
for (i = 0, j = n - 1; i < j; i++, j--) {
// 交换 arr[i] 和 arr[j]
}2.5.2 逗号运算符
for (i = 0, j = 0; i < n; i++, j += 2) {
// 逗号运算符从左到右求值,返回最后一个表达式的值
}逗号运算符在 for 循环中用于同时更新多个变量。在其他场合很少使用,因为容易降低代码可读性。
2.6 break 和 continue
2.6.1 break
break 语句用于跳出最近的 switch、while、do-while 或 for 语句。
// 在循环中搜索目标值
for (i = 0; i < n; i++) {
if (arr[i] == target) {
found = i;
break; // 找到后立即退出循环
}
}注意:break 只能跳出一层循环。如果需要跳出多层嵌套循环,可以使用标志变量或 goto。
2.6.2 continue
continue 语句跳过当前循环迭代的剩余部分,直接进入下一次迭代。
// 跳过偶数
for (i = 0; i < 10; i++) {
if (i % 2 == 0)
continue; // 跳过本次迭代的剩余部分
printf("%d ", i); // 只输出奇数
}
// 输出: 1 3 5 7 9在 while 和 do-while 中,continue 跳转到条件判断;在 for 中,continue 跳转到步进部分。
2.7 goto 语句
goto label;
...
label: statement;goto 是无条件跳转到指定标签的语句。它被认为是有害的(Dijkstra 1968年的著名论文 "Go To Statement Considered Harmful"),因为它破坏了程序的结构化,使代码难以理解和维护。
然而,K&R 也承认 goto 在某些情况下是有用的:
// 从多层嵌套中跳出
for (i = 0; i < n; i++) {
for (j = 0; j < m; j++) {
if (error_condition) {
goto cleanup;
}
}
}
cleanup:
free_resources();
return result;// 错误处理和资源清理
int process_file(const char *filename) {
FILE *fp = fopen(filename, "r");
if (!fp) goto err_open;
char *buf = malloc(BUF_SIZE);
if (!buf) goto err_buf;
// ... 处理文件 ...
free(buf);
fclose(fp);
return 0;
err_buf:
fclose(fp);
err_open:
return -1;
}在现代C编程中,goto 主要用于错误处理和资源清理——这是它被广泛接受的唯一用途。
2.8 嵌套循环与设计模式
2.8.1 嵌套循环的经典模式
// 九九乘法表
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= i; j++) {
printf("%d*%d=%-3d", j, i, i * j);
}
printf("\n");
}
// 矩阵遍历
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
matrix[i][j] = i * cols + j;
}
}
// 冒泡排序
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}2.8.2 减少嵌套层次
深层嵌套会降低代码可读性。以下技巧可以减少嵌套:
// 不好的写法:嵌套过深
if (condition1) {
if (condition2) {
if (condition3) {
do_something();
}
}
}
// 好的写法:使用提前返回或continue
if (!condition1) return;
if (!condition2) return;
if (!condition3) return;
do_something();
// 或者使用逻辑运算符合并条件
if (condition1 && condition2 && condition3) {
do_something();
}三、实战案例
3.1 案例一:简单的命令行计算器
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc != 4) {
fprintf(stderr, "Usage: %s num1 op num2\n", argv[0]);
return 1;
}
double a = atof(argv[1]);
char op = argv[2][0];
double b = atof(argv[3]);
double result;
switch (op) {
case '+': result = a + b; break;
case '-': result = a - b; break;
case '*': result = a * b; break;
case '/':
if (b == 0) {
fprintf(stderr, "Error: division by zero\n");
return 1;
}
result = a / b;
break;
default:
fprintf(stderr, "Error: unknown operator '%c'\n", op);
return 1;
}
printf("%.6g %c %.6g = %.6g\n", a, op, b, result);
return 0;
}运行示例:
$ ./calc 3.14 + 2.86
3.14 + 2.86 = 6
$ ./calc 10 / 0
Error: division by zero3.2 案例二:凯撒密码加密/解密
#include <stdio.h>
#include <string.h>
#include <ctype.h>
void caesar_cipher(char *text, int shift) {
for (int i = 0; text[i] != '\0'; i++) {
if (isalpha(text[i])) {
char base = isupper(text[i]) ? 'A' : 'a';
text[i] = (text[i] - base + shift + 26) % 26 + base;
}
}
}
int main() {
char text[256];
int shift;
printf("Enter text to encrypt: ");
fgets(text, sizeof(text), stdin);
text[strcspn(text, "\n")] = '\0';
printf("Enter shift (1-25): ");
scanf("%d", &shift);
printf("Original: %s\n", text);
caesar_cipher(text, shift);
printf("Encrypted: %s\n", text);
caesar_cipher(text, -shift);
printf("Decrypted: %s\n", text);
return 0;
}运行结果:
Enter text to encrypt: Hello World
Enter shift (1-25): 3
Original: Hello World
Encrypted: Khoor Zruog
Decrypted: Hello World3.3 案例三:简单的文本搜索(grep风格)
#include <stdio.h>
#include <string.h>
#define MAXLINE 1000
int main() {
char line[MAXLINE];
char pattern[256];
int line_number = 0;
int match_count = 0;
printf("Enter search pattern: ");
scanf("%s", pattern);
printf("Enter text (Ctrl+D to end):\n");
while (fgets(line, MAXLINE, stdin) != NULL) {
line_number++;
if (strstr(line, pattern) != NULL) {
match_count++;
printf("[%d] %s", line_number, line);
}
}
printf("\nFound %d match(es) in %d line(s).\n",
match_count, line_number);
return 0;
}四、本章小结
第3章系统讲解了C语言的控制流机制:
条件语句:if-else 用于二路分支,switch 用于多路分支。switch 的 case 穿透特性既是优势也是陷阱。三元运算符 ?: 适合简单的条件表达式。
循环语句:while 先判断后执行,do-while 先执行后判断(至少执行一次),for 将初始化、条件、步进集中在一起。三者可以互相替代,选择取决于代码的清晰度。
循环控制:break 跳出当前循环或 switch,continue 跳过当前迭代的剩余部分。
`goto` 语句:虽然被认为有害,但在错误处理和资源清理场景中仍有其价值。现代编程中应限制使用范围。
设计原则:尽量减少嵌套层次,使用提前返回或辅助函数来保持代码的清晰性。
控制流是算法的骨架。掌握了控制流,你就能够将任何算法思想转化为可执行的代码。下一章将学习如何用函数来组织代码,实现模块化和代码复用。
五、思考与练习
思考题
while(0) 和 do { ... } while(0) 有什么区别?后者在宏定义中有什么用途?
for(;;) 和 while(1) 在语义上有什么区别?编译器会生成不同的代码吗?
为什么 switch 的 case 标签必须是常量表达式?不能用变量吗?
如果一个 for 循环的步进部分写成了 i-- 而不是 i++,会发生什么?
如何用 goto 实现跳出两层嵌套循环?有没有不用 goto 的替代方案?
编程练习
实现一个简单的shell:循环读取用户输入的命令,使用 switch 分发到不同的处理函数,支持 help、quit 等内置命令。
编写一个程序:找出1到100之间所有的素数,并统计素数的个数。
实现一个猜数字游戏:程序随机生成一个1-100的数,用户通过输入猜测来逼近目标,程序给出"太大"或"太小"的提示,直到猜对为止。统计猜测次数。
编写一个程序:读取一段文本,统计其中每个字母出现的次数(不区分大小写),并按出现频率从高到低排序输出。