从零开始 / 一次只解释眼前的一步
拥有一组数据:
长度、容量与数值边界。
从固定三个测量值开始,逐步选择array、vector和string。每次先确认已有元素,再访问和修改;累计之前检查剩余数值范围。
13小时是包含阅读、推演和编码的设计估算,未经真人试学校准。可分多次学习,遇到不清楚的地方保留预测、实际结果和疑问,之后再回修教材。手机可读图与做预测,编译需要电脑终端。
这一章怎样学
先读一小段,写下预测,再运行程序。每次只改一个条件,最后关掉示例,从空文件独立写一次。遇到错误,把第一条报错和自己的修复记下来;不用赶着把页面滚到底。
每次分别记录当前元素数量、访问的位置、被修改的拥有者和累加结果。范围for与借用视图留到新09;本章使用已经学过的普通下标循环。每章学习记录单独保留,本章完成不自动通过M1或G0。
这一章要解决一个逐渐变大的问题:起初只记录三个测量值,后来输入数量会变,最后还要保证累计不超过可表示的范围。你已经会写普通函数、条件、下标计数循环和类接口;现在把这些工具用在真正拥有一组数据的对象上。
先备是新01–07,尤其是普通for、引用参数和类作用域中的静态接口。本章用普通下标for,不使用范围for、迭代器或view。忘记某个写法时可查语法速查,正式的容器合同就在下面逐节展开。
08.1 使用带参数的库类型 · 预估120分钟
要保存什么,长度是不是类型的一部分
int只说明一个整数的类型。如果想保存一组整数,还要选择管理这一组数据的类型。std::vector<int>读成“元素类型为int的vector”,尖括号里的int是类型实参。std::array<int, 3>读成“元素类型为int、固定长度为3的array”;这里int是类型实参,3是值实参。这两种实参的职责不同,不能把3当成某个元素的初值。
库已经定义了这些模板。现在学习的是把所需类型和值填入现成接口;如何自己写template留在新15。#include <vector>、#include <array>分别让当前文件知道对应声明。长度必须是编译时可确定的合适常量;本章直接写3,不让运行中变化的int决定array类型。std::array<int, 4>与长度3的版本是不同类型,vector的当前长度则不是它的类型实参。
using Values = std::vector<int>;给已有类型起一个别名。以后写Values与写右边那个完整类型含义相同,不会额外创建容器,也没有声明一个名叫Values的运行时对象。别名还可用于阅读长类型:先给一行数据的类型起名,再把该类型作为另一个库模板的实参;尖括号可以嵌套,不需要因此学习模板实现。
类型内部也可以提供类型名。Values::value_type是这个vector的元素类型,即int;Values::size_type是它用来表示元素数量的无符号整数类型。这里::仍表示到某个作用域中找名字,但找出的名字是类型,不是一次函数调用。不要在末尾误加表示调用的圆括号。
一个对象占多少字节,与它有几个元素
std::size_t来自<cstddef>,是能表示对象字节大小的无符号整数类型。sizeof(int)取得int对象的字节数,结果类型是std::size_t;sizeof value取得该表达式类型的对象大小。它不是“把value转成字节数”,也不会对本节普通表达式执行一次运行时读取。本章例子用括号写类型参数,避免阅读歧义。
C++的一个字节以char大小为单位,因此sizeof(char)等于1;不能据此断定每个环境中int都固定为4字节。sizeof类对象描述对象自身表示所需的空间。对vector,它不能告诉你管理了多少int,更不能当成所有动态元素占用的总字节数。当前元素数量必须问对象的size接口,下一节开始实际使用它。
#include <array>
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
using Values = std::vector<int>;
using Triple = std::array<int, 3>;
using Item = Values::value_type;
using Count = Values::size_type;
Item sample{7};
Count count{3};
std::size_t bytes{sizeof(sample)};
std::cout << sample << ' ' << count << ' ' << (bytes == sizeof(int)) << '\n';
std::cout << (sizeof(char) == 1) << ' '
<< (sizeof(Triple) >= 3 * sizeof(int)) << '\n';
return 0;
}
逐行先把类型名替换成你已认识的含义,再看变量的初值和比较结果。输出中的数量3是独立的整数变量,不是靠sizeof猜出某个vector的元素个数;末尾的布尔比较只核对这段程序明确写出的大小关系,不承诺机器的具体位宽。
小练习:类型参数、别名和对象,分别标出哪一个
在纸上写std::array<int, 3>、using Sample = int;和Sample sample{7};。哪一处规定固定元素数?哪一处真正建立一个整数对象?如果把Sample改名为Reading,值7会改变吗?
提示:先找声明变量的那一行,再找类型说明。
答案:array里的3规定固定元素数;sample{7}建立对象并给它初值。类型别名换名字不改变整数值。一个类型可以用于建立多个对象,别名本身不是其中一个对象。
08.2 固定序列与下标 · 预估150分钟
每个位置确实有一个元素,才能读取
std::array<int, 3> values{2, 4, 6};建立一个拥有三个int的对象,依次初始化为2、4、6。这是按元素给出的初始化列表;最外面的花括号属于values的初始化,不是一个循环体。values.size()报告3,values[i]选择第i个元素:首项是0,末项是2。下标表达式可以读取元素,也能在对象可写时作为赋值目标。
合法下标满足i < values.size(),并且i不能是负数。下面用std::size_t记录非负位置,普通for的三段执行规则与新03相同。只把容器作为例子的输入,不会免除证明每次访问合法的责任。
#include <array>
#include <cstddef>
#include <iostream>
int main() {
std::array<int, 3> values{2, 4, 6};
int total{0};
std::cout << values.size() << '\n';
for (std::size_t i{0}; i < values.size(); ++i) {
std::cout << i << ' ' << values[i] << '\n';
total += values[i];
}
std::cout << "total " << total << '\n';
return 0;
}
先盖住运行结果,按“进入本轮时i → 读到的值 → 更新后的total”写三行。应是0→2→2、1→4→6、2→6→12。更新后i到3,条件为假,values[3]根本不执行。程序先输出长度,再输出每个位置和值,最后输出总和12。这里全部输入都很小,int累计不会越界;一般输入的安全累计在08.5完成。
元素初值不足,与元素数量为零
对本节int数组,std::array<int, 3> values{2};把第一项初始化为2,剩余两项得到0。values{}则让三个int都得到0;不要与未给初值的局部std::array<int, 3> values;混为一谈,后者不能直接读取其中未初始化的int。
std::array<int, 0>是合法的零长度库容器,size为0,普通循环一次也不进入;没有可访问的第0项。它与原生的零长度数组不同,不能把两者互相替换。固定长度意味着“由类型规定长度”,不意味着长度必须大于零。
#include <array>
#include <iostream>
int main() {
std::array<int, 3> partial{2};
std::array<int, 0> empty{};
std::cout << partial[0] << ' ' << partial[1] << ' ' << partial[2] << '\n';
std::cout << empty.size() << '\n';
return 0;
}
小练习:只有初值不同,循环次数会改变吗
把长度3的数组初值从{2, 4, 6}改成{2}。长度、循环次数、三个值和总和分别是什么?只在纸上分析把循环条件写成i <= values.size()的后果,不运行越界访问。
提示:初值数量与类型中的长度是两回事。
答案:长度和循环次数仍为3,值为2、0、0,总和2。错误条件会在i等于3时进入,访问不存在的第四项;“多出来的容量”或一次没有崩溃,都不能让这个访问合法。
原生数组和字符数组:没有对象成员接口
int values[3]{2, 4, 6};也是三个连续int,但属于语言原生数组,没有.size()成员。在这种数组定义中,也可写int values[]{2, 4, 6};,省略的长度由初值个数推得3;这与下面函数形参里的空方括号规则不同。对仍然是完整数组的名字,sizeof(values)得到整个数组的字节数,sizeof(values[0])得到一个int的字节数,两者相除可取得这里的元素数量。不能把这个公式套到已经变成指针的变量上。
字符也能组成原生数组。char text[]{"cat"};会放入c、a、t和末尾的零字符'\0',因此这个数组占四个char。零字符不是打印的字符0,也不是我们此前输出的换行;它为按零终止约定读取文本的接口标记结束。只写三个char的数组不自动附送第四个位置。
普通输出一个char会打印字符;输出本例保证有终止零的字符数组时,流按字符序列读取到终止零为止。不要把缺少终止零的字符数组交给这样的操作。下面对末项的检查使用与'\0'比较,输出布尔结果,而不是希望在终端看见不可见字符。
#include <cstddef>
#include <iostream>
int main() {
int values[]{2, 4, 6};
std::size_t count{sizeof(values) / sizeof(values[0])};
int total{0};
for (std::size_t i{0}; i < count; ++i) {
total += values[i];
}
char text[]{"cat"};
std::cout << count << ' ' << total << '\n';
std::cout << text << ' ' << sizeof(text) << ' ' << (text[3] == '\0') << '\n';
return 0;
}
函数形参里的方括号不保留原数组长度
在普通函数的形参中,const int values[]会调整为const int* values。形参没有复制一个完整数组,也不知道调用者那里原来有几项;调用时数组转换为指向首元素的指针。因此例子另外传std::size_t count,约定:count大于0时,values必须指向至少count个仍可读取的int。values[i]在这里按给定位置访问指针所指序列,只使用已经证明的合法下标;一般指针算术留在新09。
const表示不能经这条参数路径改写int,不表示函数得到了长度信息。形参里的sizeof(values)只会取得指针自身大小,不是实参数组大小,即使形参拼成values[3]也不能据此校验长度。把完整数组留在调用者那里计算长度,再显式传入。
#include <cstddef>
#include <iostream>
// count must describe live elements; values may be null only when count is zero.
// The fixed teaching inputs have small sums representable as int.
int sum_raw(const int values[], std::size_t count) {
int total{0};
for (std::size_t i{0}; i < count; ++i) {
total += values[i];
}
return total;
}
int main() {
int values[]{2, 4, 6};
std::size_t count{sizeof(values) / sizeof(values[0])};
std::cout << sum_raw(values, count) << '\n';
std::cout << sum_raw(nullptr, 0) << '\n';
return 0;
}
三个小整数的总和为12。对nullptr, 0,循环条件第一次就不满足,所以不发生解引用,和为0;不能据此推断nullptr, 1也合法。另一个错误是拿实际三项数组声称有四项:它违反接口前提,应先改调用者传入的数量,不运行第四项访问。库array作为一个完整对象传给适合的引用参数时可以保留类型中的长度;本节原生数组形参是另一种合同。
08.3 vector拥有与容量 · 预估180分钟
花括号给元素,圆括号可以给数量
现在输入数量不再固定。std::vector<int> values{2, 4};拥有两个int,values.size()是2;std::vector<int> empty{};没有元素,size为0,empty.empty()返回true。size_type是表示长度的无符号类型,本章用std::size_t i{0}写普通循环。循环条件直接写i < values.size();不要先求values.size() - 1,因为空序列的无符号0减1会回绕成很大的值。
同样的两个整数,括号形式可能选择不同接口。对vector<int>,values{3, 7}是一份含3和7的两元素列表;values(3, 7)则指定数量3和每项初值7。values(3)也建立三项,int元素都为0。不能把新02里标量初始化的等价经验推广成“所有类型的花括号与圆括号都一样”。
#include <iostream>
#include <vector>
int main() {
std::vector<int> listed{3, 7};
std::vector<int> repeated(3, 7);
std::cout << listed.size() << ' ' << listed[0] << ' ' << listed[1] << '\n';
std::cout << repeated.size() << ' ' << repeated[0] << ' ' << repeated[2] << '\n';
return 0;
}
先各画一条序列再检查size,最后才读取元素。两项列表是[3,7],三个重复值是[7,7,7];类型都是vector<int>,不同的是对象的当前内容。容器对象正常销毁时会处理自己拥有的元素,不需要你逐项手工释放;如何封装拥有关系留在新13。
完整例一:追加之后,什么发生了变化
values.push_back(6)在末尾追加一个值为6的新元素,成功后size增加1。原来[2,4]的顺序保留,新序列是[2,4,6]。values[2]现在才是合法位置;不能在追加之前先访问它。push_back是成员调用,不是对旧末元素做赋值。
#include <iostream>
#include <vector>
int main() {
std::vector<int> values{2, 4};
values.push_back(6);
std::cout << values.size() << '\n';
std::cout << values[0] << ' ' << values[1] << ' ' << values[2] << '\n';
std::vector<int> empty{};
std::cout << empty.size() << '\n';
return 0;
}
这份例子只持有容器,不跨追加保存元素的指针或引用。先预测追加前后size为2和3,再记录下标0、1、2各读到2、4、6;本文件直接输出三项,下一步的独立练习才改用普通for。最后一行观察另一份空vector的size为0。对空vector,合法的普通循环是零次,而不是特判后强行读取第0项。
给未来留空间,不等于现在增加元素
capacity表示当前存储能容纳的元素数;size表示已经建立、属于当前序列的元素数。二者满足size <= capacity,但含义不能交换。reserve(3)保证成功后容量至少为3,不把空序列变成三项;具体容量可能更大,代码不能假定恰好等于3或总是翻倍。
resize(3)才把当前元素数改为3:本节空vector<int>增长后有三个值为0的新元素。若再缩到1,保留首项,后两项不再是当前元素。缩小size并不要求capacity随之减小;储存空间和元素生命周期要分开描述。
#include <iostream>
#include <vector>
int main() {
std::vector<int> values{};
values.reserve(3);
std::cout << values.size() << ' ' << (values.capacity() >= 3) << '\n';
values.resize(3);
std::cout << values.size() << ' ' << (values.capacity() >= 3) << '\n';
std::cout << values[0] << ' ' << values[1] << ' ' << values[2] << '\n';
values.resize(1);
std::cout << values.size() << ' ' << values[0] << '\n';
return 0;
}
交互图:同一份空间中的实际元素逐步记录当前size、保证成立的容量范围和合法访问,不用机器地址表示教学关系。
固定长度、实际元素与预留容量分开看
array-index的array<int,3>拥有2、4、6;类型中的3固定元素数量,合法下标只有0、1、2。
这是另一个对象:vector-growth中的vector<int>{2,4}拥有两项;capacity至少能容纳当前两项,不假定其确切数值。
push_back(6)成功后,size为3,元素是2、4、6;capacity至少为3,图没有假定增长倍数。
切换到vector-capacity的另一份空vector:reserve(3)只预留空间,size仍为0,没有任何可读取的元素。
同一个空vector随后resize(3),现在确实有三个int元素,本例新增元素都为0;只有从这时起才可访问下标0、1、2。
查看所有步骤的文字与数值
- 1 · 固定array拥有三项
类型:array<int,3>;元素:2 | 4 | 6;size:3
array-index的array<int,3>拥有2、4、6;类型中的3固定元素数量,合法下标只有0、1、2。
- 2 · vector最初拥有两项
元素:2 | 4;size:2;capacity:至少2
这是另一个对象:vector-growth中的vector<int>{2,4}拥有两项;capacity至少能容纳当前两项,不假定其确切数值。
- 3 · 追加6之后
元素:2 | 4 | 6;size:3;capacity:至少3
push_back(6)成功后,size为3,元素是2、4、6;capacity至少为3,图没有假定增长倍数。
- 4 · 独立空vector只reserve
元素:尚无元素;size:0;capacity:至少3
切换到vector-capacity的另一份空vector:reserve(3)只预留空间,size仍为0,没有任何可读取的元素。
- 5 · resize建立三个整数
元素:0 | 0 | 0;size:3;capacity:至少3
同一个空vector随后resize(3),现在确实有三个int元素,本例新增元素都为0;只有从这时起才可访问下标0、1、2。
reserve可能重新分配存储。到了新09再追踪旧引用、指针和迭代器的具体失效规则;在本章,所有追加和调整都先完成,再经当前容器重新访问元素。这样做不是忽略寿命,而是明确限制此时练习采用的访问方式。
补充 · 直接访问首尾元素(另估15分钟)
对已经确认非空的vector,values.front()访问第一个元素,values.back()访问最后一个元素。它们返回元素的引用:可写vector的首尾可经该引用修改;const vector只能取得只读引用。只有一项时,两者访问同一个元素。
两种访问都要求容器非空。 reserve(8)只预留空间,不能使空vector获得首尾元素;先用empty()判断。这里不运行空容器的front/back来猜结果。
#include <iostream>
#include <vector>
int main() {
std::vector<int> values{7, 7};
values.push_back(9);
if (values.front() != 7 || values.back() != 9) return 1;
std::cout << "first=" << values.front() << " last=" << values.back() << '\n';
values.front() = 8;
if (values[0] != 8) return 1;
std::cout << "changed-first=" << values[0] << '\n';
std::vector<int> empty{};
empty.reserve(8);
if (!empty.empty()) return 1;
if (empty.empty()) std::cout << "empty=skipped\n";
}
这份程序从[7,7]追加9,先输出first=7 last=9;经front把首项写成8,再用下标0交叉检查,输出changed-first=8。另一份vector只reserve而没有元素,因此跳过访问,输出empty=skipped。每一步都在操作完成后重新通过容器取元素,没有跨追加保存旧引用。
阅读容量变化示例04-a时也用这条合同:reserve之后记录的old_capacity至少为请求值;resize(old_capacity,7)才建立那些7,随后追加9,所以首项7、末项9。这个结论不依赖容量恰好为2。
下载此例。本补充另估15分钟,不计入原08.3的180分钟;是粗估而非试学记录。接口依据:C++序列容器的首尾访问要求。
复制预测:谁会跟着变
std::vector<int> copy{original};从一个现有vector按值复制,copy拥有自己的int元素。把copy[0]加10,不能改变original的首项。这个用法合同现在就要掌握;自己定义复制构造函数的机制在新12。
普通函数按值接收std::vector<int> values时,具名实参也按这个合同成为参数的独立副本;仅修改这个参数,不会写回调用者。需要只读访问原容器时,const std::vector<int>& values沿用新05的只读引用,不复制整组元素;需要原地修改可以用可写引用。库对象与int一样,也要分清“对象副本”与“同一个对象的另一个访问路径”。
#include <iostream>
#include <vector>
// The fixed input has at least one element and its first value is small.
void change_copy(std::vector<int> copy) {
copy[0] += 10;
std::cout << copy[0] << '\n';
}
int main() {
std::vector<int> original{3, 1, 4};
std::vector<int> copy{original};
copy[0] += 10;
std::cout << original[0] << ' ' << copy[0] << ' '
<< original.size() << ' ' << copy.size() << '\n';
change_copy(original);
std::cout << original[0] << '\n';
return 0;
}
原输入固定为[3,1,4]。先写“原首项3、副本首项13、两边长度都为3”,再运行核对;观察按值形参发生修改后,调用者的首项是否仍保持3。不要拿打印副本的结果冒充检查了原容器。
08.4 string拥有文本 · 预估150分钟
文本有自己的长度与元素
std::string text{"cat"};建立一个拥有三个char的字符串对象。使用它需要<string>。size返回存储的char元素数量,不把为零终止接口准备的末尾零字符计入文本长度。本节只修改i < text.size()的实际字符,不把终止零位置当作可以任意写的正文空间。
text[1] = 'u';把第二个字符从a改为u,得到cut,长度仍是3;text += "!";追加文本,结果cut!,长度成为4。用双引号表示字符序列,用单引号表示一个char。两个string之间的+建立连接后的字符串结果,+=修改左侧已有字符串。对本例string还可以用text += mark追加一个char,或用prefix + "b"把string与字符串字面量连接;不要把两个纯字符串字面量直接相加当成同一种接口。
#include <iostream>
#include <string>
int main() {
std::string text{"cat"};
char mark{'!'};
std::cout << text[0] << ' ' << text.size() << '\n';
text += mark;
std::string prefix{"a"};
std::string joined{prefix + "b"};
std::cout << text << ' ' << text.size() << '\n';
std::cout << joined << ' ' << joined.size() << '\n';
return 0;
}
先看实际文件:它没有执行上文把第二字符改为u的那条独立推演,而是从cat直接追加感叹号,所以三行分别为c 3、cat! 4、ab 2。把赋值加进去才会得到cut!,这是另一份已验证的变体。
输出string时会按它记录的长度处理内容,与原生零终止字符指针的约定不同。std::string可以包含内部零字符;不能看到一个零就断言它的size到此为止。这里不展开内部零字符的构造接口;下一例只对照ASCII与UTF-8的长度。
UTF-8里,一个显示字符可能占几个char
在本章的ASCII文本中,cat包含三个可见字母,也占三个char;两个计数恰好一致,不代表它们是同一个概念。UTF-8把不同字符编码为不同数量的字节。下面用三个显式字节表示“猫”:\xE7、\x8C、\xAB。字符串字面量里的\x引入十六进制转义,这些是编码数据,不是本章要你实现的位运算。当前验证环境的char是8位字节;屏幕是否正确显示,另取决于终端如何解释它们。
#include <iostream>
#include <string>
int main() {
std::string ascii{"cat"};
std::string utf8{"\xE7\x8C\xAB"};
std::cout << ascii << ' ' << utf8 << '\n';
std::cout << ascii.size() << ' ' << utf8.size() << '\n';
std::cout << (utf8.size() == 3) << '\n';
return 0;
}
不要根据string的size就宣称获得了人眼看到的字符数。对UTF-8按下标拿出或修改单个char,也可能只拿到多字节编码的一部分。这个例子的确定性检查是输出的编码字节与长度;当前UTF-8终端第一行显示cat和猫,随后两行是3 3和1。它不以字体有没有正确画出汉字作为语言层正确性的条件;更完整的Unicode文本处理不是本章先备。
渐进阶梯一:把数值与文本的独立复制一起预测
vector按值复制的合同也有一个熟悉的文本场景。std::string copy{original};拥有自己的字符序列;修改copy的第二个字符,不会让original跟着变。下面要求你同时观察两份字符串,不能只观察被修改的一份。
#include <iostream>
#include <string>
int main() {
std::string original{"cat"};
std::string copy{original};
copy[1] = 'u';
std::cout << original << ' ' << copy << '\n';
std::cout << original.size() << ' ' << copy.size() << '\n';
return 0;
}
输入为cat,副本改第二字符后第一行输出原cat、副本cut,第二行确认长度都是3。现在把08.3的vector-copy也放入同一张预测表:原[3,1,4]、副本[13,1,4],再对照两份文本;所需复制和字符操作此时都已讲完。它可以用来保存某一刻文本的快照;若只是给原对象起一个引用别名,后续看到的仍然是同一份变化中的对象。这两种需求没有谁总是更好,接口必须说明自己要保存独立内容还是访问当前内容。
练习:一次改一个条件,先说清在哪份对象上改
保持原字符串cat,把副本第二字符改成o。再单独做一次实验:仍按原例改副本为cut,但随后把original第一字符改成b。分别预测最后两份文本。
提示:画两个拥有者,每条赋值只连接到其明确命名的对象。
答案:第一组原cat、副本cot;第二组原bat、副本cut。不要把两次实验的改动混在一起再称为“只改一个条件”。若换成引用会怎样,先从新05的别名规则解释,再写另一份独立实验。
渐进阶梯二:只补会创建元素的操作
从空文件建立[2,4],补出追加6的成员调用,再用普通下标for输出全部元素。验收是size为3、值依次为2/4/6;另建一个空vector,确认size为0,不能读取它的第0项。
提示与可展开答案
提示:下标访问不会自动长出元素。对照完整例一的操作顺序,先让对象拥有新元素,再访问。
答案:建立std::vector<int> values{2, 4};后调用values.push_back(6);,以i < values.size()为条件读取values[i]。空vector用std::vector<int> empty{};建立,只查询size或empty。若想一次建立三个0,可以用resize(3),但那与“在[2,4]后追加6”是不同任务。
渐进阶梯三:找出reserve与访问之间缺少的一步
错误思路是先对空vector执行reserve(3),然后给values[0]赋值2。这里的错误不是“忘记打印”,而是size仍为0,赋值的目标元素根本不存在。只分析这段非法访问,不把它当作应该运行的实验;检测工具没报错也不能建立语言合同。
下面是可运行的修正文件:先观察空序列的size,再通过真正创建元素的操作放入2,最后才读取首项。
#include <iostream>
#include <vector>
int main() {
std::vector<int> values{};
values.reserve(3);
std::cout << values.size() << '\n';
// No element exists yet: do not evaluate values[0] here.
values.push_back(2);
std::cout << values.size() << ' ' << values[0] << '\n';
return 0;
}
两种合法修法,与它们不同的结果
只需要一个元素2,用push_back(2),成功后size为1。如果业务需要三项,则先resize(3),再把第0项赋值2,得到[2,0,0]。两种都没有把reserve解释成创建元素;选哪一种取决于你真正需要多少项。
08.5 numeric_limits与溢出前检查 · 预估180分钟
先问类型能放多少,再决定能否相加
下标合法只证明输入元素确实存在,没有证明它们的总和可表示。新02讲过unsigned的回绕规则;现在把它变成一个明确的拒绝接口。<limits>提供std::numeric_limits<unsigned>,其中的max()是静态成员函数,返回unsigned的最大可表示值。依次读成“为unsigned选择数值性质 → 在该类作用域里找max → 调用它”;不用先建立numeric_limits对象。
令这个最大值为M。若当前total和新值value都已经是unsigned,能安全累加的条件是value <= M - total。因为total的类型保证它没有超过M,先做的减法不会向负方向回绕。若value > M - total,就在执行加法之前返回失败;只有条件通过,才让total += value发生。这些假设也限制接口接收已经合法表示的unsigned;不要先把负的外部输入转换成unsigned,再期待它自动保留“非法负数”的信息。
不要先算total + value再比较它是否大于M:同一unsigned类型的结果已经回绕到可表示范围,比较无法复原被丢失的信息。也不能因为unsigned的回绕有定义,就把它当成业务上正确的总和。
完整例二:逐步保留最后一份合法累计值
下面用固定array和普通下标for。add_checked只尝试增加一个unsigned值,bool报告是否接受,unsigned引用保存此前已有的累计值;它不清零结果。每组开始前由main清零total并把accepted设为true,然后在普通for中逐项调用;失败时设为false并break。一旦拒绝,结果保留拒绝之前最后合法的累计值,不假称获得了全体输入的和。源码里的2u/4u/6u用u后缀明确unsigned字面量,unsigned是unsigned int的简写。
#include <array>
#include <cstddef>
#include <iostream>
#include <limits>
bool add_checked(unsigned value, unsigned& total) {
const unsigned M{std::numeric_limits<unsigned>::max()};
if (value > M - total) {
return false;
}
total += value;
return true;
}
int main() {
std::array<unsigned, 3> values{2u, 4u, 6u};
unsigned total{0};
bool accepted{true};
for (std::size_t i{0}; i < values.size(); ++i) {
if (!add_checked(values[i], total)) {
accepted = false;
break;
}
}
std::cout << accepted << ' ' << total << '\n';
std::array<unsigned, 0> empty{};
total = 0;
accepted = true;
for (std::size_t i{0}; i < empty.size(); ++i) {
if (!add_checked(empty[i], total)) {
accepted = false;
break;
}
}
std::cout << accepted << ' ' << total << '\n';
const unsigned M{std::numeric_limits<unsigned>::max()};
std::array<unsigned, 2> overflow{M, 1u};
total = 0;
accepted = true;
for (std::size_t i{0}; i < overflow.size(); ++i) {
if (!add_checked(overflow[i], total)) {
accepted = false;
break;
}
}
std::cout << accepted << ' ' << (total == M) << '\n';
return 0;
}
先写三个场景的轨迹。对[2,4,6],total依次0→2→6→12,返回true。对零长度array,没有迭代,结果为0且返回true。对[M,1],第一项从0累加为M,第二项面对剩余0,在加法之前拒绝,保留M。比较total == M会输出1;它验证的是端点相等,不把M的十进制拼写写死为32位。
先求剩余空间,最后才相加
第一组输入是[2,4,6];M来自numeric_limits<unsigned>::max(),不是写死32位常量。
先判断2不超过剩余空间,再执行加法;total从0变为2。
下一项4不超过M−2,更新后total为6;不是先形成一个可能回绕的和再判断。
6不超过M−6,得到12;i增到3后条件为假,输出accepted=1、total=12。
第二组独立输入长度为0;重置total=0与accepted=true,循环体不执行,不读取任何元素。
第三组是[M,1],先重置total=0;M恰好等于剩余M,因此第一项可以接受,total变成M。
现在剩余M−M为0,1超过0,于是返回false且不执行相加;total仍为M,程序输出0与total==M的真假值1。
查看所有步骤的文字与数值
- 1 · 从合法初值0开始
输入:2 | 4 | 6;total:0;第一项检查:2 ≤ M − 0
第一组输入是[2,4,6];M来自numeric_limits<unsigned>::max(),不是写死32位常量。
- 2 · 接受2
当前项:2;检查:通过;total:0 → 2
先判断2不超过剩余空间,再执行加法;total从0变为2。
- 3 · 接受4
当前项:4;检查:4 ≤ M − 2;total:2 → 6
下一项4不超过M−2,更新后total为6;不是先形成一个可能回绕的和再判断。
- 4 · 接受6并结束
当前项:6;total:6 → 12;输出:1 12
6不超过M−6,得到12;i增到3后条件为假,输出accepted=1、total=12。
- 5 · 空array也是一组输入
输入长度:0;total:0;输出:1 0
第二组独立输入长度为0;重置total=0与accepted=true,循环体不执行,不读取任何元素。
- 6 · 新输入先接受M
输入:M | 1;第一项检查:M ≤ M − 0;total:0 → M
第三组是[M,1],先重置total=0;M恰好等于剩余M,因此第一项可以接受,total变成M。
- 7 · 拒绝再加1
下一项检查:1 > 0;total:M,保持;输出:0 1
现在剩余M−M为0,1超过0,于是返回false且不执行相加;total仍为M,程序输出0与total==M的真假值1。
业务上限更小,也需要自己的拒绝规则
类型允许保存一个值,不代表业务愿意接受它。比如本次批处理只允许总分10,已经用了6,再加5在unsigned类型中完全能表示,却超过业务限制。
#include <iostream>
#include <limits>
bool add_with_limit(unsigned amount, unsigned limit, unsigned& total) {
if (total > limit || amount > limit - total) {
return false;
}
total += amount;
return true;
}
int main() {
unsigned total{6};
bool accepted{add_with_limit(5u, 10u, total)};
std::cout << accepted << ' ' << total << '\n';
const unsigned M{std::numeric_limits<unsigned>::max()};
accepted = add_with_limit(5u, M, total);
std::cout << accepted << ' ' << total << '\n';
return 0;
}
在计算limit - total之前,先证明total <= limit;若已有状态超过业务上限,也必须拒绝,不能让unsigned减法回绕后制造巨大的“剩余空间”。对合法状态再检查value <= limit - total,接受后才更新。类型最大值与业务上限是两条不同的边界,不要共用一个模糊的“没溢出”解释。
渐进阶梯四:从空文件完成可变长度的安全累计
独立写一个接受const std::vector<unsigned>& values与unsigned& total的函数,返回bool。约定:每次调用开始把total设为0;遇到不能表示的下一次加法就返回false并保留最后合法累计值;处理完整组则返回true。调用者提供的是具名vector对象,不依赖短命的输入;输出参数total是独立的unsigned对象,不能借用输入vector中的某个元素,否则清零会改变待处理输入。
验收输入是[2,4,6]、空vector、[M]、[M,1]。前三组分别成功得到12、0、M;第四组失败且total仍为M。只使用本章已经教过的库类型、普通下标for、函数、引用和条件。不要提前使用范围for、迭代器或span,也不要把M硬编码成一个你在本机见到的数字。
第一层提示:先写合同,再写循环
先清零结果,用std::size_t i{0}开始循环,以i < values.size()证明下标有效。取出当前unsigned值后,在任何加法前比较剩余范围;失败分支直接返回false,循环正常结束后返回true。
对照答案:完整程序与四组检查
#include <cstddef>
#include <iostream>
#include <limits>
#include <vector>
// Start a new sum; on rejection retain the last accepted partial sum.
// total is a separate object, not an element of values.
bool checked_sum(const std::vector<unsigned>& values, unsigned& total) {
const unsigned M{std::numeric_limits<unsigned>::max()};
total = 0;
for (std::size_t i{0}; i < values.size(); ++i) {
if (values[i] > M - total) {
return false;
}
total += values[i];
}
return true;
}
int main() {
const unsigned M{std::numeric_limits<unsigned>::max()};
std::vector<unsigned> values{2u, 4u, 6u};
std::vector<unsigned> empty{};
std::vector<unsigned> single{M};
std::vector<unsigned> overflow{M, 1u};
unsigned total{0};
bool accepted{checked_sum(values, total)};
std::cout << accepted << ' ' << total << '\n';
accepted = checked_sum(empty, total);
std::cout << accepted << ' ' << total << '\n';
accepted = checked_sum(single, total);
std::cout << accepted << ' ' << (total == M) << '\n';
accepted = checked_sum(overflow, total);
std::cout << accepted << ' ' << (total == M) << '\n';
return 0;
}输出的前两组为成功/12、成功/0;后两组用“是否等于M”的布尔值表示端点,所以是1 1、0 1。逐行对照时必须同时检查成功标志与实际结果;不能只因为第二列都是1,就把后两组都说成成功。示例的return 0只报告程序正常走到末尾;实际输出仍要与上述四组预期比较,不能把退出0当成答案正确,更不会给学习者自动记入M1。
离开本章前,用一句完整的话描述你的数据
说明你选择了哪个拥有者、当前有几项、每次访问为什么在边界内、复制后改动落在哪一份对象,以及失败时输出参数保留什么。第09章再讨论怎样遍历和借用这些对象。安全求和综合示例使用范围for,完成第09章后再整合阅读;本章的数值边界练习使用已教过的写法。
关掉参考,再做一次
把理解说出来
先完成正文的独立迁移,再回答下面六题。它们只检验第08.1–08.5节已经讲过的内容。打开答案、编译成功或填写用时,都不会自动通过本章,更不代表通过 G0。
类型实参与字节数
vector<int>和array<int,3>的尖括号里分别填什么?using、value_type、size_type与sizeof回答什么问题?
对照推理与英文回答
vector的int是元素类型;array还接收固定元素数3。using给现有类型另取名字,value_type是元素类型,size_type是该容器表示元素数的类型。sizeof返回对象或类型占多少C++字节,结果类型是size_t;sizeof(char)为1,但不能据此断言所有机器一个字节都是8位。sizeof(vector对象)也不是元素数。
The vector argument selects the element type; array also takes a fixed element count. An alias declaration introduces another name for a type. value_type names the element type and size_type represents container sizes. sizeof returns a byte count of type size_t, not a sequence length.数组与长度找错
array<int,3>{2}的三项是什么?空array能否读第0项?const int values[]作为形参会保留完整数组长度吗?
对照推理与英文回答
部分列表中的本例未显式给出的int元素为0,所以是2、0、0;空array长度0,没有第0项可读。这里的数组形参会调整为const int*,调用时普通数组转换为首元素指针,长度必须单独约定或传入;对形参做sizeof得到指针大小,不能恢复原数组元素数。字符数组cat还包含结尾零字符,数组长度4与文本长度3要分开。
The partially initialized array contains two, zero, zero. A zero-length array has no element zero. An array parameter is adjusted to a pointer, so the count must be supplied by the interface. sizeof that parameter measures a pointer. A null-terminated cat array includes a fourth element for the terminator.vector写法与容量
vector<int>{3,7}和vector<int>(3,7)各有几项?空vector.reserve(3)后哪些下标能读,怎样让三个0真正成为元素?
对照推理与英文回答
花括号版本有3、7两项;圆括号版本有三个7。空vector即使reserve(3),size仍为0,任何元素下标都不能读取;resize(3)才在这个例子里建立三个0。capacity只给出可容纳数量,不能替代size,也不能从一次运行推出固定增长倍数。
The braced vector contains two elements, three and seven. The parenthesized version contains three copies of seven. Reserving capacity in an empty vector creates no elements. Resizing it to three creates three zero-initialized integers in this example. Capacity does not define valid indices.复制与文本字节
把[3,1,4]的副本首项加10,再把cat的副本第二字符改为u,原值会变吗?为什么cat与本章UTF-8的猫都报告size为3?
对照推理与英文回答
vector副本拥有独立元素,所以原首项仍3、副本首项13,长度都3;string原文仍cat、副本为cut。按值接收vector也建立本次调用的独立副本。string的size数的是char元素,本机例中就是字节数;三个ASCII字符占3字节,而给出的猫的UTF-8编码也占3字节,size不是Unicode字符计数。
The copies own independent elements: the vector starts with three while its copy starts with thirteen, and cat stays unchanged when the string copy becomes cut. A by-value vector parameter is also an independent copy. string::size counts char elements, not Unicode characters; the given UTF-8 character uses three bytes in this environment.为什么先减后加
令M为unsigned最大值、total=M、下一项为1,为什么先检查value > M-total?把业务上限10换成M会改变什么?
对照推理与英文回答
M-total为0,所以1在相加前被拒绝,最后合法累计值仍M。若先做unsigned加法,回绕虽有语言定义,却已经失去所需数学和,不能据此认定累计正确。业务上限与类型表示上限是两个合同:6加5可由unsigned表示,但超过业务上限10;若上限真的改为M,本例可接受并得到11。
The remaining representable room is zero, so one must be rejected before addition and the partial sum remains M. Defined unsigned wraparound is not a correct mathematical sum. A business limit is separate from a type limit: six plus five is representable, yet exceeds a business limit of ten.独立实现的边界
checked_sum对[2,4,6]、空vector、[M]、[M,1]分别应返回什么并留下多少?为什么代码把后两组结果与M比较后再输出?
对照推理与英文回答
四组依次为true/12、true/0、true/M、false/M;每次调用先把total设为0,拒绝时保留本次最后合法的部分和。后两组输出total==M,避免假定unsigned恰好32位或把某台机器的最大值写成固定答案。普通下标循环只在i<size时读取,空组完全不进入循环;不需要范围for、迭代器或view。
The four outcomes are true with twelve, true with zero, true with M, and false with M. Each call starts a new sum at zero; rejection retains that call’s last valid partial sum. Comparing the result with the actual M keeps the expected output independent of the unsigned width. The indexed loop performs no access for an empty input.和正文是同一份源码
示例文件
先自己输入和预测,卡住时再下载对照。文件名相同不代表内容相同;把它们放在单独的练习目录中,避免覆盖自己的作品。
- type-arguments.cpp类型和值实参、嵌套类型与字节数
- array-index.cpp固定三个整数:先检查下标再累计
- array-init.cpp部分初值补零,空array不读取元素
- raw-characters.cpp原生数组的长度与字符数组的零终止
- array-parameter.cpp数组形参成为指针,长度单独传递
- vector-forms.cpp花括号两项与圆括号三个7
- vector-growth.cpp完整例一:从2、4追加6
- vector-capacity.cppreserve预留,resize创建或减少元素
- vector-copy.cpp副本首项13,原首项仍为3
- reserve-repair.cpp先创建元素,再合法读取第0项
- string-basics.cpp字符、长度和连接的三种观察
- string-bytes.cppASCII文本与明确的UTF-8字节
- string-copy.cpp把副本改成cut,原文保持cat
- checked-array.cpp完整例二:先判断能否相加
- checked-vector.cpp独立迁移:四组unsigned序列
- business-limit.cpp能表示11,不等于业务允许11
这16份程序均使用正文规定的安全输入;越界与reserve后直接下标写入只作静态分析,没有提供诱导运行的危险程序。完整验证记录保留在本地;这里没有调用加速卡。
可选的学习反馈
记下你真正花的时间
每完成一个学习时段,再填实际分钟。环境准备、阅读推演、独立编码和卡点排查分别记录,避免同一段时间重复计算。离开吃饭或做其他事情的时间不算进去。
记录只保存在你的浏览器,可导出给我复盘。留空表示尚未记录,不等于零耗时;页面停留时间不会自动计为学习。不要把开发者检查时间填进来。
尚无真实试学用时。
换设备:导入记录,或取回损坏的旧记录
导入会合并时段,相同编号不重复累加;发生冲突会保留现有记录。
阅读记录与课程验收分别保存。
本章资料与查证
- WG21 N4861:C++20工作草案 ↗
固定2020-04-01草案;08.1见[temp.arg]、[dcl.typedef]、[expr.sizeof],08.2见[array.overview]、[dcl.init.aggr]、[dcl.init.string]、[dcl.fct],08.3–08.4见[vector.cons]、[vector.capacity]、[string.cons],08.5见[numeric.limits.members]及[basic.fundamental]。
- Microsoft C++标准库参考:vector ↗
08.1与08.3的补充查证:value_type、size_type、构造、size、capacity、reserve、resize和push_back;仅查本章所用接口,不要求提前阅读该页的迭代器、异常与模板定义例子。
本章独立解释所需读法;资料用于核对与补充。工具版本、操作系统和实际执行状态见自己的运行记录。