前面两篇文章,我们学会了怎么在 C 语言里存储数据——用 int 存整数,用 char 存字符,用 float 和 double 存小数——也学会了用 printf 输出、用 scanf 输入、用各种运算符做计算。但你有没有发现一个问题:到目前为止,我们写的所有程序都是"一条路走到黑"的。从上往下,一行一行执行,没有任何"决策"。
真正的程序显然不能这样。一个登录系统,密码对了才能进去,错了就得提示;一个计算器,用户选加法就做加法,选减法就做减法;一个游戏,角色血量掉到 0 就 Game Over,否则继续战斗。程序需要根据条件做出不同反应——这就是"流程控制"的意义。
C 语言是结构化的程序设计语言,支持三种基本结构:顺序结构、选择结构(分支)、循环结构。这三种结构你每天都在用——"如果下雨就带伞"是选择,"每天早上跑步 30 分钟"是循环。这篇文章我们集中攻坚选择结构:if-else 语句、关系运算符、条件运算符、逻辑运算符(&&、||、!)和 switch 语句。
在开始之前,有一个贯穿全文的规则需要记住:C 语言中,0 表示"假",非 0 表示"真"。这和 Python 的 True/False、Java 的 boolean 都不一样。C 语言在条件判断时,表达式结果为 0 就不执行,非 0 就执行。关系表达式和逻辑表达式的计算结果也通常是 0(假)或 1(真)。比如 3 > 2 的结果是 1,2 > 3 的结果是 0。这个"0/非0"的规则,是 C 语言贴近硬件的设计选择——CPU 判断条件靠的就是零标志位(Zero Flag)。CPU 执行比较指令后,结果为零就置位零标志位,C 语言编译器把这种硬件行为直接映射成了 if 的判断逻辑。
if 语句——程序的第一个"决策点"
if 是 C 语言中最基本的分支语句,语法和英语里的"if"一样直觉:
if (表达式)
语句;当表达式为真(非 0)时,执行后面的语句;为假(0)时,跳过不执行。
来看第一个例子——判断一个数是不是奇数:
#include <stdio.h>
int main()
{
int num = 0;
scanf("%d", &num); /* 从键盘读入一个整数 */
if (num % 2 == 1) /* 如果 num 除以 2 余 1,说明是奇数 */
printf("%d 是奇数\n", num);
/* 如果 num%2 不等于 1(即 num 是偶数),这条 printf 不会执行 */
return 0;
}这里 num % 2 == 1 就是条件表达式。注意是两个等号 ==,不是 =。这个区别非常关键——我们稍后会讲。
光有 if 只能做"满足条件就做某事",如果还想处理"不满足条件"的情况呢?那就需要 else——二选一,必走其一:
#include <stdio.h>
int main()
{
int num = 0;
scanf("%d", &num);
if (num % 2 == 1)
printf("%d 是奇数\n", num); /* 余数为 1,走这里 */
else
printf("%d 是偶数\n", num); /* 余数为 0,走这里 */
return 0;
}再如判断是否成年:
#include <stdio.h>
int main()
{
int age = 0;
scanf("%d", &age);
if (age >= 18)
printf("成年\n");
else
printf("未成年\n");
return 0;
}if 的另一种玩法:直接判断非 0 值
因为"非 0 即真",所以条件不一定是比较表达式,任何表达式都可以:
#include <stdio.h>
int main()
{
int flag = 0;
scanf("%d", &flag);
if (flag) /* 等价于 if (flag != 0) */
printf("flag 非 0,为真\n");
else
printf("flag 是 0,为假\n");
if (!flag) /* 等价于 if (flag == 0) */
printf("flag 为 0\n");
return 0;
}这种写法在检查"是否读到文件末尾"(EOF 是负数,非 0 即真)、"函数是否调用成功"(返回 0 表示失败时用 !)时非常常见。但注意:初学者最好显式写 flag != 0,等熟练了再偷懒,否则容易把"判断值"和"判断真假"搞混。
if 控制多条语句——大括号不能省
默认情况下,if 和 else 只能控制紧跟在后面的一条语句。这是新手最容易踩的坑之一。如果你想控制多条语句,必须用大括号 {} 括起来:
#include <stdio.h>
int main()
{
int age = 0;
scanf("%d", &age);
/* 错误写法:不管 age 是多少,"可以谈恋爱了"都会打印 */
/* 因为 if 只控制 printf("成年了\n") 这一条语句 */
if (age >= 18)
printf("成年了\n");
printf("可以谈恋爱了\n"); /* 这条不受 if 控制! */
return 0;
}正确写法:
#include <stdio.h>
int main()
{
int age = 0;
scanf("%d", &age);
if (age >= 18)
{
printf("成年了\n");
printf("可以谈恋爱了\n"); /* 两条都在 if 的控制下 */
}
else
{
printf("未成年\n");
printf("不可以早恋哦\n");
}
return 0;
}大括号括起来的代码块叫复合语句。一个好的编码习惯是:哪怕 if 后面只有一条语句,也加上大括号。这样以后加代码不会出问题,代码逻辑也更清晰。很多大公司(比如 Linux 内核)的代码规范也强制要求 if 必须带大括号——这不是形式主义,而是用规则消灭一整类 bug。
if 条件里常见的中文符号坑
写 if 条件时还有一个容易被忽略的坑:条件里的比较符号。全角 >、<、= 和半角 >、<、== 在屏幕上看起来几乎一样,但编译器只认半角。如果复制了网上的全角符号,编译直接报错。中英文标点混用是新手编译报错的高发原因,遇到莫名其妙的"语法错误",先检查标点。
嵌套 if——多条件逐层判断
现实中的判断往往不是简单的"是或否"。比如判断一个数是正数、负数还是零——这是三路分支:
#include <stdio.h>
int main()
{
int num = 0;
scanf("%d", &num);
if (num == 0)
printf("输入的数字是 0\n");
else if (num > 0) /* else if 是嵌套在 else 里的 if */
printf("输入的数字是正数\n");
else
printf("输入的数字是负数\n");
return 0;
}else if 的本质就是"在 else 里面再放一个 if"。你可以无限嵌套,形成多路分支。
再看一个根据年龄判断人生阶段的例子:
#include <stdio.h>
int main()
{
int age = 0;
scanf("%d", &age);
if (age < 18)
printf("少年\n");
else if (age <= 44)
printf("青年\n");
else if (age <= 59)
printf("中老年\n");
else if (age <= 89)
printf("老年\n");
else
printf("老寿星\n");
return 0;
}注意这个逻辑:条件从严格到宽松排列。else if 按顺序判断,命中第一个为真的条件后就跳过其余的。如果你把 age <= 89 放在 age <= 44 前面,逻辑就乱了——44 岁的人会被判为"老年"。
这是 else-if 链条最重要的设计原则:条件的顺序就是判断的优先级,宽泛的条件放在后面。类比现实:先问"是不是学生",再问"是不是研究生"——如果先问"是不是学生"就把研究生也圈进去了,后面那个判断就永远执行不到。
嵌套也可以放在 if 内部——比如先判断是不是正数,再判断是奇数还是偶数:
#include <stdio.h>
int main()
{
int num = 0;
scanf("%d", &num);
if (num > 0)
{
if (num % 2 == 0) /* 嵌套在 if(num>0) 内部 */
printf("正偶数\n");
else
printf("正奇数\n");
}
else
{
printf("非正数\n");
}
return 0;
}嵌套的层级越多,代码越难读。如果嵌套超过三层,就要考虑重构——要么用 else if 拉平,要么用 switch,要么把内层判断抽成函数。代码是写给"下一个读它的人"看的(那个人很可能就是三个月后的你自己)。
悬空 else——新手最容易掉的坑
看下面这段代码,你觉得会输出什么?
#include <stdio.h>
int main()
{
int a = 0;
int b = 2;
if (a == 1)
if (b == 2)
printf("hehe\n");
else
printf("haha\n");
return 0;
}很多新手会想:"a 等于 0,不等于 1,所以走 else,输出 haha。"
错!这段代码什么都不输出。
原因在于:else 总是和离它最近的那个未配对的 if 匹配。上面的代码中,else 看起来和第一个 if 对齐,但它实际上和第二个 if (b == 2) 配对。所以这段代码等价于:
if (a == 1)
{
if (b == 2)
printf("hehe\n");
else
printf("haha\n");
}因为 a == 1 为假,整个大括号里的代码都不执行——自然什么都没输出。
这就是悬空 else 问题。它的根源是:C 语言的语法只规定了"else 匹配最近的 if",而缩进只是给人看的,编译器根本不看缩进。解决办法很简单:始终用大括号明确表达你的意图,不要依赖缩进。如果你想让 else 和第一个 if 配对:
if (a == 1)
{
if (b == 2)
printf("hehe\n");
}
else /* 现在 else 明确和第一个 if 配对 */
{
printf("haha\n");
}这个坑在面试题里出现的频率极高,考试/笔试时看到"多层 if 嵌套 + 缩进误导"的题目,第一反应就是检查 else 的配对关系。
关系操作符——比较大小的六种方式
C 语言提供了六个关系运算符:
| 运算符 | 含义 | 示例 |
|---|---|---|
> | 大于 | a > b |
< | 小于 | a < b |
>= | 大于等于 | a >= b |
<= | 小于等于 | a <= b |
== | 等于 | a == b |
!= | 不等于 | a != b |
两个需要特别注意的点:
第一,==(等于)和 =(赋值)不要搞混。 if (a = 3) 是赋值——把 3 赋给 a,然后判断 a 是否为真(3 非 0,永远为真)。if (a == 3) 才是判断相等。一个有经验的防错技巧是把常量写在左边:if (3 == a)——如果误写为 if (3 = a),编译器会直接报错,因为不能给常量赋值。
为什么 if (a = 3) 能编译通过?因为 C 语言中赋值表达式本身有值(就是赋进去的值),所以 a = 3 这个表达式的值是 3,非 0 即真。C 语言语法上允许这种写法,是历史遗留——有些老代码故意用它(比如 if ((c = getchar()) != EOF) 这种"一边赋值一边判断"的惯用法),但对初学者来说,它 99% 是笔误。编译器通常会给出警告("在条件表达式中使用赋值"),把警告当成错误来对待,能帮你拦住大部分笔误。
第二,多个关系运算符不能连用。 i < j < k 在数学里表示"j 在 i 和 k 之间",但在 C 语言里完全不是这个意思。实际执行顺序是 (i < j) < k——先算 i < j 得到 0 或 1,再用这个 0 或 1 和 k 比较。来看一个经典错误:
#include <stdio.h>
int main()
{
int age = 0;
scanf("%d", &age);
/* 错误写法:18 <= age <= 36,这个表达式永远为真! */
if (18 <= age <= 36) /* 输入 10 也会打印"青年" */
printf("青年\n");
return 0;
}你输入 10,程序会输出"青年"。因为 18 <= 10 为假(结果是 0),然后 0 <= 36 为真——所以你输入任何数都会进入 if。
正确写法是用逻辑与 && 连接两个条件:
if (age >= 18 && age <= 36) /* 两个条件必须同时满足 */
printf("青年\n");关系运算符的优先级
关系运算符的优先级低于算术运算符,高于赋值运算符:
int result = 3 + 4 > 5; /* 先算 3+4=7,再算 7>5,result = 1 */
int r2 = a < b == c < d; /* 实际是 (a<b) == (c<d):先算两个比较,再比较两个结果 */== 和 != 的优先级低于 >、<、>=、<=(即等于号更"松散")。这个细节在复杂表达式里会影响结果,建议记不住就加括号。
条件操作符——if-else 的简写版
当 if-else 的逻辑很简单(只是根据条件选一个值)时,可以用条件操作符来简化:
表达式1 ? 表达式2 : 表达式3如果表达式 1 为真,整个表达式的结果是表达式 2;否则是表达式 3。它需要三个操作数,所以也叫三目操作符。
对比两种写法——找出两个数中的较大值:
#include <stdio.h>
int main()
{
int a = 0, b = 0;
scanf("%d %d", &a, &b);
/* if-else 写法 */
int m;
if (a > b)
m = a;
else
m = b;
/* 条件操作符写法,一行搞定 */
int n = a > b ? a : b;
printf("较大值: %d (if写法), %d (三目写法)\n", m, n);
return 0;
}再如根据条件赋不同值:
int b = a > 5 ? 3 : -3; /* 如果 a>5,b=3,否则 b=-3 */条件操作符可以嵌套,但不建议嵌套太深——可读性会急剧下降。一次嵌套还可以接受(比如 a > b ? (a > c ? a : c) : (b > c ? b : c) 找三个数的最大值),两层以上的嵌套就建议拆成 if-else。
条件操作符的易错点
- 它仍然是表达式,有值——可以直接用在 printf 里:
printf("%s", a > b ? "大" : "小"); - 优先级低于大多数运算符——
a + b > c ? d : e实际是(a + b > c) ? d : e,但为了可读性请加括号 - 三个操作数的类型可能不同——
a > 0 ? 3 : 3.14的结果类型是 double(int 自动转成 double) - 嵌套三目是可读性杀手——看到
?:?:?:连写,请拆开
逻辑操作符——组合多个条件
现实世界的判断往往是多个条件组合在一起。C 语言提供了三个逻辑运算符:
| 运算符 | 含义 | 说明 |
|---|---|---|
! | 逻辑非(取反) | 真变假,假变真 |
&& | 逻辑与(并且) | 两个都为真,结果才为真 |
|| | 逻辑或(或者) | 至少一个为真,结果就为真 |
逻辑非 !
#include <stdio.h>
int main()
{
int flag = 0; /* 0 表示假 */
if (!flag) /* !0 = 1(真),条件成立 */
printf("flag 是假的,执行这里\n");
flag = 5; /* 非 0 表示真 */
if (!flag) /* !5 = 0(假),条件不成立 */
printf("这行不会打印\n");
return 0;
}! 的运算结果只有 0 或 1:!0 = 1,!任何非0值 = 0。它和位取反 ~ 完全不同——~ 是按位翻转(每一位都反过来),! 是逻辑取反(整体真假翻转)。以后学位运算时要区分清楚。
逻辑与 &&
判断月份是否在 3 到 5 月之间(春天):
#include <stdio.h>
int main()
{
int month = 0;
scanf("%d", &month);
if (month >= 3 && month <= 5)
printf("春季\n");
else
printf("不是春季\n");
return 0;
}逻辑或 ||
判断月份是否是冬季(12 月、1 月或 2 月):
#include <stdio.h>
int main()
{
int month = 0;
scanf("%d", &month);
if (month == 12 || month == 1 || month == 2)
printf("冬季\n");
else
printf("不是冬季\n");
return 0;
}注意 || 连接的每个条件都必须写完整——month == 1 || 2 是错误的写法,它实际是 (month == 1) || 2,而 2 恒为真,整个表达式恒真。这个错误非常隐蔽,面试题常考。
实战:判断闰年
闰年的规则是:(1) 能被 4 整除但不能被 100 整除,或者 (2) 能被 400 整除。用逻辑运算符组合:
#include <stdio.h>
int main()
{
int year = 0;
scanf("%d", &year);
/* 用逻辑运算符组合两个条件 */
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
printf("%d 是闰年\n", year);
else
printf("%d 不是闰年\n", year);
return 0;
}分析一下这个表达式:year % 4 == 0 && year % 100 != 0 是"能被 4 整除且不能被 100 整除",year % 400 == 0 是"能被 400 整除",两者用 || 连接。外层括号不能省——否则 && 的优先级高于 ||,会先算 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)……等等,其实不加括号结果也一样,因为 && 优先级本来就高于 ||。但写上括号是在告诉读代码的人"这是一个整体",这比编译器需不需要更重要。
实战:判断一个字符
结合上一篇的 ASCII 知识,判断输入的字符是字母、数字还是其他:
#include <stdio.h>
int main()
{
char ch = 0;
scanf("%c", &ch);
if (ch >= 'A' && ch <= 'Z')
printf("大写字母\n");
else if (ch >= 'a' && ch <= 'z')
printf("小写字母\n");
else if (ch >= '0' && ch <= '9')
printf("数字字符\n");
else
printf("其他字符\n");
return 0;
}注意 ch >= 'A' && ch <= 'Z' 这种写法依赖 ASCII 码的连续性——'A''Z' 是 6590 连续编码,所以区间判断有效。这种"字符区间判断"在输入校验里非常常用。
短路求值——逻辑运算符的"偷懒"机制
C 语言的逻辑运算符有一个重要特性:从左到右求值,一旦能确定最终结果,就不再计算右边的表达式。这叫"短路求值"。
- 对于
&&:如果左边为假(0),整个表达式必为假,右边不计算 - 对于
||:如果左边为真(非 0),整个表达式必为真,右边不计算
来看一个能体现短路效果的例子:
#include <stdio.h>
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
/* a++ 返回 0(假),发生短路!
* ++b 和 d++ 都不会执行!
* 最终:a=1, b=2(未变), c=3, d=4(未变) */
printf("a=%d, b=%d, c=%d, d=%d\n", a, b, c, d);
/* 输出:a=1, b=2, c=3, d=4 */
/* 换成 || 再看看 */
i = 0;
a = 0;
i = a++ || ++b || d++;
/* a++ 返回 0(假),不短路,继续计算 ++b
* ++b 返回 3(真),发生短路!d++ 不执行
* 最终:a=1, b=3, d=4 */
printf("a=%d, b=%d, c=%d, d=%d\n", a, b, c, d);
/* 输出:a=1, b=3, c=3, d=4 */
return 0;
}短路求值不只是性能优化——它还能用来写更安全的代码。比如先判断指针是否为 NULL 再访问:
if (ptr != NULL && ptr->value > 0) /* 如果 ptr 是 NULL,右边的访问不会执行 */
printf("%d\n", ptr->value);如果 ptr 是 NULL,ptr != NULL 为假,短路发生,ptr->value 不会被计算——避免了空指针崩溃。但反过来也要注意:不要在逻辑表达式中夹带"必须执行"的副作用操作。比如 if (a > 0 && b++ > 0)——如果 a > 0 为假,b++ 就不会执行,这可能有违你的预期。
这条"副作用警告"在实际代码里最常见的翻车场景是:while (n != 0 && n % 2 == 0) { ... } 中有人把 n-- 写进条件里,结果循环次数和预期完全对不上。条件里只放"判断",不放"修改",这是一条值得遵守的纪律。
逻辑运算符与位运算符的区别
初学者经常把 && 和 &、|| 和 | 搞混。它们的区别是本质性的:
&&是逻辑与:先看真/假,结果是 0 或 1,有短路&是位与:对两个整数的每一位做 AND,结果是一个整数,无短路
int a = 3; /* 二进制 0011 */
int b = 5; /* 二进制 0101 */
int r1 = a && b; /* 3 和 5 都是真,结果为 1 */
int r2 = a & b; /* 0011 & 0101 = 0001 = 1(巧合的是这里都等于1) */
/* 换个例子就能看出差别 */
int c = 2; /* 二进制 0010 */
printf("%d\n", c && 4); /* 1:两个都非0,逻辑结果为真 */
printf("%d\n", c & 4); /* 0:0010 & 0100 = 0000 */记法:&& 比 & 长,长的那对是"逻辑版"(返回 0/1,短路);短的那对是"位操作版"(按位运算)。还有 ! 和 ~ 的区别类似——! 逻辑取反返回 0/1,~ 按位取反翻转所有位。
操作符优先级总表(含分支相关)
前面零散提到了优先级,这里给出一张总表(从高到低),方便对照:
| 优先级 | 操作符 | 说明 |
|---|---|---|
| 1(最高) | () [] -> . | 括号、下标、成员访问 |
| 2 | ! ~ ++ -- -(负号)(类型) *(解引用)&(取地址) | 单目操作符 |
| 3 | * / % | 乘除取余 |
| 4 | + - | 加减 |
| 5 | << >> | 移位 |
| 6 | < <= > >= | 关系(比较) |
| 7 | == != | 相等性 |
| 8 | & | 按位与 |
| 9 | ^ | 按位异或 |
| 10 | | | 按位或 |
| 11 | && | 逻辑与 |
| 12 | || | 逻辑或 |
| 13 | ?: | 条件(三目) |
| 14(最低) | = += -= 等 | 赋值 |
这张表的记忆要点:
- 单目 > 算术 > 移位 > 关系 > 位运算 > 逻辑 > 三目 > 赋值,整体呈"从紧到松"
&&高于||——所以a || b && c等于a || (b && c)- 三目和赋值都是右结合(从右往左算)
- 拿不准就加括号,括号永远合法
switch 语句——多路分支的更优选择
当需要判断的条件值有很多种可能时,if-else if-else 写一长串又丑又难读。switch 语句就是为这种场景设计的:
switch (表达式)
{
case 值1: 语句1;
case 值2: 语句2;
...
default: 默认语句;
}注意两个限制:(1) switch 后的表达式必须是整型表达式——浮点数和字符串不能用在 switch 里;(2) case 后的值必须是整型常量表达式(不能是变量)。
为什么不能是浮点数或字符串?因为 switch 的底层实现是"跳转表"或"逐值比较",而浮点数的相等比较本身就不可靠(上一篇刚讲过 0.1+0.2 的问题),字符串比较在 C 里又是一个复杂话题(strcmp)。所以 C 语言把 switch 限定在整型世界——这也是"什么场景用什么结构"的典型例子。
来看一个对比——根据 n % 3 的余数输出信息。if-else 版本:
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
/* if-else 写法 */
if (n % 3 == 0)
printf("整除,余数为 0\n");
else if (n % 3 == 1)
printf("余数是 1\n");
else
printf("余数是 2\n");
return 0;
}用 switch 改写:
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
switch (n % 3)
{
case 0:
printf("整除,余数为 0\n");
break; /* 跳出 switch,必不可少! */
case 1:
printf("余数是 1\n");
break;
case 2:
printf("余数是 2\n");
break;
}
return 0;
}switch 的执行机制——从入口标签开始
理解 switch 的关键在于:case 不是"独立的块",而是"入口标签"。程序计算出表达式值之后,会直接跳到匹配的 case 那一行开始执行——然后一直往下走,直到遇到 break 或 switch 结束。这解释了为什么漏掉 break 会"穿透"。
break 在 switch 中的作用
switch 中的 case 只是一个"入口标签",不是独立的分支。如果没有 break,代码会**穿透(fall-through)**到下一个 case 继续执行。
去掉 break 看看效果:
#include <stdio.h>
int main()
{
int n = 7; /* 7 % 3 = 1,按说只走 case 1 */
switch (n % 3)
{
case 0:
printf("整除,余数为 0\n");
/* 没有 break!会继续往下走 */
case 1:
printf("余数是 1\n");
/* 没有 break!会继续往下走 */
case 2:
printf("余数是 2\n");
/* 所有 case 都被执行了! */
}
return 0;
}
/* 输出:
* 余数是 1
* 余数是 2
* ——"穿透"了!*/实际开发中,可以利用"穿透"来让多个 case 共享同一段代码——比如工作日和休息日的判断:
#include <stdio.h>
int main()
{
int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
case 2:
case 3:
case 4:
case 5:
printf("工作日\n");
break; /* 1~5 都走这里,然后跳出 */
case 6:
case 7:
printf("休息日\n");
break; /* 6、7 都走这里,然后跳出 */
default:
printf("输入错误\n");
break;
}
return 0;
}这种"多个 case 共享执行体"的模式在实际开发中很常见——代码简洁、逻辑清晰。
注意:这种"故意穿透"是有意为之,必须在注释里写明白(比如 /* fall through */),否则读代码的人会以为你漏了 break。编译器(比如 GCC 的 -Wimplicit-fallthrough)也提供了检查,帮助区分"故意穿透"和"忘记 break"。
default 子句——处理"意料之外"的值
当 switch 的表达式的值和所有 case 都不匹配时,程序会跳到 default。它不一定要放在最后——可以放在任意位置——但惯例是放在末尾:
#include <stdio.h>
int main()
{
int day = 0;
scanf("%d", &day);
switch (day)
{
case 1: printf("星期一\n"); break;
case 2: printf("星期二\n"); break;
case 3: printf("星期三\n"); break;
case 4: printf("星期四\n"); break;
case 5: printf("星期五\n"); break;
case 6: printf("星期六\n"); break;
case 7: printf("星期天\n"); break;
default:
printf("输入错误,请输入 1~7\n");
break;
}
return 0;
}养成写 default 的习惯——即使你认为所有情况都已覆盖。default 是程序的"兜底网":万一用户输入了意外值,程序至少有明确的响应,而不是静默地什么都不做。很多健壮性就是这么一点点堆出来的。
switch 里 case 后的大括号
如果某个 case 里的逻辑比较复杂,需要定义局部变量,必须加大括号,否则编译器报错("case 后不能直接定义变量"或跳过了初始化):
switch (op)
{
case 1:
{
int temp = 100; /* 需要用大括号包住才能定义变量 */
printf("%d\n", temp);
break;
}
case 2:
printf("无需变量\n");
break;
}实战:简单计算器
下面是一个综合练习——根据用户输入的运算符执行对应的运算,把今天学的全部串联起来:
#include <stdio.h>
int main()
{
double num1 = 0.0, num2 = 0.0; /* 两个操作数 */
char op = 0; /* 运算符 */
printf("请输入算式(如 3+5):");
scanf("%lf %c %lf", &num1, &op, &num2);
/* %lf 读取 double,%c 读取字符 */
switch (op)
{
case '+':
printf("%.2lf + %.2lf = %.2lf\n",
num1, num2, num1 + num2);
break;
case '-':
printf("%.2lf - %.2lf = %.2lf\n",
num1, num2, num1 - num2);
break;
case '*':
printf("%.2lf * %.2lf = %.2lf\n",
num1, num2, num1 * num2);
break;
case '/':
if (num2 != 0.0) /* 防止除零错误 */
printf("%.2lf / %.2lf = %.2lf\n",
num1, num2, num1 / num2);
else
printf("错误:除数不能为 0!\n");
break;
default:
printf("不支持的运算符:%c\n", op);
printf("支持的运算符:+ - * /\n");
break;
}
return 0;
}这个计算器虽然只有几十行,但用到了 switch、case、break、default,以及除法前判断除数的防御性编程——你已经有能力独立写出这样的程序了。
注意 scanf("%lf %c %lf", ...) 里 %c 前面的空格:它跳过了 3 和 + 之间的空格。如果写成 scanf("%lf%c%lf", ...),输入 3 + 5(带空格)时 %c 会读到空格而不是 +——这就是上一篇讲的 %c 不跳过空白字符的坑,在真实程序里立刻就会咬你一口。
if-else if 与 switch 的选择
关于 if-else if 和 switch 的选择,一个简单的原则:当判断同一个变量的不同取值时用 switch(如星期几、菜单选项),当条件涉及范围判断或不同变量时用 if-else(如温度区间、组合条件)。两种都能实现分支,选更清晰的那个。
补充一个细节:switch 在条件分支很多时,编译器可能生成"跳转表"(直接算出跳转位置),执行效率比一串 if-else 的逐个比较更高。这是 switch 的隐藏优势——不过对现在的你来说,可读性才是第一位的。
本篇思考题
- 判断下面代码的输出,并解释原因:
int a = 1, b = 2, c = 3; if (a = b) printf("%d", c);(注意a = b是赋值!) - 为什么
if (18 <= age <= 36)恒为真?正确的写法是什么? - 悬空 else 问题的根源是什么?用什么编码习惯可以一劳永逸地避开它?
int x = 5, y = 0; if (x || y++)执行后y的值是多少?如果把||换成|呢?switch里忘记写break会发生什么?"故意穿透"的正确姿势是什么?- 用
if-else if和switch各写一个判断 1~7 对应星期几的程序,对比两种写法的优劣。 - 验证:
int a = 3, b = 4, c = 5;计算a < b && b < c、a < b || c < b、!(a > b)的结果分别是多少?
思考题参考答案与详解
第 1 题:输出 3。if (a = b) 是赋值而不是比较:它把 b 的值(2,即上面声明的 b = 2)赋给 a,a 变成 2,并且这个赋值表达式本身的值就是 2(非 0 即真),所以条件成立,执行 printf("%d", c),输出 c 的值 3。想判断相等应写 a == b。
第 2 题:因为 18 <= age <= 36 会被拆成两步:先算 (18 <= age),得到 0 或 1;再用这个 0 或 1 和 36 比较——无论结果是 0 还是 1,都比 36 小,条件恒为真。正确写法是用逻辑与连接两个独立条件:age >= 18 && age <= 36。
第 3 题:根源是 C 语言的语法规则——else 总是和离它最近的那个未配对的 if 匹配,而缩进只是给人看的,编译器完全不看缩进,所以"看起来对齐"的 else 实际可能配给了内层 if。要一劳永逸地避开,只需始终用大括号 {} 明确表达 if/else 的归属,不要依赖缩进来暗示逻辑。
第 4 题:y 的值是 0。因为 x = 5 非 0 即真,|| 触发短路求值,右边的 y++ 根本不会执行,所以 y 保持最初的 0。如果把 || 换成按位或 |:| 没有短路,两边都必定执行,于是 y++ 执行,y 变成 1。
第 5 题:忘记写 break 时,switch 在处理完匹配的 case 后,会穿透(fall-through)到后面的 case 继续执行,直到遇到 break 或 switch 结束——于是多个分支被"连锁"执行。要"故意穿透",正确姿势是把多个 case 标签堆叠在一起共享同一段代码,例如:
case 1:
case 2:
case 3:
printf("工作日\n");
break;并在共享代码处加注释(如 /* fall through:1~5 共用此分支 */),让读代码的人明白这是有意的,而不是漏了 break。
第 6 题:if-else if 版本:
#include <stdio.h>
int main()
{
int day = 0;
scanf("%d", &day);
if (day == 1) printf("星期一\n");
else if (day == 2) printf("星期二\n");
else if (day == 3) printf("星期三\n");
else if (day == 4) printf("星期四\n");
else if (day == 5) printf("星期五\n");
else if (day == 6) printf("星期六\n");
else if (day == 7) printf("星期天\n");
else printf("输入错误\n");
return 0;
}switch 版本:
#include <stdio.h>
int main()
{
int day = 0;
scanf("%d", &day);
switch (day)
{
case 1: printf("星期一\n"); break;
case 2: printf("星期二\n"); break;
case 3: printf("星期三\n"); break;
case 4: printf("星期四\n"); break;
case 5: printf("星期五\n"); break;
case 6: printf("星期六\n"); break;
case 7: printf("星期天\n"); break;
default: printf("输入错误\n"); break;
}
return 0;
}对比:当判断的是同一个变量的多个离散取值时,switch 结构更清晰直观(一眼看出有几个分支、有兜底项),且编译器对分支多的情况可能生成跳转表、执行更快;if-else if 更适合范围判断(如温度区间、成绩段)或由多个不同变量组合起来的条件。
第 7 题:
a < b && b < c:3<4得 1,4<5得 1,1 && 1 = 1;a < b || c < b:3<4得 1,5<4得 0,1 || 0 = 1;!(a > b):3>4得 0,!0 = 1。
三个表达式的结果都是 1。
小结
从最基础的 if 开始,一路走到 else if 多路嵌套、悬空 else 问题、六个关系运算符(记住 == 不是 =,不能连用!)、条件操作符 ?: 的简写、三个逻辑运算符 ! && || 的规则和短路特性,再到 switch 语句的穿透与 default——C 语言的分支决策体系就这样建立起来了。
这一篇里反复出现的主题是"编译器只看语法,不看你的意图":else 匹配最近的 if、关系运算符连用被拆解、switch 没有 break 就穿透——每一个坑都源于"人以为"和"编译器实际做"之间的偏差。而唯一的解药就是:写明确、写规范、用大括号、别炫技。
下一篇我们将进入流程控制的另一半——循环。while、for、do-while 三种循环方式,break 和 continue 的控制技巧,以及一个能把你前面所有知识融会贯通的项目:猜数字游戏。准备好了吗?
还没有评论 — 第一条由你来留。