从零开始 / 一次只解释眼前的一步
类型与表达式:
先运算,再保存。
同样是除法,为什么一个结果是2,一个是2.5?先写出每一步的类型和值,再用运行结果核对自己的推理。
6小时是未经真人试学校准的设计估算。可分多次学习,遇到不清楚的地方保留预测、实际结果和疑问,之后再回修教材。手机可读图与做预测,编译需要电脑终端。
这一章怎样学
先读一小段,写下预测,再运行程序。每次只改一个条件,最后关掉示例,从空文件独立写一次。遇到错误,把第一条报错和自己的修复记下来;不用赶着把页面滚到底。
每次先看右侧表达式怎样计算,再看结果怎样保存。每章学习记录单独保留;本章完成不自动通过M1或G0。
现在你已经能保存、编译、运行一份顺序程序,也能区分“创建对象”和“给已有对象赋值”。这一章要解决另一个问题:同样写着除法,为什么有人得到2,有人得到2.5? 我们从运算发生前的信息开始查,而不靠多运行几次碰答案。
先备是第一份程序的五节。若还不确定代码应该写进文件还是终端,先完成01.1–01.2;本章不重新开一套编译教程。创建一个新的练习目录,所有数值都写在源码中,不需要键盘输入。每个文件独立编译;不要把多个main贴到同一个文件里。
六节共预估6小时,包含慢读、预测、改一个条件和独立编码。此估算尚未经过真人试学校准。你可以在章末记下疑问和实际用时;记录不替代独立完成,也不会自动改变M1或G0状态。
02.1 类型、字面量与数值范围 · 预估60分钟
一笔数量和一段耗时,应该怎样保存
假设要记录“完成7个任务,耗时7.5秒”。只学过int时,你能保存任务数,却还不能说明那半秒该放在哪里。这一节先认识几种表示方式,下一节再让它们参与运算。
类型(type) 规定一个对象能保存哪些值,以及有关运算的规则。01已经用过小整数int count{7};。这里的int是类型,count是名字,7是直接写在源码里的值,称为字面量(literal)。同一个整数对象以后可以保存别的整数,但它不会因为一次赋值就自动改成另一种类型。
double是浮点类型,能表示带小数部分的许多数值。例如double seconds{7.5};创建一个浮点对象,初值是7.5。带小数点且不带其他后缀的7.5是double字面量。7与7.0在数学上数值相同,在C++表达式里类型不同:这个小的7是int,7.0是double。02.6再检查有限精度的后果,不能把“能表示小数”理解成“所有实数都能精确存下”。
字符用char。char label{'A'};保存一个字符,输出label会看到A。'5'是字符字面量,5是整数字面量;不要因为屏幕都可能显示5就认定内部含义一样。单引号包住一个字符,双引号包住一段文本;输出固定文本和换行的读法沿用01.2。
字面量自身也有类型
先只看本章会用到的十进制写法。小的无后缀整数如7能放入int,所以类型是int。对于更大的无后缀十进制整数,编译器按int、long、long long依次选择第一个能表示它的类型;long是另一种有符号整数类型,宽度至少不小于int。不能把这条规则简化成“所有没写后缀的整数都是int”。
后缀改变候选类型。3U的U表示无符号,这个小数值的类型是unsigned int;更大的十进制U字面量会继续尝试unsigned long、unsigned long long。7000000000LL明确采用long long候选,LL是两个大写字母L,输出时不会显示后缀。负数-5由负号和字面量5组成,负号的运算在02.2解释。
有符号整数(signed integer) 能保存负数、零和正数;int、long long都属于这一类。无符号整数(unsigned integer) 从零开始,不保存负数。声明unsigned int jobs{3U};时,类型写在名字前面,字面量的U写在数值后面,两者位置与作用不同。
“整数”不意味着大小无限。C++要求int至少16位、long long至少64位,并不要求它们恰好是这个宽度。这里的位(bit) 是一个二进制位置,只能取0或1;位数有限,组合数量也有限。int至少能表示-32767到32767,long long保证能容纳本例70亿。不要根据某台电脑上的结果断言所有机器的int都能装70亿;本章也没有教位运算符。
明确要求32位时,先检查名字是否存在
标准头文件<cstdint>可以提供固定宽度整数名字。std::uint32_t若存在,就表示恰好32位的无符号整数类型,值域是0到4294967295;std::int32_t若存在,则表示相应的32位有符号类型。std::和#include的读法沿用01.2,uint32_t整个名字作为类型使用,不是在调用函数。
这些精确宽度名字是可选的:只有实现能提供相应类型时才定义。下面的types.cpp要求环境提供std::uint32_t;已在本章记录的macOS环境验证。若其他实现报告这个名字不存在,记录环境不满足这个样本的条件,不能删掉报错后仍宣称该完整文件通过。选择跨平台替代类型的接口设计不属于本次练习。普通unsigned int则不保证恰好32位。
支持样本:逐个读类型,再预测输出
下面每个声明都先写类型、再写名字和初值,输出仍使用01.2的写法。先在纸上列“名字/类型/初值/输出”四列,然后编译。
#include <cstdint>
#include <iostream>
int main() {
int count{7};
double seconds{7.5};
char label{'A'};
long long distance{7000000000LL};
unsigned int jobs{3U};
std::uint32_t packet{4000U};
std::cout << count << '\n';
std::cout << seconds << '\n';
std::cout << label << '\n';
std::cout << distance << '\n';
std::cout << jobs << '\n';
std::cout << packet << '\n';
return 0;
}
输出按行是7、7.5、A、7000000000、3、4000。输出的是数值或字符,不会自动打印类型名、U或LL。现在只把7.5改成7.0并重新编译:seconds仍是double,但默认输出可能直接显示7。终端有没有小数点,不能单独证明对象是不是浮点类型。 本章使用输出流的默认格式,不加入新的格式控制接口。
命令沿用首章的方法,在types.cpp所在目录运行;其他文件替换输入和输出名即可:
g++ -std=c++20 types.cpp -o types && ./types
小练习:用源码判断,不靠输出外观
为“12个任务”“12.5秒”“字符B”“70亿个累计事件”分别选择本节已教的类型与字面量。再解释为什么double seconds{12.0};输出12不等于声明成了int。
提示与答案:类型在哪里确定
可分别写int tasks{12};、double seconds{12.5};、char label{'B'};、long long events{7000000000LL};。最后一个数不能凭本机经验交给int。seconds的类型由声明里的double确定,输出格式省略小数部分不会改变它的类型。先核对自己每个符号是否能在本节或01找到,再输入电脑。
02.2 表达式、优先级与整数除法 · 预估90分钟
五件物品,每盒装两件,程序应该报告什么
“能装满几盒”和“平均每组多少件”是两个不同问题。它们都会写到除法。我们先把表达式拆成参与运算的值、各自的类型和运算顺序,再看结果能回答哪个问题。
表达式(expression) 是能执行计算的一段写法,例如total + 1。参与运算的值叫操作数(operand)。两个操作数之间的+表示加法,-表示减法,*表示乘法,/表示除法。放在单个数值前面的-是取相反数,所以-5表示对5取负;它和两个操作数之间的减法要按位置区分。
在本节的普通数值表达式里,单个数值前的负号先结合;乘、除、余数比加、减结合得更紧。同一层的乘除按从左到右分组。例如2 + 3 * 4先形成3 * 4,得到12,再加2,结果14。(2 + 3) * 4用圆括号要求先算2加3,所以得到20。这里圆括号用于表达式分组,和main()入口后面的括号用途不同。
这个优先级(precedence)规定表达式怎样分组,不是在给任意程序承诺全部操作数的执行先后。本章操作数只是读取小数值,没有夹带修改对象或其他函数调用,因此按这种分组求值即可。先分别写出中间结果,比一口气心算长表达式更可靠。
除法要先看参与计算的类型
两个int相除得到整数商。5 / 2的数学商是2.5,C++整数商向零截断,结果为int的2。向零截断(truncate toward zero)就是丢弃小数部分,向零靠近:正2.5变成2,负2.5变成-2。负数时不能改成向下取整,后者会得到-3。
%叫余数运算符,本章对整数使用。除数非零、整数商可表示时,商与余数满足“商 × 除数 + 余数 = 被除数”。因此5 % 2是1,-5 % 2是-1,因为(-2) * 2 + (-1)才还原成-5。余数不是一律为正。5 % -2是1,因为商为-2,(-2) * (-2) + 1等于5。不要把浮点数放进%,5.0 % 2不是本运算符接受的写法。
若两个操作数都是double,则按浮点规则计算。于是5.0 / 2.0得到2.5,不采用整数商的截断规则。这里先比较两种同类型的计算;02.4再让不同类型参与同一个表达式,并逐步解释转换。
先约定合法输入:本节除数不能是零,整数运算的数学结果必须在结果类型可表示的范围内。整数零除和不可表示的整数商属于未定义行为(undefined behavior):C++不保证这次运行得到什么,不能预测成零,也不能保证它一定崩溃。本章所有运行示例都避开这些输入。02.6再将这个边界与有定义的无符号回绕分开。
完整例一:装盒、余数和运算分组
输入固定为5件物品、每盒2件。下面给送入输出流的整个算术表达式加一层圆括号:例如std::cout << (total / capacity)先形成括号内的商,再将这个值输出。圆括号沿用本节的分组读法,不要求靠猜测算术与输出运算符的结合关系。先不运行,逐行写下即将送给cout的值和类型。
#include <iostream>
int main() {
int total{5};
int capacity{2};
std::cout << (total / capacity) << '\n';
std::cout << (total % capacity) << '\n';
std::cout << (-5 / 2) << '\n';
std::cout << (-5 % 2) << '\n';
std::cout << (2 + 3 * 4) << '\n';
std::cout << ((2 + 3) * 4) << '\n';
return 0;
}
过程是:建立两个int对象5和2;第一行计算整数商2;第二行计算余数1;第三、四行对负5求商和余数,依次为-2、-1;最后两行分别按不同分组求出14和20。程序的六行输出是2 / 1 / -2 / -1 / 14 / 20,这里用斜线分隔输出行,终端实际每个值占一行。输出语句不会修改total或capacity。
五件物品,每盒两件:商与余数分别数什么
共有5件物品,每盒固定放2件;问题是装满的盒数和剩余件数。两个输入都是int。
从5件中取走两组各2件,共用4件;没有足够物品再组成一个完整盒子。
整数5 / 2得到2,数的是完整盒子;5 % 2得到1,数的是剩余物品。它们不是同一个单位。
-5 / 2向零截断得到-2;余数必须是-1,才满足(-2) × 2 + (-1) = -5。这一步是带符号的数值演算,不再是负数盒子的物理模型。
查看所有步骤的文字与数值
- 1 · 明确输入与单位
物品总量:5件;每盒容量:2件;尚未分组:商?余数?
共有5件物品,每盒固定放2件;问题是装满的盒数和剩余件数。两个输入都是int。
- 2 · 组成两个完整盒子
第1盒:2件;第2盒:2件;未装盒:1件
从5件中取走两组各2件,共用4件;没有足够物品再组成一个完整盒子。
- 3 · 分别读两个结果
商:完整盒数:2;余数:剩余件数:1;还原总量:2 × 2 + 1 = 5
整数5 / 2得到2,数的是完整盒子;5 % 2得到1,数的是剩余物品。它们不是同一个单位。
- 4 · 用负数核对截断方向
整数商:-2;余数:-1;还原被除数:-4 + (-1) = -5
-5 / 2向零截断得到-2;余数必须是-1,才满足(-2) × 2 + (-1) = -5。这一步是带符号的数值演算,不再是负数盒子的物理模型。
只改一个条件:把total初值从5改成6。第一、二行应变为3和0,后四行仍是原结果,因为它们使用自己的固定字面量。重新编译后对照;如果你预测所有六行都变了,应回到01.3区分“读取变量”和“写死的字面量”。
小练习:把商余数还原回去
先预测7 / 3与7 % 3,再预测-7 / 3与-7 % 3。每组都写出还原被除数的等式。最后比较8 / 2 * 2和8 / (2 * 2),说明括号改变了哪一步。
提示与答案:先商,后余数
正数得到2和1,2 * 3 + 1 = 7。负数得到-2和-1,(-2) * 3 + (-1) = -7。8 / 2 * 2按(8 / 2) * 2分组,先得到4,再得到8;8 / (2 * 2)先算分母4,结果2。不要在还没处理括号时就从左到右机械读所有符号。
02.3 初始化:有初值才读取 · 预估60分钟
第一份值从哪里来,编译器又为什么拒绝一个值
01用花括号给整数初值。你现在会遇到等号、圆括号和空花括号;也可能看到一份存在却还没有可用初值的对象。这一节把这些写法放在同一个小实验里,随后观察一次有意义的编译拒绝。
初始化(initialization)发生在创建对象时。对于这里的单个int,int a{5};、int b = 5;、int c(5);都把初值设为5。第二种里的等号属于声明初始化;若对象已经存在,再写b = 7;,才是在赋值。不能只看到一个=就忽略它前面有没有类型和新声明。
int zero{};里的花括号为空,会把这个整数初始化为0。相反,块内普通局部声明int value;没有提供初值;在随后明确写入一个值之前,不得读取它。在本章C++20规则下,拿它参与计算或输出不是“随机挑一个整数”的合法机制,也不能因某次看到0就假定每次从0开始。可以先声明int value;,再执行value = 5;后读取,但在这个场景里直接提供初值更容易核查。
这些结论限定在本节的标量对象。不要自行推广到以后容器的圆括号和花括号构造,它们的选择规则不同;相关章节会先教清再要求使用。这里也不采用空的int value();来表示零初始化,这个写法会涉及后续函数声明读法。
四份确定的初值
#include <iostream>
int main() {
int a{5};
int b = 5;
int c(5);
int zero{};
std::cout << a << '\n';
std::cout << b << '\n';
std::cout << c << '\n';
std::cout << zero << '\n';
return 0;
}
依次建立a、b、c并保存5,再建立zero并保存0。四次输出分别读取自己的对象,得到5、5、5、0。只把int zero{};改成int zero{9};,最后一行应变为9,其余三行不变。这个实验比较的是初值,没有任何自动跟随关系。
花括号会挡住一类丢信息的转换
把一种类型的值交给另一种类型时,可能发生转换(conversion)。int count{3.75};要求从double变成int,浮点到整数属于列表初始化禁止的窄化(narrowing);程序必须被诊断为不合格。即便改成int count{3.0};,这仍是浮点到整数转换,不能因为数学上没有小数就绕过这条列表初始化限制。
下面是故意编译失败的完整文件。先指出应拒绝哪一行及原因,再编译观察第一条诊断。不要为它写一个“预期运行输出”,它没有本次成功构建出的程序。
#include <iostream>
int main() {
double value{3.75};
int count{value};
std::cout << count << '\n';
return 0;
}
value本身合法,保存3.75;问题发生在创建count时的类型转换。各编译器诊断措辞可能不同,判断点是浮点到整数的花括号初始化被拒绝。不要删掉有问题的整段计算来获得“通过”。若任务本来需要小数,把count类型改成double就能保留3.75;若任务确实只需要整数部分,下一节会给出有明确意图的转换及边界。
小练习:找出第一处缺少依据的读取
顺序片段int count;、int saved{count};、count = 5;的问题在哪里?把最后一条移到创建saved之前能否修复这个特定问题?另外,int x{};与int x{3.0};为什么一个接受、一个拒绝?
提示与答案:别让后面的写入替前面的读取作证
创建saved时已经读取了尚无可用初值的count。后面再赋5不能修复此前的读取。把赋5移到这次读取之前,才能让saved用确定的5初始化。空花括号给整数0;3.0是double字面量,交给int列表初始化属于被禁止的浮点到整数窄化。
02.4 转换发生在什么时候 · 预估60分钟
为什么左边写double,平均数还是少了半个
把计算结果放进浮点对象,看起来像是解决了精度问题。但我们需要检查的是:那半个是在计算时丢掉,还是保存时丢掉。下面用同一组5和2,跟踪转换发生的位置。
隐式转换(implicit conversion)没有单独写出转换操作。例如double saved = 5;用int的5初始化一个double,这里会转换成浮点的5。注意“创建并保存”发生在右侧表达式求值之后。double saved = 5 / 2;先算完int除法得到2,然后才把2转成浮点;已经舍弃的小数不会在保存时重新出现。
对于本章的小数值,若一个操作数是double、另一个是int,整数会先转成double,然后进行浮点运算。因此5.0 / 2与5 / 2.0都得到2.5。这是这对类型的规则,不能推广成“任意类型混合都自动选择我希望的类型”;本章不混合有符号与无符号操作数。
显式转换(explicit conversion)把意图写在源码中。static_cast<double>(total)读作“先读取total,把这个数值转换成double”。static_cast是C++关键字,尖括号里是目标类型,圆括号里是要转换的表达式;它不是你自己定义的普通函数,也没有修改total对象。这里尖括号不是比较运算符。
同理,static_cast<int>(3.75)得到3,static_cast<int>(-3.75)得到-3,浮点转整数时丢弃小数部分。边界也必须明确:截断后的值要能放进目标整数类型;越界不因为写了static_cast就变得安全。本章只使用可以表示的小数值。对很大的整数转浮点,也不能保证每一位都保留;02.6检查有限精度,具体范围检查到08.5。
完整例二:同一组输入,三条不同路径
#include <iostream>
int main() {
int total{5};
int samples{2};
double after = total / samples;
double before = static_cast<double>(total) / samples;
int whole = static_cast<int>(-3.75);
std::cout << after << '\n';
std::cout << before << '\n';
std::cout << whole << '\n';
return 0;
}
沿after这条路径:先读到int的5和2,整数相除得到2,再隐式转成double并保存。沿before这条路径:先把total读出的5显式转成double;与samples的2相除时,2也转成double,结果为2.5。whole则显式把负3.75变成整数负3。默认输出依次为2、2.5、-3。
同一个平均数,转换放在哪一步
total是int 5,samples是int 2。先比较两条路径,左边接收结果的变量不会倒过来改变已经完成的运算。
total / samples先得到int 2。随后保存到double after,得到浮点的2;小数部分已经丢失。
static_cast<double>(total)先产生double 5,samples的2在参与混合运算时也转成double,再得到2.5。total对象本身仍是int 5。
static_cast<double>(total / samples)要先求出括号内的整数商,再转换。这和路径A有相同的丢失点,不能仅凭出现了static_cast就判断修好了。
查看所有步骤的文字与数值
- 1 · 从相同输入出发
total:int:5;samples:int:2;待保存结果:double
total是int 5,samples是int 2。先比较两条路径,左边接收结果的变量不会倒过来改变已经完成的运算。
- 2 · 路径A:先完成整数除法
int 5 / int 2:int:2;再转成double:2.0;after默认输出:2
total / samples先得到int 2。随后保存到double after,得到浮点的2;小数部分已经丢失。
- 3 · 路径B:除法前转换操作数
先转操作数:double:5;浮点除法:5.0 / 2.0;before默认输出:2.5
static_cast<double>(total)先产生double 5,samples的2在参与混合运算时也转成double,再得到2.5。total对象本身仍是int 5。
- 4 · 转换包住整个商,仍然太晚
括号内部:int:2;括号外转换:double:2;要修改的位置:除法的操作数
static_cast<double>(total / samples)要先求出括号内的整数商,再转换。这和路径A有相同的丢失点,不能仅凭出现了static_cast就判断修好了。
对照一个位置很接近的写法:static_cast<double>(total / samples)的圆括号包住了整个除法。因此它仍然先得到整数2,再转换,不等于static_cast<double>(total) / samples。判断时先圈出转换的对象,再写中间类型,不要只在代码里搜有没有double或static_cast。
只改一个条件:把total初值改成6,前两行都会显示3。两条路径在一个输入上输出相同,不能证明它们实现了相同的计算规则。 再换回5就能看见区别。先想出能区分实现的输入,再运行,是一种简单但有效的测试方法。
小练习:只移动转换的位置
输入7和4,要得到1.75。比较static_cast<double>(7 / 4)、static_cast<double>(7) / 4和7 / 4.0。分别写每一步的类型和值;不允许把“加一个.0就行”当作完整解释。
提示与答案:最先发生的那次除法是什么类型
第一条先算int / int得到1,再转换为浮点的1;第二条先把7变成double,再按浮点运算得到1.75;第三条的4.0已经是double,7转换后参与浮点运算,也得到1.75。三条都可能产生浮点最终结果,但只有后两条在除法发生前保住了小数。
02.5 标量const与auto · 预估45分钟
哪些数不该改,哪些类型可以少写一次
一段程序里的总量可能在初始化后就不应再改;另一些中间结果的类型已经能从表达式判断。我们分别解决这两件事,并检查它们会不会让程序在以后自动重新计算。
const int total{5};声明一个常量整数对象。const对象初始化以后不能被合法地修改;直接写total = 8;会编译失败。你仍然可以读取它、用它计算,也可以用它的值创建另一个普通整数对象。这里讲的是对象本身不可写;引用和通过不同路径访问对象的合同在新05,本节不使用&。
auto samples = 2;让编译器从初始化表达式推导类型。这里右边的2是int,所以samples就是一个int对象。类型推导(type deduction)在声明时完成,不是运行中看见新值就随时换类型。auto mean = 5.0 / 2;的右侧表达式类型为double,所以mean是double;auto whole = 5 / 2;则得到int。
本节只采用auto name = expression;的标量写法,避免把后面才教的花括号列表推导与引用推导混在一起。auto仍然需要初始化表达式,单写auto value;没有依据,不能推导。它也不是“会记住算式的电子表格单元格”:初始化表达式算一次,结果保存下来,以后只会因明确的赋值改变。
const auto amount = 5.0 / 2;把两项选择组合起来:从表达式推导出double,再将这个对象声明为const。一个已经声明为const的标量被普通auto复制时,新对象并不因此自动变成const:const int original{5}; auto copy = original;得到可写的int副本。这里仍是01教过的两个独立对象;若希望copy也不可写,需要在它自己的声明中写const。
支持样本:改了输入,不会重算旧结果
#include <iostream>
int main() {
const int total{5};
auto samples = 2;
auto whole = total / samples;
auto mean = static_cast<double>(total) / samples;
samples = 4;
std::cout << whole << '\n';
std::cout << mean << '\n';
std::cout << samples << '\n';
return 0;
}
先确定类型:total是const int,samples和whole是int,mean是double。创建whole时计算5除以2得到2;创建mean时浮点除法得到2.5。随后只给samples赋4,没有给whole或mean赋值,所以输出仍是2、2.5、4。若要计算新的平均数,需要在samples改变之后再次执行平均数表达式;auto本身不提供这个动作。
再看故意编译失败的样本。先找应拒绝的那一行,再保存为独立文件编译:
#include <iostream>
int main() {
const int total{5};
total = 8;
std::cout << total << '\n';
return 0;
}
错误是尝试给const对象赋8,不是输出const对象有问题。若总量设计上不可变,就删去这次不应发生的赋值;若业务确实需要更新总量,就应重新考虑声明是否应该是const。不要机械删除所有const来让报错消失。
小练习:类型、值、可写性分三列
依次写出auto a = 7 / 4;、auto b = 7.0 / 4;、const auto c = 7.0 / 4;的类型和值,指出哪个对象不能随后赋值。再问:以后把别的变量改成8,会自动改变这三个结果吗?
提示与答案:auto不替你选择想要的计算
a是int,值1;b是double,值1.75;c是const double,值1.75,不能后续直接赋值。声明结束后,它们保存自己的值;修改其他变量不会自动重算表达式。先改变操作数的类型才能改变整数除法,auto只忠实接受右侧表达式的类型。
02.6 有限表示、边界与独立迁移 · 预估45分钟
数学上能算出一个数,就一定能存进去吗
前面的例子都选了小而可表示的值。现在要检查这种选择隐含的前提,并区分三种很容易混淆的现象。我们不执行有符号溢出或读取未初始化对象来“观察标准答案”,而是用有定义的程序和明确标注的数值模型说明边界。
无符号回绕(unsigned wraparound)是有定义的。假设一个教学模型只有3位,则可用组合共8个,对应0到7;7再加1按模8回到0,0减1按模8得到7。这是小模型,不是声称C++的unsigned int只有3位。真实unsigned int的值域是0到某个最大值M,它的算术按M+1取模:0减1得到M,M加1回到0。
#include <iostream>
int main() {
unsigned int value{0U};
value = value - 1U;
value = value + 1U;
std::cout << value << '\n';
return 0;
}
初值为无符号0;减去无符号1后是本实现的最大unsigned int;再加无符号1回到0;最后输出0。这里每个参与减法、加法的值都是unsigned int,没有引入更小整数的提升或有符号混合运算。即使回绕符合语言规则,它也可能违反“任务数不能变成巨大数字”的业务要求。08.5会在足够先备之后教溢出前检查,不能把本例当作范围检查方案。
有符号越界不同。若一个有符号整数运算的数学结果超出结果类型范围,它属于未定义行为,不能把无符号的回绕规则照搬过去。也不要把“换成long long”理解为数值无限大;它只提供更大的有限范围。比如在至少16位的int保证范围内,100乘100得到10000可表示;本章不拿一个未经范围证明的大数乘法来冒险。查最大值的标准库接口在08.5正式讲授,本节不提前出现需要读懂的调用。
浮点舍入(floating-point rounding)又是另一件事。想象一个只能保留三位十进制有效数字的教学记录器:1000可以写成“1.00乘1000”,1001若仍按最近值保留三位有效数字,会和1000落到同一格。这是十进制玩具模型,用于展示有限可表示集合,不是double的真实格式。常见二进制浮点也有有限精度;数值越大,相邻可表示值之间的距离通常越大,一次很小的增加可能保存不下来。
下面这个支持样本绑定本章验证环境:Apple Clang使用二进制64位double,并采用向最近可表示值舍入。这个格式保留53位有效二进制数字;在100000000000000000000这个量级,最末一位代表的单位是16384。原数可以精确表示,下一个更大的可表示数是100000000000000016384;两者中点距离原数8192。加1的数学结果只离原数1,却离下一格16383,故最近值仍是原数。只有“增加量比间距小”还不够作判断,必须比较到两格的距离。
据此,在运行前就能推演:large保存原数;large加1按最近值舍入后,changed仍保存原数;changed减large得到0。这个具体间距与舍入条件随验证环境记录,其他不同精度或求值规则的实现不能照抄本例的确定输出。
#include <iostream>
int main() {
double large{100000000000000000000.0};
double changed{large + 1.0};
double difference{changed - large};
std::cout << difference << '\n';
return 0;
}
实际输出0,与上面的逐步预测一致。数学上的“加1再减原数”应为1,但有限精度存储改变了中间结果。这个观察不证明所有浮点运算都不可靠,也不等于学习了后面并行归约中的误差容限。遇到计算要求,必须单独说明精度和允许误差。
四步渐进练习:同一条计算,换一组输入
这四步只使用01和本章已教的构造,不提前接入含容器与循环的旧stage-01。每一步先写预测或实现,之后再展开对应答案。
第一步:预测。 输入int的7与2,分别把total / samples、static_cast<double>(total) / samples、static_cast<double>(total / samples)保存到double对象中。写出每条路径的中间类型、数值和默认输出,再运行核对。
对照第一步
三条路径的结果依次为3、3.5、3。第一条先得到int 3再转为double;第二条先转换操作数后做浮点除法;第三条括号内部的int除法已经截断,外层转换无法追回小数。左边接收对象都是double,仍然不能抹平计算路径差异。
第二步:补关键一步。 设int total{14}; int count{5};,从double average = total / count;出发,只在右侧加入一次显式转换,让程序得到接近数学平均数2.8的结果。写清转换包住谁;不要同时改输入值来掩盖问题。这里直接观察默认输出,不加入其他检查接口。
对照第二步
写double average = static_cast<double>(total) / count;。转换的是除法发生前读到的total值,count在与double参与运算时也转成double。默认输出为2.8;有限精度下的保存值不一定等于数学上精确的十四除以五。不要先把整个整数商算完再转换。
第三步:找一个错误。 先回顾02.3的narrowing.cpp:直接把double列表初始化进int,会得到编译拒绝。再看下面能成功编译的程序;目标是输出2.5,实际会发生什么?指出最早丢掉信息的子表达式,只修改那一处。
#include <iostream>
int main() {
int total{5};
int samples{2};
double average = static_cast<double>(total / samples);
std::cout << average << '\n';
return 0;
}
对照第三步:两类错误怎样区分
这份程序实际输出2;它与窄化样本不同,是合法程序没有实现预期计算。括号内部的total / samples先用int得到2,转换发生太晚。把static_cast<double>(total / samples)改成static_cast<double>(total) / samples即可。保留修改前2与修改后2.5的两次实际构建记录,不要只删除错误版本,也不要把两种情况都说成“编译器不允许除法”。
第四步:独立迁移。 关掉正文,从空文件写程序。固定输入9与4,声明为两个const int对象;依次计算并输出整数商、余数、浮点商,预期2、1、2.25。先口述每一行,再编译。之后只把9改为-9,预期-2、-1、-2.25;再试总量0,预期0、0、0。除数始终是4,不加入零除输入。
完成后再打开参考实现
#include <iostream>
int main() {
const int total{9};
const int samples{4};
int quotient = total / samples;
int remaining = total % samples;
double average = static_cast<double>(total) / samples;
std::cout << quotient << '\n';
std::cout << remaining << '\n';
std::cout << average << '\n';
return 0;
}原输入的整数商9除以4向零截断为2,余数为1;浮点商在除法前转换操作数,得到2.25。改成-9后整数商为-2,余数必须为-1,才满足商余还原式;浮点商为-2.25。这些边界都有非零除数,且小整数结果可表示。每次先预测,再重新编译。
收尾检查
若只会复制参考,先重复第二、三步;若能独立写出程序但解释不清类型,回到02.2和02.4画出中间值。你应该能分别说出“类型是什么”“值是多少”“什么时候转换”“能否修改”“这组输入为什么合法”。下一章才开始根据条件选择不同的执行路径,本章到这里仍是一条顺序路线。
关掉参考,再做一次
把理解说出来
先完成正文的独立迁移,再回答下面六题。它们只检验本章内容;打开答案、编译成功或填写用时,都不会自动通过本章,更不代表通过 G0。
解释
5、5.0、5U、5LL和'5'有什么区别?long long是否保证正好64位?
对照推理与英文回答
这五个小字面量的类型分别是int、double、unsigned int、long long和char。字符5用于字符输出,不等于整数值5。long long至少64位,不保证恰好64位;也不能把本机int的宽度当所有实现的保证。
These literals have different types: int, double, unsigned int, long long, and char. A character is not the same as the numeric value five. long long is at least 64 bits wide, not necessarily exactly 64 bits.预测与追问
-7 / 3和-7 % 3各是多少?把除数改成0之后还能要求给出确定输出吗?
对照推理与英文回答
商为-2,余数为-1,因为商向零截断,且(-2)*3+(-1)=-7。零除不在合法输入合同内;在本章整数运算中属于未定义行为,不能预测成0,也不能以某次崩溃作为标准保证。
The quotient is minus two and the remainder is minus one. Integer division truncates toward zero. Division by zero has undefined behavior, so I cannot promise a result or even a crash.找错
double value{3.75}; int count{value}; 为什么编译失败?改成static_cast<int>(value)就总是安全吗?
对照推理与英文回答
花括号初始化拒绝从double到int的窄化,即便具体值写成3.0也一样。显式static_cast<int>(value)要求你承担转换意图;3.75得到3,但截断后的整数必须在int可表示范围内,不能把显式写法当普遍的范围检查。
List initialization rejects this floating-to-integer narrowing conversion. An explicit cast of 3.75 produces three, but it is not a range check. The truncated value still has to be representable in the target integer type.代码预测
total为5、samples为2。double a = total / samples; 与 double b = static_cast<double>(total) / samples; 的输出和类型如何推导?
对照推理与英文回答
a先执行两个int的除法得到int 2,然后转成double 2,默认输出2。b先把total转成double,除法按double计算得到2.5。左边变量的类型不倒过来决定右边先前的整数除法。
The first expression divides two integers before converting the result, so a stores 2.0. The second converts an operand before division, giving 2.5. The destination type does not retroactively change the earlier calculation.解释与找错
const int total{5}; auto samples = 2; auto mean = total / samples; samples = 4; mean会自动变为1.25吗?total可以赋成7吗?
对照推理与英文回答
mean推导为int,初始化时为2;随后修改samples不会重算mean,它仍为2。auto在声明时确定类型,不是运行时自动变型或自动更新。total是const对象,后续直接赋7会编译失败。
mean is an int initialized to two. Changing samples later does not recompute it, and auto does not make its type dynamic. The const object total cannot be assigned a new value.边界与英文追问
unsigned int从0U减1U再加1U的最后值是多少?能将这个结论套到有符号溢出或double舍入上吗?
对照推理与英文回答
最后回到0,计算按unsigned int的值域大小取模。该合同不推广到有符号溢出:后者是未定义行为。double属于有限精度表示,舍入可能让不同数学结果保存成同一个值;这也不是整数模运算。
The unsigned result returns to zero because unsigned arithmetic is modulo its range size. Signed overflow has undefined behavior. Floating-point rounding is a separate mechanism and can lose small changes; it is not integer wraparound.和正文是同一份源码
示例文件
先自己输入和预测,卡住时再下载对照。文件名相同不代表内容相同;把它们放在单独的练习目录中,避免覆盖自己的作品。
- types.cpp同样写一个值,先认清类型
- operators.cpp完整例一:装盒、余数和运算分组
- initialization.cpp四种初始化的实际结果
- narrowing.cpp故意编译失败:小数进入整数花括号 · 故意用于编译报错
- conversion.cpp完整例二:先转换与后转换
- const-auto.cpp类型固定,保存的值也不会自动更新
- const-write.cpp故意编译失败:写入const对象 · 故意用于编译报错
- wrap.cpp无符号减一再加一
- rounding.cpp在本章验证环境中,小的增加被舍入
- wrong-average.cpp找错:能运行,但平均数错了
- transfer.cpp独立迁移的参考实现
完整编译、输出和失败路径验证随本地构建记录保留;这里的程序没有调用加速卡。
可选的学习反馈
记下你真正花的时间
每完成一个学习时段,再填实际分钟。环境准备、阅读推演、独立编码和卡点排查分别记录,避免同一段时间重复计算。离开吃饭或做其他事情的时间不算进去。
记录只保存在你的浏览器,可导出给我复盘。留空表示尚未记录,不等于零耗时;页面停留时间不会自动计为学习。不要把开发者检查时间填进来。
尚无真实试学用时。
换设备:导入记录,或取回损坏的旧记录
导入会合并时段,相同编号不重复累加;发生冲突会保留现有记录。
阅读记录与课程验收分别保存。
本章资料与查证
- C++工作草案:基本类型 ↗
整数最小宽度、无符号模运算、有符号越界和浮点类型。本文按C++20限定讲授,当前草案用于查证。
- C++工作草案:整数字面量 ↗
十进制字面量与U、LL后缀的候选类型。
- C++工作草案:乘除与余数 ↗
整数商向零截断;商可表示时的商余恒等式;零除及不可表示的商。
- C++工作草案:列表初始化 ↗
空列表的值初始化和浮点到整数窄化限制。
- C++工作草案:浮点与整数转换 ↗
浮点转整数丢弃小数部分;结果不可表示时的限制。
- C++工作草案:auto占位类型 ↗
由初始化表达式推导声明的类型;本文只涵盖标量auto x = expression。
- C++工作草案:固定宽度整数头文件 ↗
精确宽度类型名的可选性,uint32_t的适用条件。
本章独立解释所需读法;资料用于核对与补充。工具版本、操作系统和实际执行状态见自己的运行记录。