返回介绍

3. 控制流

发布于 2024-10-12 21:58:08 字数 5892 浏览 0 评论 0 收藏 0

语句(statement)以分号结束,执行一到多个动作。多条语句以 大括号包含 构成复合语句块(block)。按顺序执行,或循环、跳转。

块中声明的名字具有块作用域,仅在块内可见,遮蔽外部同名对象。

块内自动变量生命周期仅限于当前块,除非是静态(static)或外部(extern)引入。

选择

根据条件表达式,选择性执行子语句。最多只有一个分支命中执行。

int main (void)
{
    int x = 10;
    char r;

    if (x > 10) {
        r = 'a';
    } else if (x > 5) {
        r = 'b';
    } else if (x > 3) {
        r = 'c';
    } else {
        r = '0';
    }

    printf("%c\n", r);

    return 0;
}

如果不使用大括号语句块,那分支仅含其后一条语句。为避免误会,通常和 if 写成一行。

int main (void)
{
    if (argc > 1) // {
        printf("%d\n", argc);     // } 仅此行!
        printf("%s\n", argv[1]);  // 并不属于 if 语句块。

    return 0;
}
note: ...this statement, but the latter is misleadingly indented as 
         if it were guarded by the 'if'

如果是无大括号嵌套,则 else 分支属于最后一个 if。有鉴于此,还是以大括号规避歧义为佳。

int main (int argc, char *argv[])
{
    if (argc > 1) // {
        if (strcmp(argv[1], "1") == 0)
            puts("ok");
        else
            puts("error");
    // }
    
    return 0;
}
warning: suggest explicit braces to avoid ambiguous 'else' [-Wdangling-else]

switch

条件表达式是整数类型。

分支必须以 break 跳出,否则在不匹配后续 case 的情况下贯穿执行下一语句块。

int main (int argc, char *argv[])
{
    if (argc < 2) return -1;
    char c = argv[1][0];

    switch (c) {
        case 'a':        // 如果匹配,因为没有 break
        case 'A':        //                   |
            puts("A");   // <-----------------+
            break;       // 如果没有 break
                         //            |
        case 'b':        //            |
        case 'B':        //            |
            puts("B");   // <----------+
            break;

        default:
            puts("error");
    }

    return 0;
}

可用 -Wimplicit-fallthrough 参数,让编译器对 break 缺失作出警告。

虽然 defautl 可选,且编译器确保它是最后选择。

但其代码位置如果不在最后,也须添加 break。为避免调整代码导致意外错误,总是为其添加 break 更好。

warning: this statement may fall through [-Wimplicit-fallthrough=]
    |             puts("A");
    |             ^~~~~~~~~

另外,case 常量值必须唯一。比如,不能同时出现 'A'65

error: duplicate case value
    |         case 65 :
    |         ^~~~
note: previously used here
    |         case 'A':
    |         ^~~~

如要在 case 块定义或声明变量,则须额外启用一个内嵌块。

switch (c) {
    case 'a': 
    case 'A':
        // {
        int x = 100;
        printf("%d\n", x);
        // }
        puts("A");
        break;
}
error: a label can only be part of a statement and a declaration is not a statement
    |             int x = 100;
    |             ^~~

循环

三种循环语句:

  • while :先判断条件,再决定是否执行循环。
  • do ... while : 先执行一次,再判断条件是否继续。
  • for : 三个表达式,分别用于初始化、控制和调整。
int x = 4;
while (x--) {
    printf("%d\n", x);
}
while (1) {        // 或 true,无限循环。
    puts("a");
}

相比较 while,do...while 有一些特定使用场合。

比如,IO 操作时,总是先尝试读取一次。如果成功,则继续循环操作。

int x = 4;
do {
    printf("%d\n", x);
} while (--x > 0);

在 for 三个表达式里:

  • 初始化 (initialization):在控制表达式前,仅执行一次。
  • 控制 (controlling):每次循环前测试,决定是否执行。
  • 调整 (adjustment):每次循环后,控制表达式再次测试前执行。(调整计数器)
for (int i = 0; i < 5; i++) {
    printf("%d\n", i);
}

变形用法:

for (int x = 0, y = 0; x < 5; x++, y--) {  // 多个初始化和调整变量。
    printf("%d: %d\n", x, y);
}
for (;;) {       // 无限循环。
}
for (; cond;) {  // 等同 while (cond)
}

跳转

无条件跳转语句。

  • goto <label> : 跳转到所在函数内任意标签位置。
  • continue : 跳过后续代码,进入下一次循环。
  • break : 终止 switch 或循环语句执行。
  • return : 终止当前函数执行。

滥用 goto 会导致代码难以维护,比如像下面这样跳转到一个循环块内部。

对 goto 的口诛笔伐由来已久(1968, Edsger Dijkstra),但在系统层面却可以看到很多 goto 使用案例。

比如:Linux kernel、CPython interpreter、Go runtime。相比其他的设计模式,goto 简单而高效。

标签可以与变量同名,可放在任意语句之前。一个语句可以有多个标签。

int main (void)
{
    goto a;

    while (1) {
a:
        break;
    }

    return 0;
}

goto/label 配合,实现错误处理案例。

int do_something(void) {

    f1 = fopen("a", "w");
    if (f1 == NULL) goto ERR_F1;

    f2 = fopen("b", "w");
    if (f2 == NULL) goto ERR_F2;

    m = malloc(10);
    if (m == NULL) goto ERR_MEM;

    free(obj);
    return 0;

ERR_MEM:                // 错误处理,注意次序。
    fclose(f2);
ERR_F2:
    fclose(f1);
ERR_F1:
    return -1;
}

注意:break/continue 无法配合 label 跳转嵌套循环,可用 goto 或标志变量代替。

int main (void)
{
    for (int x = 0; x < 6; x++) {      // 不能放在此处,那会重启 for.x
        for (int y = 0; y < 3; y++) {
            printf("%d:%d  ", x, y);
            goto a;                    // continue outer
        }

        printf("\n");                  // 可以放在 printf 前,
a:      ;                              // 或这里(do nothing)!
    }                                  // label 后面应该是语句,而不是 "}" 。

    return 0;
}

longjmp

跨函数跳转,谨慎使用。

  • setjmp : 首次执行,保存上下文,供 longjmp 跳回使用。
  • longjmp : 基于上下文跳转,向 setjmp 传值。所在函数不会返回。
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

jmp_buf buf;

void test ()
{
    longjmp(buf, 10);        // no return.
}

int main (void)
{
    int ret = setjmp(buf);  // 首次调用保存上下文,返回 0。
    if (ret != 0) {         // longjmp 跳转回来,返回其所传值。
        printf("long jump: %d\n", ret);
        return 1;
    }

    puts("exec longjmp.");
    test();
    
    return 0;
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文