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

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

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 = 59score = 60score = 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 elseswitch 最终都会被编译成跳转相关的指令。不同写法在不同编译器和优化级别下,可能生成不同的机器码。

先建立几个直觉

  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 == answerguess > answerelse
提前结束 猜中后 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 套娃 难读、难调试
switchbreak case 穿透 执行到不该执行的分支
循环变量未更新 while (i < 10) 但没有 i++ 死循环
边界差一 i <= len 可能多访问一次
输入失败没处理 scanf 返回值不检查 错误数据继续进入逻辑
无限循环没出口 for (;;) 没有退出条件 程序无法正常结束

学习检查清单

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

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