第五章 数组
一、项目引入-学生成绩管理系统
1.1 先看成品
成品长这样。五个学生的成绩,录进去,算平均,找最高最低,数不及格,最后从高到低排个名。
#include <iostream>
using namespace std;
int main() {
const int N = 5;
int scores[N];
for (int i = 0; i < N; i++) {
cout << "第 " << i + 1 << " 个学生的成绩: ";
cin >> scores[i];
}
cout << "\\n成绩单" << endl;
for (int i = 0; i < N; i++) {
cout << scores[i] << endl;
}
int sum = 0;
for (int i = 0; i < N; i++) {
sum += scores[i];
}
cout << "平均分 " << (double)sum / N << endl;
int maxIdx = 0, minIdx = 0;
for (int i = 1; i < N; i++) {
if (scores[i] > scores[maxIdx]) {
maxIdx = i;
}
if (scores[i] < scores[minIdx]) {
minIdx = i;
}
}
cout << "最高 " << scores[maxIdx] << " 最低 " << scores[minIdx] << endl;
int fail = 0;
for (int i = 0; i < N; i++) {
if (scores[i] < 60) {
fail++;
}
}
cout << "不及格 " << fail << " 人" << endl;
for (int i = 0; i < N – 1; i++) {
for (int j = 0; j < N – 1 – i; j++) {
if (scores[j] < scores[j + 1]) {
int t = scores[j];
scores[j] = scores[j + 1];
scores[j + 1] = t;
}
}
}
cout << "\\n排名" << endl;
for (int i = 0; i < N; i++) {
cout << i + 1 << " " << scores[i] << endl;
}
return 0;
}
输入 78 92 55 100 88,跑完是这个结果。
第 1 个学生的成绩: 78
第 2 个学生的成绩: 92
第 3 个学生的成绩: 55
第 4 个学生的成绩: 100
第 5 个学生的成绩: 88
成绩单
78
92
55
100
88
平均分 82.6
最高 100 最低 55
不及格 1 人
排名
1 100
2 92
3 88
4 78
5 55
数字都对得上。总分四百一十三,除以五得八十二点六;五十五分低于六十,不及格一人;降序排下来一百、九十二、八十八、七十八、五十五。这些都是手算能验的,跑完对一遍,比盲目相信屏幕强。
┌──────────────────────────────────────────────────────────┐
│ 成绩管理系统走这几步 │
├──────────────────────────────────────────────────────────┤
│ 五个功能一条主线,数据全住在一个数组里 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 录入 循环里 cin,往格子里一个个放成绩 │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 打印 循环走一遍,一个成绩一行 │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 统计 求和、平均、最高最低、不及格人数 │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 排序 冒泡上场,把成绩从高到低排 │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 每个功能都是一个循环加几个判断 │
│ 数组换成二维的,就能管多科成绩 │
└──────────────────────────────────────────────────────────┘
1.2 没有数组之前
同样的系统,没有数组要靠什么摆数据。四十个学生,四十个变量。
int score1 = 78, score2 = 92, score3 = 55;
int score4 = 100, score5 = 88;
// 后面还有 score6 到 score40
声明阶段只是难看。真正致命的是循环用不上。第四章学的 for 循环,循环变量 i 从 0 跑到 39,可 i 转不成变量名,scorei 这个东西不存在。于是遍历只能手写四十行 cout,求和只能手写四十行加号,排序只能手写交换。循环是省事的工具,前提是数据排得整齐,变量名一人一个样,循环使不上劲。
┌────────────────────────────────────────────────────────┐
│ 四十个变量对一个数组 │
├────────────────────────────────────────────────────────┤
│ 同样的四十份成绩,两种摆法 │
│ │
│ +—————-+——————+————–+ │
│ |摆法 |四十个学生的成绩 |循环救得了吗 | │
│ +—————-+——————+————–+ │
│ |四十个变量 |score1 到 score40 |救不了 | │
│ |一个数组 |score[40] |救得了 | │
│ +—————-+——————+————–+ │
│ │
│ 循环要抓的手,得是排得整齐的数据 │
│ 数组一个名字通吃,容量写在方括号里 │
└────────────────────────────────────────────────────────┘
数组把这一切翻过来。一个名字 scores,后面方括号里装下标,scores[0] 到 scores[39],下标恰好是循环变量 i。手写四十行的活,全部缩进一个循环。这就是数组存在的理由,给循环一双能抓得住的手。
1.3 数组是个什么东西
数组可以想成一家旅馆。一个名字是店名,方括号里那个数是房间号,从零号房开始排。住进去的是同一类型的数据,成绩就全是 int,姓名就全是 string,混着住不行。
房间号和房间是一对一的。scores[2] 就是第三间房,推门进去拿到的是那一份数据。房间紧挨着排,中间不留空,这也是它能被循环一趟扫完的原因。
┌───────────────────────────────────────────────────────┐
│ 数组在内存里排成一排 │
├───────────────────────────────────────────────────────┤
│ 五个 int 挨着坐,从零开始编号 │
│ │
│ +———+———+———+———+———+ │
│ |下标 0 |下标 1 |下标 2 |下标 3 |下标 4 | │
│ +———+———+———+———+———+ │
│ |78 |92 |55 |100 |88 | │
│ +———+———+———+———+———+ │
│ │
│ 名字只有一个 scores,后面跟方括号装下标 │
│ 想要第几间房,方括号里就写第几号 │
└───────────────────────────────────────────────────────┘
长度在声明的时候定死。int scores[5] 就是五间房,营业期间不加房间也不退房。嫌它硬的,后面有 vector,能伸能缩,那是后话,数组是它的底子,必须先熟。
二、一维数组
2.1 声明与初始化
声明的写法,类型、名字、方括号里装长度。
int scores[5];
double prices[10];
string names[8];
长度只能写常量或者常量表达式。C++ 老标准里变量长度不行,用变量当长度有的编译器直接拒收,统一用 const int 定一个,稳当。
不初始化的数组,格子里装的是内存残留数据。读到什么数全看运气,跟第二章说的未初始化变量一个道理。声明的时候给值,有三种写法。
int a[5] = {78, 92, 55, 100, 88}; // 完整给
int b[5] = {78, 92}; // 给一半
int c[5] = {0}; // 全清零
┌──────────────────────────────────────────────────────┐
│ 初始化有三种写法 │
├──────────────────────────────────────────────────────┤
│ 三种初始化,写法不同,开头状态不同 │
│ │
│ +—————-+—————-+————–+ │
│ |写法 |长相 |格子里是什么 | │
│ +—————-+—————-+————–+ │
│ |完整列出来 |五个数全给 |一一对应 | │
│ |给一半 |只给前两个 |剩下自动补零 | │
│ |全清零 |第一个写 0 |后面跟着补零 | │
│ +—————-+—————-+————–+ │
│ │
│ 没给值的地方补零,不是补随机数 │
│ 长度能省是情分,初始化不能省 │
└──────────────────────────────────────────────────────┘
给一半的,剩下的格子自动补零。全清零的,第一个显式写了 0,后面跟着补。补零这个行为是标准定的,靠得住。
还有一种偷懒写法,初始化的时候长度可以省,编译器自己数。
int d[] = {1, 2, 3, 4}; // 编译器数出来是 4
前提是必须初始化,不初始化编译器不知道该开几间房。这条方便,也有代价,长度藏在花括号里,改数据的时候容易看漏。
| 声明加完整初始化 | int a[5] = {1,2,3,4,5} | 明写 5 | 一一对应 |
| 声明加部分初始化 | int b[5] = {1,2} | 明写 5 | 后三个补零 |
| 全清零 | int c[5] = {0} | 明写 5 | 常用套路 |
| 省略长度 | int d[] = {1,2,3} | 编译器数,是 3 | 必须初始化 |
| 只声明 | int e[5] | 明写 5 | 里面是垃圾数据 |
2.2 下标从零开始
访问元素,方括号里写下标。下标从零开始,五个元素的合法下标是 0 到 4,没有 5。
cout << scores[0] << endl; // 第一个,78
cout << scores[4] << endl; // 最后一个,88
scores[2] = 60; // 改第三个
第一间房是 0 号房,这条没道理可讲,C 语言传下来的规矩,全家沿用。新手在这里翻车,翻的都是同一个姿势,打印第五个元素写了 scores[5]。
int a[5] = {1, 2, 3, 4, 5};
cout << a[5] << endl; // 越界,后果自负
越界是未定义行为。这四个字的分量,第二章讲过一回,这里再强调一遍。不报错,不崩溃,也不保证读到什么。可能是垃圾数字,可能是别人家的变量,可能是程序直接躺平。同一个程序这次跑好好的,下次换个编译器就崩,说的就是它。
┌───────────────────────────────────────────────────────┐
│ 越界是怎么发生的 │
├───────────────────────────────────────────────────────┤
│ 合法下标只到四,多一号就是越界 │
│ │
│ +———+———+———+———+———+ │
│ |下标 0 |下标 1 |下标 2 |下标 3 |下标 4 | │
│ +———+———+———+———+———+ │
│ |78 |92 |55 |100 |88 | │
│ +———+———+———+———+———+ │
│ │
│ 合法区域到此为止,scores[5] 已经出界 │
│ 下标错一位,坏的可能不是这一个数 │
└───────────────────────────────────────────────────────┘
防越界的土办法有个,循环条件写 i < 5 而不是 i <= 5,数元素个数的时候把 0 号房数进去。进阶办法是用 vector,它的 at 访问会老老实实抛异常,宁可慢一点也要个明白。数组不拦你,是它的设计哲学,拦你是你的事。
2.3 遍历
遍历就是挨个房间走一遍,下标从 0 到长度减一。
for (int i = 0; i < 5; i++) {
cout << scores[i] << endl;
}
C++11 起有个更懒的写法,范围 for,连下标都省了。
for (int x : scores) {
cout << x << endl;
}
冒号左边是每个元素的副本,右边是数组。它翻译成循环的时候还是老写法,只是不用手写下标。两个场合别用它。要改元素的值,得写成 int &x,加一个引用符号,不加改的是副本,白忙。要下标做别的事,比如奇偶行分开处理,还得回到老写法。
2.4 统计四件套
求和、平均、最大最小、计数,四个套路,成绩系统的地基。
求和用一个变量累加,循环里走一遍。
int sum = 0;
for (int i = 0; i < N; i++) {
sum += scores[i];
}
平均分是总和除以人数。除法那章的教训还在,两个 int 相除还是 int,小数部分直接扔。先转 double 再除。成品里图省事写的是 (double)sum 这种老式写法,第二章讲过的 static_cast 正式登场,写成 static_cast(sum) 更好认。
double avg = static_cast<double>(sum) / N;
最大最小有两种记法。记值简单,遇到更大的就换。记下标更值钱,拿到下标既能拿值,还能知道是谁。
int maxIdx = 0;
for (int i = 1; i < N; i++) {
if (scores[i] > scores[maxIdx]) {
maxIdx = i;
}
}
循环从 1 开始,0 号房先当擂主,后面挨个挑战。这个写法比记值多一行,用起来宽得多,排行榜、点名、发奖都指着下标。
计数是带条件的求和,条件进 if,计数器加一。
int fail = 0;
for (int i = 0; i < N; i++) {
if (scores[i] < 60) {
fail++;
}
}
四件套换到二维数组上,循环多套一层,套路一字不改。这是本章后半段的事。
2.5 冒泡排序
排序是成绩系统的刚需。冒泡是最直白的排序,直白到每个新手都该手写一遍。
思想一句话,相邻两个比,大的往前换。一趟走完,最小的沉到末位。再走一趟,次小的归位,大的像气泡一样一路往上冒,冒泡这个名字就是这么来的。五个数走四趟,就齐了。
┌───────────────────────────────────────────────┐
│ 冒泡排序走一趟 │
├───────────────────────────────────────────────┤
│ 冒泡一趟,相邻两个比,大的往前换 │
│ │
│ 排序前 78 92 55 100 88 │
│ 比 0 和 1 后 92 78 55 100 88 │
│ 比 1 和 2 后 92 78 55 100 88 没换 │
│ 比 2 和 3 后 92 78 100 55 88 │
│ 比 3 和 4 后 92 78 100 88 55 │
│ │
│ 一趟下来,最小的 55 沉到最后一位 │
│ 五个数要四趟,每趟比上一趟少比一次 │
└───────────────────────────────────────────────┘
代码是双层循环。外层管趟数,内层管每趟的比较。
for (int i = 0; i < N – 1; i++) {
for (int j = 0; j < N – 1 – i; j++) {
if (scores[j] < scores[j + 1]) {
int t = scores[j];
scores[j] = scores[j + 1];
scores[j + 1] = t;
}
}
}
三个细节。外层跑 N 减 1 趟,五个数四趟,最后一趟只剩一个元素,不用比。内层上限是 N 减 1 减 i,第 i 趟末尾已经有 i 个元素归位,再比是浪费。条件是小于号,走的是降序,换成大于号就是升序,一个符号的事。
交换要借一个临时变量,两个杯子换水,不倒第三杯倒不成。C++ 里能用 swap 函数省掉这三行, 头文件里现成的,知道有它就行,手写三行是基本功。
比较次数是 n 乘 n 减 1 除 2。五个数十次,四十个学生七百八十次。这个增长势头不妙,数据量上到一万,比较次数五千万起步。冒泡的场合适中小数据、教学演示和近乎有序的数据,正经排序有插入、快排、归并,那些是数据结构章节的角,这里不展开。
三、二维数组
3.1 表格观
一维数组是一条队,二维数组是一张表。三个学生,四门课,正好一张三行四列的成绩表。
int grid[3][4];
声明写两个方括号,前一个行数,后一个列数。这个顺序别反,反了的是另一种错法,编译器不拦,数据全乱。
初始化跟一维一个思路,按行给,一行一个花括号。
int grid[3][4] = {
{78, 92, 85, 90},
{88, 76, 90, 82},
{95, 89, 93, 87}
};
偷懒的规矩也通用,第一维的长度可以省,编译器数行数。第二维不能省,它得知道每行多宽。
int grid[][4] = {
{78, 92, 85, 90},
{88, 76, 90, 82},
{95, 89, 93, 87}
};
┌───────────────────────────────────────────────────────┐
│ 二维数组长成一张表 │
├───────────────────────────────────────────────────────┤
│ 二维数组就是一张表,两个下标各管一个方向 │
│ │
│ +———+———+———+———+———+ │
│ |姓名 |语文 |数学 |英语 |物理 | │
│ +———+———+———+———+———+ │
│ |张三 |78 |92 |85 |90 | │
│ |李四 |88 |76 |90 |82 | │
│ |王五 |95 |89 |93 |87 | │
│ +———+———+———+———+———+ │
│ │
│ 行下标横着数,列下标竖着数 │
│ 姓名另摆一个 string 数组,格子只装分数 │
└───────────────────────────────────────────────────────┘
可以把它看成三根一维数组并排站。grid[0] 是第一行那四个数,grid[1] 是第二行。这个看法在遍历的时候能省脑子。
3.2 双下标访问
访问一个格子,两个下标都要给,先写行再写列。
cout << grid[0][0] << endl; // 第一行第一列,78
cout << grid[2][3] << endl; // 第三行第四列,87
grid[1][2] = 95; // 改第二行第三列
下标同样从零开始,同样会越界,同样不报错。三行四列的表,行下标到 2,列下标到 3,各差一号,数的时候容易顺手写成 3 和 4。
内存不认表格,只认一长条。二维数组在内存里是按行铺的,第一行铺完铺第二行。
┌───────────────────────────────────────────────────────────┐
│ 二维数组怎么落进内存 │
├───────────────────────────────────────────────────────────┤
│ 内存不认表格,只认一长条,一行接一行铺 │
│ │
│ 第 0 行 grid[0][0] grid[0][1] grid[0][2] grid[0][3] │
│ │ │
│ v │
│ 第 1 行 grid[1][0] grid[1][1] grid[1][2] grid[1][3] │
│ │ │
│ v │
│ 第 2 行 grid[2][0] grid[2][1] grid[2][2] grid[2][3] │
│ │
│ 内存按行铺,遍历顺边走不吃亏 │
│ 缓存搬连续内存,跳着扫才吃亏 │
└───────────────────────────────────────────────────────────┘
行优先存储,C++ 一向如此。它带来一个实用后果,遍历按行走比按列走快。缓存一次搬一串连续的内存,按行扫,下一格就在旁边;按列扫,每次都跳到下一行的开头。数据量小感觉不出来,量大就差出来了。遍历的姿势养成本能,外层走行,内层走列。
3.3 双层循环遍历
遍历就是双层循环,外层行内层列,四件套搬过来照样好使。
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
cout << grid[i][j] << "\\t";
}
cout << endl; // 一行打完,换行
}
范围 for 在二维数组上不好使。它展开一层,拿到的是每一行那个一维数组,还得再套一层循环才拿到数。想用懒办法,先把它想成三根一维数组,就顺了。
for (auto &row : grid) {
for (int x : row) {
cout << x << "\\t";
}
cout << endl;
}
外层引用符号要加,不加拿到的是副本,改不动原表。
3.4 成绩表实例
把 3.1 那张表算一遍。每人总分,每科平均。
int grid[3][4] = {
{78, 92, 85, 90},
{88, 76, 90, 82},
{95, 89, 93, 87}
};
for (int i = 0; i < 3; i++) {
int total = 0;
for (int j = 0; j < 4; j++) {
total += grid[i][j];
}
cout << "第 " << i + 1 << " 个学生总分 " << total << endl;
}
for (int j = 0; j < 4; j++) {
int sum = 0;
for (int i = 0; i < 3; i++) {
sum += grid[i][j];
}
cout << "第 " << j + 1 << " 科平均 " << static_cast<double>(sum) / 3 << endl;
}
跑出来是这些数。张三总分三百四十五,李四三百三十六,王五三百六十四。语文平均八十七,数学八十五点七,英语八十九点三,物理八十六点三。手算能对上的,自己拿计算器验一遍,循环错一位都对不上。
细看两段循环的姿势。求每人总分,外层走行,内层把一行加干。求每科平均,外层走列,内层把一列加干。行和列谁在外谁在内,取决于你要锁住哪个方向,这是双层循环的唯一诀窍。
四、字符串
4.1 char 数组的老底
字符串的本相是一排字符。C 语言那会儿没有专门的字符串类型,用 char 数组凑,结尾再挂一个空字符当句号。
char name[] = "Tom";
方括号里没写长度,编译器自己数。数出来是 4,不是 3,多出来那间房给结束符住。sizeof 看到的是 4,strlen 数的是 3,结束符不算内容,只当边界。
老写法有三条硬规矩。赋值不行,name = "Bob" 编译直接拦,数组名是常量,不是能塞值的变量,要换内容得逐个字符拷,或者请出 strcpy。比较不行,== 比的是地址不是内容,要比内容用 strcmp。长度问 strlen,sizeof 问的是占多少字节,俩不是一回事。
#include <cstring>
char a[] = "Tom";
char b[] = "Tom";
cout << (a == b) << endl; // 0,比地址,俩地址不一样
cout << strcmp(a, b) << endl; // 0,内容一样
cout << strlen(a) << endl; // 3
cout << sizeof(a) << endl; // 4,带着结束符
这套老底翻出来,不是让你天天用。是读老代码、看接口文档、调试内存的时候,满眼都是它。知道 char 数组是怎么回事,那些为什么这么写就有答案了。
中文在 char 数组里还有个坑。第一章说过中文输出乱码,在字符串里一样成立,一个汉字占两三个字节,按 char 数会数出一半的长度。处理中文文本的场景,交给 string 和宽字符,初学阶段先躲开。
4.2 string 上路
C++ 给了 string,字符串的现代化摆法。它是个类,不是内置类型,用之前先请头文件 出场。
string name = "张三丰";
string greeting = "hello";
┌─────────────────────────────────────────┐
│ string 里装着什么 │
├─────────────────────────────────────────┤
│ string 里也是一排字符,长度它自己记着 │
│ │
│ 字符 张 三 丰 │
│ 下标 0 1 2 │
│ │
│ 字符一个挨一个存,位置用下标数 │
│ 长度问 length(),几个字符报几个 │
└─────────────────────────────────────────┘
赋值、拼接、比较,全是正常人的操作。
string a = "hello";
string b = a; // 赋值,拷贝一份
string c = a + " world"; // 拼接
cout << (a == b) << endl; // 比内容,1
cout << a.length() << endl; // 长度,5
== 比的是内容,不是地址。这一条就平了 char 数组最烦人的那个坑。长度 length 和 size 是同一个东西,两个名字都常用,随便挑一个顺手的。
空串判断用 empty,比 length() == 0 短,意思也直白。
string s;
if (s.empty()) {
cout << "空的" << endl;
}
string 和 char 数组能互相转,转进去直接赋值,转出来用 c_str。
string s = "Tom";
const char *p = s.c_str(); // 转 C 风格,接老接口用
4.3 常用操作
截一段用 substr,头一个参数是起点,第二个是长度。
string s = "hello world";
cout << s.substr(0, 5) << endl; // hello
cout << s.substr(6) << endl; // world,省长度就是到尾
找一个子串用 find,返回位置,找不到返回一个叫 string::npos 的常量。
string s = "hello world";
cout << s.find("world") << endl; // 6
if (s.find("abc") == string::npos) {
cout << "没找到" << endl;
}
npos 的判断姿势记一下,跟 == 配着用,跟 != 的意思正好反。找到返回下标,找不到返回 npos,没有中间态。
读一整行用 getline,第三章的老朋友。cin >> 遇空格就停,getline 读到换行为止,带空格的名字全靠它。
string line;
getline(cin, line);
4.4 姓名进系统
现在把名字加回成绩系统。两个数组并排摆,一个装名字,一个装分数,下标对齐,第 i 个名字对第 i 个分数。
const int N = 3;
string names[N] = {"张三", "李四", "王五"};
int scores[N] = {78, 92, 55};
for (int i = 0; i < N; i++) {
cout << names[i] << " " << scores[i] << endl;
}
排序的时候两个数组一起换。分数换了位,名字必须跟着走,不然名次对不上人。
for (int i = 0; i < N – 1; i++) {
for (int j = 0; j < N – 1 – i; j++) {
if (scores[j] < scores[j + 1]) {
int t = scores[j];
scores[j] = scores[j + 1];
scores[j + 1] = t;
string tn = names[j];
names[j] = names[j + 1];
names[j + 1] = tn;
}
}
}
查成绩就是挨个对名字。
string target;
cout << "查谁: ";
cin >> target;
int found = -1;
for (int i = 0; i < N; i++) {
if (names[i] == target) {
found = i;
break;
}
}
if (found >= 0) {
cout << target << " 的成绩是 " << scores[found] << endl;
} else {
cout << "没这个人" << endl;
}
found 初始值是 -1,不是 0。零号房是合法下标,拿 0 当没找到,第一个学生就要蒙冤。约定一个不可能的下标,是这类查找的标准姿势。
两个数组并排摆,有个隐藏成本。名字和分数的关系,靠你脑子里那根线牵着,代码里没写着。加个班级字段,就得多牵一根,加个家长电话,再牵一根。牵得多了,总有一根牵错的时候。数据和数据的关系写进代码里,那是结构体和类的章节,这里先埋一句,数组的好处先享受着。
五、小结
┌──────────────────────────────────────────────────────────┐
│ 本章收口 │
├──────────────────────────────────────────────────────────┤
│ 一章学完,手上该有这几样 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 一维 一条队,一个下标,循环遍历 │ │
│ └────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 二维 一张表,两个下标,双层循环 │ │
│ └────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 字符串 一排字符,string 比 char 数组省心 │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 成绩系统到此能跑,统计和排序全靠循环加下标 │
│ 代码还堆在 main 里,下一章收进函数 │
│ 函数的第一个数组参数,下一章开头就见 │
└──────────────────────────────────────────────────────────┘
| 一维数组 | 一条队,方括号装下标 | 下标从 1 开始数 |
| 初始化 | 三种写法,补零规则 | 省略长度忘了初始化 |
| 越界 | 不报错,后果自负 | 循环写成小于等于 |
| 遍历 | for 走下标,范围 for 走值 | 改元素忘了加引用 |
| 统计四件套 | 求和、平均、最大最小、计数 | int 相除丢小数 |
| 冒泡 | 相邻比较,大的往前 | 内层上限忘了减 i |
| 二维数组 | 一张表,两个下标 | 行列数反,行优先后患 |
| 双层循环 | 外层锁方向,内层扫一遍 | 行和列谁在外弄反 |
| char 数组 | 老底,结束符结尾 | sizeof 和 strlen 混 |
| string | 现代化字符串,== 比内容 | npos 判断写反 |
| 并行数组 | 名字分数两行对齐 | 排序只换一边 |
五句话带走。数组给循环一双能抓得住的手,变量编号的地狱到此为止。下标从零开始,越界不报错,这两条刻脑子里。一维是一条队,二维是一张表,外层锁方向内层扫。char 数组是老祖宗,string 是日用款,sizeof 和 strlen 别混。并行数组能用不完美,关系和字段迟早要收进结构体。
课后练习四道。录入十个成绩,打印高于平均分的那些。冒泡改成升序,再改成按分数段统计人数。用二维数组存三名学生四门课,输出每人总分和每科平均分。加姓名数组,实现按姓名查成绩,找不到要给提示,找到要报分数和排名。
附录|数组速查卡
| 一维声明 | int a[5] | 长度常量,别用变量 |
| 完整初始化 | int a[5] = {1,2,3,4,5} | 一一对应 |
| 部分初始化 | int a[5] = {1,2} | 剩下补零 |
| 全清零 | int a[5] = {0} | 套路,靠得住 |
| 省略长度 | int a[] = {1,2,3} | 必须初始化 |
| 二维声明 | int g[3][4] | 前行后列别写反 |
| 二维初始化 | int g[][4] = {{…}} | 只省第一维 |
| 访问 | a[i]、g[i][j] | 下标从零开始 |
| 范围 for | for (int x : a) | 改值要加引用 |
| string 拼接 | a + b | 内容操作,不碰地址 |
| string 长度 | length 或 size | 一样的意思 |
| string 截取 | s.substr(0, 5) | 第二个参数是长度 |
| string 查找 | s.find("x") | 和 npos 配着判 |
| char 数组 | char n[] = "Tom" | sizeof 比 strlen 多一 |
| 读到乱数 | 数组没初始化 | 声明就给值 |
| 跑着跑着崩了 | 下标越界 | 查循环上下限 |
| 平均分丢小数 | int 相除 | 先转 double |
| 排序名次对不上人 | 只换了分数 | 两个数组一起换 |
| 查第一个人总是找不到 | found 初值设成 0 | 初值设 -1 |
| 二维遍历结果怪 | 行列数反或扫反了 | 外层锁方向再看一遍 |
| sizeof 和 strlen 差一 | 结束符算字节不算内容 | 各自场合用对 |
| 中文长度数不准 | 多字节按 char 数 | 换 string 或宽字符 |

![[鼎捷 E10] 财务相关单据操作时异常问题解决(一)-171主机测评](https://www.171host.com/wp-content/uploads/2026/10/20261030154444-6ae4bb6c483bc-220x150.png)