1.“一排盒子”:数组
“一排盒子”:数组 (存储一组相同类型的数据)
本章导读 (The Hook)
到目前为止,我们每需要一个“记忆盒子”(变量),就得为它单独取一个名字。比如,要记录三个学生的成绩,我们可能要写:int score1 = 98;int score2 = 87;int score3 = 92;
如果一个班有 50 个学生呢?难道我们要声明 50 个变量吗?这简直是场灾难!不仅写起来累,管理起来也极其困难。
为了解决这个问题,C++ 提供了一种强大的新“容器”——数组 (Array)。
你可以把数组想象成一整排连在一起的、规格完全相同的储物柜。这排储物柜共享一个总的名字(比如 “三年二班成绩柜”),而每一个小柜子则通过编号(0号柜,1号柜,2号柜...)来区分。
通过数组,我们可以用一个变量名来管理一大批相同类型的数据。这不仅让代码变得整洁,更重要的是,它与我们上一章学的循环简直是天作之合!准备好,我们即将迎来编程能力的一次巨大飞跃。
专业术语速查表 (Glossary)
点击展开/折叠本章术语表
数组 (Array)
- 通俗比喻:一排连号的、大小规格完全相同的盒子,它们共享一个名字。
- 解释:一个由相同数据类型的元素组成的、大小固定的序列。这些元素在内存中是连续存储的。
元素 (Element)
- 通俗比喻:数组这排盒子中,每一个独立的盒子。
- 解释:数组中的单个数据项。
索引 (Index) / 下标 (Subscript)
- 通俗比喻:每个盒子的唯一编号。就像酒店的房间号,用于精确定位到某一个盒子。
- 重要:在 C++ 中,索引永远从 0 开始!
- 解释:一个整数,用于标识数组中特定元素的位置。
声明数组 (Array Declaration)
- 通俗比喻:向系统预订一排指定数量、指定规格的盒子。
- 解释:定义一个数组的类型、名称和大小。语法为:
数据类型 数组名[数组大小];
数组大小 (Array Size)
- 通俗比喻:这一排盒子的总数量。
- 解释:数组可以容纳的元素的最大数量。在 C++ 中,基本数组的大小在创建时就必须确定。
索引越界 (Index Out of Bounds)
- 通俗比喻:一排只有 5 个盒子(编号 0 到 4),你却试图去访问 5 号盒子。你访问到了不属于你的“危险地带”。
- 解释:尝试使用一个无效的索引(小于 0 或大于等于数组大小)来访问数组元素。这是 C++ 中最常见也最危险的错误之一。
核心概念讲解 (The Core Concept)
1. 数组的声明与初始化
声明一个数组:告诉编译器我们需要多大的空间。正确的语法是
数据类型 数组名[大小];。// 正确的声明方式 int scores[5]; // 声明了一个可以存放 5 个 int 类型元素的数组,名为 scores // 执行这行代码后,内存中就开辟了 5 个连续的、可以存放整数的“盒子”。 // 它们的索引分别是 0, 1, 2, 3, 4。初始化数组:在声明的同时,给这排盒子填入初始值。
#include <iostream> using namespace std; int main() { // 方法一:完整初始化 // 提供了 5 个值,分别放入索引为 0, 1, 2, 3, 4 的盒子 int scores = {98, 87, 92, 79, 85}; // 方法二:部分初始化 // 只提供了 3 个值,它们会依次放入前 3 个盒子 (索引 0, 1, 2) // 剩余的盒子会自动被初始化为 0 int ages = {20, 21, 19}; // 结果是 {20, 21, 19, 0, 0} // 方法三:自动推断大小 // 如果初始化时提供了所有值,可以省略方括号中的大小 // 编译器会自动计算出大小为 4 double prices[] = {9.9, 15.0, 3.25, 5.0}; return 0; }
2. 访问和修改数组元素
我们通过 数组名[索引] 的方式来精确地操作某一个元素。
#include <iostream>
using namespace std;
int main() {
int scores = {98, 87, 92, 79, 85};
// --- 访问元素 (读取) ---
// 记住!第一个元素的索引是 0!
cout << "第一个学生的分数是: " << scores << endl; // 输出 98
cout << "第三个学生的分数是: " << scores << endl; // 输出 92
// --- 修改元素 (写入) ---
cout << "第四个学生的分数原来是: " << scores << endl; // 输出 79
scores = 81; // 把索引为 3 的盒子里的值,从 79 修改为 81
cout << "第四个学生修改后的分数是: " << scores << endl; // 输出 81
return 0;
}3. 数组与 for 循环的完美配合
单独使用数组的威力有限,一旦和 for 循环结合,我们就能轻松地对整排数据进行批量操作。这被称为“遍历 (Traverse)”数组。
- 代码示例:计算全班平均分
#include <iostream> using namespace std; int main() { const int STUDENT_COUNT = 5; // 使用一个常量来表示数组大小,是好习惯! int scores[STUDENT_COUNT] = {98, 87, 92, 79, 85}; int sum = 0; // 用于累加总分 // 1. 使用 for 循环遍历数组,计算总分 // 循环变量 i 恰好可以作为数组的索引! for (int i = 0; i < STUDENT_COUNT; i++) { cout << "正在处理第 " << i+1 << " 个学生的分数: " << scores[i] << endl; sum += scores[i]; // 将当前元素的分数累加到 sum 中 } // 2. 计算平均分 double average = (double)sum / STUDENT_COUNT; // 注意!需要将 sum 转为 double 来进行精确除法 cout << "------------------------" << endl; cout << "总分是: " << sum << endl; cout << "平均分是: " << average << endl; return 0; }
动手试试 (Try It Yourself!)
寻找最大值
- 创建一个
int数组并初始化几个数字,例如int numbers[] = {22, 5, 67, 98, 1, 43};。 - 声明一个变量
maxNumber,并把它初始化为数组的第一个元素 (numbers[0])。 - 使用
for循环从第二个元素开始遍历数组 (i=1)。 - 在循环中,比较当前元素
numbers[i]和maxNumber,如果当前元素更大,就更新maxNumber的值。 - 循环结束后,打印出找到的最大值。
- 创建一个
数组反转
- 提示用户输入 5 个整数,并用
for循环将它们存入一个大小为 5 的数组中。 - 使用另一个
for循环,从后往前遍历这个数组(想一想,循环变量i应该如何初始化和更新?),并打印出每一个元素。 - 这样,你就能看到一个与输入顺序相反的输出序列。
- 提示用户输入 5 个整数,并用
“寻找最大值”参考答案 (点击展开)
#include <iostream>
using namespace std;
int main() {
// 1. 创建并初始化数组
int numbers[] = {22, 5, 67, 98, 1, 43};
// C++11 之后,我们可以用更现代的方式获取数组大小
// 但为保持入门教程的简洁性,我们先手动计算大小为 6
int size = 6;
// 2. 检查数组是否为空,这是一个好习惯
if (size == 0) {
cout << "数组是空的!" << endl;
return 0;
}
// 3. 假设第一个元素就是最大的
int maxNumber = numbers;
cout << "开始寻找最大值... 初始最大值暂定为: " << maxNumber << endl;
// 4. 从第二个元素 (索引为 1) 开始循环比较
for (int i = 1; i < size; i++) {
cout << "正在比较: 当前最大值 " << maxNumber << " 和数组元素 " << numbers[i] << endl;
// 如果发现一个比当前 maxNumber更大的数
if (numbers[i] > maxNumber) {
// 就更新 maxNumber 的值
maxNumber = numbers[i];
cout << "发现一个更大的值!更新最大值为: " << maxNumber << endl;
}
}
// 5. 循环结束后, maxNumber 中存放的就是整个数组的最大值
cout << "------------------------" << endl;
cout << "遍历完成,最终的最大值是: " << maxNumber << endl;
return 0;
}“防坑”指南与常见错误 (The Pitfalls)
C++ 头号杀手:数组索引越界
这是所有 C++ 新手都会犯,且后果最严重的错误。
int scores[5]; 这个数组,它合法的索引范围是 0, 1, 2, 3, 4。
如果你试图访问 scores[5] 或者 scores[-1],你就“越界”了。
- 为什么危险? C++ 为了追求极致的性能,它不会在运行时自动检查你的索引是否越界。你就像一个被蒙上眼睛在悬崖边跑步的人。
- 后果是什么?
- 幸运的话:程序立刻崩溃。
- 不幸的话:程序不崩溃,但你读取到了内存中的一块随机垃圾数据,或者更糟,你修改了一块本不该你碰的内存(比如另一个重要变量的值),导致程序在未来的某个时刻以一种完全无法理解的方式出错。这种 Bug 极难调试!
黄金法则:在使用 数组名[i] 时,你的脑中必须时刻有一根弦:i 的值必须永远在 0 到 大小-1 的范围内! for 循环的条件 i < size 就是保证这一点的最佳实践。
陷阱二:未初始化的数组里装着“垃圾”
和普通变量一样,如果你只声明了一个数组但没有初始化它,里面的每个元素都将是未定义的“垃圾值”。
int data[];
// 此时,data, data... 里面的值完全是随机的!
// 直接使用它们会导致无法预测的结果。
cout << data << endl; // 可能会打印出 -858993460 这样的奇怪数字最佳实践:如果你想确保数组的所有元素都从一个干净的状态开始(比如0),请使用初始化语法:int data[5] = {0}; // 这是一个常用技巧,它会将第一个元素初始化为0,其余元素自动补0
陷阱三:数组大小必须是常量
在声明一个基本数组时,方括号 [] 中的大小必须是一个在编译时就能确定的常量值。
// 正确的
const int SIZE = 10;
int arr1[SIZE];
int arr2; // 字面量也是常量
// 错误的 (虽然某些编译器作为扩展支持,但不是标准C++)
int size_var = 10;
int arr3[size_var]; // 错误!size_var 是一个变量如果你需要在程序运行时动态决定数组的大小,未来我们会学习更高级的工具,比如 std::vector。
