C语言期末速成5:程序的三大基本结构
顺序、选择、循环三大基本结构:短路求值、if/switch、三种循环与经典算法题型。
本文是《C语言期末速成》系列第 5 篇。第 1 篇说过:任何程序都只是顺序、选择、循环三种结构的嵌套组合。本篇把它们一次性讲透——顺序结构(执行次序)、选择结构(
if/switch与条件判断)、循环结构(for/while/do-while)。这一章是算法题的主战场,内容最多,请耐心啃。
考点清单
- C 语言用非 0 表示真,0 表示假;没有专门的”逻辑类型”
- 关系表达式的值只能是 1(真)或 0(假)
- 逻辑运算:
!>&&>||;短路求值是必考 - 表示”x 在 0 和 10 之间”必须写
(0 < x) && (x < 10) else与最近的、没有 else 的if配对(悬空 else)if只管后面一个语句,管多条要用{}括起来- 条件表达式:
条件 ? 表达式2 : 表达式3,口诀”真前假后” switch的 case 匹配后没有 break 会继续穿透执行;switch只跟break配,不跟continue配- 循环三种:
while、do-while(至少执行一次)、for(两个分号不能少) break退出一层循环;continue结束本次循环进入下一次while((c = getchar()) != '\n')中的括号不能省(!=优先级高于=)
第一部分:顺序结构
顺序结构最朴素——从上到下,逐条执行。它不需要任何关键字,大括号 { } 只是把多条语句”打包”成一条复合语句。
int a = 5; /* 第 1 条 */
int b = a * 2; /* 第 2 条:此时 a 已知 */
printf("%d", b); /* 第 3 条 */
注意语句顺序影响结果:
int n = 1;
printf("%d\n", n); /* 1:先打印,后修改 */
n = n + 1;
printf("%d\n", n); /* 2 */
如果两条打印的顺序换一下,输出就反了——顺序结构的核心就是”位置决定执行时机”。
空语句:单独一个分号 ; 算一条什么也不做的语句。它合法,但滥用会造成逻辑错误(例如 while(x>0); 后面多加分号会变成死循环,见循环部分)。
第二部分:选择结构
一、关系表达式:结果为 1 或 0
关系运算符 <、<=、>、>=、==、!= 比较两个值,结果只能是 1(真)或 0(假):
9 > 8 结果为 1 /* 真 */
7 < 6 结果为 0 /* 假 */
5 == 5 结果为 1
5 != 5 结果为 0
必考陷阱:x < y < z 不等于数学上的”y 在 x 和 z 之间”。因为关系运算从左到右、且前一步的结果参与后一步:
int x = 1, y = 0, z = 2;
x < y < z;
/* 第一步:x < y → 1 < 0 → 假 → 0 */
/* 第二步:0 < z → 0 < 2 → 真 → 1 */
/* 整个表达式的结果是 1(真)! */
从数学角度”1<0<2”显然不成立,但 C 里它确实算出 1。原因:< 是双目运算,只能两两比较,x<y<z 实际是 (x<y)<z。想表达区间判断,必须用逻辑与(见下节):
(0 < x) && (x < 10) /* 正确:0 < x < 10 */
二、逻辑表达式:&& || ! 与短路求值
&&(与):两边都真才真;||(或):有一边真就真;!(非):真变假、假变真。
优先级:! > && > ||(! 最高,|| 最低)。
1 && 1 → 1 1 && 0 → 0
1 || 0 → 1 0 || 0 → 0
!0 → 1 !5 → 0 /* 5 非 0 视为真,! 后为 0 */
短路求值(超高频考点)
&& 和 || 有**“见好就收”**的特性:
A && B:若A为假,结果必假,B根本不会被计算;A || B:若A为真,结果必真,B根本不会被计算。
int x = 0;
printf("%d\n", (x != 0) && (10 / x > 1));
/* x != 0 为假,短路!10/x 不会被计算,避免了除零错误 */
短路最常见的考法是看变量有没有被赋值:
int a = 0, b = 0;
int r = (a = 1) || (b = 2);
/* a=1 为真,|| 短路,b=2 不执行 → b 还是 0,r = 1 */
int a = 0, b = 0;
int r = (a = 0) && (b = 2);
/* a=0 为假,&& 短路,b=2 不执行 → b 还是 0,r = 0 */
三、if 语句:四种经典形态
① 单分支:if 只管后面一条语句,管多条要用 {}。
if (a < b) t = a; /* 只管一条 */
if (a < b) { t = a; a = b; b = t; } /* 用大括号管三条 */
必考对比:
if (a < b) t = a;
a = b; b = t; /* 这三条只有第一条受 if 控制,后两条无条件执行 */
if (a < b) { t = a; a = b; b = t; } /* 三条都受 if 控制 */
② 双分支:
if (a < b) min = a;
else min = b;
③ 嵌套:if 里再套 if。
if (a < b)
if (b > c) printf("ok!\n");
④ 多分支(if-else if-else):
if (score >= 90) printf("A");
else if (score >= 80) printf("B");
else if (score >= 60) printf("C");
else printf("D");
悬空 else(必考)
else总是与最近的、且没有配对的if结合。
if (a > 0)
if (b > 0) printf("A");
else
printf("B");
/* 这个 else 配对的不是外层 if,而是内层 if(b>0) */
用缩进欺骗不了编译器——else 永远找最近的 if。想让它配对外层,必须加 {}:
if (a > 0) {
if (b > 0) printf("A");
} else {
printf("B");
}
综合推导题(亲测经典)
int a = 1, b = 0;
if (!a) b++;
else if (a == 0)
if (a) b += 2;
else b += 3;
printf("%d", b);
逐步推导:
!a→!1→ 0(假),b++不执行;- 进入
else if (a == 0)→1 == 0→ 假,这一支也不成立; - 但注意:
if (a) b+=2; else b+=3;是挂在else if (a==0)下面的嵌套结构。整个if-else if-else链中没有任何条件成立; - 由于链条没有匹配,最后那个
else b += 3;执行 →b = 3。
答案:b = 3。这题的坑在于缩进误导人,记住”else 找最近的 if”+“链条最后落 else”就能解。
四、条件表达式(三目运算)
语法:表达式1 ? 表达式2 : 表达式3
- 表达式 1 为非 0(真)→ 整个式子的值取表达式 2;
- 表达式 1 为 0(假)→ 整个式子的值取表达式 3。
口诀:真前假后(真的时候取前面的,假的时候取后面的)。
int a = 3, b = 5;
int m = a > b ? a : b; /* a>b 假 → 取 b → m = 5 */
多级嵌套(从右往左读):
int a=1, b=2, c=3, d=4, e=5;
int k = a > b ? c : d > e ? d : e;
/* 先算最右:d>e → 4>5 → 假 → 取 e=5 */
/* 再算外层:a>b → 假 → 取 5 */
/* k = 5 */
条件表达式是”简单 if 的压缩写法”,适合赋值,但嵌套超过两层可读性就差了。
五、switch 语句:多路选择的另一只拳头
switch (表达式) {
case 常量1: 语句...;
case 常量2: 语句...;
...
default: 语句...;
}
要点:
switch(表达式)里的表达式只能是整型、字符型或枚举型;case后面必须是常量(不能是变量);- 匹配到某个
case后顺序执行下去——如果后面没有break,会继续执行后续所有 case(这就是”穿透”); - 有
break则立刻跳出 switch。
case 穿透(必考):
int x = 2;
switch (x) {
case 1: printf("one\n");
case 2: printf("two\n");
case 3: printf("three\n");
default: printf("other\n");
}
/* 输出:two three other(没有 break,一路穿透到 default) */
加上 break 才各管各的:
switch (x) {
case 1: printf("one\n"); break;
case 2: printf("two\n"); break;
default: printf("other\n");
}
/* x=2 时只输出 two */
break 是”分手一刀两断”,continue 是循环专属——switch 里只能配 break,不能配 continue(continue 只能出现在循环里)。
switch 与 if 的取舍(加深):都能实现多分支,但 switch 只适合对单个整型值的精确匹配;涉及范围比较(如 score >= 90)就必须用 if-else。case 常量还必须是唯一的。
default 的位置:default 习惯放在最后,但放哪都合法——当所有 case 都不匹配时执行它。注意穿透也作用于 default:若某个 case 没有 break,会一路执行到 default。
经典星期表示例:
switch (day) {
case 1: printf("星期一"); break;
case 2: printf("星期二"); break;
case 3: printf("星期三"); break;
case 4: printf("星期四"); break;
case 5: printf("星期五"); break;
case 6: printf("星期六"); break;
case 7: printf("星期日"); break;
default: printf("错误:day 应在 1~7 之间"); break;
}
第三部分:循环结构
一、三种循环
① while:先判断,后执行(可能一次都不执行)
int i = 1;
while (i <= 5) {
printf("%d ", i);
i++;
}
/* 输出 1 2 3 4 5 */
② do-while:先执行一次,再判断(至少执行一次)
int i = 10;
do {
printf("%d\n", i);
i++;
} while (i < 5); /* 条件为假,但 do 已经执行了一次 */
/* 输出 10 */
必考点:do-while 最后的 ; 不能丢,丢了就是语法错误。它保证循环体至少执行一次——适合”至少得做一遍”的场景(如密码输入,至少输一次)。
③ for:循环三要素写在一行
for (初始化; 条件; 更新) {
循环体;
}
for (i = 1; i <= 5; i++)
printf("%d ", i);
两个分号不能少(即使某一项为空也要写)。for(;;) 是合法写法——三个部分全空,条件为空视为”永远为真”,就成了无限循环,等价于 while(1):
for (;;) { ... } /* 无限循环,只能靠 break/return 退出 */
while (1) { ... } /* 与 for(;;) 等价 */
for 和 if 一样,只管后面一条语句:
for (i = 3; i < 6; i++)
printf("#"); /* 只这一个 printf 被循环控制 */
printf("\n"); /* 这行只执行一次 */
二、break 与 continue:循环的两个刹车
| 关键字 | 作用 | 效果 |
|---|---|---|
break | 打破整层循环 | 立即退出当前这一层循环 |
continue | 继续本层循环 | 结束本次迭代,跳到条件判断开始下一轮 |
for (i = 1; i <= 5; i++) {
if (i == 3) break; /* i=3 时跳出循环 */
printf("%d ", i);
}
/* 输出 1 2 */
for (i = 1; i <= 5; i++) {
if (i == 3) continue; /* i=3 时不打印,但继续循环 */
printf("%d ", i);
}
/* 输出 1 2 4 5 */
break 只退出”一层”:嵌套循环里,break 只跳出它所在的那一层,外层循环照常继续。要跳出多层得用标志变量或 goto(一般不建议)。
三、嵌套循环
循环里套循环,外层转一圈、内层转一整圈:
for (i = 1; i <= 3; i++) { /* 外层 3 圈 */
for (j = 1; j <= 2; j++) /* 内层每圈跑 2 次 */
printf("%d%d ", i, j);
printf("\n");
}
/* 11 12 21 22 31 32 */
记忆:两层循环通常用来处理二维数组(第 7 章)。嵌套循环里 break 只断内层。
四、经典循环题型(必背套路)
① 逆序输出一个整数(取个位 + 去个位循环):
int n = 123;
while (n != 0) {
printf("%d", n % 10); /* 打印个位 */
n = n / 10; /* 去掉个位 */
}
/* 输出 321 */
② 每行输出 5 个:
for (i = 0; i <= 100; i++) {
printf("%d ", i);
if ((i + 1) % 5 == 0) printf("\n"); /* i 从 0 开始用 i+1 */
}
若 i 从 1 开始,则判断 if (i % 5 == 0)。整除判断 i % 5 == 0、偶数 i % 2 == 0 是通用技能。
③ 求最大公约数(辗转相除):
int a = 48, b = 36;
while (b != 0) {
int t = a % b;
a = b;
b = t;
}
printf("%d\n", a); /* 12 */
④ 水仙花数(三位数,各位立方和等于本身):
for (n = 100; n < 1000; n++) {
int a = n / 100, b = n / 10 % 10, c = n % 10;
if (a*a*a + b*b*b + c*c*c == n)
printf("%d ", n);
}
/* 输出 153 370 371 407 */
五、while 的边界陷阱:--k vs k--(必考)
int k = 1;
while (--k); /* 先减再判:k 变 0,条件为假,退出 */
printf("%d\n", k);/* 0 */
int k = 1;
while (k--); /* 先判(k 为 1,真)再减;第二次判断用 0,仍为真(!0 才假?) */
printf("%d\n", k);
等等,while(k--) 里 k 会一直减到什么时候?k-- 是先取 k 的旧值判断真假,再把 k 减 1:
- k=1 → 条件取 1(真)→ k 变 0;
- k=0 → 条件取 0(假)→ 循环退出,但 k 已经自减成 -1。
所以输出是 -1。这正是**“后置自减在条件判断里会多减一次”**的经典坑。
六、getchar 与循环:注意 != 的优先级
从键盘持续读字符直到遇到特定字符:
char c;
while ((c = getchar()) != '\n') /* 读到换行停止 */
putchar(c);
括号必须加:因为 != 优先级高于 =,不加括号会变成:
while (c = (getchar() != '\n')) /* 先算 getchar()!='\n',再把结果 1/0 赋给 c */
两者语义完全不同。类比 a = 3 != 2:
a = 3 != 2→ 先算3!=2(真=1),a = 1;(a = 3) != 2→ 先赋值a = 3,再比较3 != 2(真=1)。
!= 的优先级高于 =,想先赋值必须加括号。同样的模式可以读入直到遇到 # 或 $ 停止:
while ((c = getchar()) != '#') putchar(c); /* 输入遇到 # 停止 */
第四部分:三大结构的组合
顺序、选择、循环可以任意嵌套,例如”循环里套选择”完成”输出 1~100 的偶数”:
for (int i = 1; i <= 100; i++) { /* 循环 */
if (i % 2 == 0) /* 选择 */
printf("%d ", i); /* 顺序:打印 */
}
做题时遇到复杂的嵌套,逐层拆解:先看外层循环转几次,再算每圈内层(选择/循环)做什么,不要跳步。
易错点 / 陷阱
0 < x < 10表达区间:它是(0<x)<10,结果恒为真,必须用(0<x)&&(x<10)。==写成=:if (a = 1)是赋值,条件恒为真,且把 a 改成 1。- 悬空 else:
else永远配最近的if,想改配必须加{}。 - switch 忘写 break:没有 break 会穿透,执行后续所有 case。
- do-while 忘写分号:结尾
while(...)后面必须有;。 while(--k)与while(k--)混淆:后者条件判断用旧值 0 时其实已多减一次,结果 -1。while(c=getchar()!='\n')忘括号:!=优先,先比较再赋值,语义反转。- break 想退出多层:break 只退一层。
- for 只管一条:
for(...) 语句A; 语句B;只有 A 受循环控制。 - continue 用在 switch:
continue只能在循环里,switch 只能配break。
自测小练习
第 1 题:int x=1, y=0, z=2; 求 x<y<z 的值。
第 2 题:int a=0, b=5; 求 (a=0) && (b=10) 后 a、b 的值。
第 3 题:int k=1; while(k--); printf("%d", k); 输出什么?
第 4 题:写出判断某年是否为闰年的表达式(能被 4 整除但不能被 100 整除,或能被 400 整除)。
第 5 题:switch(x){ case 1: printf("A"); case 2: printf("B"); break; default: printf("C");} 当 x=1 时输出什么?
参考答案
1<0为假(0),0<2为真(1)→ 结果是 1。a=0为假,&&短路,b=10不执行 → a=0, b=5,表达式值 0。- k 从 1 判断真减到 0,条件取 0 为假退出,但后置自减再减一次 → 输出 -1。
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)。- 匹配 case 1,无 break 穿透到 case 2 打印 B,遇到 break 退出 → 输出
AB。
下一篇:C语言期末速成 6:函数