跳转至

知识点 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 后,会从那里一路往下执行,直到遇到 breakswitch 结束——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')= 3
  • n1 = 5('1')+ 2('0')+ 1('9')= 8
  • n2 = 5('1')+ 1('2')+ 2('0')+ 1('9')= 9
  • nn = 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 = 3
  • v2 = 1 + 3 + 2 = 6
  • v3 = 2 + 2 + 2 = 6

易错:把 v1=v2=v3=0 当默认初值,算出 3,5,4

另外注意 case 3default 都没写 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

解析

这是 continueswitch 配合的经典陷阱——switch 不是循环语句,所以 continue 跳过的是外层 do-while 的剩余部分,直接去判断 while(y>=1)

第 1 轮:x = 2 → 进入 case 2

  • y = 1
  • 穿透到 case 3x += 2x = 4
  • continue → 跳过 case 4/default,直接判断 y >= 11 >= 1 为真,继续循环

第 2 轮:x = 4 → 进入 case 4

  • x += y-- → 先用 y 的值 1,x = 4 + 1 = 5,然后 y 变成 0
  • 穿透到 defaultx++x = 6
  • 判断 y >= 10 >= 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 <= 21 <= 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)=00<=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=-1,y=0
解析

循环过程: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 的值是( )。

int a = 5;
for (a = 0; a <= 5; a++)
    a += 2;
  • 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 · continuebreak

#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 · continuebreak

#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):s1-1 之间交替(s = -1*s),配合 i = 1, 3, 5, 7... 正好实现 +4/1 - 4/3 + 4/5 - 4/7...

    注意写 4.0 而不是 4——虽然 iint,但 4/i 会走整数除法(4/3=1),必须是浮点除法。tdouble,所以右侧必须有一个浮点操作数。

  • (3):返回结果。注意函数声明是 double PI(),必须有 return

本题的复习价值:它是"循环 + 累加 + 交替符号"的标准模板——i 控制分母(步长 2)、s 控制符号、fabs(t)>EPS 控制精度。识别出这三件事,填空就是填空题,不是推理题。


10 · 悬挂 if(dangling if)

执行下列语句后输出的结果是( )。

int a = 8, b = 7, c = 6;
if (a < b)
    if (b > c)
    {
        a = c; c = b;
    }

printf("%d,%d,%d\n", a, b, c);
  • 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 < b8 < 7
2 外层 if 的语句体(= 整个内层 if 语句)被整体跳过 内层 b > c 根本不被求值
3 { a = c; c = b; } 不执行 a=8, b=7, c=6 保持初值
4 printf(在两层 if 之外,照常执行) 输出 8,7,6

易错:本题的诱饵设计得很准——b > c7 > 6 是真,所以只要你的思路是"两层条件分别算一下、都真就执行",就会算出 a=6, c=7(即选项 A 6,7,7)。错在忘了内层判断以前提条件成立为前提。

真机对照:把 a 改成 1 使 a < b 成立后,程序确实输出 6,7,7(即选项 A)——选项 A 就是"漏看外层条件"的产物。

悬挂 else 的规矩:else 永远与离它最近的那个尚未配对的 if 结合。所以

if (a < b)
    if (b > c) x = 1;
    else       x = 2;   /* 这个 else 属于内层 if,不是外层! */
想让它属于外层,必须给内层 if 加上花括号。结论:只要出现嵌套 if,就一律加 {},别省。


11 · for 循环的终止值

执行完下列语句后,n 的值是( )。

int n;
for (n = 0; n < 100; n = 2 * n + 1);
  • A. 125
  • B. 126
  • C. 127
  • D. 128
答案

C(127

解析

两个要点先看清:① for 末尾那个分号就是循环体(空语句),循环体什么都不做;② 增量表达式是 n = 2 * n + 1(不是 n++),所以 n2n+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)

int m = 100;
while (1)
{
    m = m % 10;
    if (m > 100) break;
}

B)

int s = 36;
while (s)
    --s;

C)

int k = 1000;
do { k; } while (k >= 1000);

D)

for (i = 0; ; i < 2);
答案

B

解析

判断死循环只需盯两件事:① 循环条件会不会永远为真;② 循环变量的值会不会永远不变。两条里中任意一条,就是死循环。

逐项排雷:

  • A —— 死循环。while (1) 条件恒真。m 初值 100,第一轮 m = m % 10100 % 10 = 0;此后每轮都是 0 % 10 = 0m 恒为 0。而 if (m > 100)m = 0,永远不成立 → break 永远执行不到。变量不变 + 条件恒真,双料死循环。
  • B —— 正常结束 ✅。while (s)s 非 0 时继续,循环体 --s 每轮把 s 减 1:36 → 35 → … → 1 → 0s = 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 → 2b: 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 2break 已经拦住了控制流,它根本轮不到执行,不要被它干扰。(真机对照:若把 case 2break 也删掉,才会连 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 管内层,外层穿透照旧。