Lesson 9:数组
用容量、有效长度和下标不变量审计数组、多维映射与字符串边界。
学习目标
- 能解释数组类型、元素类型、长度、连续布局与合法下标之间的关系
- 能实现一维与二维数组遍历,证明每次访问都位于对象边界内
- 能分析数组传参时的指针退化与长度丢失,并选择显式长度、容器或视图修复接口
来源、版次与标准边界
“Lesson 9:数组”以 SB Creative 出版社书页核定高桥麻奈著、第 5 版、2017 年 6 月 14 日发行、ISBN 978-4-7973-9259-3;出版社页面标 596 页。CiNii Books 书目与 16 课目录记录正文 xxiii+571 页,日本国会图书馆书目记录 571 页。页数口径存在前置页差异,因此本课程不拿页码充当目录证据。
“Lesson 9:数组”的中文说明、示例、交互、练习和答案均为独立教学重写。公开资料只确认 16 个 Lesson 标题;本页列出的细分概念是课程教学映射,不冒充原书逐级小节。技术规则参考 ISO C++ 标准入口复核,但 2017 年教材的 Visual Studio 2017 语境不会被静默升级为 C++23;现代写法只能明确标为迁移说明。
围绕“怎样证明每次下标访问都落在当前有效区间,而不是只落在分配容量内?”,本页要求保留“数组容量、有效长度、每次下标、多维映射、终止字符位置和地址检测结果。”。若故障“循环条件使用小于等于长度,访问末尾后一项”无法在同一输入下制造首个分岔,应拒绝当前解释,而不是追加随机样例。
为什么数组必须同时知道长度
数组把固定数量、相同类型的元素组织成一个对象。数组初始化负责为这些元素建立首值。数组名常在表达式中转换为首元素指针,但数组对象本身仍包含完整长度;一旦接口只接收裸指针,这个长度信息就不再由类型自动携带。
↡由固定数量、同一元素类型的对象连续组成的聚合对象。 ↡数组中的单个对象,具有自己的值与生命周期,但元素类型相同。int scores[5]{80, 92, 76, 88, 95};这里数组类型可理解为“5 个 int 的数组”,合法下标是 0、1、2、3、4。长度是 5,但最后下标是 4;二者差一正是最常见边界错误来源。
下标把偏移映射到元素
scores[i] 访问从首元素起第 i 个位置,语义上与 *(scores + i) 相关。指针加一不是增加一个字节,而是移动一个元素大小;编译器依据元素类型缩放地址。
for (int i = 0; i < 5; ++i) {
std::cout << scores[i] << '\n';
}循环条件 i < 5 与合法半开区间 [0,5) 完全对齐。若写 i <= 5,最后一轮访问 scores[5],它指向尾后位置,不是数组元素;形成地址并不授权解引用。
初始化决定未显式列出元素的状态
带花括号初始化时,未提供的剩余元素会值初始化;内置整型因此为 0。没有任何初始化器的普通局部内置数组,其元素值不确定,不应读取。
int counts[4]{1, 2}; // {1, 2, 0, 0}
int zeros[4]{}; // {0, 0, 0, 0}也可以让编译器从初始化器推导长度:int primes[]{2,3,5,7};。无论哪种形式,都应从数组本身计算长度而不是在遍历中重复魔法数字。
constexpr int count = sizeof(scores) / sizeof(scores[0]);此公式只在当前作用域仍持有真实数组类型时有效;一旦参数已退化为指针,sizeof(pointer) 只得到指针对象大小。现代 C++ 可用 std::size(scores) 更直接表达。
多维数组是数组的数组
int grid[2][3] 表示 2 个元素的外层数组,每个外层元素又是 3 个 int 的数组。常用第一个下标选行,第二个选列;合法行是 [0,2),合法列是 [0,3)。
int grid[2][3]{
{1, 2, 3},
{4, 5, 6},
};
for (int row = 0; row < 2; ++row) {
for (int column = 0; column < 3; ++column) {
std::cout << grid[row][column] << ' ';
}
}在常见行主序布局中,同一行元素连续,然后进入下一行。总访问次数是 2×3=6;两层边界分别证明行和列有效,不能只检查扁平总数。
数组名转换为首元素指针时会丢长度
理解数组与指针的关系时,必须同时记住转换与长度丢失。在多数表达式和函数参数中,数组名转换为指向首元素的指针。下面两个参数声明在函数类型层面等价,函数体都不知道调用者实际数组长度。
↡数组在多数表达式或参数声明中转换为首元素指针,原数组长度不随裸指针一起传递的规则。void printScores(const int values[], int count);
void printScores(const int* values, int count);因此指针与长度必须保持一致。若传 5 个元素却写 count=8,函数会越界;写 count=3 则漏处理。更稳妥的接口可使用 std::array<int,5>、std::vector<int> 或 std::span<const int>,让数据与范围一起传递。
三种设施的所有权不同:std::array<T,N> 把固定数量元素直接作为自身成员,长度进入类型;std::vector<T> 拥有可动态调整的连续元素,长度是运行时状态;std::span<T> 只借用一段现有连续范围,保存指针与长度但不延长目标生命周期。选择接口时要同时回答“长度由谁记录”和“存储由谁拥有”。例如返回指向局部数组的 span 仍会悬空,携带长度并不能修复生命周期结束。
范围 for 能直接遍历真实数组或容器,避免手写下标,但若任务需要元素位置、相邻访问或分片边界,仍要显式维护索引并证明它合法。语法更短不会取消范围契约。
指针遍历与下标遍历共享同一边界
可以用首指针和尾后指针遍历数组。关键不在语法风格,而在始终保持 [begin,end) 范围,并且只在 current != end 时解引用。
int* begin = scores;
int* end = scores + 5;
for (int* current = begin; current != end; ++current) {
std::cout << *current << '\n';
}指针算术只在同一数组对象及其尾后位置内有定义。不能把两个无关数组的指针相减或按地址大小推断逻辑顺序。
字符数组与字符串要区分终止规则
C 风格字符串使用字符数组,并以空字符 \0 标记文本结尾。数组容量必须容纳文本字符和终止符;缺失终止符时,按 C 字符串读取会继续越过数组寻找 0。
char word[]{'C', '+', '+', '\0'};
std::string safer = "C++";std::string 负责管理存储和长度,通常比手工字符数组安全。学习字符数组仍有价值,因为文件格式、C API 和底层缓冲区经常暴露指针与容量契约。
先预测边界再运行
对长度 5 的数组先预测访问 -1、0、4、5,只运行合法的 0 与 4;越界项可由启用 AddressSanitizer 的独立练习程序观察诊断,不能在正常流程中把未定义行为当作可恢复错误。
三步验证数组访问
第一步:画布局与合法范围
标出数组长度、元素大小、首地址、每个下标及尾后位置,写出半开区间;不要把尾后地址画成元素。
正式节点与章专属证据
- 数组:在“Lesson 9:数组”中核对输入、状态变化、失败模式和可复现证据;第 1 个节点必须能回到“访问下标严格小于有效长度,多维索引映射与字符串终止规则保持一致。”。
- 数组初始化:在“Lesson 9:数组”中核对输入、状态变化、失败模式和可复现证据;第 2 个节点必须能回到“访问下标严格小于有效长度,多维索引映射与字符串终止规则保持一致。”。
- 多维数组:在“Lesson 9:数组”中核对输入、状态变化、失败模式和可复现证据;第 3 个节点必须能回到“访问下标严格小于有效长度,多维索引映射与字符串终止规则保持一致。”。
- 数组与指针:在“Lesson 9:数组”中核对输入、状态变化、失败模式和可复现证据;第 4 个节点必须能回到“访问下标严格小于有效长度,多维索引映射与字符串终止规则保持一致。”。
- 字符串:在“Lesson 9:数组”中核对输入、状态变化、失败模式和可复现证据;第 5 个节点必须能回到“访问下标严格小于有效长度,多维索引映射与字符串终止规则保持一致。”。
先用输入合同检查本页正式节点,再在相同初值下逐步比较正常和失败轨迹,最后只启用“循环条件使用小于等于长度,访问末尾后一项”完成反例与复位。三个交互都必须能独立重置,且重置后再次满足“访问下标严格小于有效长度,多维索引映射与字符串终止规则保持一致。”。
输入与状态合同
Lesson 9:数组
怎样证明每次下标访问都落在当前有效区间,而不是只落在分配容量内?
必须先声明
为数组声明输入类型、有效范围、对象生命周期和失败策略。
可复核证据
保存Lesson 9:数组的原始输入、初值与第一条可检查诊断。
正式节点:数组、数组初始化、多维数组、数组与指针、字符串
编译与运行轨迹
同一输入下比较正常与失败路径
- 01声明容量和有效长度
- 02生成合法下标
- 03完成元素访问
- 04核对最后一个合法位置
不变量:访问下标严格小于有效长度,多维索引映射与字符串终止规则保持一致。
故障定位与复位
一次只破坏一个前提
小结
- 数组是固定数量同类型元素组成的对象,长度 n 对应合法下标
[0,n) - 下标访问与指针偏移相关,尾后指针可以作为终点但不能解引用
- 多维数组是数组的数组,各维边界分别验证,总次数是各维规模组合
- 数组退化为首元素指针后不携带长度,接口必须同时传范围或改用容器/span
- C 风格字符串依赖
\0,std::string 自行管理长度;二者容量与终止契约不同
练习
- 问题 1:判断边界。 长度 5 的数组为何最后下标是 4,
array + 5又能做什么?
- 问题 2:解释退化。 函数形参
int values[5]为什么仍不知道调用数组长度?
- 问题 3:验证二维遍历。
int grid[2][3]的合法行列与总访问次数是什么?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 数组
- 固定数量、同一类型元素连续组成的聚合对象。
- 元素
- 数组内具有独立值与生命周期的单个对象。
- 下标
- 从零开始选择数组元素的偏移。
- 多维数组
- 元素仍是数组的嵌套数组类型。
- 数组退化
- 数组在多数表达式中转换为首元素指针并丢失长度的规则。
- 字符串
- 字符序列表示的文本,C 风格与 std::string 具有不同长度契约。