很多初学C语言的朋友,学到“分支和循环”这一章时,会有一种很微妙的卡壳感。前面的变量、数据类型、printf和scanf,怎么都是“写一行、执行一行”,到了if和while这里,代码突然“不听话”了——有时候部分语句不执行,有时候又反复执行。我当年也在这个位置卡过不少时间,后来才意识到,分支和循环不是两个孤立的新知识点,它是你从“写命令”升级到“写程序”的第一道分水岭。
这篇是C语言系列第3篇的上半部分,我打算把分支结构(if、else if、switch)和循环里最基础的两个兄弟(while、do-while)一次讲透,配合可复现的代码示例。内容适合完全零基础的初学者,也适合学过一遍但感觉逻辑没理顺、想重新梳理一遍的人。学完之后,你应该能自己写出“输入一个成绩判断等级”“用菜单循环操作”这类带点交互逻辑的小程序。
1. 从“直线代码”到“会拐弯的程序”:为什么这一章是分水岭
先看一个最朴素的C语言程序:它从main函数的第一行开始,一行一行往下执行,直到return 0。这就是顺序结构。代码本身没有任何悬念,计算机按顺序把每一行执行完,程序就结束了。
#include <stdio.h> int main(void) { int a = 10; int b = 20; int sum = a + b; printf("sum = %d\n", sum); return 0; }但真实世界里,几乎没有哪个需求是简单的“从上往下跑一遍”就能解决的。你做一个登录系统,要先判断用户名和密码是否正确;做一个计算器,要判断用户按了哪个运算符;统计一个班级的平均分,要反复读入每一个学生的成绩。这些场景背后,就是两件事:根据条件做选择,把同一件事重复做多次。前者叫分支,后者叫循环。
1.1 程序只会从上往下跑,判断和重复怎么来
这个道理听起来很简单,但初学者的很多困惑,恰恰是因为没把“程序从上往下跑”这句话放在心里。分支结构解决的是“走A还是走B”的问题,循环结构解决的是“这条路能不能返回去再走一遍”的问题。
打个比方:你每天出门上学,正常情况下是直线走到学校,这就是顺序。但走到路口发现下雨了,你决定回家拿伞,这是分支。到了操场要跑5圈,每跑完一圈就回到起点再跑一圈,这是循环。程序里的逻辑,本质上就是这三件事的排列组合。
代码层面,分支靠的是if、else、switch,循环靠的是while、do-while、for。这一篇先把前面几个摸透。for循环涉及的内容比较多,我打算放到下一篇“分支和循环(下)”里单独展开,否则一下子塞太多,容易消化不良。
1.2 两个必须完成的思维转换
从这一章开始,你脑子里有一些习惯要主动扭转过来。
第一,变量不只是用来存数据的,它还反过来决定程序的走向。之前你写int a = 10,只是让计算机记住这个数。现在你要习惯写if(a > 0),让a的值去影响接下来的流程。变量从“被动存储”变成了“主动指挥”。
第二,重复的活可以交给机器,前提是你要把“重复的规则”描述清楚。比如打印10行文字,没有循环的时候只能复制粘贴10次printf;有了循环,一句printf加一个循环条件就够了。但代价是,你得想清楚重复几次、从哪开始、什么时候停。这两个思维一旦建立,你才算正式开始“写程序”,而不是“抄代码”。
2. if 和 else:程序第一次有了“选择权”
2.1 if的判定规则:“非零即真”到底怎么理解
if语句的基本形式很简单:
if (条件) { 语句; }条件成立,就执行大括号里的内容;条件不成立,就跳过。所谓的“条件”,在C语言里其实就是一个表达式。而对表达式结果的判定规则只有一条:结果为0代表假,结果非0代表真。
很多初学者以为只有“成立”“不成立”这种说法,其实C语言没有专门的布尔类型(C89标准下),它就是用整数来代表真假。关系运算符比如a > b、score >= 60,运算结果只有1或0。但如果你写if(-1),这个条件也是真的,因为-1不是0。写if(a),只要a不等于0就执行。
这带来一个实用建议:判断条件尽量写清楚,不要刻意玩“非零即真”的缩写。比如判断指针是否有效,老手会写if(ptr),这是行业习惯;但判断一个数是否是0,你应该写if(a == 0)而不是if(!a),虽然两者等价,但后者读起来要绕一下。刚入门阶段,把条件表达式写直白,能少踩很多坑。
一个典型例子,判断奇偶数:
#include <stdio.h> int main(void) { int n; printf("请输入一个整数:"); scanf("%d", &n); if (n % 2 == 0) { printf("偶数\n"); } else { printf("奇数\n"); } return 0; }判断奇偶数用的是n % 2 == 0,%是取余运算,n % 2的结果只可能是0或1,等于0说明能被2整除,就是偶数。这个例子也顺便告诉你:if后面的括号里,完全可以写一个由运算符组合而成的表达式,而不是非要写一个单纯的变量。
2.2 else和哪个if配对,缩进说了不算
if和else配对,看起来简单,实际有很多人在这里踩过坑。C语言的规定是:else总是和它前面最近的一个尚未配对的if结合。缩进只是为了给人看,编译器根本不看缩进。
看这段代码:
if (a > 0) if (b > 0) printf("a和b都大于0\n"); else printf("a不大于0\n");单看缩进,else和第一个if对齐,很多人以为它属于第一个if,意思是“a不大于0时输出”。但实际上,最近的未配对if是if(b > 0),所以这个else属于第二个if。也就是说,当a > 0成立、b > 0不成立时,反而会打印“a不大于0”,完全不是你的本意。
修复办法就是加括号,把逻辑隔离清楚:
if (a > 0) { if (b > 0) { printf("a和b都大于0\n"); } } else { printf("a不大于0\n"); }这就是为什么我会强烈建议:只要if或else后面跟了语句,不管是一条还是多条,一律加大括号。这能避免两类问题:一是这种else配对歧义,二是以后往里加代码时,不会忘记加括号而改变逻辑。
2.3 用成绩等级判断把else if用熟
当条件不止一个时,可以连续写else if,把多个分支串起来。最常见的案例就是成绩等级判断:
#include <stdio.h> int main(void) { int score; printf("请输入成绩:"); scanf("%d", &score); if (score >= 90) { printf("优秀\n"); } else if (score >= 80) { printf("良好\n"); } else if (score >= 70) { printf("中等\n"); } else if (score >= 60) { printf("及格\n"); } else { printf("不及格\n"); } return 0; }这段代码有一个非常重要的顺序逻辑:判断顺序必须从高到低。如果先判断score >= 60,那么85分就会直接进入“及格”分支,后面的else if根本不会看,这显然不对。所以,区间型判断要注意每一层都覆盖了一部分可能值,并且边界值要仔细测试。
我建议你拿到这段代码后,不要只输入一个分数就完事,而是把90、80、70、60、59这几个值挨个试一遍。这个“拿边界值测试”的习惯,从今天开始就要养成,后面写循环、写数组时你会经常用到,能省去大量排查bug的时间。
3. switch 分支:离散选项的快捷通道,以及让人懵圈的case穿透
3.1 switch的语法约束:不是什么东西都能塞进case
switch语句解决的是“同一个变量有几个不同取值,每种取值走不同处理”的需求。它的语法是:
switch (表达式) { case 常量1: 语句; break; case 常量2: 语句; break; default: 语句; }C语言对case有几个硬性约束:case后面的值必须是整型常量表达式,可以是int、char、枚举,但不能是浮点数、字符串,也不能是变量。比如case 3.14、case "a"都会报错。switch的工作方式是:先计算switch括号里表达式的值,然后从上往下与各个case匹配,找到匹配项后,从那里开始执行。
执行过程中如果遇到break,直接跳出整个switch;如果没遇到break,就会继续执行后面的case。这就是下一小节要说的case穿透。另外,同一个switch里,case的值不能重复,default可以放在任意位置,但通常放在最后,表示“以上都不匹配时走这里”。
那么switch和if-else怎么选择?我的经验是:如果判断条件是大小比较、范围区间,比如成绩区间,用if-else;如果是一个变量有好几个离散的固定取值,比如菜单选项、状态码、字符按键,用switch会让代码清晰很多。switch还有一个if没有的优势——可读性,别人一眼就能看到你处理了哪几种情况。
3.2 掉进case穿透坑里的完整现场
下面这段代码,很多人第一次运行都会惊到:
#include <stdio.h> int main(void) { int x = 2; switch (x) { case 1: printf("数字1\n"); case 2: printf("数字2\n"); case 3: printf("数字3\n"); default: printf("其他数字\n"); } return 0; }运行结果是:数字2、数字3、其他数字,三行全部打印。因为x=2匹配到了case 2,执行printf("数字2\n"),然后没有break,程序继续往下走,把case 3和default也都执行了。这个现象就叫case穿透。
底层原因是switch本质上是根据case值生成的一个跳转表,跳到对应入口之后,如果没有break把流程截断,代码就会一路跑到底。理解这一点之后,你就能明白为什么“忘写break”是switch最常见的bug了。
有时候我们也会故意利用穿透,比如多个case要执行完全相同的代码:
switch (grade) { case 'A': case 'B': printf("成绩不错\n"); break; case 'C': printf("还要努力\n"); break; default: printf("输入不合法\n"); }这里case 'A'和case 'B'共用同一个打印。注意:case 'A'后面没有break,直接落到case 'B',逻辑上就是A和B都执行同一段代码。这种利用是安全的,但需要在代码里写清楚注释,否则后来维护的人挺容易误会。
3.3 用switch做菜单:一个能立刻玩起来的例子
switch最常见的应用场景就是菜单程序。用户输入一个数字,程序根据数字执行不同功能,直到用户选择退出。这里可以先用do-while做外层循环,后面我们会详细讲do-while,先感受一下组合的威力:
#include <stdio.h> int main(void) { int choice; do { printf("===== 学生成绩管理菜单 =====\n"); printf("1. 录入成绩\n"); printf("2. 查询成绩\n"); printf("3. 统计平均分\n"); printf("0. 退出\n"); printf("请选择:"); scanf("%d", &choice); switch (choice) { case 1: printf("你选择了录入成绩\n"); break; case 2: printf("你选择了查询成绩\n"); break; case 3: printf("你选择了统计平均分\n"); break; case 0: printf("感谢使用,再见\n"); break; default: printf("无效选项,请重新选择\n"); } } while (choice != 0); return 0; }这个程序里,switch的分支很清楚,default负责处理0以外的非法输入。do-while保证菜单至少显示一次,并且只要用户没有选择0,菜单就会反复出现。这就是实际项目里很常见的主菜单结构。你可以把菜单项替换成加减乘除计算器、通讯录增删查改的入口,框架都能直接复用。
4. while 与 do-while:循环里的第一对兄弟,差别不止一个分号
4.1 while是“先看条件后干活”,可能一次都不干
循环是什么?一句话:只要条件为真,循环体就一遍又一遍地执行。while的标准形态:
while (条件) { 语句; }执行过程是:检查条件,条件为真就执行循环体,执行完再回来检查条件;如果某一次检查时条件为假,就跳到循环后面的代码。一个容易忽略的细节是,如果一开始条件就是假的,那么循环体一次都不会执行。
用while求1到100的和:
#include <stdio.h> int main(void) { int sum = 0; int i = 1; while (i <= 100) { sum = sum + i; i = i + 1; } printf("1加到100的结果是:%d\n", sum); return 0; }这里有三要素:i = 1是循环变量的初始值;i <= 100是循环继续的条件;i = i + 1是让条件最终变成假的更新。很多初学者写循环忘记最后一行,结果i一直等于1,i <= 100永远为真,程序就死循环了,CPU风扇直接起飞。所以写while时,先问自己:循环里的哪一行,会让条件在未来的某一次检查中变成假?
你可能会问:为什么sum = sum + i不能反过来写成i = i + 1在前?其实先更新i再累加也可以,只是求和结果就变成2到101了。顺序不影响语法,但影响逻辑,这正是我们需要用调试或printf观察中间值的原因。
4.2 do-while是“先干一次再看条件”,菜单输入很常用
do-while的语法:
do { 语句; } while (条件);注意末尾有个分号,这是初学者最容易漏掉的。执行过程是:先执行一次循环体,然后检查条件,条件为真就再执行一次。所以do-while至少会执行一次循环体。当你的业务逻辑要求“无论条件是否满足,至少先做一次”时,用do-while最自然。
举一个实际例子:用户在按键输入y/n确认时,你必须先让他输一次,如果输了无效字符,再提示重新输入。用do-while写:
char ch; do { printf("确定要退出吗?(y/n):"); scanf(" %c", &ch); } while (ch != 'y' && ch != 'Y' && ch != 'n' && ch != 'N');这里scanf的%c前面加了一个空格,是为了把上一次输入后留在缓冲区里的换行符吃掉。这个小细节在写过几个输入程序后就会碰到,先记下来。
while和do-while的区别,我用表格总结一下:
| 对比点 | while | do-while | | 判断时机 | 先判断,后执行 | 先执行,后判断 | | 最少执行次数 | 0次 | 1次 | | 循环体结束时 | 条件不满足,跳出 | 判断条件,再决定是否回去 | | 典型应用 | 处理未知长度的输入 | 菜单、输入校验 |
这个表格看起来简单,但真的决定了很多代码该用哪个循环。一个通用的判断法则是:如果你不能确定条件第一次检查时是否成立,用while;如果业务逻辑上必须至少执行一次,用do-while。
4.3 差一错误和死循环:两个必踩的坑与排查方法
循环最容易出两类问题:差一错误和死循环。
先看差一错误。你想让某个动作恰好执行5次,用i < 5还是i <= 5?如果初始i=0,i