C1-04 复杂的分支结构
if 嵌套、逻辑运算符与 switch 语句
学习目标:理解什么时候需要多个条件共同判断;学会使用嵌套
if分层处理条件;掌握&&、||、!三个逻辑运算符及其短路求值特点;能够用逻辑运算符写出范围和组合条件;了解switch语句的适用场景、case、break与default的作用。
一、为什么需要更复杂的分支
1. 第三课的分支有什么局限
第三课中,我们用一个比较条件决定程序走哪条路:
if (score >= 60) {
cout << "及格" << endl;
} else {
cout << "不及格" << endl;
}
这里的条件只有一个:score >= 60。
但有些问题不能只判断一个条件:
- 一个数在
10到99之间,需要同时满足n >= 10和n <= 99; - 一个字符是字母,可能是小写字母,也可能是大写字母;
- 简易计算器要根据输入的运算符
+、-、*、/选择不同的计算方法; - 先判断是否及格,再把及格的同学分成“优秀”和“及格”两类。
这些问题都属于复杂的分支结构:程序需要组合多个条件、逐层判断,或根据一个值选择多个固定分支。
2. 本课的三种工具
| 工具 | 核心作用 | 适合的情况 |
|---|---|---|
嵌套 if |
在一个 if 里面继续判断 |
条件有先后顺序,像“先分大类,再分小类” |
| 逻辑运算符 | 把多个条件合成一个条件 | 条件之间是“并且”“或者”“取反”关系 |
switch |
根据同一个值匹配多个固定选项 | 等值判断,例如星期几、菜单编号、运算符 |
可以把它们记成:
层层判断 → 嵌套 if
组合条件 → &&、||、!
固定选项 → switch
3. 本课的学习范围
本课仍然只讨论分支结构,不学习循环。
逻辑运算符可以把多个比较条件组合起来,例如:
n >= 10 && n <= 99
(ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
后续学习循环后,复杂条件还会经常和 for、while 一起使用。
二、嵌套 if:先分大类,再分小类
1. 什么是嵌套 if
在一个 if 或 else 的花括号里,再写一个 if,叫作嵌套 if。
它适合有明显先后关系的判断。例如,先判断“是否及格”,只有及格后才继续判断“是否优秀”。
2. 基本语法
if (条件 1) {
if (条件 2) {
// 条件 1、条件 2 都成立时执行
} else {
// 条件 1 成立、条件 2 不成立时执行
}
} else {
// 条件 1 不成立时执行
}
3. 嵌套 if 的流程图
flowchart TD
Start([开始]) --> Cond1{条件 1?}
%% 1. 先定义 true 分支,使其排在左侧
Cond1 -->|true 真| Cond2
%% 2. 使用子图包裹“条件 1 成立”后的嵌套逻辑
subgraph TrueBranch ["条件 1 成立时的嵌套逻辑"]
direction TB
Cond2{条件 2?}
Cond2 -->|true 真| Action2["执行两个条件<br>都成立时的语句"]
Cond2 -->|false 假| Action3["执行条件 1 成立、<br>条件 2 不成立时的语句"]
end
%% 3. 后定义 false 分支,使其排在右侧(在子图外部)
Cond1 -->|false 假| Action1["执行条件 1<br>不成立时的语句"]
%% 4. 汇聚所有分支
Action1 --> Continue["继续执行后面的语句"]
Action2 --> Continue
Action3 --> Continue
Continue --> End([结束])
%% 5. 添加样式(增强子图的嵌套视觉)
classDef condition fill:#e1f5fe,stroke:#0288d1,stroke-width:2px;
classDef action fill:#fff9c4,stroke:#fbc02d,stroke-width:1px;
class Cond1,Cond2 condition;
class Action1,Action2,Action3,Continue action;
%% 给子图添加虚线边框和浅色背景,突出嵌套区域
style TrueBranch fill:#fafafa,stroke:#9e9e9e,stroke-width:2px,stroke-dasharray: 5 5;
外层条件不成立时,程序不会进入内层,所以内层条件也不会被判断。
4. 示例:判断优秀、及格和不及格
int score;
cin >> score;
if (score >= 60) {
if (score >= 90) {
cout << "优秀" << endl;
} else {
cout << "及格" << endl;
}
} else {
cout << "不及格" << endl;
}
输入 95 时:
score >= 60成立,进入外层if;score >= 90也成立,输出“优秀”。
输入 75 时,外层条件成立、内层条件不成立,输出“及格”。
输入 50 时,外层条件不成立,直接输出“不及格”,内层条件不会执行。
5. 嵌套时一定加花括号
嵌套 if 中,else 会和最近的、还没有配对的 if 配对。这叫作悬空 else问题。
下面的缩进看起来像是 else 对应第一个 if:
if (a > 0)
if (b > 0)
cout << "A" << endl;
else
cout << "B" << endl;
实际上,它等价于:
if (a > 0) {
if (b > 0) {
cout << "A" << endl;
} else {
cout << "B" << endl; // else 对应内层 if
}
}
关键规则:
else总是与最近的、未配对的if配对;缩进不会改变配对关系。
因此,嵌套 if 时要一律加花括号:
if (a > 0) {
if (b > 0) {
cout << "A" << endl;
}
} else {
cout << "B" << endl; // 现在 else 明确对应外层 if
}
6. 什么时候适合用嵌套 if
嵌套 if 的重点不是“能不能写”,而是“写出来是否更清楚”。
适合嵌套的情况:
- 外层判断决定内层判断是否有必要;
- 不同层次需要输出不同的处理结果;
- 程序逻辑本身就是“先……再……”。
如果只是几个并列条件都要满足,使用下一节的 && 往往更简洁。
三、逻辑运算符:把多个条件组合起来
1. 什么是逻辑运算符
逻辑运算符用来连接或改变条件的真、假结果。连接后得到的新条件叫作复合条件,结果仍然是 true 或 false。
例如:
n >= 10 && n <= 99
左边和右边都是比较条件;它们通过 && 连接,组成一个复合条件。
2. 三个逻辑运算符
| 运算符 | 名称 | 读法和含义 | 示例 |
|---|---|---|---|
&& |
逻辑与 | “并且”;两边都真才真 | a > 0 && b > 0 |
| ` | ` | 逻辑或 | |
! |
逻辑非 | “非”;把真变假、把假变真 | !(a > 0) |
如果单独输出逻辑表达式的结果,也要把整个表达式放进括号:
cout << (3 < 4 && 5 > 2) << endl; // 输出 1
cout << (3 > 4 || 5 < 2) << endl; // 输出 0
cout << !(3 < 4) << endl; // 输出 0
因为输出运算符 << 的优先级高于比较和逻辑运算符,括号可以保证先计算完整的条件。
3. &&:两边都成立
&& 表示“并且”。只有左右两边的条件都成立,整个条件才成立。
| 条件 A | 条件 B | A && B |
|---|---|---|
| 真 | 真 | 真 |
| 真 | 假 | 假 |
| 假 | 真 | 假 |
| 假 | 假 | 假 |
例如,判断一个整数是否是两位数:
int n;
cin >> n;
if (n >= 10 && n <= 99) {
cout << "是两位数" << endl;
} else {
cout << "不是两位数" << endl;
}
n >= 10 和 n <= 99 必须同时成立。
注意: 不能写成
10 <= n <= 99。C++ 会先计算10 <= n,得到0或1,再把它和99比较,结果不是“判断范围”。
4. ||:至少一边成立
|| 表示“或者”。只要左右两边至少有一个条件成立,整个条件就成立。
| 条件 A | 条件 B | A || B |
|---|---|---|
| 真 | 真 | 真 |
| 真 | 假 | 真 |
| 假 | 真 | 真 |
| 假 | 假 | 假 |
例如,判断一个字符是否是英文字母:
char ch;
cin >> ch;
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
cout << "yes" << endl;
} else {
cout << "no" << endl;
}
这里有两种可能:
ch在'a'到'z'之间,是小写字母;ch在'A'到'Z'之间,是大写字母。
两种情况满足一种即可,所以用 || 连接。两个范围各自用括号包起来,代码会更清楚。
5. !:把条件取反
! 表示“不是”。它会把一个条件的结果反过来。
| 条件 A | !A |
|---|---|
| 真 | 假 |
| 假 | 真 |
int n = 5;
cout << !(n > 0) << endl; // n > 0 为真,取反后输出 0
cout << !(n < 0) << endl; // n < 0 为假,取反后输出 1
简单条件通常可以直接改写,读起来更自然:
!(n > 0) // n 不大于 0
n <= 0 // 含义相同,更直接
! 更适合给一个较复杂的整体条件取反。
6. 德摩根定律:与、或的否定
当 ! 放在一个由 && 或 || 连接的整体条件前面时,不能只把最前面的一个条件取反。需要同时完成两件事:
- 括号里的每个条件分别取反;
&&和||互换。
这就是德摩根定律,也叫反演律。
“与”的否定
!(条件 1 && 条件 2)
等价于:
(!条件 1 || !条件 2)
也就是说:
不是“条件 1 并且条件 2”成立
↓
条件 1 不成立,或者条件 2 不成立
例如:
!(a > 0 && b > 0)
等价于:
a <= 0 || b <= 0
“a 和 b 不都是正数”,就是“a 不是正数,或者 b 不是正数”。
“或”的否定
!(条件 1 || 条件 2)
等价于:
(!条件 1 && !条件 2)
也就是说:
不是“条件 1 或者条件 2”成立
↓
条件 1 不成立,并且条件 2 不成立
例如:
!(a > 0 || b > 0)
等价于:
a <= 0 && b <= 0
“a 和 b 中没有正数”,就是“a 不是正数,并且 b 也不是正数”。
用表格理解
| 原条件 | 取反后的等价条件 |
|---|---|
!(条件 1 && 条件 2) |
`!条件 1 |
| `!(条件 1 |
可以记成一句话:
整体取反:每个条件都取反,同时 && 和 || 互换。
在具体比较条件中,常见的取反关系包括:
| 原条件 | 取反后 |
|---|---|
x > 0 |
x <= 0 |
x >= 10 |
x < 10 |
x == 5 |
x != 5 |
x != 5 |
x == 5 |
注意:
!(A && B)不能写成!A && !B;!(A || B)也不能写成!A || !B。运算符&&和||必须互换。
6. 逻辑运算符的计算顺序
比较运算符会先算,再进行逻辑运算。逻辑运算符之间的优先级是:
!(非) > &&(且) > ||(或)
因此:
a || b && c
等价于:
a || (b && c)
不过,初学阶段不要依赖记忆优先级。条件稍复杂时,主动加括号:
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
cout << "闰年" << endl;
}
7. 短路求值:有些条件不会继续计算
&& 和 || 都具有短路求值特点:
A && B:如果A已经为假,B不再计算,因为结果一定为假;A || B:如果A已经为真,B不再计算,因为结果一定为真。
短路求值可以避免危险操作。例如,除法前要先确认除数不为 0:
int a, b;
cin >> a >> b;
if (b != 0 && a / b > 2) {
cout << "商大于 2" << endl;
}
当 b == 0 时,左边 b != 0 为假,右边的 a / b 不会计算,因此不会发生除以 0。
下面的顺序则是危险的:
if (a / b > 2 && b != 0) { // b 为 0 时,已经先做了除法
cout << "商大于 2" << endl;
}
记忆: 用
&&做安全检查时,把“安全条件”放在左边,把可能出错的操作放在右边。
8. 综合示例:判断闰年
闰年满足下面两种情况中的一种:
- 能被
4整除,并且不能被100整除; - 能被
400整除。
int year;
cin >> year;
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
cout << "闰年" << endl;
} else {
cout << "平年" << endl;
}
建议用下面四个年份测试:
| 年份 | 结果 | 原因 |
|---|---|---|
2000 |
闰年 | 能被 400 整除 |
1900 |
平年 | 能被 100 整除,但不能被 400 整除 |
2004 |
闰年 | 能被 4 整除,且不能被 100 整除 |
2001 |
平年 | 不能被 4 整除 |
四、嵌套 if 和逻辑运算符怎样选择
有些题目可以同时写成嵌套 if 和逻辑运算符两种形式。
例如,判断 n 是否为两位数:
// 嵌套 if 写法
if (n >= 10) {
if (n <= 99) {
cout << "是两位数" << endl;
}
}
// 逻辑运算符写法
if (n >= 10 && n <= 99) {
cout << "是两位数" << endl;
}
在这个例子中,&& 更简洁;但在需要对不同情况分别处理时,嵌套 if 更清楚。
| 场景 | 更适合的写法 |
|---|---|
| 多个并列条件必须同时满足 | && |
| 多种情况满足一种即可 | ` |
| 先判断大类,再根据结果判断小类 | 嵌套 if |
| 每一层失败时都要给出不同提示 | 嵌套 if |
| 条件很长、很复杂 | 用括号分组,必要时拆成嵌套 if |
嵌套层数太多会让代码不断向右缩进。一般超过三层时,应考虑能否拆分条件或调整判断顺序。
五、switch:一个值对应多个固定选项
1. switch 适合什么问题
switch 语句会计算一个表达式的值,再根据这个值匹配不同的 case。
例如,输入 1、2、3 分别表示不同菜单选项;输入 +、-、*、/ 表示不同运算符。这些都是“同一个值与多个固定值比较”的问题。
switch 适合:
day == 1
day == 2
day == 3
switch 不适合判断大小范围:
score >= 60
age < 18
范围判断仍然使用 if-else if-else。
2. 基本语法
switch (表达式) {
case 值 1: {
// 表达式等于值 1 时执行
break;
}
case 值 2: {
// 表达式等于值 2 时执行
break;
}
default: {
// 所有 case 都不匹配时执行
break;
}
}
推荐写法: 每个
case和default后面都加花括号。这样每个分支都有清楚的范围,可以减少变量声明、缩进和漏写语句造成的错误。break写在花括号里面,仍然表示跳出整个switch。
3. switch 的流程图
%%{init: {"themeVariables": {"edgeLabelBackground": "#FFFFFF"}}}%%
flowchart TD
Start([开始]) --> Decision[计算可匹配表达式的值]
Decision --> Case1{匹配 case 1?}
Case1 -->|是| Action1[执行 case 1 的语句]
Case1 -->|否| Case2{匹配 case 2?}
Case2 -->|是| Action2[执行 case 2 的语句]
Case2 -->|否| More[继续检查后面的 case]
More --> Match{存在匹配的 case?}
Match -->|是| Action3[执行匹配 case 的语句]
Match -->|否| Default[执行 default 的语句]
Action1 --> Break[遇到 break,跳出 switch]
Action2 --> Break
Action3 --> Break
Default --> Break
Break --> Continue[继续执行后面的语句]
Continue --> End([结束])
classDef condition fill:#e1f5fe,stroke:#0288d1,stroke-width:2px;
classDef action fill:#fff9c4,stroke:#fbc02d,stroke-width:1px;
classDef merge fill:#f5f5f5,stroke:#9e9e9e,stroke-width:1.5px;
class Decision,Case1,Case2,Match condition;
class Start,End,Action1,Action2,Action3,Default action;
class More,Break,Continue merge;
4. case、break 和 default
| 部分 | 作用 | 注意事项 |
|---|---|---|
case 值: |
表示一种固定取值 | 值 必须是常量,多个 case 的值不能重复 |
break; |
立刻跳出整个 switch |
大多数 case 末尾都要写 |
default: |
所有 case 都不匹配时执行 |
类似 if-else 中的 else,建议保留 |
5. switch 条件表达式可以放什么
switch (表达式) 中的表达式,最终必须能够得到整数类型、字符类型或枚举类型的值。初学阶段可以按下面的范围理解:
| 可以使用 | 示例 | 说明 |
|---|---|---|
| 整数变量 | int n、long long n |
根据整数值匹配 case |
| 字符变量 | char op |
字符本质上也有整数编码,适合匹配 case '+' |
| 布尔值 | bool ok |
语法上可以使用,但通常不如 if 清楚 |
| 枚举值 | Color color |
枚举的每个选项可以作为固定分支 |
| 结果为整数或字符的表达式 | x % 3、day + 1 |
先计算表达式,再匹配结果 |
例如,下面的写法可以使用:
int n;
cin >> n;
switch (n % 3) {
case 0: {
cout << "余数是 0" << endl;
break;
}
case 1: {
cout << "余数是 1" << endl;
break;
}
case 2: {
cout << "余数是 2" << endl;
break;
}
}
下面这些类型或写法不能直接用于 switch:
| 不能直接使用 | 示例 | 应该怎么做 |
|---|---|---|
| 小数类型 | float、double |
使用 if-else 判断范围或小数关系 |
| 字符串 | string、std::string |
使用 if-else 比较字符串,或转换成编号 |
| 字符串字面量 | switch ("add") |
使用字符 case '+',或用 if (command == "add") |
| 范围条件 | case score >= 60: |
使用 if-else if-else |
例如,下面的写法是错误的:
double score;
cin >> score;
// 错误:switch 不能直接判断 double
switch (score) {
// ...
}
补充说明: 比较表达式的结果是
bool,所以switch (x > 0)在语法上可能成立,但它实际上只有“真”和“假”两个选项,直接使用if (x > 0)更清楚。switch最适合的是一个值对应多个固定选项。
6. case 标签可以写什么
case 后面的值必须是编译时就能确定的整数常量、字符常量或枚举常量:
const int MENU_START = 1;
switch (choice) {
case MENU_START: {
cout << "开始" << endl;
break;
}
case 2 + 1: {
cout << "设置" << endl;
break;
}
default: {
cout << "未知选项" << endl;
break;
}
}
不能把普通变量、double、字符串或范围写在 case 后面:
int x = 2;
switch (choice) {
// 错误:x 不是编译时常量
case x: {
break;
}
// 错误:case 不能表示范围
// case 1 <= choice <= 3:
// 错误:字符串不是字符常量
// case "add":
}
多个 case 的值也不能重复,否则同一个输入无法确定应该进入哪一个分支。
7. 示例:根据星期编号输出星期
int day;
cin >> day;
switch (day) {
case 1: {
cout << "星期一" << endl;
break;
}
case 2: {
cout << "星期二" << endl;
break;
}
case 3: {
cout << "星期三" << endl;
break;
}
default: {
cout << "输入有误" << endl;
break;
}
}
输入 2 时,程序从 case 2: 开始执行,输出“星期二”,遇到 break 后跳出 switch。
8. 示例:简易计算器
运算符是一个字符,特别适合用 switch 判断。
int a, b;
char op;
cin >> a >> op >> b;
switch (op) {
case '+': {
cout << a + b << endl;
break;
}
case '-': {
cout << a - b << endl;
break;
}
case '*': {
cout << a * b << endl;
break;
}
case '/': {
if (b == 0) {
cout << "除数不能为 0" << endl;
} else {
cout << a / b << endl;
}
break;
}
default: {
cout << "未知运算符" << endl;
break;
}
}
这个例子说明:switch 的某个 case 内部仍然可以继续写 if-else。当运算符是 / 时,还需要判断除数是否为 0。
注意: 字符常量使用英文单引号,例如
case '+':;不能写成case "+":。
9. 忘记 break 会发生什么
如果 case 末尾忘记写 break,程序会继续执行后面的 case,这种现象叫作穿透。
int n = 1;
switch (n) {
case 1: {
cout << "A" << endl;
}
case 2: {
cout << "B" << endl;
break;
}
}
上面会输出:
A
B
因为 case 1 执行完后没有 break,程序继续执行了 case 2。
初学阶段可以记住:除非明确想让多个 case 共用后面的语句,否则每个 case 后面都写 break;。即使要演示穿透,也建议保留每个 case 的花括号,让分支范围清楚。
10. switch 和 if-else if 的选择
| 情况 | 推荐写法 |
|---|---|
| 判断一个值是否等于多个固定选项 | switch |
| 判断范围,如分数等级、年龄区间 | if-else if-else |
条件中有 &&、` |
|
某个 case 内还要继续判断特殊情况 |
switch 内嵌 if |
六、常见错误与注意事项
1. 有编译器提示的错误
| 编号 | 错误 | 识别信息(关键特征) | 后果 | 改正 |
|---|---|---|---|---|
| E1 | case 后写变量,如 case x: |
常见 not usable in a constant expression |
case 不能使用普通变量 |
写成固定常量,如 case 1: |
| E2 | switch 判断 double |
常见 switch quantity not an integer |
switch 不能直接判断小数 |
改用 if-else |
| E3 | 字符 case 写成双引号,如 case "+": |
常见 case label does not reduce to an integer constant |
双引号是字符串,不是单个字符 | 使用 case '+': |
| E4 | 花括号或圆括号没有配对 | 常见 expected '}'、expected ')' |
分支结构无法正确编译 | 检查 { } 和 ( ) 是否成对 |
2. 没有固定报错,但结果或逻辑不对
下面的问题通常可以编译通过,需要通过阅读代码和测试数据发现。
| 编号 | 错误 | 识别信息(关键特征) | 后果 | 改正 |
|---|---|---|---|---|
| E5 | && 写成 &,或 ` |
写成 |
` | |
| E6 | 把“且”写成 ` | ,或把“或”写成 &&` |
用一组只满足部分条件的数据测试 | |
| E7 | 范围判断写成 10 <= n <= 99 |
出现连续两个比较运算符 | C++ 会分两次比较,结果错误 | 写成 n >= 10 && n <= 99 |
| E8 | 复杂条件没有分组 | &&、` |
` 同时出现且没有括号 | |
| E9 | 安全检查写在 && 右边 |
先发生除法、数组访问等危险操作 | 可能出现运行错误 | 先检查安全条件,例如 b != 0 && a / b > 2 |
| E10 | 嵌套 if 没有花括号 |
else 的缩进看起来不清楚 |
else 可能配对到错误的 if |
嵌套时一律加花括号 |
| E11 | case 后漏写 break |
一个输入却输出了多个分支的内容 | 发生穿透,继续执行后面的 case |
在每个 case 末尾写 break; |
| E12 | 用 switch 判断范围 |
写出类似 case score >= 60: |
case 不能表示范围 |
范围判断使用 if-else if-else |
| E13 | 没有处理未匹配的输入 | 只写了几个 case,没有 default |
非法输入时没有输出 | 根据题意添加 default |
3. 分支题怎样测试
复杂分支不能只测试一个“普通样例”,还要测试每一条路。
例如,判断两位数时,至少测试:
9 不是两位数
10 是两位数
99 是两位数
100 不是两位数
例如,简易计算器至少测试:
3 + 4 加法分支
8 / 2 除法分支
8 / 0 除数为 0 的特殊分支
5 ^ 2 default 分支
测试习惯: 每个分支至少走一次;涉及边界时,测试边界前、边界和边界后三种数据。
七、本课小结
多个条件都要满足 → &&
多个条件满足一个即可 → ||
把一个条件反过来 → !
先分大类,再分小类 → 嵌套 if
一个值匹配固定选项 → switch
写复杂条件时,记住三件事:
- 先用中文说清楚条件之间是“且”还是“或”;
- 复杂条件主动加括号,嵌套
if一律加花括号; switch只处理固定的等值选项,大多数case后面要写break;。