本文是《C语言期末速成》系列第 5 篇。第 1 篇说过:任何程序都只是顺序、选择、循环三种结构的嵌套组合。本篇把它们一次性讲透——顺序结构(执行次序)、选择结构(if/switch 与条件判断)、循环结构(for/while/do-while)。这一章是算法题的主战场,内容最多,请耐心啃。


上一篇C语言期末速成 4:格式化输入输出

考点清单

  • 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
  • 循环三种:whiledo-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 && 11      1 && 00
1 || 01      0 || 00
!01          !50   /* 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);

逐步推导:

  1. !a!1 → 0(假),b++ 不执行;
  2. 进入 else if (a == 0)1 == 0 → 假,这一支也不成立
  3. 但注意:if (a) b+=2; else b+=3; 是挂在 else if (a==0) 下面的嵌套结构。整个 if-else if-else 链中没有任何条件成立;
  4. 由于链条没有匹配,最后那个 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: 语句...;
}

要点

  1. switch(表达式) 里的表达式只能是整型、字符型或枚举型
  2. case 后面必须是常量(不能是变量);
  3. 匹配到某个 case顺序执行下去——如果后面没有 break,会继续执行后续所有 case(这就是”穿透”);
  4. 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,不能配 continuecontinue 只能出现在循环里)。

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(;;) 等价 */

forif 一样,只管后面一条语句

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:

  1. k=1 → 条件取 1(真)→ k 变 0;
  2. 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);             /* 顺序:打印 */
}

做题时遇到复杂的嵌套,逐层拆解:先看外层循环转几次,再算每圈内层(选择/循环)做什么,不要跳步。


易错点 / 陷阱

  1. 0 < x < 10 表达区间:它是 (0<x)<10,结果恒为真,必须用 (0<x)&&(x<10)
  2. == 写成 =if (a = 1) 是赋值,条件恒为真,且把 a 改成 1。
  3. 悬空 elseelse 永远配最近的 if,想改配必须加 {}
  4. switch 忘写 break:没有 break 会穿透,执行后续所有 case。
  5. do-while 忘写分号:结尾 while(...) 后面必须有 ;
  6. while(--k)while(k--) 混淆:后者条件判断用旧值 0 时其实已多减一次,结果 -1。
  7. while(c=getchar()!='\n') 忘括号!= 优先,先比较再赋值,语义反转。
  8. break 想退出多层:break 只退一层。
  9. for 只管一条for(...) 语句A; 语句B; 只有 A 受循环控制。
  10. continue 用在 switchcontinue 只能在循环里,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. 1<0 为假(0),0<2 为真(1)→ 结果是 1。
  2. a=0 为假,&& 短路,b=10 不执行 → a=0, b=5,表达式值 0。
  3. k 从 1 判断真减到 0,条件取 0 为假退出,但后置自减再减一次 → 输出 -1。
  4. (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
  5. 匹配 case 1,无 break 穿透到 case 2 打印 B,遇到 break 退出 → 输出 AB

下一篇C语言期末速成 6:函数

评论区