知识点 04 · 控制结构¶
本页最后更新于 ,内容可能过时
1 · switch 穿透(fall-through)¶
#include <stdio.h>
int main()
{
char *s = "120119110";
int n0, n1, n2, nn, i;
n0 = n1 = n2 = nn = i = 0;
do {
switch (s[i++]) {
default: nn++;
case '0': n0++;
case '1': n1++;
case '2': n2++;
}
} while (s[i]);
printf("n0=%d,n1=%d,n2=%d,nn=%d\n", n0, n1, n2, nn);
return 0;
}
写出程序运行结果。
答案
n0=3,n1=8,n2=9,nn=1
解析
必背的一句话:switch 匹配到某个 case 后,会从那里一路往下执行,直到遇到 break 或 switch 结束——case 标签只是入口,不是边界。
所以 default 不一定要放最后:本题它就写在最前面,一样会向下穿透。
本题穿透顺序是 default → case '0' → case '1' → case '2',每种字符的"落点"是:
| 字符 | 从哪个标签进入 | 会执行的自增 |
|---|---|---|
'0' |
case '0' |
n0++、n1++、n2++ |
'1' |
case '1' |
n1++、n2++ |
'2' |
case '2' |
n2++ |
其它(本题的 '9') |
default |
nn++、n0++、n1++、n2++ |
字符串 "120119110" 共 9 个字符,逐轮统计:'1' 出现 5 次、'0' 出现 2 次、'2' 出现 1 次、'9' 出现 1 次。
| 第几轮 | s[i++] 取到 |
执行的自增 |
|---|---|---|
| 1 | '1' |
n1=1, n2=1 |
| 2 | '2' |
n2=2 |
| 3 | '0' |
n0=1, n1=2, n2=3 |
| 4 | '1' |
n1=3, n2=4 |
| 5 | '1' |
n1=4, n2=5 |
| 6 | '9' |
nn=1, n0=2, n1=5, n2=6 |
| 7 | '1' |
n1=6, n2=7 |
| 8 | '1' |
n1=7, n2=8 |
| 9 | '0' |
n0=3, n1=8, n2=9 |
n0= 2(来自'0')+ 1(来自'9')= 3n1= 5('1')+ 2('0')+ 1('9')= 8n2= 5('1')+ 1('2')+ 2('0')+ 1('9')= 9nn= 1
易错:① 以为
default必须放最后、或以为它不穿透;② 忽略do-while的终止条件while(s[i])是读完最后一个字符后判断的(此处s[9]是'\0',正好结束,不会越界)。补充结论:
switch(表达式)的表达式必须是整型/字符型/枚举,不能是浮点型、字符串。
2 · switch 穿透(fall-through)¶
#include <stdio.h>
int main()
{
int i = 0, v1 = 0, v2 = 1, v3 = 2;
for (i = 1; i < 10; i++) {
switch (i % 4) {
case 1: v1++;
case 2: v2++; break;
default: v3++;
}
}
printf("%d,%d,%d\n", v1, v2, v3);
return 0;
}
写出程序运行结果。
答案
3,6,6
解析
注意初值是 v1=0, v2=1, v3=2(不是全 0,这是本题第一大坑)。
i 从 1 到 9,i%4 的取值:
i%4 |
对应的 i |
执行的语句 | 次数 |
|---|---|---|---|
| 1 | 1, 5, 9 | v1++ 然后穿透 v2++,break |
3 |
| 2 | 2, 6 | v2++,break |
2 |
| 3 | 3, 7 | v3++ |
2 |
| 0 | 4, 8 | v3++ |
2 |
v1= 0 + 3 = 3v2= 1 + 3 + 2 = 6v3= 2 + 2 + 2 = 6
易错:把
v1=v2=v3=0当默认初值,算出3,5,4。另外注意
case 3和default都没写break,但它们是switch的最后一段,穿透"落到"}就结束,结果与写break相同(这也是它容易骗人的地方)。
3 · switch 穿透(fall-through)¶
#include <stdio.h>
int main()
{
int x = 2, y;
do
switch (x) {
case 1: y = 0;
case 2: y = 1;
case 3: x += 2; continue;
case 4: x += y--;
default: x++;
} while (y >= 1);
return 0;
}
该程序执行完毕后,x 的值为( )。
- A. 3
- B. 4
- C. 5
- D. 6
答案
D(x = 6)
解析
这是 continue 与 switch 配合的经典陷阱——switch 不是循环语句,所以 continue 跳过的是外层 do-while 的剩余部分,直接去判断 while(y>=1)。
第 1 轮:x = 2 → 进入 case 2
y = 1- 穿透到
case 3:x += 2→x = 4 continue→ 跳过case 4/default,直接判断y >= 1→1 >= 1为真,继续循环
第 2 轮:x = 4 → 进入 case 4
x += y--→ 先用y的值 1,x = 4 + 1 = 5,然后y变成 0- 穿透到
default:x++→x = 6 - 判断
y >= 1→0 >= 1为假 → 退出循环
最终 x = 6 ✅
易错:① 以为
continue只影响switch;② 忽略case 4会穿透到default。 记牢:continue永远作用于最近的外层循环。
4 · do-while 与"循环条件恒真"陷阱¶
#include <stdio.h>
int main()
{
int y, x = 3;
do {
y = x--;
if (!y) {
printf("*"); break;
}
printf("#");
} while (1 <= x <= 2);
}
选项正确的是( )。
- A. 输出
###* - B. 输出
### - C. 输出
## - D. 以上都不对
答案
A(输出 ###*)
解析
必背:while (1 <= x <= 2) 这种写法永远为真!
因为 C 里关系运算符是左结合,先算 1 <= x 得到一个 0/1,再算 0 <= 2 或 1 <= 2,结果恒为 1。
想表达"x 在 [1,2] 之间"必须写
while (x >= 1 && x <= 2)。
逐轮过程:
| 轮次 | 进入时 x |
y = x-- |
!y |
输出 | 1<=x<=2 |
|---|---|---|---|---|---|
| 1 | 3 | y=3, x=2 | 假 | # |
(1<=2)<=2 → 真 |
| 2 | 2 | y=2, x=1 | 假 | # |
(1<=1)<=2 → 真 |
| 3 | 1 | y=1, x=0 | 假 | # |
(1<=0)=0,0<=2 → 真 |
| 4 | 0 | y=0, x=-1 | 真 | * 然后 break |
— |
输出 ###*。✅
易错:以为
while(1<=x<=2)会在x=0时退出(实际恒真,是靠break才停的)。
5 · do-while 与"循环条件恒真"陷阱¶
#include <stdio.h>
int main()
{
int y, x = 3;
do {
y = x--;
if (!y) {
printf("*"); break;
}
printf("#");
} while (1 <= x <= 2);
printf("\nx=%d,y=%d", x, y);
}
写出程序运行结果。
解析
循环过程:x 从 3 递减到 0,前三轮 y 非 0 各输出一个 #,第四轮 y = 0 输出 * 后 break。
本题多了一个 printf,此时 x = -1(第 4 轮 y = x-- 把 x 从 0 减到了 -1),y = 0。
评分细则:单个答案错误扣 2 分;没有写逗号分隔、没有换行,各扣 2 分。说明阅卷很看重输出格式——考试时务必把 printf 里的逗号、空格、\n 都照着写。
6 · 循环体内修改循环变量¶
以下 C 语言语句执行后,a 的值是( )。
- A. 5
- B. 6
- C. 7
- D. 8
答案
B(6)
解析
注意循环体里也在改 a,而且 a 在增量表达式 a++ 里每次只加 1:
| 轮次 | 循环条件 | 循环体 a += 2 后 |
a++ 后 |
|---|---|---|---|
| 1 | a=0 ≤ 5 ✓ | 2 | 3 |
| 2 | a=3 ≤ 5 ✓ | 5 | 6 |
| 3 | a=6 ≤ 5 ✗ | — | — |
退出时 a = 6。
易错:心算成"每次 +3,0→3→6,所以 a=6"——结论对,但如果循环体是
a += 3或初值不同,一定要老老实实列表。另外初始的
int a = 5;是干扰项:for的初始化表达式a = 0立刻把它覆盖掉了。
7 · continue 与 break¶
#include <stdio.h>
int main() {
int a[5] = {2, 4, 6, 8, 10};
int i, sum = 0;
for (i = 0; i < 5; i++) {
if (a[i] % 4 == 0)
continue;
sum += a[i];
}
printf("sum = %d\n", sum);
return 0;
}
写出该程序的功能和运行结果。
答案
- 程序功能:将数组
a中所有不能被 4 整除的元素累加求和。 - 程序输出:
sum = 18
解析
continue 的作用是跳过后面的语句、直接进入下一轮循环,所以 %4==0 的元素被跳过:
| 轮次 | a[i] |
a[i] % 4 |
是否 continue |
sum |
|---|---|---|---|---|
| 1 | 2 | 2 ≠ 0 | 否,累加 | 2 |
| 2 | 4 | 0 | 是,跳过 | 2 |
| 3 | 6 | 2 ≠ 0 | 否,累加 | 8 |
| 4 | 8 | 0 | 是,跳过 | 8 |
| 5 | 10 | 2 ≠ 0 | 否,累加 | 18 |
评分细则:功能 3 分、输出 2 分。"功能"类题目必须用完整句子描述清楚("把……中所有……的元素累加求和"),不能只写"求和"。
辨析:本题若把
continue换成break,第 2 轮就会直接退出循环,输出sum = 2——continue结束"本次",break结束"整个"。
8 · continue 与 break¶
#include <stdio.h>
int main()
{
int a = 1, b = 2, c = 10, s = 0, i;
for (i = 0; i < c; ++i) {
s += a;
a += b;
}
printf("%d", s);
return 0;
}
写出程序运行结果。
答案
100
解析
循环体是"先累加、再更新"两步,逐轮看:
轮次 i |
进入时 a |
s += a 后 |
a += b(+2)后 |
|---|---|---|---|
| 0 | 1 | 1 | 3 |
| 1 | 3 | 4 | 5 |
| 2 | 5 | 9 | 7 |
| 3 | 7 | 16 | 9 |
| … | … | … | … |
| 9 | 19 | 100 | 21 |
a 依次取 1, 3, 5, 7, …(首项 1、公差 2 的等差数列),循环 10 次,s = 前 10 个奇数之和 = 10² = 100。
小技巧:前 n 个奇数之和 = n²。 注意:
b只作为"步长"使用,值恒为 2,不要误以为它也在变。
9 · 循环 + 累加(填空)¶
利用公式 π = 4/1 − 4/3 + 4/5 − 4/7 + … 计算 π 的近似值。在横线处填入正确内容。
#include <stdio.h>
#include <math.h>
#define EPS 1e-6
double PI(){
double pi,t;
int i,s;
________(1)________
for(i=1,s=1;fabs(t)>EPS;i+=2,s=-1*s){ /*fabs 函数返回绝对值*/
________(2)________
pi+=t;
}
________(3)________
}
int main(){
printf("pi=%f\n",PI());
return 0;
}
答案
| 空 | 答案 |
|---|---|
| (1) | pi = 0; t = 1; ← 必须连 t 一起初始化(只写 pi = 0; 程序会输出 pi=0.000000,见下) |
| (2) | t = 4.0 / i * s;(或 t = s * 4.0 / i;) |
| (3) | return pi; |
解析
- (1):这一空要同时给两个变量初始化:累加器
pi清零,循环变量t也要给一个大于EPS的初值。只写pi = 0;的话,t未初始化,而fabs(t)>EPS在第一次判断时就要读t——t的赋值发生在循环体内,判断在赋值之前。真机实测(gcc 15.2.0):只写
pi = 0;时t恰为 0,fabs(t)>EPS一开始就为假 → 循环体一次都不执行 → 输出pi=0.000000;写成pi = 0; t = 1;才输出pi=3.141593。所以这一空的完整答案是pi = 0; t = 1;。 - (2):
s在1和-1之间交替(s = -1*s),配合i = 1, 3, 5, 7...正好实现+4/1 - 4/3 + 4/5 - 4/7...。注意写
4.0而不是4——虽然i是int,但4/i会走整数除法(4/3=1),必须是浮点除法。t是double,所以右侧必须有一个浮点操作数。 - (3):返回结果。注意函数声明是
double PI(),必须有return。
本题的复习价值:它是"循环 + 累加 + 交替符号"的标准模板——
i控制分母(步长 2)、s控制符号、fabs(t)>EPS控制精度。识别出这三件事,填空就是填空题,不是推理题。
10 · 悬挂 if(dangling if)¶
执行下列语句后输出的结果是( )。
- A.
6,7,7 - B.
6,7,8 - C.
8,7,6 - D.
8,7,8
答案
C(8,7,6)
解析
必背:C 语言的 if 语句不带花括号时,语句体只有紧跟其后的那一条语句;而 if 本身也是一条语句——所以本题外层 if (a < b) 的语句体,就是整个内层 if 语句(含它自己的 {})。
这两个 if 是嵌套关系,不是并列关系。后面那个 printf 一句不属于任何 if,无论条件真假都会执行。
关键在于:外层条件不成立时,内层 if 连"判断"的机会都没有——整个嵌套语句被整体跳过。
| 步骤 | 判断 / 动作 | 结果 |
|---|---|---|
| 1 | a < b → 8 < 7 |
假 |
| 2 | 外层 if 的语句体(= 整个内层 if 语句)被整体跳过 |
内层 b > c 根本不被求值 |
| 3 | { a = c; c = b; } 不执行 |
a=8, b=7, c=6 保持初值 |
| 4 | printf(在两层 if 之外,照常执行) |
输出 8,7,6 |
易错:本题的诱饵设计得很准——
b > c即7 > 6是真,所以只要你的思路是"两层条件分别算一下、都真就执行",就会算出a=6, c=7(即选项 A6,7,7)。错在忘了内层判断以前提条件成立为前提。真机对照:把
a改成1使a < b成立后,程序确实输出6,7,7(即选项 A)——选项 A 就是"漏看外层条件"的产物。悬挂 else 的规矩:
想让它属于外层,必须给内层else永远与离它最近的那个尚未配对的if结合。所以if加上花括号。结论:只要出现嵌套if,就一律加{},别省。
11 · for 循环的终止值¶
执行完下列语句后,n 的值是( )。
- A.
125 - B.
126 - C.
127 - D.
128
答案
C(127)
解析
两个要点先看清:① for 末尾那个分号就是循环体(空语句),循环体什么都不做;② 增量表达式是 n = 2 * n + 1(不是 n++),所以 n 按 2n+1 飞快增长。
for 的执行顺序是:初始化 → 判断条件 → 循环体 → 增量 → 再判断……:
判断时 n |
n < 100 |
循环体 | 增量后 n |
|---|---|---|---|
| 0 | 真 | 空 | 1 |
| 1 | 真 | 空 | 3 |
| 3 | 真 | 空 | 7 |
| 7 | 真 | 空 | 15 |
| 15 | 真 | 空 | 31 |
| 31 | 真 | 空 | 63 |
| 63 | 真 | 空 | 127 |
| 127 | 假 | — | 退出循环 |
序列是 0, 1, 3, 7, 15, 31, 63, 127(即 2^(k+1) − 1),退出时 n = 127。
易错:算出
127后又顺手再算一次2*127+1 = 255,报成255。增量表达式只在循环体执行完、且还要回去判断时才执行——127 < 100已经不成立,程序直接退出,255这一步根本不会发生。循环结束时变量的值 = 第一个让条件失败的取值,别再往后推一步。
补充:
for (n = 0; n < 100; n = 2 * n + 1);这种写法里,条件、增量都是"表达式",可以任意写;唯独三个表达式之间的两个分号不能少(少了就成了别的语法)。
12 · 死循环的判定¶
在下列选项中,没有构成死循环的程序是( )。
A)
B)
C)
D)
答案
B
解析
判断死循环只需盯两件事:① 循环条件会不会永远为真;② 循环变量的值会不会永远不变。两条里中任意一条,就是死循环。
逐项排雷:
- A —— 死循环。
while (1)条件恒真。m初值 100,第一轮m = m % 10→100 % 10 = 0;此后每轮都是0 % 10 = 0,m恒为 0。而if (m > 100)里m = 0,永远不成立 →break永远执行不到。变量不变 + 条件恒真,双料死循环。 - B —— 正常结束 ✅。
while (s)在s非 0 时继续,循环体--s每轮把s减 1:36 → 35 → … → 1 → 0,s = 0时条件为假,退出。共执行 36 轮。条件会变假,是唯一"活着"的选项。 - C —— 死循环。循环体
{ k; }里那句k;只是一条空转的表达式语句,k的值根本没变(注意它不是k--;,题目正是把自减写漏了来做陷阱)。于是k恒为 1000,1000 >= 1000恒真 → 死循环。 - D —— 死循环。这里极易看错:
for的两个分号把三个表达式隔开,i = 0是初始化,中间条件位置是空的,i < 2落在第三个位置(增量表达式)上。空条件是恒真;而增量表达式i < 2只是比较了一下、没给i赋值,等于什么都没做 →i恒为 0,条件恒真 + 变量不变,死循环。
| 选项 | 循环条件恒真? | 循环变量变化? | 结论 |
|---|---|---|---|
| A | 是(while(1)) |
m 变成 0 后不再变 |
死循环 |
| B | 否(s 递减到 0) |
s 每轮 −1 |
正常结束(选它)✅ |
| C | 是(k 恒为 1000) |
不变(k; 是空转语句) |
死循环 |
| D | 是(条件空缺 = 恒真) | 不变(i < 2 只比较不赋值) |
死循环 |
真机实测:四个选项分别编译运行。A、C、D 用
timeout 3全部超时(确证为死循环),B 正常退出并输出s=0(循环 36 轮)。易错:① 把 D 的
i < 2当成循环条件(它在第二个分号之后,是增量表达式的坑位);② 把 C 的k;看成k--;而误判成有终止。记牢:空条件 = 恒真(
for(;;)与while(1)等价),这是 D 和 A 的共同命门。
13 · 嵌套 switch 的穿透¶
以下程序的输出是( )。
#include <stdio.h>
int main()
{
int x = 1, y = 0, a = 0, b = 0;
switch (x)
{
case 1: switch (y)
{
case 0: a++; break;
case 1: b++; break;
}
case 2: a++; b++; break;
case 3: a++; b++;
}
printf("\na=%d,b=%d", a, b);
return 0;
}
- A.
a=1,b=0 - B.
a=2,b=1 - C.
a=1,b=1 - D.
a=2,b=2
答案
B(a=2,b=1)
解析
必背的一句话:break 只能跳出它所在的那一层 switch(或循环)。
本题内层 switch (y) 里的 break 只结束内层,控制流随即回到外层 case 1: 的后面——而那里没有 break,于是照旧向下穿透到 case 2:。
把执行路径逐层摊开:
| 层 | 当前位置 | 判断 / 动作 | 效果 |
|---|---|---|---|
| 外层 | switch (x),x = 1 |
匹配 case 1 |
进入 case 1 |
| 内层 | switch (y),y = 0 |
匹配 case 0 |
进入 case 0 |
| 内层 | case 0: a++; |
执行自增 | a: 0 → 1 |
| 内层 | break; |
只跳出内层 switch |
回到外层 case 1 之后 |
| 外层 | case 1: 后面没有 break |
穿透(fall-through)到 case 2 |
— |
| 外层 | case 2: a++; b++; |
两条都执行 | a: 1 → 2,b: 0 → 1 |
| 外层 | break; |
跳出外层 switch |
case 3 不执行 |
最终 a = 2, b = 1 ✅
换个算法更直观:a 一共加了两次(内层 case 0 一次 + 外层 case 2 一次),b 只加了外层 case 2 那一次。所以 a=2, b=1。
易错:① 以为内层
break会跳出整个嵌套switch——那样结果是a=1,b=0,正好是选项 A(这是本题最主要的干扰项);② 漏看case 1后面没有break,忘了外层还会继续穿透。
case 3是幌子:它自己也没写break,但因为case 2的break已经拦住了控制流,它根本轮不到执行,不要被它干扰。(真机对照:若把case 2的break也删掉,才会连case 3一起吃进去,输出变成a=3,b=2。)真机旁证:gcc 15.2.0 编译本题时会直接给出
-Wimplicit-fallthrough警告this statement may fall through,并指出落点是case 2: a++; b++; break;——编译器等于帮我们把"case 1穿透到case 2"这条路径点名确认了一遍。一句话总结层次:内层
break管内层,外层穿透照旧。