清风日常

Stay curious. Stay gentle.

听一会儿

4 首收藏
LOVE 2000 封面

LOVE 2000

遠野ひかる

0:004:23
首页/C Primer Plus/正文

C Primer Plus(第6版)· 第7章:C 控制语句:分支和跳转

循环决定“是否继续重复”,分支决定“当前执行哪条路径”,跳转语句则能提前结束或略过某段处理。

文章目录 45 个章节

第7章:C 控制语句:分支和跳转

循环决定“是否继续重复”,分支决定“当前执行哪条路径”,跳转语句则能提前结束或略过某段处理。

本章可以沿着一条主线学习:先写清条件,再组合条件,最后明确每种控制语句影响的范围。

1. if:满足条件才执行

c
if (temperature < 0.0) {
    ++cold_days;
}

条件为非零时执行花括号里的操作,为零时跳过。执行完后,程序继续往后走,不会像循环一样自动返回条件处。

if 后面控制的是一条语句。多个操作要用花括号组成复合语句;缩进不能代替花括号。

c
if (count > 0) {
    double average = total / count;
    printf("average: %.2f\n", average);
}

这里先确认分母非零,再进入计算。if 条件不成立时,整个块都不执行。

注意不要在条件后误加分号。if (condition); 控制的是空语句,后面的块可能变成无条件执行。

2. if else 与 else if

2.1 两条路径选择一条

c
if (count > 0) {
    printf("average: %.2f\n", total / count);
} else {
    printf("No data.\n");
}

条件为真走第一条路径,为假走第二条路径。对这个结构,每次执行只会选择其中一个分支。

这也比先写 if (count > 0),后面再独立写一个相反条件,更直接地表达互斥关系。

2.2 从多个条件中选择第一个满足的

c
if (score >= 90) {
    printf("A\n");
} else if (score >= 80) {
    printf("B\n");
} else if (score >= 60) {
    printf("C\n");
} else {
    printf("D\n");
}

假定 score 已是合法分数,分支从上往下检查,第一个成立的分支执行后,后面各项不再检查。

到达 score >= 80 时,前面的 score >= 90 已为假,因此这条路径实际覆盖 [80, 90)。

条件排列会影响结果。如果把 score >= 60 放在最前面,高分也会先进入这一分支。

2.3 多个独立 if 可以同时成立

c
if (score >= 60) {
    printf("Passed.\n");
}
if (score >= 90) {
    printf("Excellent.\n");
}

分数为 95 时,两条消息都会输出。这适合多个独立条件,不能与只选一条路径的 else if 链混为一谈。

2.4 分段计算要累计此前各段

对于阶梯费用一类纯算法示例,先用阈值选择区间,再计算“已跨过各段的总量 + 当前区间的增量”。

不能一进入较高区间,就把所有用量都乘上这个区间的单价,除非需求明确采用这种规则。

可以把各段阈值、系数与累计基数命名,集中表达规则。边界处还应分别检查“恰好等于阈值”和“略高于阈值”的路径。

3. 嵌套分支与 else 配对

没有明确花括号改变结构时,else 与最近的、尚未配对的 if 结合。

c
if (enabled) {
    if (ready) {
        printf("Run.\n");
    } else {
        printf("Wait.\n");
    }
}

这里的 else 属于 if (ready)。若 enabled 为假,整个外层块都跳过,既不输出 Run.,也不输出 Wait.。

如果希望禁用时输出另一条消息,应把分支写在外层:

c
if (enabled) {
    if (ready) {
        printf("Run.\n");
    }
} else {
    printf("Disabled.\n");
}

用花括号明确嵌套结构,能避免缩进表达的意图与实际语法不同。

4. 字符输入输出:getchar 与 putchar

4.1 一个读取,一个输出

c
int ch = getchar();

if (ch != EOF) {
    putchar(ch);
}

getchar 从标准输入读取下一个字符,成功时以 int 返回相应的无符号字符值,输入结束或读取失败时返回 EOF。

所以,保存其返回值应使用 int,先检查 EOF,不要先存入 char 再判断,否则可能丢失区分信息。

putchar 把实参转换为相应的无符号字符值后写向标准输出。两者都由 <stdio.h> 提供接口。

4.2 常见字符读取循环

c
int ch;

while ((ch = getchar()) != EOF && ch != '\n') {
    putchar(ch);
}

赋值两侧的括号不可省略。否则 ch = getchar() != EOF 会按比较后赋值解释,使 ch 得到 0 或 1,而不是读取到的字符。

这里也同时检查输入结束,避免仅等待换行、却在文件结束后永远等不到换行的问题。

5. <ctype.h>:按字符类别处理

5.1 测试函数

函数 测试的类别
isalpha 字母
isdigit 十进制数字字符
isalnum 字母或数字字符
islower、isupper 小写、大写字母
isspace 空白,包括空格、换行、水平制表等
isblank 空白分隔字符,在 "C" locale 中为空格和水平制表
ispunct 标点类字符
isprint 可打印字符,包含空格
isgraph 可打印的非空格字符
iscntrl 控制字符
isxdigit 十六进制数字字符

测试成立返回非零,不成立返回零。不要假定真值一定等于 1,应直接使用 if (isalpha(ch)) 这样的条件。

字符类别可能受 locale 影响。与手写 ASCII 区间相比,这些函数更明确地表达了分类意图,也减少对连续编码的依赖。

5.2 大小写映射返回新值

tolower 返回相应小写形式,toupper 返回相应大写形式;不能映射的值保持原样。

c
ch = toupper(ch);

只是调用 toupper(ch); 而不使用返回值,不会修改原来的变量。

5.3 参数必须处于允许范围

这些函数的实参必须是 EOF,或能由 unsigned char 表示的值。

如果 ch 直接来自尚未缩窄的 getchar 返回值,按前述方式使用即可。若数据来自一个可能为负的普通 char 对象,应先转换:

c
if (isalpha((unsigned char)text[index])) {
    /* 处理字符 */
}

这类按字节工作的函数不等于完整的 Unicode 文本处理工具,不能直接承担 UTF-8 中文分词等任务。

5.4 一个完整的大小写转换程序

c
#include <stdio.h>
#include <ctype.h>

int main(void)
{
    int ch;

    while ((ch = getchar()) != EOF && ch != '#') {
        if (islower(ch)) {
            ch = toupper(ch);
        }
        putchar(ch);
    }

    return 0;
}

输入 Hello, C!#,输出 HELLO, C!。空格和标点保持原样,结束标记 # 不输出。

6. 逻辑运算符:组合完整条件

6.1 与、或、非

运算符 含义 结果为真的条件
&& 逻辑与 两边都为真
|| 逻辑或 至少一边为真
! 逻辑非 操作数为假

这三个运算符的结果都是 int 类型的 0 或 1。操作数本身则按零与非零解释真假。

c
if (score >= 0 && score <= 100) {
    printf("In range.\n");
}

&& 与 || 不能用按位运算符 &、| 替代;按位运算既有不同的数据含义,也不提供相同的短路规则。

6.2 逻辑真值对照

A B A 与 B A 或 B
假 假 假 假
假 真 假 真
真 假 假 真
真 真 真 真

!A 则把真假反转。通过逻辑非,可以把“在单词内”写成 in_word,把“不在单词内”写成 !in_word。

6.3 范围测试不能照搬数学连写

0 <= score <= 100 不表示分数在零到一百之间。它按 (0 <= score) <= 100 解释,前一项只有 0 或 1,因而这个例子总会成立。

正确形式是:

c
score >= 0 && score <= 100

判断不在范围内,则可写:

c
score < 0 || score > 100

6.4 每一项都要写完整

判断字符既不是小写 q,也不是大写 Q:

c
ch != 'q' && ch != 'Q'

不能写 ch != 'q' && != 'Q',因为后半项缺少操作数。

也不能把这里的 && 改为 ||。一个字符至少会不同于这两个字符中的一个,所以两个“不等于”用或连接会恒真。

判断“是其中任意一个”则使用 ch == 'q' || ch == 'Q'。

7. 短路求值与优先级

7.1 && 与 || 都先求值左边

形式 何时不求值右边
left && right 左边为假,结果已确定为假
left || right 左边为真,结果已确定为真

左侧完成后,如果还需要右侧,才会继续求值。这既是顺序保证,也是条件保护手段。

c
if (divisor != 0 && 10 / divisor > 1) {
    printf("Matched.\n");
}

divisor 为零时,不执行右侧除法。

数组检查也可以先验证边界,再访问元素:

c
if (index >= 0 && index < count && values[index] > 0) {
    printf("Positive value.\n");
}

这里假定 index、count 为相应的有符号整数,且 count 正确描述数组范围。检查必须放在访问之前。

7.2 短路会影响副作用是否发生

c
int count = 0;
int result = 0 && ++count;

result 为 0,count 仍为 0,因为右侧根本不执行。

所以,不应把每轮必须执行的更新操作随意放在可能被短路的分支里。简短的表达式也要能够清楚解释执行路径。

7.3 本章涉及的优先级关系

text
一元 !
  高于 算术运算
  高于 < <= > >=
  高于 == !=
  高于 &&
  高于 ||
  高于 条件运算符 ?:
  高于 赋值运算符

这里仅展示相关层级,不是完整运算符表。对复杂逻辑,可以用括号明确分组,而不必强迫读者记住每个层级。

尤其要区分 !a == b 与 !(a == b):前者比较的是 !a 与 b,后者才是否定相等判断。

7.4 备选拼写

在本章的 C99/C11 环境中,可以包含 <iso646.h>,使用 and、or、not 对应 &&、||、!。

它们在 C 中由头文件提供宏形式的替代名称。常见代码仍以符号写法为主,阅读时知道这种对应即可。

8. 嵌套判断与标志变量:识别素数

对于大于一的整数,如果有一个非平凡因数,就一定能配成两个因数,其中至少一个不超过平方根。因此,无须一直试除到原数减一。

c
bool is_prime(unsigned long value)
{
    if (value < 2) {
        return false;
    }

    for (unsigned long divisor = 2;
         divisor <= value / divisor;
         ++divisor) {
        if (value % divisor == 0) {
            return false;
        }
    }

    return true;
}

使用时包含 <stdbool.h>,并在调用前提供原型或完整定义。

这个版本先排除 0、1,发现因数便直接返回。divisor <= value / divisor 避免在条件中计算 divisor * divisor,从而避免平方可能超出整数范围的问题。

如果任务是列出所有因数,而不是只判断素数,就不能在找到第一个因数后立即返回。可以用布尔标志记录是否找到,并继续检查剩余候选项。

列因数时还应处理完全平方数:两个配对因数相等时,只显示一次。

9. 条件运算符 ?:

9.1 根据条件产生一个值

c
int larger = (first > second) ? first : second;

基本形式:

text
条件 ? 条件为真时的表达式 : 条件为假时的表达式

条件求值后,只执行两个候选分支中的一个。它适合表达简短的值选择,不适合把复杂操作层层嵌进去。

候选分支的类型也参与结果类型的确定,尤其要避免无意混合有符号与无符号整数。

9.2 商与余数组合成向上取整

对非负 items 与正整数 capacity,所需组数可以写成:

c
int groups = items / capacity;
groups += (items % capacity != 0) ? 1 : 0;

例如,items 为 11、capacity 为 5,需要三组;恰好十件时,需要两组。

这个写法先求商和余数,不必先计算可能溢出的 items + capacity - 1。仍需保证输入满足约定,尤其不能把零作为除数。

还可以直接在输出中选择文本:

c
printf("%d %s\n", groups, groups == 1 ? "group" : "groups");

10. continue:略过本轮剩余步骤

c
for (int number = 1; number <= 5; ++number) {
    if (number == 3) {
        continue;
    }
    printf("%d\n", number);
}

输出 1、2、4、5。它没有结束循环,只是跳过第三轮剩余部分。

10.1 不同循环中的下一步

所在循环 continue 后的执行位置
while 回到循环条件
do while 进入尾部条件判断
for 先执行更新部分,再判断条件

如果 while 的更新写在循环体末尾,而 continue 提前跳过了更新,就可能反复遇到相同状态。这也是把 for 直接改写为 while 时必须检查的区别。

10.2 用来筛掉无效项目

c
while (scanf("%d", &score) == 1) {
    if (score < 0 || score > 100) {
        printf("Skipped.\n");
        continue;
    }

    total += score;
    ++accepted;
}

合法处理留在较浅的缩进层级,非法输入则提前跳过。这里非法指已经读到整数、但不符合分数范围;不能把它当作解决所有文本解析错误的代码。

11. break:结束最近的循环或 switch

c
for (int number = 1; number <= 5; ++number) {
    if (number == 3) {
        break;
    }
    printf("%d\n", number);
}

输出 1、2,然后直接执行循环后面的语句。与 continue 不同,for 的更新部分也被跳过。

语句 影响
continue 跳过本轮剩余部分,按循环规则继续
break 结束最近一层循环或 switch
return 结束当前函数,必要时返回值

嵌套循环中的一个 break 只结束最近的内层循环。若想结束整个函数,可以用 return;如果外层仍有工作需要执行,则应选择标志变量等合适结构。

12. switch:按离散整数值选择入口

12.1 基本结构

c
switch (command) {
case 'a':
    printf("Add.\n");
    break;
case 'd':
    printf("Delete.\n");
    break;
default:
    printf("Unknown.\n");
    break;
}

程序对控制表达式求值,跳到匹配的标签,再从那里向后执行。无匹配时,存在 default 就进入它,否则直接离开。

关键限制:

  • 控制表达式必须是整数类型,可包括字符和枚举值。
  • case 必须使用整型常量表达式,不能依赖普通运行时变量。
  • 同一 switch 中转换后相同的标签值不能重复。
  • default 可省略,同一个 switch 最多一个。

const int choice = 1; 不会在 C 中自动使 choice 成为可用的 case 整型常量表达式。宏展开为合适整数常量、或使用枚举常量,是不同的情况。

12.2 标签不会自动形成互斥块

c
switch (level) {
case 2:
    printf("Advanced.\n");
    /* 有意继续执行下一个分支 */
case 1:
    printf("Basic.\n");
    break;
default:
    printf("None.\n");
    break;
}

level 为 2 时,两条消息都会输出。因为标签只是入口,程序不会在下一条 case 前自动停止。

如果只是漏写 break,就可能误执行后面的路径;有意贯穿时,应明确说明理由。

12.3 多个标签共用处理

c
switch (ch) {
case 'y':
case 'Y':
    printf("Yes.\n");
    break;
case 'n':
case 'N':
    printf("No.\n");
    break;
default:
    printf("Unknown.\n");
    break;
}

也可以在分类前使用 tolower 或 toupper,减少重复标签。

12.4 与循环嵌套时的范围

当 switch 放在循环里时,其中的 break 结束的是 switch,不是外层循环。

continue 不能独立用于没有循环包围的 switch。若外部确实有循环,它跳向最近外层循环的继续位置,而不是下一个 case。

12.5 如何选择 switch 与 if

场景 更直接的选择
按命令字符、整数编号或枚举值选择 switch
判断大小范围、浮点条件 if else
组合多个不同变量的逻辑条件 if else
从两种简单结果中选一个值 ?:

标准 C99/C11 的 case 不直接表达连续数值区间,也不能用字符串内容作为标签。不要把选择结构的用途差异简单等同于某一种一定运行更快。

13. 状态标志:统计单词与行

13.1 先定义“单词”

这里把单词定义为连续的非空白字节序列。多个空格、制表符或换行只是分隔;标点若没有被空白分开,仍属于同一个单词。

因此,这个程序是简单的空白分词器,不承担语言学意义的中文分词。

13.2 识别状态变化

text
当前不在单词内
  ├─ 空白 → 保持原状态
  └─ 非空白 → 单词数加一,进入单词

当前在单词内
  ├─ 非空白 → 继续当前单词
  └─ 空白 → 离开单词

计数发生在进入单词的时候,而不是每遇到一个非空白字符就增加。这样连续多个分隔符不会产生空单词。

13.3 完整程序

c
#include <stdio.h>
#include <ctype.h>
#include <stdbool.h>

int main(void)
{
    int ch;
    int previous = '\n';
    size_t bytes = 0;
    size_t words = 0;
    size_t lines = 0;
    bool in_word = false;

    while ((ch = getchar()) != EOF && ch != '#') {
        ++bytes;

        if (ch == '\n') {
            ++lines;
        }

        if (isspace(ch)) {
            in_word = false;
        } else if (!in_word) {
            in_word = true;
            ++words;
        }

        previous = ch;
    }

    size_t partial_line = (previous != '\n') ? 1 : 0;

    printf("bytes: %zu, words: %zu, lines: %zu, partial: %zu\n",
           bytes, words, lines, partial_line);

    return 0;
}

输入 one two 后换行,再输入 three#,结果为:

text
bytes: 14, words: 3, lines: 1, partial: 1

其中,两个空格和一个换行都计入字节总数;# 是停止标记,不计数。最后一段内容没有换行,因此记为一条不完整行。

初始 previous = '\n' 使空输入不会被误认为有不完整行。若最后一个有效字符就是换行,partial 为零。

14. 菜单输入:只处理每行首字符

c
#include <stdio.h>
#include <ctype.h>

int main(void)
{
    int command;

    printf("Commands: h=help, r=run, q=quit\n");

    while ((command = getchar()) != EOF) {
        if (command == '\n') {
            continue;
        }

        int rest;
        while ((rest = getchar()) != '\n' && rest != EOF) {
            /* 丢弃本行首字符之后的内容 */
        }

        switch (tolower(command)) {
        case 'h':
            printf("Help shown.\n");
            break;
        case 'r':
            printf("Run selected.\n");
            break;
        case 'q':
            printf("Bye.\n");
            return 0;
        default:
            printf("Unknown command.\n");
            break;
        }

        printf("Next command:\n");
    }

    return 0;
}

每行第一个字符被当作命令,后面的内容被消费。输入 run 只会处理 r,不会把 u 和 n 留到下一轮。

这个界面忽略空行,但不自动跳过行首空格;需要怎样解释空白,应由交互约定决定。

内层丢弃循环同时检查换行和 EOF,所以最后一行没有换行也能结束。switch 中的 break 结束选择结构,之后仍会输出下一次提示;q 分支通过 return 结束整个程序。

15. goto:同一函数内跳到标签

c
goto finished;

/* 中间的其他处理 */

finished:
printf("Finished.\n");

标签由名称和冒号组成,必须位于同一个函数中。goto 可以跳到前面或后面的标签,但不能跨函数跳转。

大多数普通选择和重复任务,用 if、循环、break、continue 更容易看清结构。goto 不应替代这些基本结构,制造在各处来回跳转的路径。

某些需要一次离开多层循环、集中进入同一个结束处理位置的场景,可以考虑它。使用时要保证标签位置清楚,也不能跳过必要准备,之后却继续使用尚未获得有效值的数据。

如果任务只是结束当前函数,优先考虑直接 return;如果只结束最近一层循环,break 已能明确表达意图。

Aron 的清风日常

作者 Aron · 发布于 2026.07.24

愿这份记录能为你提供一点参考。发现问题,欢迎通过 GitHub 或 邮件交流。

如果这篇记录对你有用,留下一份喜欢。

← 所有文章继续阅读C Primer Plus(第6版)· 附录整理:语法与标准库速查 →
SEARCH MY NOTES

在记录里,找一点答案

搜索标题、标签与全文 · 按 Esc 关闭