导学案5 · 数组:让程序记住一串数据¶
前置要求:导学案4(会写 for 循环,会用 break,理解"外层走一步、内层走一整圈") 参考课程:浙江大学 翁恺《C 语言程序设计》第 8 章(数组) 预计用时:2 小时(按需自取) 本节目标:会定义、初始化、遍历数组,掌握求和 / 最值 / 查找 / 排序四个套路,认识二维数组和字符串 编者说明:导学案5开始之前,如果你不熟练前面的内容,建议翁凯的第六章可以先练一练,同时回顾数据类型 这里编者没注意翁凯的教程中是先讲的函数,顺序发生了一点点错乱,还是那句话,依旧按需自取
5.0 序¶
来个任务:读入 5 个同学的成绩,求平均分,并把所有高于平均分的成绩打印出来。
你现在只能这么写:
五个还凑合。但是:
- 如果是 50 个人呢?你要声明 50 个变量,写 50 个
scanf参数。 - 更要命的是第二问:"把所有高于平均分的打印出来"——这要求你把数据先留住,算完平均之后再回头挨个看一遍。用 50 个变量,你就得写 50 个
if,而且没法用循环,因为它们名字都不一样。 - 最要命的是:个数常常在运行时才知道。比如串口收到了多少个字节,你写代码的时候根本不知道,也就没法提前声明那么多个变量。
数组就是来解决这件事的:一次要一排连号的盒子,用同一个名字加编号来访问。
本节路线:定义与初始化 → 遍历 → 四个常见套路 → 二维数组 → 字符数组与字符串 → 数组与 STM32 → 练习
本节不讲:函数(所以所有代码都写在
main里)、指针、动态内存malloc、字符串处理库的完整用法。它们属于下一轮。
5.1 定义与初始化¶
5.1.1 一排连号的盒子¶
下标从 0 开始,这是 C 语言的规矩。score[0] 是第一个,score[4] 是最后一个,没有 score[5]。
用起来和单个变量没区别:
关键在于:下标可以是变量,也可以是表达式。 score[i] 意味着"第 i 个盒子",i 一变,访问的盒子就跟着变。这就是数组的灵魂——正因如此,没有循环的数组几乎毫无意义。
顺便说一句:
int a[5];里的5表示"我要 5 个格子",而a[5]里的5表示"第 6 个格子"。同一个写法,两个意思,别混。
5.1.2 初始化¶
#include <stdio.h>
int main(void)
{
int a[5] = {90, 85, 77, 68, 99}; /* 全部写出来 */
int b[5] = {1, 2}; /* 只写前两个,剩下的自动补 0 */
int c[5] = {0}; /* 全部清零,最常用的写法 */
int d[] = {1, 2, 3}; /* 不写长度,让编译器自己数 */
printf("a[0] = %d, a[4] = %d\n", a[0], a[4]);
printf("b[0] = %d, b[1] = %d, b[2] = %d\n", b[0], b[1], b[2]);
printf("c[3] = %d\n", c[3]);
printf("d 有 %d 个元素\n", (int)(sizeof(d) / sizeof(d[0])));
a[2] = 100; /* 修改第 3 个元素 */
printf("改完之后 a[2] = %d\n", a[2]);
return 0;
}
输出:
int c[5] = {0}; 这个写法请记牢:数组不会自动清零(和单个变量一样,里面是垃圾值),而= {0} 是最省事的全清零方式。
5.1.3 越界:本节最重要的警告¶
/* 反面示例:越界访问 */
int a[5] = {1, 2, 3, 4, 5};
a[5] = 99; /* 根本没有 a[5]!这是第 6 个格子,不属于你 */
printf("%d\n", a[5]);
C 语言不检查数组越界。 越界之后可能发生:读到垃圾值、悄悄改掉隔壁变量的值、程序当场崩溃,或者什么都不发生。最后一种最坑——你的程序看起来一切正常,直到某天在飞机上出事。
请从现在开始养成习惯:每次写下 a[i],都问自己一句"i 会不会超出范围?"
5.1.4 数组长度用什么写¶
#define N 5 /* 推荐:宏常量,全大写是行业习惯 */
int a[N];
/** 上一行的这个推荐是AI写的,编者这里做几个说明:
* 宏的作用是在编译之前把前面的内容替换为后面的内容
* 关于宏的详细内容本节不展开
* 实际使用中不建议使用像 N 这种过于简单的宏,很容易造成程序混乱
*/
- 数组长度要用编译时就确定的常量:直接写字面量
5,或者用#define出来的宏。 - 别写
const int N = 5;然后int a[N];。 在 C 语言里const变量不算"常量表达式",Visual Studio 会直接报错;gcc 会把它当变长数组放行,于是你换个编译器就炸了。要常量就用#define N 5。 - C99 允许用变量当长度(叫"变长数组"),但不推荐。
5.2 遍历数组¶
"遍历"就是从头到尾把每个元素都过一遍,这是数组最基本的操作,靠 for 循环完成。
#include <stdio.h>
int main(void)
{
int score[5] = {90, 85, 77, 68, 99};
for (int i = 0; i < 5; i++) {
printf("score[%d] = %d\n", i, score[i]);
}
return 0;
}
输出:
注意循环条件是 i < 5,不是 i <= 5。 "下标从 0 数到 n−1"是新手最常错的地方,可以养成一个习惯,遍历数组时,前面初始化临时变量为 0 ,后面终止条件写 < (你的数组长度) 。
5.2.1 不要把 5 写死:用 sizeof 算长度¶
#include <stdio.h>
int main(void)
{
int score[5] = {90, 85, 77, 68, 99};
int len = (int)(sizeof(score) / sizeof(score[0]));
printf("这个数组有 %d 个元素\n", len);
for (int i = 0; i < len; i++) {
printf("%d ", score[i]);
}
printf("\n");
return 0;
}
输出:
道理很简单:sizeof(score) 是整个数组占的字节数(5 × 4 = 20),sizeof(score[0]) 是一个元素占的字节数(4),一除就是个数。这样以后数组改成 8 个元素,循环一个字都不用动。
这个套路只在数组定义所在的地方有效,一旦数组被传给函数就不灵了(原因和指针有关,下一轮讲)。现在照用就行。
5.2.2 从键盘读入数组¶
#include <stdio.h>
int main(void)
{
int score[5] = {0};
printf("请输入 5 个成绩,用空格隔开:");
for (int i = 0; i < 5; i++) {
scanf("%d", &score[i]);
}
for (int i = 0; i < 5; i++) {
printf("第 %d 个:%d\n", i + 1, score[i]);
}
return 0;
}
输入 90 85 77 68 99:
5.3 四个常用基础模板¶
数组的操作来来去去就这么几类,把它们变成肌肉记忆,以后写单片机代码时你就只用想业务,不用想语法。
5.3.1 模板一:求和与平均¶
#include <stdio.h>
int main(void)
{
int score[5] = {90, 85, 77, 68, 99};
int len = (int)(sizeof(score) / sizeof(score[0]));
int sum = 0;
for (int i = 0; i < len; i++) {
sum += score[i];
}
printf("总分 = %d\n", sum);
printf("平均分 = %.2f\n", (double)sum / len);
return 0;
}
输出:
现在可以回答 5.0 里那个"高于平均分"的问题了——数组让你能回头看第二遍:
/* 片段:接在上面的代码后面 */
printf("高于平均分的有:");
for (int i = 0; i < len; i++) {
if ((double)score[i] > (double)sum / len) {
printf("%d ", score[i]);
}
}
printf("\n");
编者注:事实上,求均值的时候如果你不在意程序是否需要记忆,可以考虑直接对输入累加后除以个数,下面几个模板同理。
5.3.2 模板二:最大值、最小值¶
#include <stdio.h>
int main(void)
{
int score[5] = {90, 85, 77, 68, 99};
int len = (int)(sizeof(score) / sizeof(score[0]));
int max = score[0];
int min = score[0];
for (int i = 1; i < len; i++) { /* 每个上来的新数字与最值比较是否替换 */
if (score[i] > max) {
max = score[i];
}
if (score[i] < min) {
min = score[i];
}
}
printf("最高分 = %d,最低分 = %d\n", max, min);
return 0;
}
输出:
注意 max = score[0],不是 max = 0。 如果数组里全是负数(比如冬天的温度、电压的偏差量),从 0 开始就全错了。
5.3.3 模板三:查找(flag + break)¶
#include <stdio.h>
int main(void)
{
int score[5] = {90, 85, 77, 68, 99};
int len = (int)(sizeof(score) / sizeof(score[0]));
int target = 0;
int found = 0; /* 0 = 还没找到,1 = 找到了 */
printf("请输入要找的分数:");
scanf("%d", &target);
for (int i = 0; i < len; i++) {
if (score[i] == target) {
found = 1;
printf("找到了,它是第 %d 个(下标 %d)\n", i + 1, i);
break; /* 找到就收工,别再白找 */
}
}
if (found == 0) {
printf("没有找到 %d\n", target);
}
return 0;
}
输入 77:
这个 flag(标志变量)+ break 的结构,和导学案4练习 7 判断素数时用的完全一样,是编程里出现频率最高的套路之一。
5.3.4 模板四:冒泡排序¶
#include <stdio.h>
int main(void)
{
int a[6] = {5, 2, 9, 1, 7, 3};
int len = (int)(sizeof(a) / sizeof(a[0]));
for (int i = 0; i < len - 1; i++) { /* 外层:一共走 len-1 趟 */
for (int j = 0; j < len - 1 - i; j++) { /* 内层:相邻两个比一比 */
if (a[j] > a[j + 1]) {
int t = a[j]; /* 三杯水交换 */
a[j] = a[j + 1];
a[j + 1] = t;
}
}
}
for (int i = 0; i < len; i++) {
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
输出:
思路:每一趟都把当前最大的那个"冒"到末尾,所以第 i 趟之后,末尾 i 个数已经排好了,内层只需要走到 len - 1 - i。
编者注:冒泡排序其实效率并不很高,但还算比较好理解,建议掌握。
5.4 二维数组:数组的数组¶
数组的每个元素也可以是一个数组,这样就排成了表格(矩阵)。
访问方式:table[1][2],先写行号,再写列号,都从 0 开始,上图里它是 7。
初始化:
遍历需要两层循环:外层管行,内层管列。
#include <stdio.h>
int main(void)
{
int t[3][4] = {
{ 1, 2, 3, 4},
{ 5, 6, 7, 8},
{ 9, 10, 11, 12}
};
for (int i = 0; i < 3; i++) { /* 行 */
for (int j = 0; j < 4; j++) { /* 列 */
printf("%4d", t[i][j]);
}
printf("\n");
}
return 0;
}
输出:
(%4d 让每个数占 4 位、右对齐,表格才整齐。)
二维数组在单片机里同样常见:一张地图、一个棋盘、一幅图像的像素、一块传感器的标定表、姿态解算里的矩阵,都是二维的。练习 5 会让你算主对角线之和。
5.5 字符数组与字符串初步¶
C 语言里没有"字符串类型"。所谓字符串,就是一个字符数组,末尾用一个特殊字符 '\0' 标记结束。
char word[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; /* 一个一个字符写出来 */
char text[] = "hello"; /* 完全等价,简洁得多 */
'\0' 是结束标记,它占 1 个字节,但不算在内容里。所以装下 5 个字母需要 6 个格子——这就是为什么上面写的是 char word[6]。
用双引号写字符串时,编译器会自动帮你把 '\0' 加上,你不用管。
5.5.1 打印与遍历¶
#include <stdio.h>
int main(void)
{
char text[] = "hello";
printf("%s\n", text);
printf("sizeof 数格子 = %d\n", (int)sizeof(text));
int count = 0;
while (text[count] != '\0') { /* 数到结束标记为止 */
count++;
}
printf("字符个数 = %d\n", count);
printf("第一个字符 = %c\n", text[0]);
printf("逐个打印:");
for (int i = 0; text[i] != '\0'; i++) {
printf("%c-", text[i]);
}
printf("\n");
return 0;
}
输出:
注意三件事:
%s会从头一个个打印,遇到'\0'就停,所以你不需要告诉它有几个字符。sizeof(text)是 6,因为它数的是"占了几个格子"(含'\0'),不是字符个数。printf("%s", text)里不写&,直接写数组名。
5.5.2 读入字符串¶
#include <stdio.h>
int main(void)
{
char name[20] = "";
printf("请输入你的名字(不含空格):");
scanf("%s", name); /* 这里也不写 & */
printf("你好,%s!\n", name);
return 0;
}
输入 Mike:
四条要点:
scanf("%s", name)不写&。 为什么?和指针有关,本轮不展开——现在只需要记住"字符串是例外"。%s遇到空格、Tab、回车就停止,所以读不进Hello World这种带空格的句子,只会拿到Hello。- 绝对不要用
gets()。 它不检查长度,输入太长就会越界,是历史上无数安全漏洞的源头,已经被 C 标准废除了。 - 数组要留够空间:
char name[20]最多装 19 个字符(还要留一格给'\0')。输入超长同样会越界。
想知道字符串长度,除了自己写循环数,C 标准库有现成的
strlen()(需要#include <string.h>),还有strcpy()(复制)、strcmp()(比较)。本轮不展开,你想用可以自己查——但请先能用循环自己数出来,那说明你真的懂它。
5.5.3 一个必须提前打的预防针¶
单片机的内存小得可怜:常见的 STM32F103C8T6 只有 20 KB 的 RAM,而你的电脑有 16 GB(差了将近一百万倍)。数组开太大,程序会直接跑飞,或者根本编译不过。 在电脑上随手写 int a[1000000] 的习惯,到了单片机上会要你的命。以后每写一行数组定义,都先问自己一句:"我真的需要这么多吗?"
5.6 综合练习¶
本节练习用于自练与自查,不强制完成、不需要提交;需要提交的是实验室统一布置的作业(作业一 / 作业二)。 参考代码统一放在本节末尾,做完之前不要翻。
练习 1|成绩统计¶
先读入人数 n(不超过 20),再读入 n 个成绩,输出最高分、最低分、平均分(保留两位)、及格人数。
提示:数组要开 20 个格子(因为写代码时不知道
n是几),但循环只走到n。这就是 5.0 里说的"个数在运行时才知道"的解法。n不合法(小于等于 0 或超过 20)时要拦下来,别让它进循环。
练习 2|逆序¶
读入 10 个整数,倒着打印出来。
提示:最简单的做法是循环从
i = 9走到i = 0(i--),数组根本不用动。 选做:真的把数组本身倒过来(首尾交换:a[0]和a[9]换、a[1]和a[8]换……)。想想循环只需要走几圈?走多了会发生什么?
练习 3|不及格名单¶
用练习 1 的数据,把所有不及格(低于 60)的分数打印出来,每行一个;如果全都及格,就打印一句"全部及格"。
提示:一个
for+ 一个if+ 一个 flag(想想 5.3 套路三怎么写的)。这题不给答案,它比套路三还简单。
练习 4|排序¶
读入 8 个整数,用冒泡排序从大到小输出。
提示:把 5.3 套路四的代码敲一遍,然后只改一个符号。改完请自测:输入
3 1 4 1 5 9 2 6,正确输出是9 6 5 4 3 2 1 1。
练习 5|主对角线之和¶
定义一个 3×3 的二维数组,用循环求出主对角线(左上到右下)三个数之和。
提示:主对角线上的元素有什么共同特点?(行号和列号的关系)
练习 6|单词处理¶
读入一个不含空格的单词,输出它的字符个数和倒序(例如输入 hello,输出 5 和 olleh)。
提示:自己用循环数到
'\0'来求长度(5.5 讲过)。倒序打印就是练习 2 的思路,只不过对象换成了字符数组。
练习 7|斐波那契数列¶
把斐波那契数列的前 20 项存进数组并打印(1 1 2 3 5 8 13 ...,从第三项起,每一项是前两项之和)。
提示:
a[i] = a[i - 1] + a[i - 2];,所以循环要从i = 2开始,前两项先手动填好。 顺便思考:如果用int,到第几项就会溢出?换成long long又能撑到第几项?(回收导学案2 2.5)
5.7 本节小结与自查¶
学完本节,你应该能做到:
- 定义、初始化一个数组,说清下标为什么从 0 开始、
a[5]意味着什么 - 用
for遍历数组,并且不越界(i < len而不是i <= len) - 用
sizeof(a) / sizeof(a[0])算出数组元素个数 - 默写四个套路:求和与平均、打擂台求最值、flag 查找、冒泡排序
- 解释为什么求最大值要从
max = a[0]开始,而不是max = 0 - 定义并遍历二维数组,说清
t[i][j]里哪个是行、哪个是列 - 说清字符串和字符数组的关系,
'\0'是干什么的,sizeof数出来的为什么比字符个数多 1 - 知道
scanf("%d", &a[i])要写&,而scanf("%s", word)不写
本轮到此为止。 你已经掌握了顺序、选择、循环三大结构,加上数组。 下一轮会讲:函数(把重复的逻辑打包)、指针(真正用上"地址"这件事)、字符串处理、结构体(把不同类型的数据捆在一起)。
5.8 练习参考代码(做完再看)¶
练习 1 参考代码¶
#include <stdio.h>
#define MAX 20
int main(void)
{
int score[MAX] = {0};
int n = 0;
int sum = 0;
int pass = 0;
printf("请输入人数(不超过 %d):", MAX);
scanf("%d", &n);
if (n <= 0 || n > MAX) {
printf("人数不合法\n");
return 1; /* 返回非 0,告诉操作系统"这次不正常" */
}
printf("请依次输入 %d 个成绩:", n);
for (int i = 0; i < n; i++) {
scanf("%d", &score[i]);
sum += score[i];
if (score[i] >= 60) {
pass++;
}
}
int max = score[0];
int min = score[0];
for (int i = 1; i < n; i++) {
if (score[i] > max) {
max = score[i];
}
if (score[i] < min) {
min = score[i];
}
}
printf("最高分 = %d\n", max);
printf("最低分 = %d\n", min);
printf("平均分 = %.2f\n", (double)sum / n);
printf("及格人数 = %d\n", pass);
return 0;
}
return 1;这一句回收了导学案1讲过的"程序状态码":0 表示正常结束,非 0 表示出了状况。
练习 2 参考代码¶
#include <stdio.h>
int main(void)
{
int a[10] = {0};
printf("请输入 10 个整数:");
for (int i = 0; i < 10; i++) {
scanf("%d", &a[i]);
}
printf("倒着打印:");
for (int i = 9; i >= 0; i--) {
printf("%d ", a[i]);
}
printf("\n");
/* 选做:真的把数组倒过来,只走一半 */
for (int i = 0; i < 10 / 2; i++) {
int t = a[i];
a[i] = a[9 - i];
a[9 - i] = t;
}
printf("倒过来之后:");
for (int i = 0; i < 10; i++) {
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
只走一半(
i < 10 / 2)是关键。如果走完 10 圈,前半段和后半段会再换回去,等于白忙一场。
练习 4 参考代码(只给出与套路四不同的那一行)¶
其余部分(读入 8 个数、两层循环、打印)与 5.3 套路四完全一样。
练习 5 参考代码¶
#include <stdio.h>
int main(void)
{
int t[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
int sum = 0;
for (int i = 0; i < 3; i++) {
sum += t[i][i]; /* 主对角线:行号 == 列号 */
}
printf("主对角线之和 = %d\n", sum);
return 0;
}
输出:主对角线之和 = 15(1 + 5 + 9)
选做:再求副对角线(
t[i][2 - i]),以及每一行的平均值。
练习 6 参考代码¶
#include <stdio.h>
int main(void)
{
char word[32] = "";
int len = 0;
printf("请输入一个不含空格的单词:");
scanf("%s", word); /* 注意:不写 & */
while (word[len] != '\0') {
len++;
}
printf("长度 = %d\n", len);
printf("倒序 = ");
for (int i = len - 1; i >= 0; i--) {
printf("%c", word[i]);
}
printf("\n");
return 0;
}
输入 hello:
数组是"一个名字管一排数据",它让程序第一次拥有了记忆。
从今天起,你写的不再是"算一次就忘"的脚本,而是"能攒一批数据再统一处理"的程序
作者:Mike-lin300
最后更新:2026-09-24
更新说明:无