3C程序设计语言(第2版)

控制流

程序的逻辑结构

if-elseswitch循环breakcontinue

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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 是最基本的条件判断语句:

c
if (expression)
    statement;

if (expression)
    statement;
else
    statement;

if (expression)
    statement;
else if (expression)
    statement;
else
    statement;

核心规则:if 后面的表达式,只要值非零就为"真",值为零就为"假"。在C语言中,没有专门的布尔类型(C99之前),任何非零值都被视为真。

c
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` 配对:

c
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 条件表达式(三元运算符)

c
condition ? expression1 : expression2
c
int max = (a > b) ? a : b;

// 等价于
int max;
if (a > b)
    max = a;
else
    max = b;

三元运算符适合简单的条件赋值。对于复杂的逻辑,使用 if-else 更清晰。

2.2 switch 语句

switch 语句用于多路分支选择:

c
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。

c
switch (c) {
    case 'a':
    case 'e':
    case 'i':
    case 'o':
    case 'u':
        printf("vowel\n");
        break;
    default:
        printf("consonant\n");
        break;
}

这种穿透特性可以用于将多个 case 合并处理。但如果不注意忘记写 break,就会产生bug:

c
// 危险的穿透
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

特性switchif-else
条件类型只能是整型常量表达式任意表达式
比较方式只能判断相等可以判断范围
性能编译器可优化为跳转表,O(1)逐个判断,O(n)
可读性case多时更清晰条件复杂时更灵活
c
// 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 循环

c
while (expression)
    statement;

while 循环先判断条件,再执行循环体。如果初始条件为假,循环体一次也不执行。

c
// 经典的计数循环
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 循环

c
do
    statement;
while (expression);

do-while 先执行循环体,再判断条件。循环体至少执行一次。

c
// 菜单程序:至少显示一次菜单
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 循环

c
for (initialization; condition; increment)
    statement;

for 循环将初始化、条件判断、步进三个部分集中在一起,是最常用的循环结构。

c
// 标准计数循环
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 的三个部分都可以省略或替换:

c
// 省略初始化(变量已在外部初始化)
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 逗号运算符

c
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 语句。

c
// 在循环中搜索目标值
for (i = 0; i < n; i++) {
    if (arr[i] == target) {
        found = i;
        break;      // 找到后立即退出循环
    }
}

注意:break 只能跳出一层循环。如果需要跳出多层嵌套循环,可以使用标志变量或 goto。

2.6.2 continue

continue 语句跳过当前循环迭代的剩余部分,直接进入下一次迭代。

c
// 跳过偶数
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 语句

c
goto label;
...
label: statement;

goto 是无条件跳转到指定标签的语句。它被认为是有害的(Dijkstra 1968年的著名论文 "Go To Statement Considered Harmful"),因为它破坏了程序的结构化,使代码难以理解和维护。

然而,K&R 也承认 goto 在某些情况下是有用的:

c
// 从多层嵌套中跳出
for (i = 0; i < n; i++) {
    for (j = 0; j < m; j++) {
        if (error_condition) {
            goto cleanup;
        }
    }
}
cleanup:
    free_resources();
    return result;
c
// 错误处理和资源清理
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 嵌套循环的经典模式

c
// 九九乘法表
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 减少嵌套层次

深层嵌套会降低代码可读性。以下技巧可以减少嵌套:

c
// 不好的写法:嵌套过深
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 案例一:简单的命令行计算器

c
#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 zero

3.2 案例二:凯撒密码加密/解密

c
#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 World

3.3 案例三:简单的文本搜索(grep风格)

c
#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的数,用户通过输入猜测来逼近目标,程序给出"太大"或"太小"的提示,直到猜对为止。统计猜测次数。

编写一个程序:读取一段文本,统计其中每个字母出现的次数(不区分大小写),并按出现频率从高到低排序输出。

3/8控制流
0%