3. 控制流.md 9.5 KB

3 控制流

3.1 完整概念讲解

控制流语句决定了程序的执行路径。C 语言提供选择结构(if-else、switch)、循环结构(for、while、do-while)和跳转语句(break、continue、goto),组合起来可以表达任意复杂的逻辑。

if-else / else if

if (condition1) {
    /* ... */
} else if (condition2) {
    /* ... */
} else {
    /* ... */
}
  • 条件为非零值时进入对应分支
  • else if 可以无限链式延伸
  • else 捕获所有未匹配的情况

switch-case-break-default

switch (expression) {
    case constant1:
        /* ... */
        break;
    case constant2:
        /* ... */
        break;
    default:
        /* ... */
        break;
}
  • expression 必须是整型(intcharenum 等)
  • case 后必须是编译时常量(不能是变量)
  • 不写 break 会导致 fall-through(穿透到下一个 case)
  • default 可选,建议总是写上
  • 多个 case 可共享同一段代码

for 循环

for (init; condition; update) {
    /* ... */
}
  • init 只在循环开始前执行一次
  • condition 每次迭代前检查,为 0 时退出
  • update 每次迭代后执行
  • 三个表达式均可省略(死循环:for (;;)

while 循环

while (condition) {
    /* ... */
}

先判断条件,再执行循环体。条件为 0 时退出。

do-while 循环

do {
    /* ... */
} while (condition);

先执行循环体,再判断条件。至少执行一次。常用于菜单交互、输入校验等必须先执行一次的场景。

break / continue / goto

  • break:跳出当前循环或 switch
  • continue:跳过本次循环的剩余部分,进入下一次迭代
  • goto:无条件跳转到标签位置

goto 在嵌入式错误处理中的合理使用

在内核和嵌入式代码中,goto 常用于统一错误处理路径(资源清理):

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 特性

switch (val) {
    case 1:
    case 2:
    case 3:
        printf("1, 2, or 3\n");
        break;
    case 4:
        printf("4\n");
        break;
}

C99 for 循环的声明式初始化

for (int i = 0; i < 10; i++) {  /* C99: 变量声明在 for 内 */
    /* ... */
}

3.3 代码示例

示例 1:if-else 与 switch-case

#include <stdio.h>

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;
}

编译命令:

gcc -std=c99 -Wall -Wextra -o control1 control1.c
./control1

示例 2:三种循环对比

#include <stdio.h>

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;
}

编译命令:

gcc -std=c99 -Wall -Wextra -o control2 control2.c
./control2

示例 3:goto 错误处理模式

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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;
}

编译命令:

gcc -std=c99 -Wall -Wextra -o goto_err goto_err.c
./goto_err

3.4 注意事项与易错点

  1. if 后面多余分号

    if (x == 5);    // 空语句,if 什么也没做
    { printf("ok"); }  // 这个块总是执行
    
  2. switch 漏写 break:忘记 break 导致 fall-through 是最常见的 switch bug。C17 引入 [[fallthrough]] 属性(C23 为标准关键字)可显式标注意图。

  3. for 循环的悬挂分号

    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) 利用缓存行大小优化步长。