从零开始 / 一次只解释眼前的一步
控制流:
选择与重复。
怎样数出0到n减1的偶数,n为0时怎么办?沿着条件和跳转,一步步说明接下来执行哪一条语句。
10小时是未经真人试学校准的设计估算。可分多次学习,遇到不清楚的地方保留预测、实际结果和疑问,之后再回修教材。手机可读图与做预测,编译需要电脑终端。
这一章怎样学
先读一小段,写下预测,再运行程序。每次只改一个条件,最后关掉示例,从空文件独立写一次。遇到错误,把第一条报错和自己的修复记下来;不用赶着把页面滚到底。
每次先指出下一条实际执行的语句,再记录该语句执行后的状态。每章学习记录单独保留;本章完成不自动通过M1或G0。
前两章的程序从上往下走,每条语句执行一次。现在的问题是:同样一份程序,怎样在输入不同的时候选择不同的动作,又怎样让一段工作重复恰好需要的次数?这一章始终追问一句话:接下来执行哪一条,凭什么?
先备是新02:类型与表达式及它之前的第一份程序。输入仍写在每份源码的初始化处,每个文件独立编译。复用01的编译流程、02的算术与余数;本章不需要键盘解析、容器或自己定义函数。
六节预估10小时,含预测、运行、改一个条件和独立编码。这个数未用真人试学校准,可以拆成多次学习。前半学习条件、分支和while,后半学习for、跳转与其他控制形式;不要把看过全部答案当成做完。
03.1 布尔与逻辑条件 · 预估90分钟
“还有位置吗”怎样成为一个可以计算的问题
你要决定是否继续放入读数,不能只保存“已经放了5个”,还需要判断它与容量的关系。这一节先建立判断的结果,再组合两个判断。我们还会检查:表达式里写着一次除法,运行时是否一定会做它?
**布尔类型(bool)**保存两种值:true表示真,false表示假。bool ready{true};创建并初始化一个布尔对象,和前两章创建整数的读法相同。true和false是关键字,不加引号。在本书当前没有开启布尔文字格式的默认输出流中,输出true显示1,输出false显示0;对象的类型仍是bool,不能仅凭看到1就认成int。
对于本章的两个小整数,a < b检查a是否小于b,a <= b检查是否小于或等于;>和>=检查大于、大于或等于。==检查相等,!=检查不相等。这些**比较(comparison)**产生bool结果,本身不改写a或b。count == 5是提问,count = 5是把5写入已有对象,两个符号不能互换。
当一个普通整数被转换为bool时,0变成false,其他值变成true。例如bool any = count;读取count再保存转换结果;不会改变count。后面条件位置也允许这样的转换,但本章通常明确写count != 0。这说明了为什么把==误写为=不一定被编译器当作语法错误:对本节int变量执行赋值后,若继续读取这个赋值表达式的数值,得到刚写入的值。例如count = 0会写入0,若随后将这个结果转成bool,就得到false;独立写count = 0;时并没有要求判断条件。判断是否正确,要看它究竟读了什么、改了什么,不能只看是否编译成功。
组合条件时,要知道右边有没有执行
!ready是逻辑非(logical NOT):ready为true时得到false,反之得到true。它读取ready,不把ready永久改成另一个值。left && right是逻辑与(AND):先算left;若它为false,整个条件已经是false,right不求值;只有left为true才需要算right。left || right是逻辑或(OR):先算left;若它为true,整个条件已为true,right不求值;只有left为false才算right。这个按需要跳过右侧的规则叫短路(short-circuit evaluation)。
本章讲的是bool和普通标量上的内建逻辑运算符。它们明确保证左侧先于可能发生的右侧求值;不要把这条保证扩展为“所有运算都从左到右执行”。比较比逻辑与、逻辑或结合得紧,逻辑与又比逻辑或结合得紧。初学时可以写(count > 0) && (count < 10),先按括号理解比较的分组,再计算左边;仅当短路规则要求时才计算右边。0 < count < 10并不表示数学上的连续区间判断,本章使用两个比较和&&表达范围。
这里的&&写在.cpp文件里,组合C++条件;第一章终端里编译命令 && 运行命令由shell解释,检查的是前一条命令退出状态。两者分别属于两种语言,不能把C++的bool输出1与shell退出码0的成功约定混成一张表。
从比较到保护一次除法
下面所有送入输出流的完整条件都加了圆括号,先得到bool,再输出。divisor初始为0,但不能仅看到源码含count / divisor就断定运行会零除;要先沿着短路路径走一遍。
#include <iostream>
int main() {
int count{5};
int limit{10};
bool ready{true};
bool empty{false};
std::cout << (count < limit) << "\n";
std::cout << (count <= 5) << "\n";
std::cout << (count > limit) << "\n";
std::cout << (count >= 5) << "\n";
std::cout << (count == limit) << "\n";
std::cout << (count != limit) << "\n";
std::cout << (!empty) << "\n";
std::cout << (ready && count < limit) << "\n";
int divisor{0};
std::cout << (divisor != 0 && count / divisor > 1) << "\n";
std::cout << (divisor == 0 || count / divisor > 1) << "\n";
return 0;
}
前六行比较5和10、5和5,依次得到1、1、0、1、0、1。empty为false,所以逻辑非输出1;ready为true,且5小于10,逻辑与输出1。倒数第二行先算divisor != 0,结果false,右边除法被跳过,输出0。最后一行先算divisor == 0,结果true,右边也被跳过,输出1。因此十行输出是1、1、0、1、0、1、1、1、0、1,没有执行零除。
保护放在左边:这次除法究竟会不会执行
count是5,divisor是0。需要先确定右侧除法是否会被执行。
divisor != 0比较0和0,结果false。比较没有修改divisor。
内建&&在左侧false时已有确定结果;count / divisor不会求值,没有运行零除。
先前的false变为true,需要算右侧:整数5 / 2为2,2 > 1为true,整体true输出1。
查看所有步骤的文字与数值
- 1 · 从同一份输入开始
count:5;divisor:0;整个条件:左 && 右
count是5,divisor是0。需要先确定右侧除法是否会被执行。
- 2 · 先算左侧
左侧比较:0 != 0;左侧结果:false;右侧除法:尚未执行
divisor != 0比较0和0,结果false。比较没有修改divisor。
- 3 · 左侧为假,跳过右侧
左侧:false;右侧:不求值;输出bool:0
内建&&在左侧false时已有确定结果;count / divisor不会求值,没有运行零除。
- 4 · 只把除数改成2
左侧 2 != 0:true;右侧 5 / 2 > 1:true;输出bool:1
先前的false变为true,需要算右侧:整数5 / 2为2,2 > 1为true,整体true输出1。
只改divisor为2。先写下最后两行的路径:不等于零为true,才执行整数5 / 2得到2,再比较2大于1;另一行等于零为false,因此也要走右侧。两行都会输出1。重新编译确认,不能只修改源码就运行上次的程序。
小练习:能否把保护条件换到右边
输入count=5、divisor=0。把倒数第二个条件换成count / divisor > 1 && divisor != 0,还能预测为false吗?再手算!true || false和true && !false。最后解释count = 0与count == 0分别会不会改写count。
提示与答案:先标出必定求值的一侧
交换后先求除法,零除已经发生,右边的保护来不及补救;这是02讲过的整数未定义行为,不运行它来碰输出。两个合法逻辑表达式分别为false和true。赋值把count改为0;相等比较只读取count并生成bool结果。编译器可能对可疑赋值发出警告,是否警告不是这两种语义的定义。
03.2 if/else与块 · 预估90分钟
一份输入,哪些动作应该发生
读数的个数可能不合法,可能为零,也可能是正数。输出三句话并不能实现“只报告一种情况”;我们需要让程序根据条件选择路径。选完以后,还要知道是回到公共的下一行,还是直接结束这次运行。
if语句写成if (条件) { 语句 }:先计算括号中的条件,真时进入花括号内,假时跳过。花括号包住的一组语句叫块(block)。else { 语句 }紧接一个if分支,表示条件为假时走这里;同一次选择不会把if和对应else两边都执行。else if (另一个条件)让未被前一条件接纳的输入继续接受下一次判断。
块可以放在块里面,if里面也可以再放if,这叫嵌套(nesting)。缩进帮助人阅读,但决定边界的是花括号。没有花括号时,if只控制紧接的一条语句,else归属于尚未匹配else的最近if。为了让每条路径清晰,本章始终给分支加花括号,不用缩进替代语法边界。条件后的)与{之间也不要随手放;;单独的分号可以构成一条空语句,从而让后面的块失去原先想要的条件控制。
第一章已教return 0;和return 1;从main返回运行结果。现在把return放到分支内:一旦执行到return,main剩余语句不再执行。没有走到那一条return的路径,则按分支结构继续。用非零退出表示这份小程序拒绝输入,是我们选定的程序合同;退出码的观察方式仍沿用01的echo $?。
非法、零、正偶数、正奇数
下面先拒绝负数,再处理零;正数分支里嵌套一次余数判断。余数的读法由02提供,此处只把“余数等于零”的bool结果用作条件。
#include <iostream>
int main() {
int n{5};
if (n < 0) {
std::cout << "invalid\n";
return 1;
} else if (n == 0) {
std::cout << "zero\n";
} else {
if (n % 2 == 0) {
std::cout << "even\n";
} else {
std::cout << "odd\n";
}
}
std::cout << "done\n";
return 0;
}
输入n=5。第一个条件假,else-if里的等于零也假,进入最后的else;5除以2的余数为1,内层条件假,输出odd。内层选择结束、外层选择结束后,到公共语句输出done,最后返回0。将n改为0,只输出zero和done;改为4,输出even和done;改为-1,第一分支输出invalid并返回1,不输出done。用这四个输入分别覆盖四条路径。
一个名字可以从哪里读到
局部变量名在声明以后、所在块以内可用,也可以在更深的内层块使用。出了声明它的那个块,就不能再用这个局部名字查找它。这叫名字的**作用域(scope)**规则。它回答“这行代码能不能用这个名字”,和以后要深入学习的资源寿命不是同一份合同。此处只使用普通局部标量。
#include <iostream>
int main() {
int count{2};
if (count > 0) {
int saved{count};
count = 3;
std::cout << saved << "\n";
}
std::cout << count << "\n";
return 0;
}
count在main块内声明,因此if内部能读取和改写它。saved在if块内用count当时的2初始化,随后count被改成3,saved仍是自己的2。块内输出saved得到2,块外输出count得到3。两份对象的值独立,复用01的初始化规则;这里新增的是名字的可见范围。
下面是故意编译失败的文件。先指出超出哪个名字的作用域,再编译;不要为这份失败构建预测终端输出。
#include <iostream>
int main() {
int count{2};
if (count > 0) {
int saved{count};
std::cout << saved << "\n";
}
std::cout << saved << "\n";
return 0;
}
块内的saved及其输出合法;块外第二次输出的saved找不到此局部声明。即使count为正、你确信分支会执行,也不能让块外的名字突然可见。若确实需要块外继续访问,应在外层声明有明确初值的对象,再在内层更新,或者把使用移回合法范围。不要把一个新声明误当作恢复原来那个对象。
小练习:写出共同的下一行
为branches.cpp的n=0与n=-1分别画出会执行的输出行和return行;说明为什么只有一个会到done。再从scope.cpp出发,把count的初值改为0:saved会不会建立?程序最后输出什么?
提示与答案:画实际经过的路径
n=0经过zero、done、return 0;n=-1经过invalid、return 1并结束。scope.cpp的count=0使if条件假,块整体跳过,saved不建立;块外读取原count并输出0。名字的作用域和某一次分支是否进入是两层问题,不能用一个代替另一个。
03.3 while与更新 · 预估120分钟
同一行代码怎样处理几个不同的数
假设要累计0、1、2、3。如果手写四条加法,输入改了就要改程序结构。循环让同一条加法多次工作,但正确与否取决于每次进入前保存的状态,以及什么时候停止。
while循环写成while (条件) { 循环体 }。执行顺序是:先检查条件;假则直接到循环之后;真则执行循环体;循环体走完,再回去检查条件。while不是“先执行一次再判断”,第一次就可能不进入。
先看简短的更新写法。对本章不发生溢出的普通int变量,total += i读取total和i的值,相加后保存回total;等价的数值变化是total = total + i。i -= 1减去1再保存。一般复杂左侧表达式的求值次数不能靠复制文本推断;此处左侧始终只是一个变量名。
把循环分成四栏:初值 → 条件 → 循环体 → 更新。初值通常在循环前设一次;更新可能写在循环体最后,它不是while自动提供的步骤。若每次处理后不改变i,而条件又一直为真,就不能指望“运行一会儿自己结束”。本节n限定为0到100,i与total都在小整数范围内,不靠溢出让循环停下。
先看每轮进入值,再看累计值
#include <iostream>
int main() {
int n{4};
int i{0};
int total{0};
while (i < n) {
total += i;
std::cout << i << " " << total << "\n";
i += 1;
}
std::cout << "total=" << total << "\n";
return 0;
}
输入n=4,i从0开始,total从0开始。第一轮条件0小于4为真,加0以后total仍为0,输出0 0,再把i变成1。第二轮加1,输出1 1;第三轮加2,输出2 3;第四轮加3,输出3 6。第四次更新后i为4,再次检查4小于4为假,离开循环,最后输出total=6。更新i发生在输出后,不能把下一轮的i配给上一轮的total。
| 判断时的i | 条件i小于4 | 本轮相加后的total | 本轮结束的i |
|---|---|---|---|
| 0 | 真 | 0 | 1 |
| 1 | 真 | 1 | 2 |
| 2 | 真 | 3 | 3 |
| 3 | 真 | 6 | 4 |
| 4 | 假 | 不执行相加,仍为6 | 不再更新 |
只改n为0。第一次条件已经假,四条过程输出一条都没有,只有最后的total=0。这不是特殊补丁,而是同一套规则处理“没有要累计的数”。
自增自减:对象更新与表达式结果要分开
++x与x++都会把x增加1。区别是表达式交给使用它的位置什么值:前缀++x先增加,再提供增加后的值;后缀x++先保留旧值作为这次表达式的结果,再完成x的增加。相应地,--x先减1并提供新值,x--提供旧值而把x减1。这里所有值都可表示,不讨论越界更新。
当它们单独写成一句++x;或x++;时,表达式结果没有被保存,下一条语句看到的x都已增加1。若用来初始化另一个整数对象,前后缀区别就可见。每次更新和观察在不同的完整语句中;不要把多次修改同一对象挤在一条复杂表达式里猜求值次序。
#include <iostream>
int main() {
int x{4};
int old = x++;
std::cout << old << " " << x << "\n";
int current = ++x;
std::cout << current << " " << x << "\n";
int before = x--;
std::cout << before << " " << x << "\n";
int after = --x;
std::cout << after << " " << x << "\n";
x += 2;
x -= 1;
++x;
x--;
std::cout << x << "\n";
return 0;
}
从x=4开始,old接收后缀的旧值4,随后x是5,第一行输出4 5。current接收前缀增加后的6,第二行6 6。before接收后缀减之前的6,x变5,第三行6 5。after接收前缀减之后的4,第四行4 4。接着四条独立更新让x依次变为6、5、6、5,最后输出5。
小练习:让循环结束,不能只要求它“重复”
在while-sum.cpp里删掉i += 1;,不运行,写出前两次判断与相加的状态:为什么永远到不了i=4?再把该更新改为++i;,预测合法n=1的全部输出。最后从x=4分别预测int a = ++x;与另一次独立实验中的int a = x++;。
提示与答案:检查哪一条语句改变条件所读的状态
缺更新时两次进入都看到i=0,加0,total仍为0;后续重复同样状态。++i;仍每轮增加1,n=1输出0 0和total=0。独立的前缀实验a和x都是5;后缀实验a为4、x为5。不要在第一次实验改变x后,又假装第二次仍从4开始。
03.4 普通for · 预估120分钟
把一轮需要的控制信息放在一起
上一节把i的初值放在循环前、更新放在循环体里。读循环时需要来回找它们。普通for把这三处控制信息放在同一行;我们用它重写同一个累计任务,检查行为是否真的保持。
三段式for循环写成for (初始化; 条件; 更新) { 循环体 }。先执行一次初始化,然后检查条件;真则执行循环体,正常走完后执行更新,再检查条件;假则到循环之后。圆括号里的两个分号分隔三段,不表示每段每轮都执行。这里int i{0}声明的名字i可以在条件、更新和循环体内使用,不能在整个for结束后继续使用这个名字。
for (int i{0}; i < n; ++i)令i依次为0、1,直到小于n的最后一个整数。数学记号**[0,n)**表示包含左端0、不包含右端n,叫半开区间。若n=4,处理0、1、2、3;若n=0,没有整数需要处理。记号不是C++数组语法,本节并没有数组。n在这里是处理项数或停止边界,循环条件必须由这个含义推出。
完整例一:累计的是0到n减1
#include <iostream>
int main() {
int n{4};
int total{0};
for (int i{0}; i < n; ++i) {
total += i;
std::cout << i << " " << total << "\n";
}
std::cout << "total=" << total << "\n";
return 0;
}
先初始化n=4与total=0。进入for时只创建一次i=0;每轮先total += i,再显示本轮i和累计结果,最后在for的更新位置执行++i。四轮依次显示0 0、1 1、2 3、3 6;第五次检查时i已经为4,条件失败,不执行循环体也不再更新,随后输出total=6。它与上一节while版本的输出相同,但本节i的可见范围只覆盖for。
for的一次初始化、重复判断与更新
total=0;for建立i=0。下一步是条件判断,不是直接加一次。
0 < 4为真。total加0仍为0,输出0 0;正常走完循环体后更新i到1。
1 < 4为真。total从0变1,输出1 1,然后更新i到2。
2 < 4为真。total从1变3,输出2 3,然后更新i到3。
3 < 4为真。total从3变6,输出3 6,然后更新i到4。
4 < 4为假,跳到for后面输出total=6。本次判断不执行相加,也不再执行更新。
查看所有步骤的文字与数值
- 1 · 初始化,只做一次
n:4;i / total:0 / 0;下一步:判断 i < n
total=0;for建立i=0。下一步是条件判断,不是直接加一次。
- 2 · 第一轮,加0
进入 i / total:0 / 0;输出 i / total:0 / 0;更新后的 i:1
0 < 4为真。total加0仍为0,输出0 0;正常走完循环体后更新i到1。
- 3 · 第二轮,加1
进入 i / total:1 / 0;输出 i / total:1 / 1;更新后的 i:2
1 < 4为真。total从0变1,输出1 1,然后更新i到2。
- 4 · 第三轮,加2
进入 i / total:2 / 1;输出 i / total:2 / 3;更新后的 i:3
2 < 4为真。total从1变3,输出2 3,然后更新i到3。
- 5 · 第四轮,加3
进入 i / total:3 / 3;输出 i / total:3 / 6;更新后的 i:4
3 < 4为真。total从3变6,输出3 6,然后更新i到4。
- 6 · 再判断,才知道结束
判断 4 < 4:false;循环体 / 更新:都不执行;循环后输出:total=6
4 < 4为假,跳到for后面输出total=6。本次判断不执行相加,也不再执行更新。
累计量放在哪里,会改变问题。 total在循环外初始化一次,因此后一次相加能读到前一次留下的累计值。若把int total{0};放在循环体内,每次进入块都会建立自己的total并从0开始,无法跨轮累积;出了循环还不能用那个块内名字。这把03.2的作用域规则放进了重复执行的场景。
渐进阶梯一:先预测
遮住上面的运行过程,自己写n=4的每轮i、进入前total、相加后total,以及最后一次失败的条件。再只改n为0。不要只交最后的6;过程表用来定位少算、多算或错误更新发生在哪一轮。
提示与答案:最后一次判断也是过程的一部分
四轮进入前total分别为0、0、1、3,相加后为0、1、3、6;更新后i依次为1、2、3、4。最后判断4小于4失败。n=0第一次判断0小于0失败,直接输出total=0。它不需要先进行一次加法。
渐进阶梯二:补上条件与更新
另建一个练习文件,按上例输入n、在循环外初始化total,写完整for。只参考“处理0到n减1”这句话补出条件与更新,再分别用n=0、1、4验证。不要把三个答案硬写成三个分支。
提示与答案:一个范围,三份输入
初始化i为0,条件是i < n,更新是++i或独立意义相同的i += 1。累计结果应分别为0、0、6;n=1只处理0,所以它与空组的最后总和相同,过程次数却不同。可保留上例的每轮输出观察差别,最后再只输出结果。
03.5 break/continue与嵌套 · 预估90分钟
跳过一个值,与结束全部工作有什么区别
现在有两种要求:遇到某一项只跳过它,遇到另一项停止后面的全部项。把两个要求都写成“跳出去”会出错,因为它们各自的下一条语句不同。
continue结束本轮循环体剩余部分,转到这类循环的继续位置:普通for先执行更新段,再检查条件;while直接重新检查条件;do/while转到末尾条件。break结束包住它的最近一层循环或switch,转到那一层结构之后。它不是return,不结束main;外面还有循环时,外层仍可继续。
标出跳转后第一条执行的语句
#include <iostream>
int main() {
for (int i{0}; i < 6; ++i) {
if (i == 1) {
continue;
}
if (i == 4) {
break;
}
std::cout << i << "\n";
}
std::cout << "done\n";
return 0;
}
i=0时两个if都为假,输出0,再更新为1。i=1时continue跳过后面的if和输出,执行for更新,i变2;i=2、3分别输出2、3。i=4时break结束整个for,不执行本轮输出、也不执行for的更新,随后输出done。于是完整输出为0、2、3、done。若把continue误当break,会漏掉2和3;若以为continue跳回for初始化,则会把i错误地重置为0。
while没有单独更新段。假设i=0,在while (i < 3)内先判断if (i == 0) { continue; },把++i;放在后面,实际会永远跳过更新;重新判断时i仍为0。这个错误只做路径分析,不运行无限循环。修复时必须保证每条需要继续处理的路径都能推进状态,例如先完成当前项所需的读取,再把更新放到可能执行continue之前;不能在不了解计算含义时随便搬动加一。
两层循环,只退出哪一层
#include <iostream>
int main() {
for (int row{0}; row < 2; ++row) {
for (int column{0}; column < 3; ++column) {
if (column == 1) {
break;
}
std::cout << row << ":" << column << "\n";
}
}
return 0;
}
外层row从0走到1。每进入一轮外层,内层都会重新创建column=0。column=0时输出行列;column=1时break结束内层,于是本次row不会到column=2。外层正常更新后,下一行仍从column=0开始。完整输出0:0、1:0。这里冒号是引号里的输出文字,不是新的C++控制形式。
小练习:只改一处,推演更多路径
把jumps.cpp的停止条件从i == 4改为i == 2,写出全部输出。再只在nested.cpp里把break改成continue,保留其他代码,预测各对行列。说明最后一轮continue后,内层先去哪里。
提示与答案:给跳转画一个精确终点
jumps只输出0和done:1被跳过,2结束循环。嵌套版本改continue后输出0:0、0:2、1:0、1:2;column=1跳过输出但仍执行内层for的++column,到2继续判断。最后处理column=2后更新到3,条件假才返回外层,continue没有直接退出外层。
03.6 其他控制形式 · 预估90分钟
先做一次、按整数编号选择、产生一个值
while与if足以表达很多任务,但读别人的代码时还会遇到另外三种形式。这里分别弄清它们的执行路径,不因为名称短就省略过程。每种只使用前面已经教过的小整数和输出。
do/while:第一次检查发生在哪里
do { 循环体 } while (条件);先进入循环体,正常走到末尾或遇到continue后才检查条件;真则再做一轮,假则结束。结尾这个分号是此语法需要的部分。若循环体提前执行break或return,则按相应跳转离开,不会为了“至少一次检查”而再算末尾条件。
#include <iostream>
int main() {
int limit{0};
int before{0};
while (before < limit) {
before += 1;
}
int after{0};
do {
after += 1;
} while (after < limit);
std::cout << before << " " << after << "\n";
return 0;
}
limit=0,while开始时before=0,第一次判断假,所以before保持0。do的after也从0开始,却先执行加一变成1,随后1小于0为假,结束。因此输出0 1。只改limit为3,while与do都最后得到3,输出3 3;相同的最终结果不能证明两者第一次检查的位置相同。
switch:从一个标签进入,为什么还会走到下一个
本章switch (code)使用一个int值进行选择。case 1:是值为1的入口标签,case 2:是值为2的入口,default:是没有case匹配时的入口。case在本例中写固定整数,不能写成case code < 3:来表达任意运行时范围。相同switch中的case值不能重复。若没有匹配case也没有default,switch内部就不执行。
进入匹配的标签后,语句按正常顺序往下走,下一个case标签本身不阻止执行。要结束switch,执行break,或按照已学规则return结束main。省略break而继续走到后面,叫贯穿(fallthrough)。下面故意保留一次贯穿,用于观察路径;不要加尚未讲授的属性或把所有case误读成各自独立的if。 本章严格编译检查记录使用实际的Apple Clang;真正的GNU GCC在部分警告设置下会对缺break给出警告,若同时把警告当错误,可能拒绝这份故意贯穿的示例。警告不意味着贯穿违反C++语言规则;跨编译器结果未在此冒充已执行。
#include <iostream>
int main() {
int code{2};
switch (code) {
case 1:
std::cout << "A\n";
break;
case 2:
std::cout << "B\n";
case 3:
std::cout << "C\n";
break;
default:
std::cout << "other\n";
break;
}
return 0;
}
code=2,直接进入case 2,输出B;这里没有break,继续来到case 3的输出,输出C,再执行break离开switch。因此输出两行B、C,不输出A。只改code=1,输出A后立刻break;改为3,只输出C;改为9,从default进入,输出other。若业务规定编号2只能报告B,就应在输出B之后补break,而不是改编号来躲开问题。
此处case后只有输出与break,没有声明局部变量。后续若在分支内声明对象,应先明确块边界和初始化路径,不能把本例当作任意跨标签跳转都合法的保证。switch里的break只结束这个switch;若switch放在循环中,它不会自动结束外层循环。
条件运算符:选择的是一个表达式结果
条件 ? 真时表达式 : 假时表达式先判断条件,然后只求值被选中的一侧,用这一侧的结果参与后面的计算。问号与冒号共同组成条件运算符(conditional operator),不是if语句的两段语句块。这里两侧都为int数值表达式,所以结果也按本节的int用途处理;混合类型及引用结果不在这份窄合同里。
#include <iostream>
int main() {
int n{-3};
int nonnegative = (n < 0) ? 0 : n;
std::cout << nonnegative << "\n";
int total{5};
int divisor{0};
int quotient = (divisor == 0) ? 0 : (total / divisor);
std::cout << quotient << "\n";
return 0;
}
n=-3,条件n小于0为真,选择0初始化nonnegative,第一行输出0。divisor=0,等于零为真,选择0,另一侧的除法不求值,第二行仍输出0。只改divisor为2,第二个条件假,执行整数5除2,输出2。把除法搬到条件运算符之前先算,并不能保留这份保护;路径顺序仍然决定是否执行危险运算。
渐进阶梯三:找出最小边界反例
以下程序能编译运行,但不符合“累计0到n减1”的合同。先用n=0观察,再选择最小的正整数输入使结果与正确版本不同,指出只需要修哪个运算符。
#include <iostream>
int main() {
int n{1};
int total{0};
for (int i{0}; i <= n; ++i) {
total += i;
}
std::cout << total << "\n";
return 0;
}
提示与答案:零项总和可能掩盖多处理了一项
n=0时错误版本处理了一次i=0,仍得到0,最后总和碰巧与正确版本一样。最小正输入n=1时,它处理0和1,错误总和为1;正确半开区间只含0,总和为0。把i <= n改成i < n即可修复这份范围错误。n=0的最后输出相同不能证明循环次数相同,过程与结果都值得观察。
渐进阶梯四:独立统计偶数
从空文件完成:输入n写在源码中,合同要求0到100,包含两端;统计整数区间[0,n)里偶数的个数。范围外先输出invalid并用非零退出,不进入计数循环。范围内只输出个数,用零退出。先写下你认为n=0、1、5、100分别是多少,再编码。这里0也是偶数,因为它除以2的余数为0;判断依据是02已学的余数及本章比较。
先说明三件事:计数器在哪里初始化;i能取到哪些值;哪一条语句使计数器增加。再用-1和101检查拒绝路径。不要将每个指定输入硬写成独立答案分支。
提示:先拒绝,再遍历,最后只在条件成立时计数
范围外可写n < 0 || n > 100,其中任一条件成立就提前结束。合法路径把count初始化为0,以普通for遍历i从0到小于n;当i % 2 == 0时执行一次++count。没有被条件接纳的i不改变count。
完整例二与参考答案:自己完成以后逐行对照
#include <iostream>
int main() {
int n{5};
if (n < 0 || n > 100) {
std::cout << "invalid\n";
return 1;
}
int count{0};
for (int i{0}; i < n; ++i) {
if (i % 2 == 0) {
++count;
}
}
std::cout << count << "\n";
return 0;
}n=5,范围检查为假,建立count=0。i=0的余数为0,count变1;i=1不变;i=2变2;i=3不变;i=4变3;更新到5后循环结束,输出3并返回0。n=0不进入循环,输出0;n=1只处理0,输出1;n=100处理0到99,其中0、2、4一直到98共50个偶数,输出50。n=-1与101都输出invalid并返回1,count的声明和循环均不会执行。
离开本章前,交一份可解释的程序
保存你自己的偶数计数程序,保留六种输入的预测、实际输出和退出码;再用两句话说出为什么<不能随意换成<=、continue在for里为什么仍会更新。若某一步只能说“运行后就是这样”,回到对应小节重新走路径。本章不要求自定义函数、引用或容器;下一章将在这些已经能预测的动作外面建立普通函数接口。
关掉参考,再做一次
把理解说出来
先完成正文的独立迁移,再回答下面六题。它们只检验本章内容;打开答案、编译成功或填写用时,都不会自动通过本章,更不代表通过 G0。
解释与预测
默认输出流下bool的true/false显示什么?divisor为0时,divisor != 0 && total / divisor > 1是否执行除法?
对照推理与英文回答
显示1和0。内建&&先检查左边,false时不求值右边,因此此处不执行除法;把除法放到左侧就失去保护。
With the default stream formatting, true prints as 1 and false as 0. Built-in logical AND evaluates the left operand first and skips the right operand when the left is false. The guard must come before the division.找错与追问
count == 0与count = 0有什么差别?if分支内return 1之后,main末尾的done还能执行吗?
对照推理与英文回答
比较不改写count,赋值会把count写成0;不能靠编译成功证明用了正确运算符。实际走到return 1时,main后续语句不再执行。没有走到该分支则沿正常路径继续。
Equality compares without changing count. Assignment writes a new value. Once a return statement in main executes, the remaining statements in main are skipped. A branch that was not taken does not return.状态预测
两次独立实验都从x=4开始:int a = x++;和int a = ++x;分别保存什么?只写x++;时下一条语句看到什么?
对照推理与英文回答
后缀实验a=4、x=5;前缀实验a=5、x=5。独立自增语句执行完后,下一条语句读到x=5。将表达式的结果与被修改对象的状态分别记账。
Post-increment gives the old value to a, while pre-increment gives the incremented value. In both experiments x becomes five. After a standalone increment statement, the next statement observes the updated value.边界与解释
for从i=0开始、条件i<n时,n=0与n=1各执行几轮?把条件改成i<=n,为什么只检查最终总和0可能漏错?
对照推理与英文回答
正确版本分别0轮和1轮;n=1的唯一项是0。错误版本n=0也处理一次0,最终总和仍为0;n=1则错误加上1。要检查范围和过程,不能只看恰好相同的结果。
The half-open loop executes zero times for n equal to zero and once for n equal to one. An inclusive bound can process an extra zero without changing the sum. A small positive input exposes the extra right endpoint.追踪跳转
continue在普通for里下一步到哪里?嵌套循环的break会退出所有循环吗?while的更新放在continue后面有什么风险?
对照推理与英文回答
for先执行更新段再检查条件;break只结束最近的循环或switch。while的continue直接去条件,如果必要更新被跳过,状态可能永远不推进。
In a regular for loop, continue proceeds to the update expression, then the condition. Break exits only the nearest enclosing loop or switch. A while loop can get stuck if continue skips the update that would make progress.代码路径与英文复述
do/while初始条件为假还会执行循环体吗?switch匹配一个case后为何能输出下一case的内容?条件?:会把两边都算出来再选吗?
对照推理与英文回答
进入do时先做循环体,再到条件(除非break或return先离开)。case是入口标签,之后顺序执行,缺break可能贯穿。内建?:先求条件,只求值选中的一侧;本章例子两侧均为int。
A do-while starts with the body before testing its condition, unless control leaves earlier. A case label does not stop execution; a missing break can allow fallthrough. The conditional operator evaluates only the selected operand after its condition.和正文是同一份源码
示例文件
先自己输入和预测,卡住时再下载对照。文件名相同不代表内容相同;把它们放在单独的练习目录中,避免覆盖自己的作品。
- conditions.cpp比较与短路:保护一次除法
- branches.cpp分支、嵌套与提前退出
- scope.cpp块内名字与外层更新
- scope-error.cpp故意编译失败:块外读取局部名字 · 故意用于编译报错
- while-sum.cppwhile的进入值与累计值
- increment.cpp前后缀更新:表达式结果与新状态
- for-sum.cpp完整例一:普通for累计半开区间
- jumps.cppcontinue和break的不同下一步
- nested.cppbreak只结束内层循环
- do-first.cpp先检查与先做一次
- switch-fallthrough.cpp有意保留的switch贯穿
- conditional.cpp条件运算符只求值所选分支
- wrong-boundary.cpp找错:多算了右端点
- even-count.cpp完整例二:独立统计偶数
完整编译、输出和失败路径验证随本地构建记录保留;这里的程序没有调用加速卡。
可选的学习反馈
记下你真正花的时间
每完成一个学习时段,再填实际分钟。环境准备、阅读推演、独立编码和卡点排查分别记录,避免同一段时间重复计算。离开吃饭或做其他事情的时间不算进去。
记录只保存在你的浏览器,可导出给我复盘。留空表示尚未记录,不等于零耗时;页面停留时间不会自动计为学习。不要把开发者检查时间填进来。
尚无真实试学用时。
换设备:导入记录,或取回损坏的旧记录
导入会合并时段,相同编号不重复累加;发生冲突会保留现有记录。
阅读记录与课程验收分别保存。
本章资料与查证
- C++工作草案:语句与条件 ↗
语句与条件的条款;正文限定C++20已有规则,当前草案用于查证。
- C++工作草案:整数到bool的转换 ↗
整数到bool的转换的条款;正文限定C++20已有规则,当前草案用于查证。
- C++工作草案:关系比较 ↗
关系比较的条款;正文限定C++20已有规则,当前草案用于查证。
- C++工作草案:相等比较 ↗
相等比较的条款;正文限定C++20已有规则,当前草案用于查证。
- C++工作草案:内建逻辑与与短路 ↗
内建逻辑与与短路的条款;正文限定C++20已有规则,当前草案用于查证。
- C++工作草案:内建逻辑或与短路 ↗
内建逻辑或与短路的条款;正文限定C++20已有规则,当前草案用于查证。
- C++工作草案:if/else选择 ↗
if/else选择的条款;正文限定C++20已有规则,当前草案用于查证。
- C++工作草案:块作用域 ↗
块作用域的条款;正文限定C++20已有规则,当前草案用于查证。
- C++工作草案:while循环 ↗
while循环的条款;正文限定C++20已有规则,当前草案用于查证。
- C++工作草案:前缀自增减 ↗
前缀自增减的条款;正文限定C++20已有规则,当前草案用于查证。
- C++工作草案:后缀自增减 ↗
后缀自增减的条款;正文限定C++20已有规则,当前草案用于查证。
- C++工作草案:赋值与复合赋值 ↗
赋值与复合赋值的条款;正文限定C++20已有规则,当前草案用于查证。
- C++工作草案:普通for执行顺序 ↗
普通for执行顺序的条款;正文限定C++20已有规则,当前草案用于查证。
- C++工作草案:continue ↗
continue的条款;正文限定C++20已有规则,当前草案用于查证。
- C++工作草案:break ↗
break的条款;正文限定C++20已有规则,当前草案用于查证。
- C++工作草案:do/while ↗
do/while的条款;正文限定C++20已有规则,当前草案用于查证。
- C++工作草案:switch标签与贯穿 ↗
switch标签与贯穿的条款;正文限定C++20已有规则,当前草案用于查证。
- C++工作草案:条件运算符 ↗
条件运算符的条款;正文限定C++20已有规则,当前草案用于查证。
本章独立解释所需读法;资料用于核对与补充。工具版本、操作系统和实际执行状态见自己的运行记录。