这一篇开始整理程序的执行路径。
前面几章更多是在学习“数据怎么表示、表达式怎么算”,这一章要解决的是“程序应该走哪条路、什么时候重复执行、什么时候退出”。后面做输入校验、菜单程序、文件解析、网络数据处理和安全实验时,条件判断和循环边界会反复出现。
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 最终都会被编译成跳转相关的指令。不同写法在不同编译器和优化级别下,可能生成不同的机器码。
先建立几个直觉
- 简单的
if 往往对应条件判断和跳转。
- 连续的
else if 可能变成一串比较和跳转。
switch 在取值密集时,编译器可能生成跳转表。
- 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 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; }
|
复盘时重点看:
while (running) 为什么能维持菜单循环。
scanf 返回值为什么要判断。
switch 每个 case 为什么要写 break。
choice = 0 时程序如何退出。
- 如果用户输入未知命令,程序如何兜底。
常见错误整理
| 错误 |
示例 |
问题 |
| 忘记花括号 |
单行 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 循环 |
待复盘 |
| 能说明三种循环适合的场景 |
待复盘 |
| 能避免死循环和边界差一错误 |
待复盘 |
| 能写出一个带输入校验和退出条件的菜单程序 |
待复盘 |
| 能完成猜大小案例,并解释每条分支 |
待复盘 |
这一篇的核心是“执行路径”。写完一段条件或循环后,要能说清楚:入口在哪里,哪些输入会走哪条分支,循环什么时候继续,什么时候退出。