语句
掌握 C++ 控制流——if/switch分支、while/for/do-while循环、break/continue跳转,能写出正确的条件判断和循环逻辑
学习目标
- 能写出带
if/else if/else的条件分支和带switch/case/default的多路分支,并区分两者的适用场景 - 能实现
while、for、do-while循环逻辑,并在循环体内用break/continue控制执行流 - 能回答:
int x = 5; if (x = 0) cout << "yes"; else cout << "no";输出什么?为什么和你直觉的相反?
机制总览
语句:机制路径
- 1
直觉:程序不能只会一条路走到黑
到目前为止你写的程序都是「一条直线」——从第一行执行到最后一行,没有分岔、没有回头路。但真实世界的逻辑不是这样的:ATM 取钱要先判断余额够不够、登录要判断密码对不对、游戏里怪物死了要重新跑一轮——程序需要 根据条件选路 、需要 重复执行某段代码 。
- 2
第一种选择:if 和 else
程序中需要判断的条件可以分成两类:一类是 范围判断 ——「分数是不是大于 60」「余额是不是不够 100」,这类条件用布尔表达式表达,然后让程序根据 true 或 false 走不同路径。官方把 if 与 switch 统称为 conditional statements(条件语句);最基本的工具是 if/else 语句 。
- 3
第二种选择:switch——对号入座
if/else if 链虽然能处理多个分支,但如果你只是根据一个 离散值 来做选择——比如根据「星期几(1-7)」做不同的事——if-else 链就显得啰嗦了。C++ 提供了 switch 语句 —— 把它理解成一个自动分拣机 :你把一个号码扔进去,机器直接把你弹到对应编号的出口,不用像 if-else 一样逐个检查。
章级决策实验
语句:机制与证据
切换《语句》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 直觉:程序不能只会一条路走到黑
到目前为止你写的程序都是「一条直线」——从第一行执行到最后一行,没有分岔、没有回头路。但真实世界的逻辑不是这样的:ATM 取钱要先判断余额够不够、登录要判断密码对不对、游戏里怪物死了要重新跑一轮——程序需要 根据条件选路 、需要 重复执行某段代码 。
可核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「直觉:程序不能只会一条路走到黑」的契约。
学完《语句》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
语句:失效与核验
直觉:程序不能只会一条路走到黑
典型失效
若把「直觉:程序不能只会一条路走到黑」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「直觉:程序不能只会一条路走到黑」的契约。
第一种选择:if 和 else
典型失效
若把「第一种选择:if 和 else」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「第一种选择:if 和 else」的契约。
第二种选择:switch——对号入座
典型失效
若把「第二种选择:switch——对号入座」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「第二种选择:switch——对号入座」的契约。
直觉:程序不能只会一条路走到黑
到目前为止你写的程序都是「一条直线」——从第一行执行到最后一行,没有分岔、没有回头路。但真实世界的逻辑不是这样的:ATM 取钱要先判断余额够不够、登录要判断密码对不对、游戏里怪物死了要重新跑一轮——程序需要根据条件选路、需要重复执行某段代码。
想象一条工厂流水线,前面来了一个零件:质检员先看一眼——合格就送去组装线,不合格丢进废料筐,这是分岔。组装线上要用螺丝刀拧 8 颗螺丝——同一个动作重复 8 次,这是循环。站在流水线前你不会茫然——因为你知道「如果怎样就怎样」和「重复几次就停」的规矩。C++ 里的这套规矩,就是这一章要讲的东西。
没有这些规矩会怎样?你的程序永远是一条直线——没法判断密码对错、没法翻页显示数据、没法把 100 条记录逐个处理。本章教你让程序「学会做选择」和「学会反复干」——这是从玩具程序迈入实用程序的关口。
第一种选择:if 和 else
程序中需要判断的条件可以分成两类:一类是范围判断——「分数是不是大于 60」「余额是不是不够 100」,这类条件用布尔表达式表达,然后让程序根据 true 或 false 走不同路径。官方把 if 与 switch 统称为 conditional statements(条件语句);最基本的工具是↡C++ 的条件分支语句;从上到下检查条件,只执行首个成立分支或最终 else 分支。。
把它理解成流水线上的质检站:零件来了,质检员看一眼标尺——「合格?」是就送左边组装线,不是就送右边废料筐。如果有多种等级,就多摆几个质检员排成一排——每个只看自己的标准,哪一个先点头,零件就进哪条线。
程序是怎么做判断的?靠条件表达式——一个能算出 true 或 false 的东西。x > 5 是一个条件,balance >= 100 也是一个条件。把条件写在 if 后面的括号里,程序就能在两条路之间选了:
int score = 85;
if (score >= 90) {
cout << "A"; // 只有 score >= 90 时才走这里
} else if (score >= 80) {
cout << "B"; // 90 > score >= 80 才走这里
} else if (score >= 70) {
cout << "C"; // 依此类推
} else {
cout << "D"; // 以上全不是,走这里
}同一个 if-else if-else 链里从头到尾最多只有一块代码会执行。条件是按从上到下的顺序检查的——85 ≥ 90?不是。85 ≥ 80?是——输出 B,然后整个 if 链直接结束,后面的 C 和 D 不会再被检查。
第二种选择:switch——对号入座
if/else if 链虽然能处理多个分支,但如果你只是根据一个离散值来做选择——比如根据「星期几(1-7)」做不同的事——if-else 链就显得啰嗦了。C++ 提供了↡C++ 的多路分支语句。根据一个整型表达式的值,跳转到匹配的 `case` 标签执行对应代码。遇到 `break` 跳出 switch;如果所有 case 都不匹配,执行 `default`。适合「根据离散值做选择的场景」,比一长串 if-else if 更清晰。——把它理解成一个自动分拣机:你把一个号码扔进去,机器直接把你弹到对应编号的出口,不用像 if-else 一样逐个检查。
switch 的语法和 if 完全不同——记住这四个关键点:
int day = 3;
switch (day) { // 括号里必须是整型(int、char、enum)
case 1: // case 后面是常量值
cout << "周一";
break; // 执行完就跳出 switch——必须写!
case 2:
cout << "周二";
break;
case 3:
cout << "周三";
break;
default: // 所有 case 都不匹配时走这里(可选)
cout << "不是周一二三";
}switch 和 if 最大的区别不止语法——忘记写 break 会导致贯穿(fall-through):程序从命中 case 开始执行,一直到碰到 break 或者 switch 结尾才停。很多 bug 就是少写一个 break 导致的。
三种重复:while、for、do-while
程序里最常见的需求不是「选一条路」,而是「把一件事重复做 N 遍」。C++ 给了你三件循环工具——它们都能重复执行一段代码,区别只在什么时候判断要不要继续。
↡C++ 的三种循环语句。while 先判断条件再执行(可能一次都不跑);for 把初始化、条件、更新三件事写在头部(最常用的计数器循环);do-while 先执行再判断(至少跑一次)。三者本质上可以互相转换,选择哪个看场景。——把它理解成流水线上的一段首尾相连的传送带,零件在上面转圈,每转一圈加工一次,直到质检员喊停。
三种循环的代码模板每行都值得看——它们是日常写 C++ 最高频出现的结构之一:
// ① for:最常用——计数器循环。次数已知时用它。
for (int i = 0; i < 10; i++) { // 初始化; 条件; 每轮后执行
cout << i << ' '; // 输出 0 1 2 ... 9
}
// ② while:条件驱动——次数未知时用它。
int x = 1024;
while (x > 1) { // 每次先判断条件
x /= 2; // 条件 false 就停止
}
// 循环结束时 x 一定 ≤ 1
// ③ do-while:最不常用但必要的——至少跑一次。
int pass;
do {
cout << "输入密码:";
cin >> pass;
} while (pass != 1234); // 先跑一次再判断——注意末尾有分号循环体内的刹车:break 与 continue
循环不是非得从头跑到尾——你可以在循环体里主动控制什么时候停、什么时候跳过。两把钥匙:
↡循环体内的控制语句。break 立即终止当前所在的循环——循环结束,跳到循环体之后的代码继续执行;continue 跳过当前这一轮循环的剩余代码,直接进入下一轮迭代的条件判断。两者都只影响最内层的那一层循环(嵌套循环中不影响外层)。——break 是整个流水线急停按钮,continue 是「这个零件不过这道工序,直接送下一轮」。
// break:找到目标就撤——再也不跑后续的循环了
for (int i = 0; i < 100; i++) {
if (data[i] == target) {
cout << "找到了!下标是 " << i;
break; // 一找到就跳出——i+1..99 全部跳过
}
}
// continue:跳过不符合条件的——剩下的还得接着跑
for (int i = 0; i < 100; i++) {
if (data[i] < 0) continue; // 负数不理它,直接看下一个
total += data[i]; // 只有非负数才会走到这里
}最后一道防线:异常处理
前面的条件判断和循环是你主动控制程序流程的工具——你知道在哪些地方会有分支、会重复。但有一种情况你控制不了:深层代码出了意外。比如你写了一个除法函数,调用方传了个 0 当除数——除到一半才发现,这时候怎么把「出错了」这个消息传回去?靠返回值吗?如果调用方忘了检查返回值呢?
C++ 提供了一套专门处理这种「意料之外的错误」的机制——↡C++ 的错误处理机制;throw 报告错误,try 标记受保护代码,catch 按类型捕获并处理,传播时自动退出中间作用域。——把它理解成火灾报警系统:一楼仓库起火了(throw),一楼办公室的人和二楼的人不是一个个去灭火——他们直接拉响警报,火情自动传递到消防中心(catch),由专业人士处理。
#include <stdexcept>
#include <iostream>
using std::runtime_error;
int divide(int a, int b) {
if (b == 0)
throw runtime_error("除数不能为零!"); // throw:拉响警报
return a / b;
}
int main() {
try { // try:警戒区——里面可能起火
int result = divide(10, 0);
cout << result;
} catch (const runtime_error& e) { // catch:消防中心——灭火
cout << "错误:" << e.what(); // 输出:错误:除数不能为零!
}
}三步走:① throw 抛出异常——代码立即停止,异常对象沿调用栈往上飞;② try 包裹可能出错的代码;③ catch 在调用栈合适的层级捕获并处理。catch 只捕获类型匹配的异常——如果抛的是 runtime_error,catch int 是抓不住的。如果一路都没有匹配的 catch 捕获它(最终连 main 也没接住),程序就会调用 std::terminate() 直接终止。
Simple statements、复合块与作用域
官方 Chapter 5 先从 simple statements 与 statement scope 开始,因为 if、循环和 jump 的合法性都依赖“什么算一条 statement”和“对象何时进入/离开作用域”。
↡以分号结束的表达式语句或空语句,以及用花括号把多个声明和语句组合成一条语法语句的复合块。中,value += 1; 是 expression statement,单独的 ; 是 null statement,{ ... } 是 compound statement/block。空语句有合法用途,例如把工作放在循环条件中,但意外多写分号会让 while (condition); 形成空循环。
↡由块或控制语句创建的名字可见范围;其中声明的对象在进入声明点后可见,并在离开对应作用域时销毁。要求多条受控语句使用花括号。if (condition) { int x = 1; } 中的 x 在后续不可见;在 for (int i=0; ...) 中,i 也只活到循环结束。Switch 的 case 只是 label,不自动创建 scope;若跳转越过带初始化的 declaration,应为各 case 加块。
↡通过 break、continue 或 goto 改变顺序执行位置的语句;控制转移必须遵守对象初始化和作用域边界。(jump statements)不只有 break/continue。goto label; 能跳到同一函数内 label,但不能向前跳过需要初始化的对象后再进入其 scope;向外跳会正常销毁离开的 automatic objects。结构化循环和函数通常更清晰,goto 只在少数多层清理/状态机场景谨慎使用。
条件语句还有两个边界:else 总与最近的 unmatched if 配对(dangling else),需要花括号表达意图;switch 从匹配 label 开始顺序执行,fallthrough 必须是显式设计,而不是漏写 break。
动手:一步步看 for 循环怎么跑
下面从零开始把一个 for 循环拆开——每一步都展示程序走到了 for 语句的哪个阶段、循环变量 i 当前是什么值、是否满足条件、有没有触发 break 或 continue 的逻辑。
猜一猜:
for (int i = 0; i < 4; i++) { if (i == 2) continue; if (i == 3) break; cout << i; }这段循环到底输出什么?每个 i 经过 continue 和 break 之后去了哪里?通过下面 Stepper 一步步看。
① 进入循环之前:init 只执行一次
程序走到 for (int i = 0; ...)。第一步永远是执行 init——创建一个 int i 并初始化为 0。这件事在循环的整个生命周期里只做一次。此时 i = 0,还没检查任何条件。
代码逐段拆解:把本章所有工具串起来
这一节覆盖本章五种语句的核心用法——每段一个主题,读完就能把条件分支、循环、跳转、异常处理这四件武器写进自己的代码里。
if-else if-else 的典型用例:成绩评级
#include <iostream>
using std::cout; using std::cin;
int main() {
int score;
cout << "输入成绩(0-100):";
cin >> score;
if (score < 0 || score > 100) {
cout << "非法分数!" << '\n';
return 1;
}
if (score >= 90) {
cout << "等级:A" << '\n';
} else if (score >= 80) {
cout << "等级:B" << '\n';
} else if (score >= 70) {
cout << "等级:C" << '\n';
} else if (score >= 60) {
cout << "等级:D" << '\n';
} else {
cout << "等级:F" << '\n';
}
return 0;
}要点:先做输入合法性校验——这是好习惯。if-else if 按分数从高往低判断,利用「第一个为 true 就停」的特性。注意 score >= 90 先于 score >= 80——如果把顺序反过来写会怎样?80 分被第一个 >= 80 截住,永远不会走到 >= 90 那行。顺序从小往大写是错误的。
switch 的典型用例:简单计算器
#include <iostream>
using std::cout; using std::cin;
int main() {
double a, b;
char op;
cout << "输入算式(如 3 + 4):";
cin >> a >> op >> b;
switch (op) {
case '+':
cout << a + b << '\n';
break;
case '-':
cout << a - b << '\n';
break;
case '*':
cout << a * b << '\n';
break;
case '/':
if (b != 0)
cout << a / b << '\n';
else
cout << "错误:除数为零" << '\n';
break;
default:
cout << "不支持的运算符" << '\n';
}
return 0;
}要点:op 是个 char——字符在 C++ 里本质是整数,所以 char 可以放进 switch。每个 case 以 break 结束——漏一个就贯穿到下面。除法 case '/' 里还嵌套了一个 if-else——这说明 switch 的 case 块里可以写任意复杂的逻辑。
用 for 循环遍历数组并统计
int scores[] = {88, 95, 73, 64, 91, 55};
int count = sizeof(scores) / sizeof(scores[0]); // 数组元素个数
int sum = 0;
int max = scores[0];
for (int i = 0; i < count; i++) {
sum += scores[i]; // 累加
if (scores[i] > max) // 更新最大值
max = scores[i];
}
cout << "平均分:" << static_cast<double>(sum) / count << '\n';
cout << "最高分:" << max << '\n';这段展示了 for 循环最经典的用法——遍历数组。int i = 0; i < count; i++ 是 C++ 里最熟悉的「计数器循环」模式。循环体里做了两件事:累加和找最大值。注意最后用 static_cast<double> 做浮点除法——否则 sum / count 两个 int 相除得到整数结果。
用 while 读未知长度的输入
#include <iostream>
#include <vector>
using std::cin; using std::cout; using std::vector;
int main() {
vector<int> nums;
int val;
cout << "输入整数(Ctrl+D/Ctrl+Z 结束):";
while (cin >> val) { // 读到非整数或 EOF 时条件为 false
nums.push_back(val);
}
cout << "共输入 " << nums.size() << " 个数\n";
int sum = 0;
for (auto n : nums) sum += n;
cout << "总和:" << sum << '\n';
return 0;
}这是 while 的最佳用武之地——你不知道用户会输多少个数字。while (cin >> val) 是 C++ 里一个很巧妙的条件:cin >> val 在成功读到数据时返回 cin 本身,而 cin 用在这种上下文中会自动转成 true;读到 EOF 或非法输入时转成 false,循环结束。比你先 cin >> val 再在循环体内判断简洁得多。
do-while 最典型的场景:菜单循环
#include <iostream>
using std::cout; using std::cin;
int main() {
int choice;
do {
cout << "\n--- 菜单 ---\n";
cout << "1. 开始游戏\n";
cout << "2. 设置\n";
cout << "3. 退出\n";
cout << "请选择(1-3):";
cin >> choice;
if (choice == 1)
cout << "游戏开始!\n";
else if (choice == 2)
cout << "进入设置...\n";
} while (choice != 3);
cout << "再见!\n";
return 0;
}do-while 保证菜单至少显示一次——你不可能连看都没看到就被拒之门外。循环体执行完后再检查 choice != 3——如果用户选了 3,条件 false,循环退出。注意 while (choice != 3) 末尾的分号——do-while 的语法要求这分号必须写,忘记的话编译器能给你报 20 行让你一头雾水的错误。
break 和 continue 的综合示范
// 在一串数字里找第一个能被 7 整除但跳过负数
int data[] = {-3, 5, 14, -7, 21, 8};
int n = sizeof(data) / sizeof(data[0]);
for (int i = 0; i < n; i++) {
if (data[i] < 0) continue; // 负数跳过——不管它
if (data[i] % 7 == 0) {
cout << "第一个正数且能被7整除的是 " << data[i];
cout << "(位置 " << i << ")" << '\n';
break; // 找到了——退出循环
}
}
// 输出:第一个正数且能被7整除的是 14(位置 2)这段用 continue 过滤掉所有负数,只在正数中找第一个能被 7 整除的。一找到就 break 退出——后面的 21 不会再被看到。如果把 break 去掉会怎样?输出会变成 14 和 21 两条——但「第一个」的需求只需要 14。
异常处理的完整示例:安全除法
#include <iostream>
#include <stdexcept>
using std::cout; using std::cin;
using std::runtime_error;
double safe_divide(double a, double b) {
if (b == 0.0)
throw runtime_error("除数不能为零");
return a / b;
}
int main() {
double x, y;
cout << "输入两个数(用空格分隔):";
while (cin >> x >> y) {
try {
double result = safe_divide(x, y);
cout << x << " / " << y << " = " << result << '\n';
} catch (const runtime_error& e) {
cout << "错误:" << e.what() << '\n';
}
cout << "继续输入(Ctrl+D 退出):";
}
return 0;
}这里展示了几个重要模式:① safe_divide 只负责检查并抛异常——它不处理、不打印,职责单一。② try 包住可能出错的那一行调用,不要把无关代码也放进去——try 块越小越好。③ catch (const runtime_error& e) 用引用捕获——避免额外拷贝。④ e.what() 拿到当初 throw 时写的错误消息。
容易踩的坑
小结
if/else if/else处理「范围判断」和「布尔条件」——从上到下依次检查,第一个 true 的分支被执行,其余跳过switch处理「离散值等值匹配」——代码更清晰,但必须写break防止贯穿;只接受整型表达式for(计数器循环)、while(条件驱动循环)、do-while(至少跑一次)——三者可以互换,选哪个看场景break一刀切断循环——之后再也跑不了;continue只跳过本轮剩余代码——下一轮照常执行- 异常处理
try/catch/throw用于「意料之外」的错误——能跨多层函数传播,比返回错误码更清晰但开销更大
练习
问题 1(改代码型) 下面这段代码想要判断用户输入的是星期几并输出中文名,但它有三处 bug。找出它们并写出修正后的完整代码。
#include <iostream>
int main() {
int day;
std::cin >> day;
switch (day) {
case 1: std::cout << "周一";
case 2: std::cout << "周二";
case 3: std::cout << "周三";
default: std::cout << "输入不合法";
}
return 0;
}问题 2(独立实现题) 编一个函数 fizzbuzz(int n),对 1 到 n 之间的每个整数按下述规则输出:能被 3 整除输出 "Fizz";能被 5 整除输出 "Buzz";能被 3 和 5 同时整除输出 "FizzBuzz";其他情况输出这个整数本身。每行一个。要求用 for 循环和 if/else if/else 来完成。
提示:先判断「能同时被 3 和 5 整除」——这个条件放在 if-else if 链的最前面。为什么?
问题 3(问答型) §5 Stepper 里那段 for 循环最终只输出了 0 和 1。如果把 continue 和 break 判断的顺序互换——即先判断 if (i == 3) break; 再判断 if (i == 2) continue;——输出会变成什么?为什么顺序会影响结果?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- if/else 语句
C++ 最基本的条件分支语句。
if (条件) { ... } else if (条件) { ... } else { ... }——条件从上到下依次检查,第一个为 true 的块被执行,其余跳过。如果全为 false 且有 else,则执行 else 块。适合「范围判断」和「布尔条件」。详见本章「第一种选择」一节和 IfElseFlowDiagram 图示。- switch 语句
C++ 的多路分支语句。
switch (整型表达式) { case 常量: ... break; default: ... }——根据表达式的值直接跳转到匹配的 case,比 if-else if 链更清晰高效但只接受整型表达式。忘记写 break 会导致贯穿(fall-through)。详见本章「第二种选择」一节和 SwitchFlowDiagram 图示。- 循环语句(loop statement)
C++ 的三种循环:
for(计数器循环,初始化/条件/更新在同一行,次数已知时最常用)、while(条件驱动循环,先判断再执行、可能一次都不跑)、do-while(先执行再判断、至少跑一次)。三者可以互相转换,选哪个看场景。详见本章「三种重复」一节和 LoopComparisonDiagram 图示。- break 与 continue
循环体内的两个跳转关键字。
break——立即终止当前所在循环,循环就此结束,程序跳到循环体之后。continue——跳过本轮循环的剩余代码,直接进入下一轮迭代的条件判断,循环本身还活着。两者都只影响最内层的那一层循环。详见本章「循环体内的刹车」一节和 BreakContinueDiagram 图示。- 异常处理(exception handling)
C++ 的错误处理机制:
throw抛出异常(报告错误);try包裹可能出错的代码(警戒区);catch捕获并处理特定类型的异常(处理点)。异常可以跨越多层函数调用传播,比每层检查返回错误码更干净。#include <stdexcept>可用标准异常类。详见本章「最后一道防线」一节。- 简单语句与复合语句
表达式加分号形成简单语句,单独分号是空语句;花括号把多个声明与语句组合成一条复合语句,并通常创建块作用域。
- 语句作用域
控制语句或复合块内名字的可见和生命周期范围;离开作用域时自动对象销毁,switch label 本身不会创建新作用域。
- 跳转语句
break、continue、goto等改变控制位置的语句;不能通过跳转绕过对象初始化后进入其作用域。