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 的独立练习程序观察诊断,不能在正常流程中把未定义行为当作可恢复错误。

三步验证数组访问

分步1 / 3

第一步:画布局与合法范围

标出数组长度、元素大小、首地址、每个下标及尾后位置,写出半开区间;不要把尾后地址画成元素。

正式节点与章专属证据

  • 数组:在“Lesson 9:数组”中核对输入、状态变化、失败模式和可复现证据;第 1 个节点必须能回到“访问下标严格小于有效长度,多维索引映射与字符串终止规则保持一致。”。
  • 数组初始化:在“Lesson 9:数组”中核对输入、状态变化、失败模式和可复现证据;第 2 个节点必须能回到“访问下标严格小于有效长度,多维索引映射与字符串终止规则保持一致。”。
  • 多维数组:在“Lesson 9:数组”中核对输入、状态变化、失败模式和可复现证据;第 3 个节点必须能回到“访问下标严格小于有效长度,多维索引映射与字符串终止规则保持一致。”。
  • 数组与指针:在“Lesson 9:数组”中核对输入、状态变化、失败模式和可复现证据;第 4 个节点必须能回到“访问下标严格小于有效长度,多维索引映射与字符串终止规则保持一致。”。
  • 字符串:在“Lesson 9:数组”中核对输入、状态变化、失败模式和可复现证据;第 5 个节点必须能回到“访问下标严格小于有效长度,多维索引映射与字符串终止规则保持一致。”。

先用输入合同检查本页正式节点,再在相同初值下逐步比较正常和失败轨迹,最后只启用“循环条件使用小于等于长度,访问末尾后一项”完成反例与复位。三个交互都必须能独立重置,且重置后再次满足“访问下标严格小于有效长度,多维索引映射与字符串终止规则保持一致。”。

输入与状态合同

Lesson 9:数组

怎样证明每次下标访问都落在当前有效区间,而不是只落在分配容量内?

必须先声明

为数组声明输入类型、有效范围、对象生命周期和失败策略。

可复核证据

保存Lesson 9:数组的原始输入、初值与第一条可检查诊断。

正式节点:数组、数组初始化、多维数组、数组与指针、字符串

编译与运行轨迹

同一输入下比较正常与失败路径

  1. 01声明容量和有效长度
  2. 02生成合法下标
  3. 03完成元素访问
  4. 04核对最后一个合法位置

不变量:访问下标严格小于有效长度,多维索引映射与字符串终止规则保持一致。

故障定位与复位

一次只破坏一个前提

基线可继续:访问下标严格小于有效长度,多维索引映射与字符串终止规则保持一致。

小结

  • 数组是固定数量同类型元素组成的对象,长度 n 对应合法下标 [0,n)
  • 下标访问与指针偏移相关,尾后指针可以作为终点但不能解引用
  • 多维数组是数组的数组,各维边界分别验证,总次数是各维规模组合
  • 数组退化为首元素指针后不携带长度,接口必须同时传范围或改用容器/span
  • C 风格字符串依赖 \0,std::string 自行管理长度;二者容量与终止契约不同

练习

  1. 问题 1:判断边界。 长度 5 的数组为何最后下标是 4,array + 5 又能做什么?
  1. 问题 2:解释退化。 函数形参 int values[5] 为什么仍不知道调用数组长度?
  1. 问题 3:验证二维遍历。 int grid[2][3] 的合法行列与总访问次数是什么?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

数组
固定数量、同一类型元素连续组成的聚合对象。
元素
数组内具有独立值与生命周期的单个对象。
下标
从零开始选择数组元素的偏移。
多维数组
元素仍是数组的嵌套数组类型。
数组退化
数组在多数表达式中转换为首元素指针并丢失长度的规则。
字符串
字符序列表示的文本,C 风格与 std::string 具有不同长度契约。

资料与写作方式声明

本章以高桥麻奈 2017 年 C++ 入门教材第 5 版权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…