第07章 C 控制语句:分支与跳转
这一章在干嘛?
上一章学会了循环,程序已经能”重复做一件事”;这一章教程序”看情况办事”:用 if/else 和 switch 让程序根据条件选择不同路径,用逻辑运算符(logical operator)组合更复杂的判断,再用 continue、break、goto 三种跳转语句灵活控制流程。学完本章,程序就真正”有脑子”了。
7.1 if 语句
先看一个例子:读入一批每日最低气温(摄氏度),统计总天数及低于冰点(0℃)的天数占比。
// colddays.c -- 计算低于冰点的天数百分比(程序清单 7.1,节选主体)
#include <stdio.h>
int main(void)
{
const int FREEZING = 0;
float temperature;
int cold_days = 0;
int all_days = 0;
while (scanf("%f", &temperature) == 1)
{
all_days++;
if (temperature < FREEZING) // 核心新语句:低于冰点才计数
cold_days++;
}
if (all_days != 0)
printf("%d days total: %.1f%% were below freezing.\n",
all_days, 100.0 * (float) cold_days / all_days);
if (all_days == 0)
printf("No data entered!\n");
return 0;
}10 days total: 30.0% were below freezing.逐段讲解:
while (scanf("%f", &temperature) == 1):用 scanf() 的返回值做循环条件——成功读到 1 个数就继续,遇到q这类非数字输入就退出(第 6 章的经典套路)。if (temperature < FREEZING) cold_days++;:温度小于 0 就给 cold_days 加 1,否则跳过。- 百分比计算用
(float)强制转换避免整数除法截断成 0(100.0 * cold_days 其实也会先触发自动类型转换,显式转换更多是表明意图、防止日后改错)。
if 语句(if statement)也叫分支语句(branching statement)或选择语句(selection statement),它是程序的”岔路口”:if (expression) statement——expression 非零(真)就执行 statement,否则跳过。statement 可以是单条语句,也可以是用花括号包住的复合语句(compound statement);整个 if 结构在语法上只算一条语句。
7.2 给 if 加个 else
if else 让你在两条语句中二选一。用它可以把 7.1 里测两次的代码改优雅:
if (all_days != 0)
printf("%d days total: %.1f%% were below freezing.\n", ...);
else // 走到这里必然 all_days == 0,不用再测
printf("No data entered!\n");C 不强制缩进,但缩进是标准风格,一眼能看出哪些语句依赖测试条件。
else 之前只能有一条语句
要在 if 和 else 之间放多条语句,必须用花括号组成一个块。下面这样写会编译报错——编译器把
printf()当成 if 的语句,x++脱离了 if,于是 else 成了”没爹的孩子”:if (x > 0) printf("Incrementing x:\n"); x++; // 已经脱离 if 了! else // 编译错误 printf("x <= 0 \n");

7.3 再举个例子:getchar() 与 putchar()
C 有一对专门做字符 I/O 的函数:ch = getchar(); 从输入读下一个字符(等价于 scanf("%c", &ch);),putchar(ch); 打印字符 ch(等价于 printf("%c", ch);)。它们不需要格式说明符,比 scanf()/printf() 更快更紧凑。
程序清单 7.2 用它们做”字符加密”:把每个非空格字符替换成 ASCII 序列中的下一个字符(读法:先读一个字符 → 循环处理 → 循环末再读下一个):
ch = getchar(); /* 读一个字符 */
while (ch != '\n') /* 不是行尾就继续 */
{
if (ch == SPACE) /* 空格保持不变 */
putchar(ch);
else
putchar(ch + 1); /* 其他字符加 1 */
ch = getchar(); /* 读下一个字符 */
}CALL ME HAL.
DBMM NF IBM/C 灵活的语法允许把”读取+测试”合并进一个表达式 while ((ch = getchar()) != '\n'),省掉循环前的单独读取。
括号一个都不能少
如果误写成
while (ch = getchar() != '\n'),由于!=优先级高于=,会先算getchar() != '\n'得到 0 或 1,再把 0/1 赋给 ch——ch 拿到的根本不是字符!记住:(ch = getchar())必须用括号包住。这个惯用法(idiom)在 C 里极常见。另外
putchar(ch + 1)说明字符本质是整数:ch 被提升为 int 参与计算,putchar() 接收 int 参数,只用最后一个字节决定显示哪个字符。
7.4 ctype.h 字符函数家族
清单 7.2 把句点也变成了斜杠(斜杠的 ASCII 码比句点大 1),但若只想转换字母呢?逐个列举”不是空格、不是逗号……”太啰嗦。ANSI C 提供了标准字符分析函数,原型在 ctype.h:参数属于某类字符就返回非零(真),否则返回 0(假)。
// cypher2.c -- 改变输入,保留非字母字符(程序清单 7.3)
#include <stdio.h>
#include <ctype.h> // isalpha() 在这里
int main(void)
{
char ch;
while ((ch = getchar()) != '\n')
{
if (isalpha(ch)) // 是字母就转换
putchar(ch + 1);
else // 否则原样打印
putchar(ch);
}
putchar(ch); // 打印换行符
return 0;
}Look! It's a programmer!
Mppl! Ju't b qsphsbnnfs!大小写字母都被加密,空格和标点原样——一个 isalpha() 搞定手写一堆条件的事。
表 7.1 ctype.h 字符测试函数(参数是某类字符时返回真):
| 函数 | 参数是什么时为真 |
|---|---|
| isalnum() | 字母数字(字母或数字) |
| isalpha() | 字母 |
| isblank() | 标准空白字符(空格、水平制表符、换行)等 |
| iscntrl() | 控制字符,如 Ctrl+B |
| isdigit() | 数字 |
| isgraph() | 除空格外的任何可打印字符 |
| islower() | 小写字符 |
| isprint() | 可打印字符 |
| ispunct() | 标点符号(除空格和字母数字外的可打印字符) |
| isspace() | 空白字符(空格、换行、换页、回车、制表符等) |
| isupper() | 大写字符 |
| isxdigit() | 十六进制数字字符 |
表 7.2 ctype.h 字符映射函数:
| 函数 | 行为 |
|---|---|
| tolower() | 参数是大写字母就返回其小写形式,否则原样返回 |
| toupper() | 参数是小写字母就返回其大写形式,否则原样返回 |
映射函数不修改原参数
tolower(ch);对 ch 毫无影响!它只是返回转换后的值,必须接住:ch = tolower(ch);。初学者最容易在这里栽跟头——“函数明明调了,怎么没变化?“
7.5 多重选择 else if
生活中常常不止两个选项,用 else if 可把 if else 扩展成多路选择。示例:电费分三档计价(前 360 度、接下来 320 度、超过 680 度):
/* electric.c -- 计算电费(程序清单 7.4) */
#include <stdio.h>
#define RATE1 0.12589 /* 前 360 度的电价 */
#define RATE2 0.17901 /* 接下来 320 度的电价 */
#define RATE3 0.20971 /* 超过 680 度的电价 */
#define BREAK1 360.0 /* 第一个计价分界点 */
#define BREAK2 680.0 /* 第二个计价分界点 */
#define BASE1 (RATE1 * BREAK1) /* 360 度的费用 */
#define BASE2 (BASE1 + (RATE2 * (BREAK2 - BREAK1))) /* 680 度的费用 */
int main(void)
{
double kwh; /* 用电量 */
double bill; /* 账单 */
printf("Please enter the kwh used.\n");
scanf("%lf", &kwh); /* double 类型用 %lf */
if (kwh <= BREAK1)
bill = RATE1 * kwh;
else if (kwh <= BREAK2) /* 用电量在 360 到 680 之间 */
bill = BASE1 + (RATE2 * (kwh - BREAK1));
else /* 用电量超过 680 */
bill = BASE2 + (RATE3 * (kwh - BREAK2));
printf("The charge for %.1f kwh is $%1.2f.\n", kwh, bill);
return 0;
}Please enter the kwh used.
580
The charge for 580.0 kwh is $84.70.其实 else if 只是嵌套 if 的另一种写法——else 后面跟的正是另一条 if else,整个结构算一条语句,所以嵌套时不需要花括号。但写成一行更好读,一眼看出是三路选择;缩进嵌套的形式留给真正要测两个独立量的场合(如”夏季超 680 度再加收 10%”)。要点:费率、分界点都用符号常量集中定义,调价只改一处,BASE1、BASE2 联动更新(预处理器只做替换不做计算,BASE1 先替换成 0.12589 * 360.0,再由编译器算出 45.3204);只有 kwh ≥ 360 才可能走到第一个 else,所以 else if (kwh <= BREAK2) 实际等价于”360 < kwh ≤ 680”——每层判断都在为下一层”兜底”。

7.6 else 与 if 的配对及多层嵌套
if 和 else 一多,哪个 else 配哪个 if?规则:else 与它最近的、还没配过对的 if 配对,除非花括号另有安排。

所以下面代码里 else 配的是第二个 if:number 为 5 什么都不打印,为 10 打印 You’re close!,为 15 打印 Sorry…。注意编译器无视缩进——缩进看起来 else 像配第一个 if,纯属误导。真想让 else 配第一个 if,就把内层 if 用花括号包住。
多层嵌套的应用:找约数与判断素数
嵌套 if 的另一用法是”选定分支后再进一步选择”。经典问题:给定整数,打印所有约数;没有约数就报告它是素数(prime)。两个优化点:
- 只需测试到 num 的平方根——约数成对出现(144:2 和 72、3 和 48……12 和 12),越过平方根就开始重复。用
div * div <= num代替开平方,整数乘法快得多。 - 用旗标变量(flag)记录”是否进过 if”:循环结束后旗标还是初值,说明从没找到过约数,num 是素数。
// divisors.c -- 用嵌套 if 显示一个数的约数(程序清单 7.5,节选核心)
#include <stdbool.h>
...
unsigned long num, div; // 待检查的数、候选约数
bool isPrime; // 素数旗标
while (scanf("%lu", &num) == 1)
{
for (div = 2, isPrime = true; (div * div) <= num; div++)
{
if (num % div == 0)
{
if ((div * div) != num) // 非完全平方数:打印一对约数
printf("%lu is divisible by %lu and %lu.\n",
num, div, num / div);
else // 完全平方数:只打印一次
printf("%lu is divisible by %lu.\n", num, div);
isPrime = false; // 有约数,不是素数
}
}
if (isPrime)
printf("%lu is prime.\n", num);
...
}运行示例:输入 36 依次报告可被 2 和 18、3 和 12、4 和 9 整除,最后单独报告 6;输入 149 则报告 is prime。
旗标 + 逗号运算符的组合拳
旗标思路很普适:循环外设初值,循环内满足条件就翻转,循环后检查旗标即可知道”事件是否发生过”;for 初始化部分的
div = 2, isPrime = true用逗号运算符保证每测一个新数字都重置旗标。C99 的 bool 类型(含 stdbool.h 后可用 true/false)比 int 更贴合旗标语义。(小提示:这个程序会把 1 判成素数,严格说 1 不是——学了逻辑运算符你就能排除它。)
7.7 逻辑运算符
C 有三个逻辑运算符(logical operator):
| 运算符 | 含义 | 规则 |
|---|---|---|
| && | 与(and) | 两个操作数都为真才为真 |
| || | 或(or) | 至少一个为真就为真 |
| ! | 非(not) | 真变假、假变真 |
举例:5 > 2 && 4 > 7 为假;5 > 2 || 4 > 7 为真;!(4 > 7) 为真(等价于 4 <= 7)。C99 在 iso646.h 里为它们提供了 and、or、not 三个替代拼写,照顾打不出这些符号的键盘。优先级:! 极高(与 ++ 同级);&& 高于 ||;两者都低于关系运算符、高于赋值——a > b && b > c || b > d 等价于 ((a > b) && (b > c)) || (b > d),但主动加括号更清晰。
求值顺序:C 保证逻辑表达式从左到右求值,&& 和 || 是序列点(sequence point),且一旦能确定整体真假就立即停止(短路求值,short-circuit evaluation)。两个实用技巧:
// 技巧一:防除零——number 为 0 时第二个条件根本不会执行
if (number != 0 && 12 / number == 2)
printf("The number is 5 or 6.\n");
// 技巧二:先给 c 赋值再用 c 判断——顺序有保证
while ((c = getchar()) != ' ' && c != '\n')
...程序清单 7.6 统计一句话中除单引号、双引号外的字符数(句点表示句子结束),核心就一行逻辑判断:
while ((ch = getchar()) != PERIOD) /* PERIOD 即 '.' */
if (ch != '"' && ch != '\'') /* 双引号和单引号都不算 */
charcount++;
// 末尾输出:There are 50 non-quote characters.用 && 测试范围
测 90~100 之间的分数要写成两个完整的关系表达式。
千万别照抄数学写法 90 <= range <= 100
这是语义错误而非语法错误,编译器最多给个警告。由于
<=左结合,表达式被解释成(90 <= range) <= 100——左边先算出 0 或 1,而 0 和 1 都小于 100,整个条件永远为真!同理ch != 'q' && != 'Q'也是错的——&& 两侧必须是两个完整的测试表达式,正确写法ch != 'q' && ch != 'Q'。判断小写字母时
ch >= 'a' && ch <= 'z'依赖字符编码连续(ASCII 成立,EBCDIC 不成立),更可移植的做法是 islower(ch)。
字数统计程序
综合运用以上知识可写一个统计字符数、行数、单词数的程序(程序清单 7.7 wordcnt.c)。单词定义为”不含空白字符的字符序列”,核心逻辑是维护一个旗标 inword:
while ((c = getchar()) != STOP) /* STOP 在第 7 章用 '|' */
{
n_chars++; /* 统计字符 */
if (c == '\n')
n_lines++; /* 统计行 */
if (!isspace(c) && !inword) /* 非空白且不在单词中 */
{
inword = true; /* 新单词开始 */
n_words++; /* 单词数加 1 */
}
if (isspace(c) && inword)
inword = false; /* 单词结束 */
prev = c; /* 保存字符,用于识别不完整的行 */
}if (!isspace(c) && !inword) 比逐个测 c != ' ' && c != '\n' && c != '\t' 简洁得多;布尔变量的惯用写法是直接 if (inword),不必写 if (inword == true)。用 | 结束输入不够优雅,第 8 章会用 EOF 把它改造成能统计文件的通用程序。
7.8 条件运算符 ?:
条件运算符(conditional operator)是 C 唯一的三元运算符(ternary operator),是 if else 的速写:
expression1 ? expression2 : expression3
// expression1 为真整个表达式取 expression2 的值,否则取 expression3 的值典型用途:x = (y < 0) ? -y : y;(求 |y|)、max = (a > b) ? a : b;(求较大值)。程序清单 7.8 用它解决”油漆桶要买整数桶”:int 除法 215/200 截断得 1,有余数就补 1 桶——cans += ((sq_feet % COVERAGE == 0)) ? 0 : 1;,实现向上取整;printf() 里 cans == 1 ? "can" : "cans" 还演示了条件表达式的操作数可以是字符串。
7.9 循环助手:continue 与 break
continue 语句
continue 在三种循环中都能用:跳过本轮剩余语句,直接开始下一轮;在嵌套结构中只作用于最内层。清单 7.9(skippart.c)用它过滤无效分数,核心结构:
while (scanf("%f", &score) == 1)
{
if (score < MIN || score > MAX)
{
printf("%0.1f is an invalid value. Try again: ", score);
continue; // 越界值:跳过本轮后续处理,直接读下一个
}
/* ...处理有效分数:更新 min/max、累加 total、计数 n... */
}
/* 结束后:n > 0 则输出平均分与最低/最高分,否则提示没有有效分数 */输入 188 这类越界值时,continue 让程序跳过后面的处理语句直接读下一个输入。不用 continue 也行(把剩下的语句放进 else 块),但 continue 能减少一层缩进,主逻辑较深时更易读。它还有个占位妙用——空语句的孤立分号难发现,while (getchar() != '\n') continue; 一眼就懂(读走并丢弃一行剩余内容)。
continue 之后程序从哪里继续?while/do while 回到测试表达式;for 循环先执行更新表达式(count++)再测试——两种循环行为有细微差别,计数时前者不含被跳过的字符、后者含。
break 语句
break 让程序跳出包含它的整个循环(或 switch),继续执行循环后面的语句;在嵌套循环中只跳出最内层。

清单 7.10 计算矩形面积,长度或宽度任一输入非数字就退出——循环里有两个 scanf(),break 比 while (scanf("%f %f", ...) == 2) 更方便回显每个输入值:
while (scanf("%f", &length) == 1)
{
...
printf("Enter its width:\n");
if (scanf("%f", &width) != 1)
break; // 宽度输入非数字,退出循环
...
}注意 break 跳出循环时跳过了 for 的更新部分,这是它与 continue 的又一区别。另外别滥用跳转:while ((ch = getchar()) != '\n' && ch != '\t') putchar(ch); 把两个退出条件写在同一处,比循环里藏一个 if + break 更清晰——原则是让逻辑更简单才用,否则就合并条件。
7.10 多重选择 switch 与 break
多路选择用 if else if…else 会很啰嗦,switch 更方便。清单 7.11(animals.c)读入一个字母,打印以它开头的动物名,核心结构如下(每个 case 末尾都有 break):
while ((ch = getchar()) != '#')
{
if ('\n' == ch)
continue; // 忽略回车
if (islower(ch)) // 只处理小写字母
switch (ch)
{
case 'a': printf("argali, a wild sheep of Asia\n");
break;
case 'b': printf("babirusa, a wild pig of Malay\n");
break;
/* ...case 'c' 到 'f' 同理... */
default: printf("That's a stumper!\n");
} // switch 结束
else
printf("I recognize only lowercase letters.\n");
while (getchar() != '\n')
continue; // 跳过输入行剩余部分
printf("Please type another letter or a #.\n");
} // while 结束dab
desman, aquatic, molelike critter
Q
I recognize only lowercase letters.
#
Bye!switch 的工作原理
- 括号里的整型表达式求值,程序在 case 标号中找匹配值,找到就跳到那一行;没有匹配就走 default(如果有),否则继续执行 switch 后面的语句。
- 表达式必须是整型值(包括 char);case 标号必须是整型(含 char)常量或整型常量表达式——不能用变量做标号,也不能像 Pascal 那样用区间做 case。
switch 忘写 break 会"贯穿"到底
匹配到一个 case 后,如果没有 break,程序会顺序执行后面所有 case 的语句直到 switch 结束。假如删掉所有 break 后输入字母 d,desman、echidna、fisher、That’s a stumper! 会全部冒出来!所以每个分支末尾的 break 不是可选项,而是必需品(除非刻意利用”贯穿”)。注意:break 在循环和 switch 里都能用,continue 只能用于循环(switch 位于循环内时可用 continue 跳过本轮循环)。

只读每行的第一个字符与多重 case 标号
animals.c 还演示了交互程序的输入技巧:输入 dab 只处理第一个字符 d——结尾的 while (getchar() != '\n') continue; 把回车前的字符全部读走丢弃,下一轮 getchar() 读到的正是下一行的第一个字符;开头的 if ('\n' == ch) continue; 处理直接按回车的情况。(常量写在左边 '\n' == ch 是个防手滑技巧——漏写成 = 编译器会报错。)
同一语句还可以有多个 case 标号。清单 7.12(vowels.c)统计元音字母,大小写各配一对标号:
case 'a':
case 'A': a_ct++;
break;
/* ...e、i、o、u 同理... */
default: break;ch 是 i 时跳到 case ‘i’:,那里没有语句也没有 break,流程顺势落到 i_ct++;;ch 是 I 时直接落到同一条语句——两个标号共享一段代码。也可以先用 toupper(ch) 统一转大写再 switch,省掉成对标号。
switch 与 if else 怎么选? 依据浮点表达式选择时只能用 if else;变量落在某个范围(如 3 到 999)时 if else 方便——switch 得写几百个 case;选项是有限个整型/字符常量时用 switch,代码更短、运行通常也更快。
7.11 goto 语句
goto 有两部分:goto 和标号名,标号语句写在函数内任意位置,标号后跟冒号:
goto part2;
...
part2: printf("Refined analysis:\n");K&R 说 goto “极其容易被滥用”,建议”能不用就不用”。老式 BASIC/FORTRAN 的 goto 套路在 C 中都有更好的替代:if 后多条语句→复合语句 { };二选一→if else;无限循环→while;跳到循环末尾→continue;跳出循环→break(break/continue 本质是特化的 goto,好处是名字自解释、没有标号放错位置的风险)。
唯一被许多 C 程序员容忍的用法:从多层嵌套循环中一口气跳出(单个 break 只能跳出最内层)——在最内层检测到问题就 goto help;,标号语句 help: 善后处理; 写在所有循环之外。
本章速记
if/else/switch 加逻辑运算符构成 C 全部的”看情况办事”能力:任何非零值都是真,0 是假;测范围必须用 && 连两个完整表达式;switch 必配 break,continue 只管循环;ctype.h 系列是字符分类利器;短路求值是安全网——
n != 0 && 100/n > 5永不除零。
本章自测
自测题(5 题,点击展开答案)
1. 判断真假:
100 > 3 && 'a' > 'c';100 > 3 || 'a' > 'c';!(100 > 3)。答案:
'a'的 ASCII 是 97,'c'是 99,所以'a' > 'c'为假。第一个:真 && 假 = 假;第二个:真 || 假 = 真;第三个:!(真) = 假。2. 写出表达式:(a) number 大于等于 90 且小于 100;(b) ch 不是 q 也不是 k;(c) number 在 1 到 9 之间(含端点)但不等于 5;(d) number 不在 1 到 9 之间。
答案:(a)
number >= 90 && number < 100;(b)ch != 'q' && ch != 'k';(c)number >= 1 && number <= 9 && number != 5;(d)number < 1 || number > 9(取反后”与”变”或”)。3. 下面的写法有什么问题?
if (90 <= range <= 100)和while ((ch = getchar() != '\n'))。答案:前者是语义错误——
<=左结合,先算90 <= range得 0 或 1,都 ≤ 100,整个条件永远为真;应写成range >= 90 && range <= 100。后者少一对括号——!=优先级高于=,getchar() 的返回值先和 ‘\n’ 比较得 0/1,再把 0/1 赋给 ch;应写成(ch = getchar()) != '\n'。4. switch 的 case 标号能用变量吗?忘写 break 会怎样?
答案:不能用。case 标号必须是整型(含 char)常量或仅含常量的整型表达式,C 的 case 也不支持区间。忘写 break 时,匹配到该 case 后程序会顺序”贯穿”执行后面所有 case 的语句,直到遇到 break 或 switch 结束。
5. 读程序:
int i = 0; while (i < 3) { switch(i++) { case 0: printf("fat "); case 1: printf("hat "); case 2: printf("cat "); default: printf("Oh no!"); } putchar('\n'); }输出是什么?答案:i=0 匹配 case 0,无 break 一路贯穿:
fat hat cat Oh no!;i=1 匹配 case 1:hat cat Oh no!;i=2 匹配 case 2:cat Oh no!。共三行——正是”忘 break 导致贯穿”的标准演示。