分支与循环:控制流入门
if/else-if/switch 三级递进、for/while/do-while 与范围 for、break/continue、条件运算符与循环三要素心智模型;以一元二次方程求根的三次迭代演示为什么需要分支,练习改编自真实课件题集(闰年、成绩五档、出租车计费、九九乘法表、素数、金字塔),并补输入校验循环等工程场景。
知识点地图
- 知识类别:控制流(control flow)——分支语句(if/switch)与循环语句(for/while/do-while/范围 for),对应 C++ Primer Plus 第 5、6 章与 cppreference Statements 主线。
- 解决什么问题:程序目前只会从上到下直着走;分支让程序「看情况办事」,循环让程序「重复干活不复制粘贴」。没有它们,一个求根程序连「无实根」都表达不了。
- 什么时候用到:所有业务判断(成绩分档、计费规则)、所有重复任务(遍历、累加、重试)、所有交互程序的主循环与输入校验。
前置知识
- 基本语法与第一个程序:会写 main、会
std::cin/std::cout、编译运行两步走得通; - 类型系统:知道 int/double/bool 是什么,本文直接使用。
学习目标
读完本文你将能够:
- 用 if / else-if / else 写出互斥的多分支,并用 switch 表达离散取值的分派;
- 说出循环三要素(初始化、条件、推进)在 for 与 while 里各写在哪个位置;
- 按「已知次数用 for、先看条件用 while、至少跑一次用 do-while、遍历容器用范围 for」选对循环;
- 用 break 与 continue 精确控制循环的「跳出」与「跳过」;
- 独立完成闰年判断、成绩五档、出租车分段计费、九九乘法表、素数判断、金字塔打印六道经典题。
预计 70 分钟,含 1 个找错环节与 3 道动手任务。
1. 为什么需要分支:一元二次方程的三次迭代
从真实课件里的一道贯穿例题看「没有分支的程序有多脆」。求 的根,第一版:
#include <iostream>
#include <cmath>
int main() {
double a, b, c;
std::cin >> a >> b >> c;
double delta = b * b - 4 * a * c;
double x1 = (-b + std::sqrt(delta)) / (2 * a);
double x2 = (-b - std::sqrt(delta)) / (2 * a);
std::cout << "x1=" << x1 << " x2=" << x2 << '\n';
}输入 1 2 5(delta = 4 - 20 = -16):std::sqrt(-16) 返回 NaN,程序不崩溃但输出两个 NaN——程序没有能力表达「无实根」这件事。第二版加一个保护:
if (delta >= 0) {
double x1 = (-b + std::sqrt(delta)) / (2 * a);
double x2 = (-b - std::sqrt(delta)) / (2 * a);
std::cout << "x1=" << x1 << " x2=" << x2 << '\n';
}
// delta < 0 时什么都不输出——还是不完整第三版才真正完整:三分支各答其事。
#include <iostream>
#include <cmath>
int main() {
double a, b, c;
std::cin >> a >> b >> c;
double delta = b * b - 4 * a * c;
if (delta > 0) {
double x1 = (-b + std::sqrt(delta)) / (2 * a);
double x2 = (-b - std::sqrt(delta)) / (2 * a);
std::cout << "两个不等实根: x1=" << x1 << ", x2=" << x2 << '\n';
} else if (delta == 0) {
std::cout << "两个相等实根: x=" << -b / (2 * a) << '\n';
} else {
std::cout << "无实根(delta = " << delta << " < 0)\n";
}
}逐段讲解:
if (条件)的条件是 bool 语境:delta > 0为真走第一支。条件里写=而不是==是经典笔误——if (delta = 0)把 0 赋给 delta 再判断,恒为假,编译器大多只给警告(030 篇的 UB 教训在此同理:警告别不当错误)。else if链自上而下逐个检查,命中一支后整条链结束——分支互斥不用自己再写判断。- 换别的写法会发生什么:把
delta == 0放最前面判断,逻辑等价但阅读顺序反了(先问最常见的再问特例);浮点相等判断在「算出来的 delta」上其实不可靠(第 040 篇浮点精度),本题 delta 由整数系数算出才敢直接比,一般工程写法是std::abs(delta) < 1e-9。
2. if / else-if / else:互斥的多分支
2.1 成绩五档(90/80/70/60 分界)
#include <iostream>
int main() {
int score;
std::cin >> score;
if (score < 0 || score > 100) {
std::cout << "非法成绩\n";
} else if (score >= 90) {
std::cout << "A\n";
} else if (score >= 80) {
std::cout << "B\n";
} else if (score >= 70) {
std::cout << "C\n";
} else if (score >= 60) {
std::cout << "D\n";
} else {
std::cout << "E\n";
}
}逐段讲解:
- 边界靠继承:能走到
score >= 80,说明前面>= 90已经不成立——每个条件只写「自己的下界」,等价于区间80 <= score < 90。写成score >= 80 && score < 90也对,但重复且漏写上界就是 bug(else if (score >= 80 && score < 90)本身没错,错的是有人顺手写成> 80,81 分的同学就掉档了)。 - 先拦非法值:
||表示「或」,score < 0 || score > 100挡掉输入错误。这个习惯来自真实工程:成绩录错了分档,比不分档更糟。 - 易错点:
60 <= score <= 100在数学里对、在 C++ 里错——它先算60 <= score得 bool(0 或 1),再用 0/1 与 100 比较,恒为真。取值范围必须拆成两个条件用&&连接。
2.2 出租车分段计费(if 版与 switch 版对照)
题面:三种车型,起步价与单价不同——夏利 3 元起步、超 3 公里每公里 2.1 元;富康 4 元起步、超 3 公里每公里 2.4 元;桑塔纳 5 元起步、超 3 公里每公里 2.7 元。
#include <iostream>
int main() {
int carType; // 1 夏利 2 富康 3 桑塔纳
double km;
std::cin >> carType >> km;
double base = 0, perKm = 0;
switch (carType) {
case 1: base = 3.0; perKm = 2.1; break; // 每个 case 以 break 收尾
case 2: base = 4.0; perKm = 2.4; break;
case 3: base = 5.0; perKm = 2.7; break;
default: std::cout << "无此车型\n"; return 1;
}
double fare = base;
if (km > 3) {
fare += (km - 3) * perKm; // 分段:起步价覆盖前 3 公里
}
std::cout << "车费 " << fare << " 元\n";
}逐段讲解:
switch适合「离散取值分派」:carType 是 1/2/3 的整数标签。case只是入口标签,不写 break 会一路穿透到下一个 case(fall-through)——忘了 break,选夏利会被富康的单价覆盖,逻辑错但不报错,是课件易错点清单的第一条。default兜住非法输入,所有 switch 都该写 default(哪怕只打一行日志);C++ 对「枚举 switch 漏分支」有-Wswitch警告,配合 420 篇会讲的[[nodiscard]]类属性能进一步收紧。- 分段计费的「分段」落在 if 里:起步价管前 3 公里,超出部分按单价补差。换别的写法:全部用 if-else 判 carType 也能跑,但三种车型七种组合的嵌套深度会失控——课件总结原话「不提倡 if 内嵌 switch」「if_else 嵌套过深降低效率与可读性」。
case里声明变量要加花括号(case 1: { double t = ...; }),否则跨 case 的变量声明会编译报错。
3. 循环:三要素与四种写法
3.1 循环三要素心智模型
每个循环都在回答三个问题(课件原话的工程版):
| 要素 | 问题 | 在 for 里的位置 |
|---|---|---|
| 初始化 | 从哪开始? | for (int i = 0; ...) 第一段 |
| 条件 | 什么时候停? | 第二段,每轮开头检查 |
| 推进 | 怎么走向结束? | 第三段,每轮结尾执行 |
三要素缺一即是死循环:忘了推进(i++ 漏写)是最常见的一种。检查任何死循环,先按这三问过一遍。
3.2 for / while / do-while 三种写法同一道题
同一道「打印 1 到 5」:
for (int i = 1; i <= 5; ++i) {
std::cout << i << ' ';
}
int j = 1; // 初始化在循环外
while (j <= 5) { // 先判断,可能一次都不执行
std::cout << j << ' ';
++j; // 推进在循环体内
}
int k = 1;
do { // 先执行后判断,至少执行一次
std::cout << k << ' ';
++k;
} while (k <= 5);选型规则:
- 已知次数 → for:次数、变量、推进一目了然;
- 不知道次数、先看条件 → while:例如「读输入直到 EOF」;
- 必须至少跑一次 → do-while:例如「先显示菜单再问要不要继续」。
三种循环可以互相转换(课件用同一道「10 道加减乘除测验题」演示过三写法),所以选型是可读性问题而非能力问题。
易错点:do-while 的 while (条件) 后面有分号,漏写编译报错;while 的条件永远为真且体内无 break/return 就是死循环。
3.3 范围 for:遍历容器专用(C++11)
#include <iostream>
#include <string>
int main() {
std::string s = "hello";
for (char ch : s) { // 逐字符遍历,下标都不用写
std::cout << ch << ' ';
}
int arr[] = {10, 20, 30};
for (int x : arr) {
std::cout << x << ' ';
}
// 要修改元素时用引用:
for (int& x : arr) {
x *= 2;
}
}for (int x : arr) 把每个元素拷贝进 x;for (int& x : arr) 用引用直达元素、可修改且免拷贝。范围 for 不能用来「跳着走」或反向走——那些需求回到下标 for(vector/array 详见 045 篇)。
4. break、continue 与条件运算符
4.1 九九乘法表:嵌套循环模板
#include <iostream>
int main() {
for (int i = 1; i <= 9; ++i) { // 外层控行
for (int j = 1; j <= i; ++j) { // 内层控列,列数随行号增长
std::cout << j << '*' << i << '=' << j * i << '\t';
}
std::cout << '\n'; // 每行结束换行
}
}逐段讲解:外层 i 是行号,内层 j 跑到 i 为止——内层边界依赖外层变量,这是「三角形/金字塔」类打印的通用模板。金字塔打印只是再加一个内层循环先打空格:第 i 行打 9 - i 个空格、2*i - 1 个星号(见动手任务三)。
4.2 break 与 continue:素数判断
#include <iostream>
int main() {
int n;
std::cin >> n;
bool isPrime = true;
for (int d = 2; d * d <= n; ++d) { // 试除到 sqrt(n) 即可
if (n % d == 0) {
isPrime = false;
break; // 找到因子,不必再试:跳出整个循环
}
}
std::cout << n << (n > 1 && isPrime ? " 是素数" : " 不是素数") << '\n';
}// continue:跳过本轮,打印 100~200 中不能被 3 整除的数
for (int i = 100; i <= 200; ++i) {
if (i % 3 == 0) {
continue; // 跳到下一轮(for 跳到 i++ 那一段)
}
std::cout << i << ' ';
}逐段讲解:
break跳出最近一层循环;continue只跳过本轮剩余语句。语义区别一句话:break 是「不干了」,continue 是「这个不要,下一个」。- continue 在 for 里跳到「推进表达式」(i++),在 while/do-while 里直接跳到条件判断——若推进语句写在循环体末尾,while 里用 continue 会跳过推进造成死循环,这是两种循环在 continue 上的真实差异。
- 试除上界写
d * d <= n而不是d <= sqrt(n):省一次浮点转换与头文件。课件补充题「不用 continue 改写」的答案就是把条件翻过来:if (i % 3 != 0) std::cout << i << ' ';——continue 永远可以用条件取反改写,可读性二选一。 - 条件运算符
? :是「返回值的 if」:n > 1 && isPrime ? "是" : "不是"。判断逻辑简单时它最紧凑;嵌套两层以上就换 if,可读性优先(课件连「每月天数」都给过三目运算符版,但那是展示语法,不是推荐写法)。
5. 工程场景:输入校验循环
交互程序的经典骨架——「输入不合法就重问」,把 do-while、break、状态分支串起来:
#include <iostream>
#include <limits>
#include <string>
int readIntInRange(const std::string& prompt, int lo, int hi) {
int value;
while (true) { // 服务循环:直到拿到合法值
std::cout << prompt;
if (std::cin >> value && value >= lo && value <= hi) {
return value; // 合法:返回即出口
}
std::cin.clear(); // 清除 fail 状态(输入 abc 时的必做步骤)
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 丢弃坏输入
std::cout << "请输入 " << lo << " 到 " << hi << " 之间的整数\n";
}
}
int main() {
int age = readIntInRange("年龄(1-120): ", 1, 120);
std::cout << "收到: " << age << '\n';
}逐段讲解:
while (true)+ return 的写法把「重试」交给循环、「成功」交给 return,出口唯一。换成do { cin >> v; } while (v < lo || v > hi);也能跑,但用户输入字母时cin进入失败态,v 不再更新,同样死循环——输入校验的关键不是循环结构,是处理 cin 的失败状态。cin.clear()复位错误标志,cin.ignore(...)把缓冲区里的坏字符扔掉,两步缺一不可:只 clear 不 ignore,坏字符还在缓冲区,下一轮继续失败。- 这个模式在真实工程里无处不在:菜单主循环、配置端口范围、游戏里的年龄/数量输入。第 030 篇的 cin/cout 只完成「读一次」,这里补上「读对为止」的完整闭环。
6. 动手实践
任务一:闰年与季节(热身)
写程序输入年份:输出「闰年/平年」(能被 400 整除,或能被 4 整除但不能被 100 整除);再用月份 1-12 输出季节(3-5 春、6-8 夏、9-11 秋、12/1/2 冬),分别用 if-else 与 switch 各写一版季节判断,对比哪个更顺眼。
提示:闰年条件 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);switch 版的 12/1/2 三个月可以「故意」利用 case 穿透写成 case 12: case 1: case 2: ...——这是穿透唯一的正当用途。
参考实现要点(先自己写完再展开):
bool isLeap(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
std::string seasonOf(int month) { // switch 版
switch (month) {
case 3: case 4: case 5: return "春";
case 6: case 7: case 8: return "夏";
case 9: case 10: case 11: return "秋";
case 12: case 1: case 2: return "冬";
default: return "非法月份";
}
}任务二:找错——这段循环为什么停不下来(实战)
#include <iostream>
int main() {
int n = 100;
while (n != 0) {
if (n % 2 == 0) {
std::cout << n << " 偶数\n";
continue;
}
std::cout << n << " 奇数\n";
n--;
}
}提示:continue 在 while 里跳去了哪里?推进语句 n-- 还能被执行到吗?
参考实现(先自己写完再展开):
int main() {
int n = 100;
while (n > 0) { // 顺手把 != 0 换成 > 0,防负数意外
if (n % 2 == 0) {
std::cout << n << " 偶数\n";
n--; // 修正:continue 跳过了 n--,必须自己推进
continue;
}
std::cout << n << " 奇数\n";
n--;
}
}原代码 n 为偶数时 continue 直接跳回条件判断,n-- 永远不执行,n 卡在 100 死循环。这正是 continue 在 while 与 for 里的差异:for 的推进写在第三段,continue 跳过去时推进照样执行,while 的推进写在体内就危险。另一个修法是改用 for 循环,把推进交给第三段。
任务三:金字塔打印(综合)
输入行数 n(用第 5 节的 readIntInRange 校验 1-20),打印居中金字塔:第 i 行有 n - i 个空格与 2*i - 1 个星号。
提示:三层嵌套——行循环里先打空格循环再打星号循环;先在纸上画出 i=3、n=5 的一行数一数再动手。
参考实现要点(先自己写完再展开):
for (int i = 1; i <= n; ++i) {
for (int s = 0; s < n - i; ++s) std::cout << ' ';
for (int t = 0; t < 2 * i - 1; ++t) std::cout << '*';
std::cout << '\n';
}任务四:出租车计费 switch 版迁移
把第 2.2 节的出租车程序补上第 5 节的输入校验(车型 1-3、公里数 0.1-1000),并新增第 4 种车型「新能源:起步 2 元、超 3 公里每公里 1.8 元」——只许改 switch 的 case 与数据,不许动计费逻辑。
提示:能只改数据就加车型,说明「分派」与「计费」分得够开;这正是 055 篇函数拆分的前奏。
7. 参考与致谢
- ISO C++ 标准与 cppreference:Statements 页(https://en.cppreference.com/w/cpp/language/statements ,CC BY-SA 许可)——if/switch/for/while/do 的语义与 fall-through 规则以官方文档为准,本文代码为原创示例;
- Stephen Prata, C++ Primer Plus(第 6 版)第 5 章「循环和关系表达式」、第 6 章「分支语句和逻辑运算符」的主题划分(本文为其入门前置,代码自写);
- 分支与循环例题(一元二次方程贯穿例、闰年、成绩五档、出租车计费、九九乘法表、素数、金字塔、continue 纠错题)改编自仓库扫描素材的 C 课件题集(见
.workflow-tmp/scan/e-core-c-cpp-godot-net.mdA1/A3 节),全部用现代 C++ 语法重写。