从零开始 / 一次只解释眼前的一步
遍历与借用:
这些位置还能读取吗。
把下标循环连到迭代器和范围for,再用span与string_view借用已有数据。画清拥有者、访问区间,以及最后一次合法读取。
9小时是包含阅读、推演和编码的设计估算,未经真人试学校准。可分多次学习,遇到不清楚的地方保留预测、实际结果和疑问,之后再回修教材。手机可读图与做预测,编译需要电脑终端。
这一章怎样学
先读一小段,写下预测,再运行程序。每次只改一个条件,最后关掉示例,从空文件独立写一次。遇到错误,把第一条报错和自己的修复记下来;不用赶着把页面滚到底。
每次记录本轮访问的位置、访问方式、真正改变的对象以及拥有者是否仍有效。涉及悬空或越界时,只找第一次破坏合同的位置,不运行错误访问来猜结果。本章后还有独立M1;阅读记录不自动通过M1或G0。
09.1 指针算术与迭代器范围
有了三个整数,怎样表达“从这里开始,处理到那里之前”?本节把下标写法连接到半开区间(half-open range)。先备是新06的有效指针和新08的数组、vector;现在仍只操作同一组实际存活的元素。
一个位置,加上几个元素
原生数组int values[]{2, 4, 6};拥有三个int。这里int* first{values};使用数组名转换出的首元素指针;它指向values[0]。first + 1指向下一项,增加的是一个int元素的位置,不是把数值2加成3,也不要求你猜机器地址增加几字节。values + 3是尾后位置:可以形成和比较,不能读取它指向的“第四项”。
同一数组中的两个位置可以相减,得到带符号的元素距离;结果类型是std::ptrdiff_t,声明来自<cstddef>。这段很小的数组保证距离可表示。不要对两份独立数组的指针做减法,也不要先形成数组之外的指针再说“反正不读取”。移动的结果必须仍指向本数组元素或合法尾后。
#include <cstddef>
#include <iostream>
int main() {
int values[]{2, 4, 6};
int* first{values};
int* last{values + 3};
const std::ptrdiff_t forward{last - first};
const std::ptrdiff_t backward{first - last};
std::cout << forward << ' ' << backward << '\n';
std::cout << *first << ' ' << *(first + 1) << ' ' << *(last - 1) << '\n';
return 0;
}
先算位置再读值:last−first为3,first−last为−3;first、first+1、last−1分别选第0、1、2项,所以两行输出是3 -3和2 4 6。*(last - 1)先退回最后一项再读取;*last会越界,两者不能混淆。
用begin和end表达同一个范围
对本章的array和vector,values.begin()取得首迭代位置,values.end()取得尾后。迭代器(iterator)是库提供的位置类型;不必假设它的内部就是裸指针。auto position = values.begin();让编译器推导该类型。*position访问当前元素,++position走到下一项,position != values.end()判断是否还没到终点。只有判断通过时才读取当前项,也不对end继续递增。
#include <iostream>
#include <vector>
int main() {
const std::vector<int> values{2, 4, 6};
int total{0};
for (auto position = values.begin(); position != values.end(); ++position) {
std::cout << *position << '\n';
total += *position;
}
std::cout << "total " << total << '\n';
const std::vector<int> empty{};
int empty_total{0};
for (auto position = empty.begin(); position != empty.end(); ++position) {
empty_total += *position;
}
std::cout << "empty " << empty_total << '\n';
return 0;
}
输入[2,4,6]依次输出2、4、6,再输出total 12。空vector的begin与end相等,第一次判断就停止,得到empty 0。这就是[first,last)的好处:范围非空时first可以读,last只作边界;两者相等自然表示零项,不必先“读一下首元素”。
半开范围把终点留给判断,不留给读取
pointer-range的同一个原生数组拥有2、4、6。first指向下标0,last是values+3;区间[first,last)覆盖三项。
first+1指向下标1,解引用得到4。加1移动一个int元素的位置,不是把元素值加1,也不由程序写死字节数。
last-1回到下标2,因此可以读取6。这里两个端点始终属于同一数组的合法位置或其尾后位置。
位置到3后不再读取。last-first为3,first-last为-3,结果以有符号ptrdiff_t表达;不能把来自不同数组的指针按这个合同相减。
切换到iterator-range的空vector:begin等于end,循环条件第一次就为假,empty_total保持0;不解引用空范围端点。
查看所有步骤的文字与数值
- 1 · 从首元素开始
数组:2 | 4 | 6;当前位置:0 → 2;last:3,不可读
pointer-range的同一个原生数组拥有2、4、6。first指向下标0,last是values+3;区间[first,last)覆盖三项。
- 2 · 前进一步仍在数组内
数组:2 | 4 | 6;当前位置:1 → 4;last:3,不可读
first+1指向下标1,解引用得到4。加1移动一个int元素的位置,不是把元素值加1,也不由程序写死字节数。
- 3 · last减1是最后实际元素
数组:2 | 4 | 6;当前位置:2 → 6;last:3,不可读
last-1回到下标2,因此可以读取6。这里两个端点始终属于同一数组的合法位置或其尾后位置。
- 4 · 尾后只参与边界判断
当前位置:3,停止;last-first:3;first-last:−3
位置到3后不再读取。last-first为3,first-last为-3,结果以有符号ptrdiff_t表达;不能把来自不同数组的指针按这个合同相减。
- 5 · 另一个空vector没有第一轮
元素:空;begin与end:相等;empty_total:0
切换到iterator-range的空vector:begin等于end,循环条件第一次就为假,empty_total保持0;不解引用空范围端点。
本章选用的array/vector迭代器还能加上或减去整数。例如begin()+1选第二项,begin()+4选第五项所在位置;只把第1、2、3项交给区间构造时,第4项正好是排除的终点。这种偏移能力属于当前这些序列,不能直接推广到所有容器的迭代器。
范围也能用来建立一个拥有副本
std::vector<int> middle(first, last);在合法[first,last)中复制元素,创建自己的vector。这里的两个实参是迭代器;不是新08的“数量、重复初值”两个整数。first和last必须描述同一来源中的合法可达区间,不能把不同容器的首尾拼在一起。
#include <iostream>
#include <vector>
int main() {
const std::vector<int> source{3, 1, 4, 1, 5};
auto first = source.begin() + 1;
auto last = source.begin() + 4;
std::vector<int> middle(first, last);
std::cout << middle.size() << ' ' << middle[0] << ' '
<< middle[1] << ' ' << middle[2] << '\n';
middle[0] += 10;
std::cout << source[1] << ' ' << middle[0] << '\n';
const std::vector<int> empty(first, first);
std::cout << empty.size() << '\n';
return 0;
}
source是[3,1,4,1,5]。begin()+1到begin()+4之前复制[1,4,1],首行3 1 4 1。只给middle[0]加10,下一行是1 11:source[1]没变。用(first,first)构造的vector长度为0。区间提供读取位置,是否拥有新元素由调用的接口决定;这个vector构造确实复制元素。
先预测一个变化,再对答案
只把range-construction的last改成source.begin()+3,middle应是[1,4],长度2。原文件后面的middle[2]不再合法,因此不能直接照原输出语句运行;先把第三项的输出移除,再验证2 1 4、1 11和0。改变输入范围时,后面的访问合同也必须重新检查。
09.2 范围for:每轮拿到副本还是原元素
如果只需要依次处理所有元素,范围for(range-based for)可以省去自己维护位置。冒号右边是范围,左边声明本轮使用的名字。进入下一轮时,这个名字对应下一项;空范围一次也不执行。
| 写法 | 本轮拿到什么 | 给value赋值会怎样 |
|---|---|---|
for (auto value : values) |
当前int元素的副本 | 修改本轮副本,原元素不变 |
for (auto& value : values) |
当前元素的可写引用 | 修改原元素 |
for (const auto& value : values) |
当前元素的只读引用 | 不能经value写入;其它合法路径仍可能修改元素 |
这里复用05.4 auto与引用推导,新增的是每一轮怎样绑定到序列元素。const int reading和const auto reading在当前整数范围里都是每轮只读副本;const不会把值变量变成引用。
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::vector<int> values{2, 4, 6};
int indexed{0};
for (std::size_t i{0}; i < values.size(); ++i) {
indexed += values[i];
}
int copied{0};
for (auto value : values) {
copied += value;
}
int borrowed{0};
for (const auto& value : values) {
borrowed += value;
}
std::cout << indexed << ' ' << copied << ' ' << borrowed << '\n';
for (auto& value : values) {
value += 2;
}
std::cout << values[0] << ' ' << values[1] << ' ' << values[2] << '\n';
int changed{0};
for (const auto& value : values) {
changed += value;
}
std::cout << changed << '\n';
return 0;
}
普通下标for、按值范围for、只读引用范围for都累计[2,4,6],首行是12 12 12。接着用auto&每项加2,容器变[4,6,8],后两行是4 6 8和18。决定会不会修改原序列的是访问方式,不是循环看起来更短。
讲解例一,也是阶梯的预测步
先不运行。给[3,1,4,1,5]画五行,分别填写本轮输入、进入时total、离开时total;再换成迭代器和范围for,写出同一张表。最后独立预测空组、[5]以及每项原地加2。
#include <array>
#include <cstddef>
#include <iostream>
int main() {
std::array<int, 5> readings{3, 1, 4, 1, 5};
int total{0};
std::cout << "index\n";
for (std::size_t i{0}; i < readings.size(); ++i) {
total += readings[i];
std::cout << total << '\n';
}
total = 0;
std::cout << "iterator\n";
for (auto position = readings.begin(); position != readings.end(); ++position) {
total += *position;
std::cout << total << '\n';
}
total = 0;
std::cout << "range\n";
for (const auto& reading : readings) {
total += reading;
std::cout << total << '\n';
}
const std::array<int, 0> empty{};
int empty_total{0};
for (const auto& reading : empty) {
empty_total += reading;
}
const std::array<int, 1> single{5};
int single_total{0};
for (const auto& reading : single) {
single_total += reading;
}
std::cout << "empty " << empty_total << '\n';
std::cout << "single " << single_total << '\n';
for (auto& reading : readings) {
reading += 2;
}
int calibrated{0};
for (const auto& reading : readings) {
calibrated += reading;
}
std::cout << "calibrated " << calibrated << '\n';
return 0;
}
三种循环各自从0累计,五次离开时依次是3、4、8、9、14。程序在每一种写法前都把total重置,不是把三个循环接成42。空组得0、单组得5;原地加2后元素为[5,3,6,3,7],最终24。先核对你的过程表,再看程序打印的index、iterator、range三段。
读完整stage-01/02之前,补齐三处读法
for (const int reading : std::array<int, 1>{5})在冒号右边直接创建一个没有名字的array对象,长度1、内容5。这个直接作为范围的完整临时array会活过该循环,reading每轮复制其中的值;std::array<int, 0>{}同样是完整空范围。这里只使用这一条窄规则,不把它推广成“复杂表达式里所有临时对象都延寿”。
stage-02中的int raw=0, adjusted=0;是一条int声明中的两个变量,各自初始化为0;可以拆写成int raw=0;和int adjusted=0;来阅读。逗号在这里分隔声明。嵌套调用则回用04.4的调用过程:calibrated(reading,2)先形成要交给外层add_reading的那个整数值,不把calibrated的形参变成原数组元素。
最后认清单语句循环体:for循环体只有一条语句时,可以不写包住循环体的花括号,循环只控制紧接的这一条语句。stage-01中的for(const int reading:std::array<int,0>{}) empty_total+=reading;,等价于for(const int reading:std::array<int,0>{}) { empty_total+=reading; };增加的是循环体外面这一对花括号,不是array初始化的{}。
这里空范围使加法执行零次;后续语句仍在循环之外,不会因为缩进或写在同一行就变成循环体。stage-01的单元素循环以及稍后stage-03的单语句循环也按同一条规则阅读。自己写代码时保留循环体花括号,能让这个边界更清楚。
现在可以完整读取并运行stage-01.cpp与stage-02.cpp。前者两行是count=5 total=14与empty=0 single=5;后者两行是raw=14 calibrated=24与original=3 returned=5。把每次add_reading的目标对象圈出来,再解释为什么校准后的总和变化,而readings没有变化。
只改一个条件:把校准偏移2改成0
只改stage-02循环中calibrated的第二个实参,raw和adjusted都变成14;后面独立的calibrated(original,2)没有改,original仍3、copy仍5。但原文件的检查仍要求adjusted为24,所以这次运行会先以1退出,不会打印那两行。先预测状态与退出点,再把“更新检查期望为14”作为另一个明确修改步骤,才能验证新的校准合同;不要把两个修改叫作只改一个条件。
09.3 span与string_view:一张借条,不是一份副本
上一节复制一个int很容易。现在要把已有的一整段数据交给函数读取,同时保留“从哪里开始、共有几项”,可以使用视图(view)。本节的视图借用连续元素,不负责创建或销毁那些元素;把拥有者和视图分别画成两个对象。
span保留首位置和长度
<span>提供std::span<int>,这里省略第二个模板实参,长度在运行时保存在视图中。std::span<int> view{values};可以借用当前这个具名array或vector;不会复制元素,也不会把它们的拥有权拿走。view.size()是视图包含的元素数量,view[i]仍要求i小于该数量;view.empty()询问长度是否为0。空span可以传递和查询长度,不能读取第0项。
std::span<const int>限制通过视图修改元素。相反,const std::span<int>限制视图对象本身的重新赋值,元素类型仍是int,所以能通过它修改元素。这与“指针自己const”和“指向const元素”的区别相似,不能只数出现了几个const。
#include <array>
#include <iostream>
#include <span>
int main() {
std::array<int, 3> values{2, 4, 6};
std::span<int> view{values};
std::span<const int> read{values};
std::cout << view.size() << ' ' << view[1] << '\n';
const std::span<int> fixed{view};
fixed[1] = 7;
std::cout << read[1] << '\n';
const std::span<const int> empty{};
std::cout << empty.size() << ' ' << empty.empty() << '\n';
return 0;
}
view和read都借用同一个[2,4,6]。先输出3 4;复制出const视图对象fixed没有复制元素,fixed[1]=7使read[1]也观察到7。空的只读span输出0 1,其中1是empty()的true,不是存在一个元素。
对已有的连续存储,还可以显式给出首位置与数量:std::span<int> middle{values + 1, 2};从第二项开始借两项。第二个参数是数量,不是尾下标或指针。调用者必须保证这两项实际存在并在使用期间存活;span不会把错误长度变成真实存储。
#include <iostream>
#include <span>
int main() {
int values[]{2, 4, 6, 8};
std::span<int> middle{values + 1, 2};
std::cout << middle.size() << ' ' << middle[0] << ' ' << middle[1] << '\n';
middle[0] = 9;
std::cout << values[0] << ' ' << values[1] << ' '
<< values[2] << ' ' << values[3] << '\n';
return 0;
}
[2,4,6,8]的中间窗口是[4,6],先输出2 4 6。middle[0]对应原来的values[1];修改成9后,拥有者四项输出2 9 6 8。同一数据可以有不同长度的视图,它们的下标0未必指向拥有者的下标0。
阶梯补全:只借用输入的total函数
先从空文件写int total(std::span<const int> values)。输入最多100项,每项在−100到100之间,所有部分和在int范围内;这里只累计小整数,不重新讲数值上限。函数从0开始,遍历借来的元素,返回总和;不保存视图以供返回后再用。
在main中建立原序列[3,1,4,1,5]、它的只读视图和独立vector副本。只给副本首项加10,分别核对原借用、副本视图、再次原借用和空span,应得14、24、14、0。先写完再展开参考。
提示:被复制的是视图,还是拥有者
函数按值接收span只复制视图描述,不复制元素;新08的vector复制才创建独立元素。每次total调用都有自己的result,空范围保持初始0。只读span不允许函数经此路径改元素,但也不冻结拥有者。
对照一个满足合同的实现
#include <iostream>
#include <span>
#include <vector>
// At most 100 values in [-100, 100]; every partial sum fits in int.
int total(std::span<const int> values) {
int result{0};
for (const auto& value : values) {
result += value;
}
return result;
}
int main() {
const std::vector<int> original{3, 1, 4, 1, 5};
const std::span<const int> borrowed{original};
std::vector<int> copy{original};
copy[0] += 10;
const std::span<const int> copied_view{copy};
const std::span<const int> empty{};
std::cout << total(borrowed) << '\n';
std::cout << total(copied_view) << '\n';
std::cout << total(borrowed) << '\n';
std::cout << total(empty) << '\n';
return 0;
}四次调用分别输出14、24、14、0;复制出的vector与原vector独立,两个视图的拥有者不同。请再用自己初始化的[5]验证结果5,不靠记住四个输出写分支。
对当前动态长度的span<const int>参数,标准库也允许从合适、仍存活的array或vector实参构造这个只读视图。因此,接口是total(span)时,直接传具名vector也是在调用处借用其连续元素,不是把vector复制进函数。完整stage-03.cpp使用这一种接口写法;先将每次实参标成拥有者或已有视图,再回访其owner=14 copy=24 borrowed=14。这一合同只说明当前库接口的使用,不要求你实现隐式构造或模板约束。
阶梯找错:五行输出正确,原数组却没改
任务要求把原序列每项加2。下列程序是合法的,却没有满足任务;先预测前五行和最后一行,指出需要修改的一个访问方式。
#include <array>
#include <iostream>
int main() {
std::array<int, 5> readings{3, 1, 4, 1, 5};
for (auto reading : readings) {
reading += 2;
std::cout << reading << '\n';
}
int total{0};
for (const auto& reading : readings) {
total += reading;
}
std::cout << "original " << total << '\n';
return 0;
}
提示与修复
前五行确实是5、3、6、3、7,但它们来自每轮副本;最后一行是original 14。只把第一个循环的auto改为auto&,才会改原元素。修复后前五行相同,最后一行变为original 24。观察循环内的临时结果不够,还要检查任务真正要求改变的对象。
#include <array>
#include <iostream>
int main() {
std::array<int, 5> readings{3, 1, 4, 1, 5};
for (auto& reading : readings) {
reading += 2;
std::cout << reading << '\n';
}
int total{0};
for (const auto& reading : readings) {
total += reading;
}
std::cout << "original " << total << '\n';
return 0;
}讲解例二:借来的文本和拥有的快照
<string_view>提供std::string_view,借用字符区间,不能通过view[i]修改字符。显式写view{letters, 3}表示从letters的首字符开始取3个字符;不像只给一个字符指针的构造那样依赖查找结尾的零字符。下面的原生数组只有c、a、t三项,没有额外的'\0'。
流输出一个string_view按它保存的长度读取字符,所以这三项能安全输出cat。std::string snapshot{view};则调用string的视图构造,复制这三个字符,建立独立拥有的文本。复制string_view本身仍只是复制借条,不会得到这个快照效果。
同一份字符的借用,与另一份拥有快照
letters为三个char:c、a、t,没有终止零。合法下标是0、1、2;后续用明确长度3建立视图。
view记录letters的起点和长度3;string snapshot从view复制三个字符。此时两种输出都为cat,但它们访问的拥有关系不同。
letters[1]从a改为u。view仍指向原数组,所以输出cut;snapshot自己的字符没有改变,仍输出cat。
最后输出view.size和snapshot.size,都为3。按长度输出view不需要尾部零;它也没有因为持有长度而延长letters的存活时间。
查看所有步骤的文字与数值
- 1 · 三个字符,没有第四项
letters:c | a | t;元素数量:3;终止零:未提供
letters为三个char:c、a、t,没有终止零。合法下标是0、1、2;后续用明确长度3建立视图。
- 2 · view借用,snapshot复制
letters:cat;view:借用:cat;snapshot:拥有:cat
view记录letters的起点和长度3;string snapshot从view复制三个字符。此时两种输出都为cat,但它们访问的拥有关系不同。
- 3 · 改写letters的第二项
letters:cut;view:看到cut;snapshot:保持cat
letters[1]从a改为u。view仍指向原数组,所以输出cut;snapshot自己的字符没有改变,仍输出cat。
- 4 · 长度没有因修改而变化
view.size:3;snapshot.size:3;读取条件:letters仍存活
最后输出view.size和snapshot.size,都为3。按长度输出view不需要尾部零;它也没有因为持有长度而延长letters的存活时间。
#include <iostream>
#include <string>
#include <string_view>
int main() {
char letters[]{'c', 'a', 't'};
const std::string_view view{letters, 3};
const std::string snapshot{view};
std::cout << view << ' ' << snapshot << '\n';
letters[1] = 'u';
std::cout << view << ' ' << snapshot << '\n';
std::cout << view.size() << ' ' << snapshot.size() << '\n';
return 0;
}
第一行cat cat。把letters[1]从a改成u,view继续观察同一数组,变成cut;snapshot保持cat,第二行是cut cat。长度都没变,第三行3 3。只读访问路径没有冻结底层元素,也没有记住过去。
本章遇到的array/vector/span/string_view的.data()表示首位置访问接口;它不附带一个供其它函数自动读取的长度。特别是string_view的data不承诺视图结束处有零字符,不能把它直接当作“这段文本正好在此结束”的C字符串。让接收方同时知道长度,或按上例建立拥有的string;不要靠碰巧相邻的字节为0证明安全。
阶梯迁移:关掉参考,自己验证三种对象
独立创建只含c/a/t的字符数组、借用它的string_view,以及修改前创建的string快照。改第二字符为u,用自己的检查确认view读到cut、快照仍cat;所有读取都安排在字符数组仍存活时。至少再改变一个条件:把第一字符改成b,或将视图长度改成2。预测必须写明你改的是哪个对象、哪个位置、哪段长度。
提示与判分要点
长度3且只将首字符改为b时,view是bat、快照是cat。只把视图长度改成2、仍修改第二字符时,view是cu、事前快照是ca。不要把视图长度改为4;原数组只有3项。合格实现必须真的从视图创建拥有副本,而不是手写字符串cat去假装复制成功。
把拥有者的结束点画在最后一次读取后面
view对象可以先结束,拥有者仍继续存在。反过来,拥有者先结束就不能再读借来的元素。下面把span放在内层块,array放在外层;顺序是创建owner、创建view、修改与读取、结束view、再次读取owner、重新借用。
#include <array>
#include <iostream>
#include <span>
int main() {
std::array<int, 3> owner{2, 4, 6};
{
std::span<int> view{owner};
view[1] = 9;
std::cout << view[1] << '\n';
}
std::cout << owner[1] << '\n';
const std::span<const int> again{owner};
std::cout << again.size() << ' ' << again[0] << '\n';
return 0;
}
三行是9、9和3 2。view结束没有销毁owner中的整数。若把owner移到函数局部,并把指向它的span作为返回值交给调用者,返回后的span不会使已结束的owner复活。这里只判断生命周期破坏点,不运行失效访问,也不给它编一个“通常输出”。
09.4 失效后重新取得位置
拥有者还在,借用就一定安全吗?不一定。vector对象可以继续存在,但原元素所在存储已经更换。失效(invalidation)说的是保存下来的元素位置不再可用;不能只检查容器变量是否还在作用域中。
确定发生重分配,不猜容量增长倍数
新08已解释size、capacity和reserve。本节使用max_size()询问容器允许的元素数量上限;它不是当前大小、当前容量,也不是当前机器保证能分配出的数量。std::numeric_limits<std::size_t>::max()沿用08.5的模板实参与静态成员读法,将类型填成已学的无符号size_t,取得该类型可表示的最大值。程序先确认旧容量还能加1,再请求严格大于旧capacity的reserve。成功后一定发生重分配;实际分配仍可能失败,本章小输入的执行路径假设分配成功,异常处理留到新16。
一旦重分配,旧元素的指针、引用、迭代器以及旧end都失效。下面只在重分配前读取旧指针;之后直接用新取得的地址给指针变量赋值,没有读取或比较它保存的旧地址。
#include <cstddef>
#include <iostream>
#include <limits>
#include <vector>
int main() {
std::vector<int> values{2, 4, 6};
int* borrowed{&values[0]};
std::cout << *borrowed << '\n';
const std::size_t before{values.capacity()};
if (before >= values.max_size() || before == std::numeric_limits<std::size_t>::max()) {
std::cout << "capacity limit\n";
return 1;
}
values.reserve(before + 1);
// Replace the invalid pointer value without reading or comparing it.
borrowed = &values[0];
std::cout << (values.capacity() > before) << ' ' << values.size() << '\n';
std::cout << *borrowed << ' ' << values[1] << ' ' << values[2] << '\n';
return 0;
}
第一行2来自仍有效的旧访问。reserve成功后,容量大于before且size仍为3,所以第二行1 3;重新取得指针后的第三行是2 4 6。没有输出具体capacity或地址,不依赖“每次翻倍”。若前置容量检查不能通过,程序输出capacity limit并以1结束,不能把它叫作成功演示。
| 当前操作 | 保存的哪些位置需要重新取得 |
|---|---|
| reserve请求大于旧capacity,成功发生重分配 | 所有旧元素指针、引用、迭代器及旧end |
| push_back没有发生重分配 | 旧元素访问保持有效,但旧end失效 |
| vector单位置erase | 擦除点及其后的位置,包括旧end;擦除点之前的元素访问保持 |
没有重分配不等于没有失效。范围for也维护遍历位置,不能一边依赖原范围循环,一边随意改变这个vector的长度。本章的原地校准只修改既有int值,不追加或删除。
erase返回“接下来从哪里继续”
单位置调用values.erase(position)要求position是可读取的元素位置,不能传end。它删除该元素,让后续元素前移,并返回删除之后的后继位置;若删除最后一项,返回新的end。保存这个返回的新位置,才能继续判断和读取;不要继续递增旧的失效迭代器。
#include <iostream>
#include <vector>
int main() {
std::vector<int> values{3, 2, 1, 4, 1, 5};
auto position = values.begin();
while (position != values.end()) {
if (*position % 2 == 0) {
position = values.erase(position);
} else {
++position;
}
}
std::cout << values.size() << '\n';
for (const auto& value : values) {
std::cout << value << '\n';
}
return 0;
}
输入[3,2,1,4,1,5]。保留3时前进;删除2后,返回的位置现在对应原来紧随其后的1,必须检查它,不能再无条件前进一次。接着保留1、删除4、保留1和5。最后先输出长度4,再逐行输出3、1、1、5。每次循环都重新比较当前end。
改变存储或位置后,重新取得可用访问路径
vector-reallocation先拥有2、4、6,borrowed指向首项并合法输出2。把当前容量记作C,不假定C等于3。
实际先通过C小于max_size且C+1可表示的检查。reserve(C+1)成功后容量大于C,size仍为3;这次必然重分配,原元素访问路径失效。
borrowed = &values[0]重新取得当前首项地址;赋值不读取或比较旧指针值。随后输出容量比较为1、size为3及当前2、4、6。
erase-continue最初为[3,2,1,4,1,5]。首项3不删除;位置前进到下标1,当前值2是偶数。
删除2后序列为[3,1,4,1,5],返回的当前位置是下标1的1。检查这个1后再前进,下一项才是4。
删除4后为[3,1,1,5],返回下标2的1;继续检查1与5,没有跳过紧随删除位置的元素。
最后position等于当前end,不解引用。输出size为4,然后逐项输出3、1、1、5;没有保存或使用旧end跨越erase。
查看所有步骤的文字与数值
- 1 · 重分配前的借用有效
元素:2 | 4 | 6;borrowed:首项2;容量:C ≥ 3
vector-reallocation先拥有2、4、6,borrowed指向首项并合法输出2。把当前容量记作C,不假定C等于3。
- 2 · reserve请求超过原容量
元素值:2 | 4 | 6;当前容量:大于C;旧borrowed:不能读取
实际先通过C小于max_size且C+1可表示的检查。reserve(C+1)成功后容量大于C,size仍为3;这次必然重分配,原元素访问路径失效。
- 3 · 覆盖旧指针,再读取当前位置
新borrowed:当前首项2;size:3;元素输出:2 | 4 | 6
borrowed = &values[0]重新取得当前首项地址;赋值不读取或比较旧指针值。随后输出容量比较为1、size为3及当前2、4、6。
- 4 · 切换到独立删除例子
当前序列:3,2,1,4,1,5;position:1 → 2;当前判断:删除2
erase-continue最初为[3,2,1,4,1,5]。首项3不删除;位置前进到下标1,当前值2是偶数。
- 5 · 接住erase返回值,不再额外加一
当前序列:3,1,4,1,5;返回位置:1 → 1;下一次删除:下标2的4
删除2后序列为[3,1,4,1,5],返回的当前位置是下标1的1。检查这个1后再前进,下一项才是4。
- 6 · 第二次删除,继续检查补上来的元素
当前序列:3 | 1 | 1 | 5;返回位置:2 → 1;仍待检查:1,然后5
删除4后为[3,1,1,5],返回下标2的1;继续检查1与5,没有跳过紧随删除位置的元素。
- 7 · 到达当前end后停止
当前end:尾后,停止;size:4;最后元素:3 | 1 | 1 | 5
最后position等于当前end,不解引用。输出size为4,然后逐项输出3、1、1、5;没有保存或使用旧end跨越erase。
边界练习:空输入、全删、相邻两个偶数
分别把输入改为空、[2,4]、[3,2,4,5]。应得到长度0、长度0、长度2且元素3和5。空输入不能先解引用;全删时erase返回的end让循环停止;相邻偶数不能因多递增一次而漏删。每次修改后重新构建,不能仅凭原例跑通说循环正确。
下标是位置,业务ID才是你选择的身份
本例的Record沿用07.2简单聚合初始化:两个公开int成员按声明顺序接收id、value。vector最外层列出三个Record,每个内层花括号初始化一个Record,不需要在这里实现新的构造函数。
#include <cstddef>
#include <iostream>
#include <vector>
struct Record {
int id;
int value;
};
int main() {
std::vector<Record> records{{101, 3}, {202, 7}, {303, 9}};
const std::size_t saved_position{1};
const int wanted_id{202};
std::cout << records[saved_position].id << '\n';
records.erase(records.begin());
std::cout << records[saved_position].id << ' ' << records[0].id << '\n';
for (const auto& record : records) {
if (record.id == wanted_id) {
std::cout << record.id << ' ' << record.value << '\n';
}
}
return 0;
}
开始下标1是ID202。删除第一条后,原ID202来到下标0,原ID303来到下标1,所以第二行是303 202。再按wanted_id逐项查找,第三行是202 7。下标1依然在范围内,却已经指向另一条逻辑记录;单纯“不越界”不足以满足业务要求。此处用小范围线性查找,不提前引入哈希表。
字符没换位置,与字符长度发生变化分开处理
std::string_view view{owner};从仍存活的string取得当前字符起点和长度,不复制字符。下面先用这个构造让view借用string已有的三个字符。只改owner[1],view观察到cut。准备追加之前先结束旧view,再追加感叹号并创建新view,最后得到cut! 4。新视图的长度来自当前string;旧视图不会自动跟随拥有者变长。
#include <iostream>
#include <string>
#include <string_view>
int main() {
std::string owner{"cat"};
{
const std::string_view view{owner};
std::cout << view << '\n';
owner[1] = 'u';
std::cout << view << '\n';
}
owner += "!";
const std::string_view current{owner};
std::cout << current << ' ' << current.size() << '\n';
return 0;
}
不要猜短字符串这次追加会不会更换存储。这里先结束借用、再改结构、再取得新视图,避开依赖旧位置的需要。原来的只改字符实验则刻意保持长度和存储关系,清楚展示“借用看到当前值”。
完成本章后,可以完整阅读安全求和综合示例:数值检查归08.5,直接临时array的范围for归09.2,成功输出sum=12 overflow=rejected。标准算法的综合阅读安排在第10章。接下来做独立M1,使用自己的预测、代码和口述证据判断是否继续;看过本章参考实现不等于已经通过M1。
关掉参考,再做一次
把理解说出来
先完成正文的独立迁移,再回答下面六题。它们只检验第09.1–09.4节已经讲过的内容。打开答案、编译成功或填写用时,都不会自动通过本章,更不代表通过 G0。
范围与尾后
同一三项数组中last=first+3时,last-first和first-last各是什么类型和结果?为什么不能读取*last?空vector的begin/end怎样控制循环?
对照推理与英文回答
可表示的同数组距离用有符号ptrdiff_t表达,分别为3和-3;last是尾后边界,不是第四个元素,不能解引用。空vector的begin等于end,循环体零次。这里不能推广到不同数组的指针相减,也不能把所有迭代器都当成支持随机偏移的指针。
A representable difference within this array has type ptrdiff_t: three in the forward direction and minus three in reverse. The one-past pointer is a boundary, not an element to dereference. An empty vector has equal begin and end iterators, so the loop performs no reads. Random-access operations are not available on every iterator type.区间构造与复制
从[3,1,4,1,5]的begin()+1到begin()+4构造vector得到什么?first等于last时呢?修改新vector的首项会改原输入吗?
对照推理与英文回答
半开区间包含下标1、2、3,得到[1,4,1],长度3;first等于last形成合法空范围,新vector长度0。这个vector拥有区间元素的独立副本,首项加10变成11时,原序列下标1仍为1。first/last必须描述合法、可达且来自相应范围的端点,不能倒置或拼接两个不同容器的端点。
The half-open range copies indices one, two and three, producing [1, 4, 1]. Equal valid endpoints describe an empty range. The new vector owns independent elements, so changing its first element to eleven leaves the source element at one. The endpoints must form a valid reachable range.范围for找错
对[3,1,4,1,5],auto reading加2和auto& reading加2会让原数组总和分别变成多少?const auto&适合怎样的读取?
对照推理与英文回答
auto reading每轮建立int副本;局部打印虽依次是5、3、6、3、7,原数组总和仍14。auto&直接借用当前元素,原数组改为[5,3,6,3,7],后续重新读取原数组的和为24。const auto&只读借用元素,不能经该名称改写;不要只观察循环局部值就声称完成原地更新。
With auto, each iteration changes a separate integer, leaving the original sum at fourteen. With auto&, the original elements change and their new sum is twenty-four. const auto& provides read-only access to each element. Verify the owner after the loop instead of treating printed local copies as proof of an in-place update.span长度与const层次
const span<int>与span<const int>各限制哪一层?指针加长度建立中间两项的视图后,修改会落在哪里?空视图和按值传span会复制元素吗?
对照推理与英文回答
const span<int>限制视图对象自身,仍可经它修改可写int元素;span<const int>限制元素访问,不能经它给元素赋值。values+1和长度2借用原数组下标1、2,写view[0]就是改原下标1。空动态span长度0,不访问第0项;按值传span复制的是访问范围,不是元素,也不延长拥有者寿命。
A const span<int> makes the view object const while still permitting writes to mutable elements. span<const int> provides read-only element access. A pointer plus a count borrows the specified existing elements; it does not create them. Passing a span by value copies the view, not the elements, and does not extend the owner’s lifetime.字符借用与快照
c/a/t三字符数组没有终止零,怎样合法建立并输出string_view?事前建立string快照后把a改为u,二者各输出什么?data()能自动补一个终止零吗?
对照推理与英文回答
按指针和明确长度3构造view;输出view遵守其长度,不依赖数组后面的零。修改后view为cut,事前string快照仍为cat,长度均3。data()只提供起点,不补零也不附带长度;不能把该结果直接当零终止C字符串输出。拥有者结束或范围失效后不能继续读view,保存view对象本身不保活字符。
Construct the view with the character pointer and the explicit count of three, then output the view by its length. After the mutation the view reads cut while the earlier owning snapshot remains cat. data() neither adds a terminator nor carries the length. Keeping the view does not keep its characters alive or valid.失效与身份追问
成功reserve(capacity()+1)后还能比较或读取旧元素指针吗?erase循环为什么接住返回值?删除首记录后,下标1还合法就说明它仍是ID202吗?
对照推理与英文回答
先检查+1可表示且请求不超过max_size;成功请求超过原容量会重分配,旧元素指针/引用/迭代器不再可用。本章不读取或比较旧指针,而重新从当前容器取得位置。erase使被删位置及其后的旧迭代器失效,返回下一元素的有效位置或当前end,接住它避免跳项。示例删除ID101后,下标1仍合法但指向ID303;ID202在下标0,按唯一且保持不变的ID重新查找才确认身份。
After a successful reserve request beyond the old capacity, reacquire element access from the current vector instead of using old handles. erase returns a valid position for continuing, possibly the new end; use that return value to avoid skipping elements. A valid index identifies a position, not a persistent record identity. In this example ID 202 moves to index zero, while index one now holds ID 303.和正文是同一份源码
示例文件
先自己输入和预测,卡住时再下载对照。文件名相同不代表内容相同;把它们放在单独的练习目录中,避免覆盖自己的作品。
- pointer-range.cpp同数组移动、相减与尾后位置
- iterator-range.cpp从begin读到end之前,空范围零次
- range-construction.cpp半开区间建立独立拥有副本
- range-bindings.cpp普通for、元素副本与可写引用
- traversal-trace.cpp完整例一:三种遍历跟踪同一组读数
- span-basics.cpp视图的长度、元素const与空视图
- span-window.cpp指针加长度只借用中间两项
- span-total.cpp补全借用求和:14、24、14与空组0
- calibration-value.cpp可运行的逻辑错误:只改每轮副本
- calibration-reference.cpp修复访问方式,核对原数组为24
- string-view-snapshot.cpp完整例二:没有终止零的cat与拥有快照
- view-lifetime.cpp视图先离块,外层拥有者继续存活
- vector-reallocation.cpp强制重分配后覆盖旧位置再读
- erase-continue.cpp用erase返回的当前位置继续遍历
- position-id.cpp下标仍合法,却已不是原记录
- view-refresh.cpp结束旧文本视图,追加后重新借用
这16份程序均使用正文规定的安全输入;悬空、尾后读取与越界只作静态分析,没有提供诱导运行的危险程序。完整验证记录保留在本地;这里没有调用加速卡。
可选的学习反馈
记下你真正花的时间
每完成一个学习时段,再填实际分钟。环境准备、阅读推演、独立编码和卡点排查分别记录,避免同一段时间重复计算。离开吃饭或做其他事情的时间不算进去。
记录只保存在你的浏览器,可导出给我复盘。留空表示尚未记录,不等于零耗时;页面停留时间不会自动计为学习。不要把开发者检查时间填进来。
尚无真实试学用时。
换设备:导入记录,或取回损坏的旧记录
导入会合并时段,相同编号不重复累加;发生冲突会保留现有记录。
阅读记录与课程验收分别保存。
本章资料与查证
- WG21 N4861:C++20工作草案 ↗
固定2020-04-01草案;09.1查[expr.add]、[iterator.requirements.general]、[vector.cons],09.2查[stmt.ranged],09.3查[span.cons]、[span.elem]、[string.view.cons]、[string.view.io]、[string.cons],09.4查[vector.capacity]、[vector.modifiers]。
- Microsoft C++标准库参考:span ↗
09.3查构造、size、empty与元素访问;正文限定C++20的无检查下标,不把“safer”理解为自动发现悬空或越界。只查所用接口,不要求提前阅读模板定义。
- Microsoft C++标准库参考:basic_string_view ↗
09.3查按指针/长度构造、size、data与非拥有关系;结合固定C++20草案核对按长度输出及string拥有快照。
本章独立解释所需读法;资料用于核对与补充。工具版本、操作系统和实际执行状态见自己的运行记录。