这一篇开始整理程序的执行路径。

前面几章更多是在学习“数据怎么表示、表达式怎么算”,这一章要解决的是“程序应该走哪条路、什么时候重复执行、什么时候退出”。后面做输入校验、菜单程序、文件解析、网络数据处理和安全实验时,条件判断和循环边界会反复出现。

5.0 多行 IF 语句

多行 if 用来表达“满足条件时执行一段代码”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(void)
{
int score = 75;

if (score >= 60)
{
printf("pass\n");
printf("score = %d\n", score);
}

return 0;
}

推荐始终写花括号

即使 if 里只有一行,也建议保留 {}。

1
2
3
4
if (score >= 60)
{
printf("pass\n");
}

这样后续增加日志、计数、返回值时,不容易因为漏括号造成逻辑错误。

if else

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(void)
{
int score = 55;

if (score >= 60)
{
printf("pass\n");
}
else
{
printf("failed\n");
}

return 0;
}

else 表示前面的条件不成立时执行。复盘时可以用断点分别测试 score = 59、score = 60、score = 100,看程序分别走哪条路。

5.1 多条件 IF 语句

多条件判断常见写法是 else if。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

int main(void)
{
int score = 88;

if (score >= 90)
{
printf("A\n");
}
else if (score >= 80)
{
printf("B\n");
}
else if (score >= 60)
{
printf("C\n");
}
else
{
printf("D\n");
}

return 0;
}

顺序很重要

条件会从上到下依次判断。只要某个条件成立,后面的分支就不会再执行。

1
2
3
4
5
6
7
8
if (score >= 60)
{
printf("pass\n");
}
else if (score >= 90)
{
printf("excellent\n");
}

这段代码里,score >= 90 永远没有机会执行,因为 90 也满足 score >= 60。写分数段、权限等级、错误码分类时,要特别注意条件顺序。

范围判断

1
2
3
4
5
6
7
8
if (score >= 0 && score <= 100)
{
printf("valid score\n");
}
else
{
printf("invalid score\n");
}

涉及输入数据时,先判断范围是否合法,再进入业务逻辑。

安全开发里,很多问题不是“没有判断”,而是判断顺序错了、边界没包含、非法输入提前进入了后续逻辑。范围判断要尽量写在靠前的位置。

5.2 嵌套 IF 语句

嵌套 if 用来表达多层条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

int main(void)
{
int is_login = 1;
int is_admin = 0;

if (is_login)
{
if (is_admin)
{
printf("admin panel\n");
}
else
{
printf("user panel\n");
}
}
else
{
printf("please login\n");
}

return 0;
}

嵌套不要太深

嵌套层级太深时,代码会越来越难读。可以通过“提前返回”减少层级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>

int check_score(int score)
{
if (score < 0 || score > 100)
{
printf("invalid score\n");
return 0;
}

if (score >= 60)
{
printf("pass\n");
}
else
{
printf("failed\n");
}

return 1;
}

int main(void)
{
check_score(75);
return 0;
}

这类写法适合处理输入校验:先把不合法的情况挡出去,再进入正常逻辑。

5.3 SWITCH 语句

switch 适合处理一个变量对应多个固定取值的情况,比如菜单选择、命令编号、状态码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>

int main(void)
{
int choice = 2;

switch (choice)
{
case 1:
printf("add\n");
break;
case 2:
printf("delete\n");
break;
case 3:
printf("query\n");
break;
default:
printf("unknown command\n");
break;
}

return 0;
}

break 很重要

如果没有 break,程序会继续执行后面的 case,这叫穿透。

1
2
3
4
5
6
7
8
switch (choice)
{
case 1:
printf("one\n");
case 2:
printf("two\n");
break;
}

有些场景会故意利用穿透,但初学阶段和复盘笔记里,除非明确说明原因,否则每个 case 都写 break。

switch 的适用范围

适合 不适合
固定整数、字符、枚举状态 复杂范围判断
菜单编号 字符串内容比较
错误码分类 多个条件组合

5.4 反汇编角度论证执行速度

从底层看,if else 和 switch 最终都会被编译成跳转相关的指令。不同写法在不同编译器和优化级别下,可能生成不同的机器码。

先建立几个直觉

  1. 简单的 if 往往对应条件判断和跳转。
  2. 连续的 else if 可能变成一串比较和跳转。
  3. switch 在取值密集时,编译器可能生成跳转表。
  4. Debug 和 Release 下的反汇编差异可能很大。

这一节不用急着得出“谁一定更快”的结论。对现在的我来说,更重要的是先把逻辑写正确、边界写清楚;等到后面学逆向和性能分析时,再结合反汇编观察编译器具体做了什么。

适合观察的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int command(int op)
{
switch (op)
{
case 1:
return 10;
case 2:
return 20;
case 3:
return 30;
default:
return -1;
}
}

可以在 Visual Studio 里打开反汇编窗口,看 switch 最后是连续比较,还是被优化成其他结构。

5.5 while 循环

while 是先判断条件,再执行循环体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(void)
{
int i = 0;

while (i < 5)
{
printf("i = %d\n", i);
i++;
}

return 0;
}

while 的三个关键点

  1. 循环变量要有初始值。
  2. 循环条件要能变成假。
  3. 循环体里要更新状态,否则可能死循环。
1
2
3
4
5
6
7
int i = 0;

while (i < 10)
{
printf("%d\n", i);
i++;
}

常见用途

场景 说明
输入重试 不知道用户会输几次
读取数据 不知道文件或输入什么时候结束
菜单循环 程序持续运行,直到用户选择退出
条件等待 等待某个状态满足

5.6 do…while 循环

do...while 是先执行一次循环体,再判断条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

int main(void)
{
int choice = 0;

do
{
printf("1. add\n");
printf("2. delete\n");
printf("0. exit\n");
printf("choice: ");

#ifdef _MSC_VER
scanf_s("%d", &choice);
#else
scanf("%d", &choice);
#endif

printf("your choice = %d\n", choice);

} while (choice != 0);

return 0;
}

适合菜单程序

菜单至少要显示一次,所以 do...while 很适合菜单循环。

如果循环里有输入,一定要考虑输入失败、非法输入和退出条件。否则程序可能卡住、死循环,或者把错误输入继续传给后续逻辑。

5.7 for 循环

for 循环适合次数比较明确的循环。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(void)
{
for (int i = 0; i < 5; i++)
{
printf("i = %d\n", i);
}

return 0;
}

for 的三个部分

1
2
3
4
for (初始化; 条件; 更新)
{
循环体;
}
部分 作用
初始化 循环开始前执行一次
条件 每轮循环前判断
更新 每轮循环体结束后执行

遍历固定范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(void)
{
int sum = 0;

for (int i = 1; i <= 100; i++)
{
sum += i;
}

printf("sum = %d\n", sum);
return 0;
}

循环边界要写清楚:从哪里开始、到哪里结束、是否包含最后一个值。

5.8 for 循环变形拓展

for 的三个表达式都可以省略,但省略后要自己保证逻辑清楚。

省略初始化

1
2
3
4
5
6
int i = 0;

for (; i < 5; i++)
{
printf("%d\n", i);
}

省略更新

1
2
3
4
5
for (int i = 0; i < 5;)
{
printf("%d\n", i);
i++;
}

无限循环

1
2
3
4
5
for (;;)
{
printf("loop\n");
break;
}

这类写法在服务程序、消息循环、菜单循环中能看到,但必须有清晰的退出条件。

多变量循环

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(void)
{
for (int left = 0, right = 5; left < right; left++, right--)
{
printf("left = %d, right = %d\n", left, right);
}

return 0;
}

多变量循环不要写得太炫,能读懂比短更重要。

5.9 经典案例:猜大小

猜大小适合把选择和循环放到一起练习。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>

int main(void)
{
int answer = 37;
int guess = 0;
int count = 0;
const int max_try = 5;

printf("Guess a number between 1 and 100.\n");

while (count < max_try)
{
printf("input: ");

#ifdef _MSC_VER
if (scanf_s("%d", &guess) != 1)
#else
if (scanf("%d", &guess) != 1)
#endif
{
printf("input error.\n");
return 1;
}

count++;

if (guess == answer)
{
printf("correct, used %d times.\n", count);
return 0;
}
else if (guess > answer)
{
printf("too big\n");
}
else
{
printf("too small\n");
}
}

printf("failed, answer = %d\n", answer);
return 0;
}

这个案例练什么

内容 对应代码
循环次数控制 count < max_try
输入检查 scanf 返回值判断
多分支判断 guess == answer、guess > answer、else
提前结束 猜中后 return 0
失败兜底 循环结束后输出答案

以后写安全工具的小命令行程序,也经常是这种结构:初始化状态、进入循环、读取输入、判断命令、执行操作、满足条件后退出。

本章最小复盘代码

这段代码把菜单、switch、循环、输入校验和退出条件放在一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>

int main(void)
{
int choice = -1;
int running = 1;

while (running)
{
printf("\n==== menu ====\n");
printf("1. scan\n");
printf("2. report\n");
printf("0. exit\n");
printf("choice: ");

#ifdef _MSC_VER
if (scanf_s("%d", &choice) != 1)
#else
if (scanf("%d", &choice) != 1)
#endif
{
printf("input error.\n");
return 1;
}

switch (choice)
{
case 1:
printf("start scan...\n");
break;
case 2:
printf("show report...\n");
break;
case 0:
printf("exit.\n");
running = 0;
break;
default:
printf("unknown command.\n");
break;
}
}

return 0;
}

复盘时重点看:

  1. while (running) 为什么能维持菜单循环。
  2. scanf 返回值为什么要判断。
  3. switch 每个 case 为什么要写 break。
  4. choice = 0 时程序如何退出。
  5. 如果用户输入未知命令,程序如何兜底。

常见错误整理

错误 示例 问题
忘记花括号 单行 if 后新增第二行 第二行不受条件控制
条件顺序错误 先判断 >= 60 再判断 >= 90 高等级分支永远进不去
嵌套过深 多层 if 套娃 难读、难调试
switch 漏 break case 穿透 执行到不该执行的分支
循环变量未更新 while (i < 10) 但没有 i++ 死循环
边界差一 i <= len 可能多访问一次
输入失败没处理 scanf 返回值不检查 错误数据继续进入逻辑
无限循环没出口 for (;;) 没有退出条件 程序无法正常结束

学习检查清单

检查项 状态
能写出多行 if 和 if else 待复盘
能正确安排 else if 的判断顺序 待复盘
能用嵌套 if 表达多层条件 待复盘
能通过提前返回减少嵌套层级 待复盘
能写出带 default 和 break 的 switch 待复盘
能理解 switch 在底层可能被优化成跳转结构 待复盘
能写出 while、do...while、for 循环 待复盘
能说明三种循环适合的场景 待复盘
能避免死循环和边界差一错误 待复盘
能写出一个带输入校验和退出条件的菜单程序 待复盘
能完成猜大小案例,并解释每条分支 待复盘

这一篇的核心是“执行路径”。写完一段条件或循环后,要能说清楚:入口在哪里,哪些输入会走哪条分支,循环什么时候继续,什么时候退出。