C1-08 数组
数组声明、下标、循环读入与基本遍历
学习目标:理解数组是“装着一排同类型数据的盒子”;掌握一维数组的声明、初始化和元素访问;理解下标从
0开始;能够用for循环完成数组的读入、输出、求和、求最大值;养成先看数据范围、再检查下标边界的编程习惯。
一、为什么需要数组
1.只用变量会遇到什么问题
前面我们学习了变量和循环。比如,读入 n 个数并求和,可以边读边加:
int n;
cin >> n;
int sum = 0;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
sum += x;
}
cout << sum << endl;
这个程序可以求和,但是每个数读进来以后就被处理掉了。如果题目要求:
- 先读入一组数,再从后往前输出;
- 先求平均值,再统计大于平均值的数;
- 读入以后多次查询某个位置的数;
就必须把这些数保存下来。
如果要保存 5 个数,可以定义:
int a1, a2, a3, a4, a5;
如果要保存 100 个数,就要定义 100 个变量。这样的写法既长又容易出错。
2.数组是什么
数组是一组相同类型数据的集合。可以把它想象成一排连续的抽屉:
┌──────┬──────┬──────┬──────┬──────┐
│ a[0] │ a[1] │ a[2] │ a[3] │ a[4] │
└──────┴──────┴──────┴──────┴──────┘
- 每个抽屉只能放一种类型的数据;
- 每个抽屉都有一个编号,这个编号叫下标;
- 一个数组名可以管理很多个数据;
- 通过“数组名 + 下标”可以找到其中某一个元素。
第 7 课解决的是“读入一个、处理一个”;本课学习“先把数据存起来,再反复使用”。
3.本课学习范围
本课只学习一维数组:
- 数组的声明;
- 数组的下标;
- 数组的初始化;
- 用
for循环读入和输出; - 遍历数组求和、求最大值;
- 数组逆序输出。
二维数组、字符串数组和更复杂的数据结构放在后续课程学习。
二、一维数组的声明
1.基本语法
类型 数组名[大小];
例如:
int a[5]; // 能存 5 个 int
double score[10]; // 能存 10 个 double
char letters[26]; // 能存 26 个 char
数组声明有三个要素:
| 要素 | 作用 | 示例 |
|---|---|---|
| 类型 | 决定每个元素保存什么数据 | int、double、char |
| 数组名 | 给这一排数据起一个名字 | a、score |
| 大小 | 表示最多能放多少个元素 | 5、100 |
2.数组大小要提前准备
定义数组时,先根据题目给出的最大数据个数,提前准备好一块足够大的空间。最直接的写法,是把具体数字写在方括号中:
int a[105];
如果题目保证 n 不超过 100,可以多开 5 个位置,写成 int a[105]。多出的几个位置可以看作“保险空间”,但实际使用时仍然只使用前 n 个元素。
题目如果说:
n 不超过 100
就可以准备:
int a[105]; // 100 个数据 + 5 个保险位置
当程序中有多个长度相同的数组时,为了以后修改大小更方便,可以把数组长度写成一个常量:
const int N = 1005;
int a[N], b[N], c[N];
以后如果题目范围发生变化,只需要修改 N 这一行,三个数组会一起改变大小。
现在的 C++ 标准已经支持把运行时读入的 n 用作数组长度,例如:
int n;
cin >> n;
int a[n];
不过,这种写法会让数组大小依赖运行时数据,不适合作为信奥入门阶段的固定写法。为了代码更规范、更容易迁移,仍然建议提前准备常量大小的数组:
const int N = 1005;
int a[N];
无论采用哪种声明方式,都必须保证实际使用的元素个数不超过数组容量。
3.数组不是“所有位置都必须使用”
int a[105];
这表示数组最多可以存 105 个整数。如果题目实际输入 n = 5,我们只使用:
a[0], a[1], a[2], a[3], a[4]
剩下的位置暂时不用。
三、数组下标:从 0 开始
1.下标和第几个元素
如果定义:
int a[5];
数组中有 5 个元素,但下标不是 1 到 5,而是:
a[0] a[1] a[2] a[3] a[4]
第1个 第2个 第3个 第4个 第5个
数组下标从 0 开始。
大小为 N 的数组,有效下标范围是:
0 到 N - 1
所以:
- 第 1 个元素是
a[0]; - 第 2 个元素是
a[1]; - 第
n个元素是a[n - 1]; - 最后一个元素是
a[N - 1]。
2.访问数组元素
数组元素的访问格式是:
数组名[下标]
示例:
int a[5];
a[0] = 10; // 给第 1 个元素赋值
a[1] = 20; // 给第 2 个元素赋值
a[4] = 50; // 给第 5 个元素赋值
cout << a[0] << endl; // 输出 10
cout << a[4] << endl; // 输出 50
数组元素和普通变量一样,可以出现在赋值、输入、输出和计算中:
a[2] = a[0] + a[1];
cin >> a[3];
cout << a[3] << endl;
3.数组越界
对于 int a[5],有效下标只有 0、1、2、3、4。
下面的写法是错误的:
a[5] = 100; // 越界,数组没有第 6 个位置
a[5] 并不是“第 5 个元素”,而是下标为 5 的元素,也就是第 6 个位置。C++ 通常不会自动提醒你越界,程序可能出现乱码、运行错误或其他不可预测的结果。
重点记忆: 数组大小是
N,最大下标是N - 1,不是N。
4.用表格理解下标
int a[4] = {7, 3, 9, 2};
| 下标 | a[0] |
a[1] |
a[2] |
a[3] |
|---|---|---|---|---|
| 元素值 | 7 | 3 | 9 | 2 |
| 第几个 | 第 1 个 | 第 2 个 | 第 3 个 | 第 4 个 |
四、数组初始化
1.完整初始化
声明数组时,可以直接给元素赋初值:
int a[5] = {10, 20, 30, 40, 50};
对应关系是:
a[0] = 10
a[1] = 20
a[2] = 30
a[3] = 40
a[4] = 50
2.部分初始化
如果只写出一部分初值,前面的元素按顺序赋值,没有写出的元素自动补 0:
int a[5] = {10, 20};
实际结果是:
a[0] = 10
a[1] = 20
a[2] = 0
a[3] = 0
a[4] = 0
这条规则很重要:花括号中没有写出的元素,会自动初始化为 0。
3.全部初始化为 0
常见写法:
int a[100] = {};
空花括号表示“没有指定任何初值”,因此所有元素都会自动初始化为 0。
有时候会看到下面的写法:
int a[100] = {0};
但要注意,int a[100] = {0}; 从语法本质上属于部分初始化:它只明确写出了第一个初值 0,其余没有写出的元素再按照规则自动补 0。因为补出来的也是 0,所以最终效果仍然是整个数组为 0。
4.不初始化的数组
int a[100];
如果这是一个局部数组,里面的值可能是内存中原来留下的内容,直接读取会得到不确定的结果,犹如“开盲盒”,开到什么就是什么:
cout << a[0] << endl; // 不要这样直接读取a[0]并输出,会输出不确定的值
如果数组后面会通过 cin 给每个位置赋值,那么可以不先清零:
int a[100];
for (int i = 0; i < n; i++) {
cin >> a[i];
}
如果数组中有些位置可能暂时不赋值,建议使用:
int a[100] = {};
五、用 for 循环读入数组
数组下标是连续的整数 0、1、2、...,这和 for 循环非常适合配合。
1.从下标 0 开始:读入模板
下面把数组定义、读入和输出放在一起。数组实际使用的位置是 a[0] 到 a[n - 1]:
const int N = 105;
int a[N];
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
cout << endl;
循环变量 i 同时充当数组下标:
第 1 轮:i = 0,读入 a[0]
第 2 轮:i = 1,读入 a[1]
第 3 轮:i = 2,读入 a[2]
……
最后一轮:i = n - 1,读入 a[n - 1]
2.从下标 1 开始:另一种读入模板
也可以把第一个数据放在 a[1] 中,实际使用的位置是 a[1] 到 a[n]:
const int N = 105;
int a[N];
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= n; i++) {
cout << a[i] << " ";
}
cout << endl;
两种写法都可以,关键是让循环起点、循环条件和数组下标保持一致。后面的数组遍历、求和、求最值和逆序输出例子,均以从下标 0 开始为例。
3.完整示例:读入并原样输出
#include <iostream>
using namespace std;
const int N = 105;
int a[N];
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
cout << endl;
return 0;
}
输入:
5
10 20 30 40 50
输出:
10 20 30 40 50
4.为什么条件是 i < n
当 n = 5 时,实际使用的下标是:
0、1、2、3、4
所以循环应写成:
for (int i = 0; i < n; i++)
如果误写成:
for (int i = 0; i <= n; i++)
那么 i 会取到 5,程序会访问 a[5],这就越界了。
在从下标
0开始的写法中,i < n正好访问a[0]到a[n - 1],共n个元素。
4.输入和数组的对应关系
输入:
4
8 6 5 4
读入过程:
循环变量 i |
读入的数据 | 保存位置 |
|---|---|---|
| 0 | 8 | a[0] |
| 1 | 6 | a[1] |
| 2 | 5 | a[2] |
| 3 | 4 | a[3] |
六、用 for 循环输出数组
1.正序输出
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
cout << endl;
这里 i 从 0 走到 n - 1,所以输出顺序是:
a[0] → a[1] → a[2] → …… → a[n-1]
2.倒序输出(逆序输出)
for (int i = n-1; i >=0; i--) {
cout << a[i] << " ";
}
cout << endl;
这里 i 从 n-1 走到 0,所以输出顺序是:
a[n-1] → a[n-2] → a[n-3] → …… → a[0]
逆序循环的两个常见错误:
- 错误一:假设从下标 0 开始存储,访问时却从
i = n开始,第一次访问的就是a[n],已经越界。正确起点是i = n - 1; - 错误二:假设从下标 0 开始存储,for循环的判断条件写作
i > 0,这会漏掉a[0]。正确条件是i >= 0。
3.输出数组中指定位置的元素
题目中的“第几个”通常从 1 开始数,而数组下标从 0 开始。因此,正数第 k 个和倒数第 k 个的写法如下:
正数第 k 个 |
倒数第 k 个 |
|---|---|
a[k - 1] |
a[n - k] |
第 1 个是 a[0] |
倒数第 1 个是 a[n - 1] |
第 n 个是 a[n - 1] |
倒数第 n 个是 a[0] |
例如:
// 假设数组 a[i] 从下标 0 开始存储
int k;
cin >> k;
cout << a[k - 1] << endl; // 正数第 k 个
cout << a[n - k] << endl; // 倒数第 k 个
如果题目大量使用“第 1 个、第 2 个、……、第 n 个”这样的位置说法,而且程序主要按位置访问,也可以考虑从下标 1 开始存放数据。这样正数第 k 个可以直接写成 a[k],倒数第 k 个可以写成 a[n - k + 1],不需要做 -1 的转换。
例如:
// 假设数组 a[i] 从下标 1 开始存储
int k;
cin >> k;
cout << a[k] << endl; // 正数第 k 个
cout << a[n - k + 1] << endl; // 倒数第 k 个
七、数组遍历:求和与求最值
把数组中的元素一个一个访问,这个过程叫作遍历数组。
1.遍历求和
int sum = 0;
for (int i = 0; i < n; i++) {
sum += a[i];
}
cout << sum << endl;
执行过程:
sum = 0
→ 加上 a[0]
→ 加上 a[1]
→ 加上 a[2]
→ ……
→ 加上 a[n-1]
求和时,累加器要从 0 开始,因为 0 不会改变第一次加法的结果。
完整程序:
#include <iostream>
using namespace std;
int a[105];
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
int sum = 0;
for (int i = 0; i < n; i++) {
sum += a[i];
}
cout << sum << endl;
return 0;
}
输入:
5
1 2 3 4 5
输出:
15
2.遍历求最大值
求最大值时,可以使用“打擂台”的方法。先选一个数当作擂主,再让后面的元素依次来挑战它:
- 先让第一个元素
a[0]当擂主; - 从第二个元素
a[1]开始逐个比较; - 如果当前元素比擂主大,就让它成为新的擂主;
- 所有元素都比较完后,最后的擂主就是最大值。
例如,数组中的数据是:
3 7 2 9 4
打擂台的过程如下:
| 比较对象 | 比较前的擂主 | 比较结果 | 比较后的擂主 |
|---|---|---|---|
初始 a[0] = 3 |
— | 先让 3 当擂主 | 3 |
a[1] = 7 |
3 | 7 更大,换擂主 | 7 |
a[2] = 2 |
7 | 2 不够大,擂主不变 | 7 |
a[3] = 9 |
7 | 9 更大,换擂主 | 9 |
a[4] = 4 |
9 | 4 不够大,擂主不变 | 9 |
因此,最大值是 9。把这个过程写成代码:
int maxValue = a[0];
for (int i = 1; i < n; i++) {
if (a[i] > maxValue) {
maxValue = a[i];
}
}
cout << maxValue << endl;
这里从 i = 1 开始,是因为 a[0] 已经先当过一次擂主,不需要再次和自己比较。
初始擂主的其他写法
上面使用 a[0] 作为初始最大值,最稳妥,因为不需要提前知道数据的正负和范围。也可以先准备一个“比所有数据都小”的数:
#include <climits>
int maxValue = INT_MIN;
如果题目数据范围明确,也可以使用常见的保险初值:
int maxValue1 = -1000000000; // int 常用极小值
int maxValue2 = -1e9; // int 常用写法
long long maxValue3 = -1000000000000000000LL; // long long 常用极小值
long long maxValue4 = -1e18; // long long 常用写法
使用这些初值时,必须确认题目中的所有数据确实不会比初值更小。求最小值时则相反,可以使用 INT_MAX,或者根据数据范围使用 1e9、1e18 等极大值。
求最值的检查重点:
- 数组不能为空,至少要有一个元素,才能使用
a[0]作为初值; - 用
a[0]当擂主时,循环从i = 1开始; - 如果使用
0作为初值,遇到全负数时可能得到错误答案; - 使用
INT_MIN、-1e9或-1e18时,要和题目的数据类型、数据范围对应。
例如数组为:
-8 -3 -10 -5
过程如下:
| 当前元素 | maxValue 比较前 |
比较结果 | 更新后 |
|---|---|---|---|
初始 a[0] = -8 |
— | — | -8 |
a[1] = -3 |
-8 | -3 更大 | -3 |
a[2] = -10 |
-3 | -10 不更大 | -3 |
a[3] = -5 |
-3 | -5 不更大 | -3 |
最后最大值是 -3。
3.遍历求最小值
求最小值只需要改变比较方向:
int minValue = a[0];
for (int i = 1; i < n; i++) {
if (a[i] < minValue) {
minValue = a[i];
}
}
cout << minValue << endl;
4.统计某个数出现的次数
数组可以让数据被反复使用。例如,统计 x 出现了几次:
int x;
cin >> x;
int count = 0;
for (int i = 0; i < n; i++) {
if (a[i] == x) {
count++;
}
}
cout << count << endl;
这里的 count 是计数器,必须在循环外初始化为 0。
八、综合例题:读入、求和、求最大值
1.题目
输入一个正整数 n,再输入 n 个整数。请输出这 n 个整数的总和和最大值。
2.分析
需要完成三件事:
- 读入
n个数并保存到数组; - 遍历数组求和;
- 遍历数组求最大值。
同一数组可以被遍历多次,这正是“先存下来”的价值。
3.参考代码
#include <iostream>
using namespace std;
int a[105];
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
int sum = 0;
for (int i = 0; i < n; i++) {
sum += a[i];
}
int maxValue = a[0];
for (int i = 1; i < n; i++) {
if (a[i] > maxValue) {
maxValue = a[i];
}
}
cout << sum << " " << maxValue << endl;
return 0;
}
输入:
5
3 7 2 9 4
输出:
25 9
九、常见错误与注意事项
| 编号 | 错误写法或想法 | 后果 | 改正 |
|---|---|---|---|
| E1 | int a[5]; 却访问 a[5] |
数组越界,结果不可预测 | 大小为 5 时,下标只能是 0 到 4 |
| E2 | for (int i = 0; i <= n; i++) |
多访问一次 a[n] |
正序遍历通常写 i < n |
| E3 | 逆序从 i = n 开始 |
第一次就访问 a[n] |
从 i = n - 1 开始 |
| E4 | 逆序条件写成 i > 0 |
漏掉第一个元素 a[0] |
写成 i >= 0 |
| E5 | int a[n]; 虽然现在的 C++ 标准已经支持 |
数组大小依赖运行时数据,代码不便于统一修改和迁移 | 仍建议根据最大范围提前准备 int a[105] 或 const int N = 1005; int a[N]; |
| E6 | 数组未初始化就直接读取 | 可能读到不确定的值 | 使用 int a[105] = {};,或先给每个位置输入 |
| E7 | 初始值个数超过数组大小 | 编译错误或警告 | 初始值不能多于数组元素个数 |
| E8 | 求和时 sum 没有初始化 |
结果不确定 | int sum = 0; |
| E9 | 求最大值时把 maxValue 设为 0 |
全部为负数时结果错误 | 使用 a[0] 作为初始最大值 |
| E10 | 求最大值从 i = 0 开始但逻辑混乱 |
可能重复处理或写错初值 | maxValue = a[0] 后从 i = 1 开始 |
| E11 | 把“第 k 个”直接写成 a[k] |
位置是否错位 | 需要看存储的起始下标,若从下标 0 开始存储,则第 k 个元素通常是 a[k - 1],若从下标 1 开始存储,则第 k 个元素通常是 a[k] |
| E12 | 只读入数据但没有保存 | 后面无法逆序或二次处理 | 先写入 a[i],再遍历数组 |