跳转至

导学案5 · 数组:让程序记住一串数据

前置要求:导学案4(会写 for 循环,会用 break,理解"外层走一步、内层走一整圈") 参考课程:浙江大学 翁恺《C 语言程序设计》第 8 章(数组) 预计用时:2 小时(按需自取) 本节目标:会定义、初始化、遍历数组,掌握求和 / 最值 / 查找 / 排序四个套路,认识二维数组和字符串 编者说明:导学案5开始之前,如果你不熟练前面的内容,建议翁凯的第六章可以先练一练,同时回顾数据类型 这里编者没注意翁凯的教程中是先讲的函数,顺序发生了一点点错乱,还是那句话,依旧按需自取


5.0 序

来个任务:读入 5 个同学的成绩,求平均分,并把所有高于平均分的成绩打印出来。

你现在只能这么写:

int s1 = 0, s2 = 0, s3 = 0, s4 = 0, s5 = 0;

scanf("%d %d %d %d %d", &s1, &s2, &s3, &s4, &s5);

五个还凑合。但是:

  1. 如果是 50 个人呢?你要声明 50 个变量,写 50 个 scanf 参数。
  2. 更要命的是第二问:"把所有高于平均分的打印出来"——这要求你把数据先留住,算完平均之后再回头挨个看一遍。用 50 个变量,你就得写 50 个 if,而且没法用循环,因为它们名字都不一样。
  3. 最要命的是:个数常常在运行时才知道。比如串口收到了多少个字节,你写代码的时候根本不知道,也就没法提前声明那么多个变量。

数组就是来解决这件事的:一次要一排连号的盒子,用同一个名字加编号来访问。

本节路线:定义与初始化 → 遍历 → 四个常见套路 → 二维数组 → 字符数组与字符串 → 数组与 STM32 → 练习

本节不讲:函数(所以所有代码都写在 main 里)、指针、动态内存 malloc、字符串处理库的完整用法。它们属于下一轮。


5.1 定义与初始化

5.1.1 一排连号的盒子

int score[5];        /* 一次要 5 个连续的 int 盒子,统一叫 score */
下标:    0     1     2     3     4
        ┌─────┬─────┬─────┬─────┬─────┐
score   │     │     │     │     │     │
        └─────┴─────┴─────┴─────┴─────┘

下标从 0 开始,这是 C 语言的规矩。score[0] 是第一个,score[4] 是最后一个,没有 score[5]。

用起来和单个变量没区别:

score[2] = 90;              /* 给第 3 个盒子赋值 */
printf("%d\n", score[0]);   /* 取出第 1 个盒子 */

关键在于:下标可以是变量,也可以是表达式。 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;
}

输出:

a[0] = 90, a[4] = 99
b[0] = 1, b[1] = 2, b[2] = 0
c[3] = 0
d 有 3 个元素
改完之后 a[2] = 100

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;
}

输出:

score[0] = 90
score[1] = 85
score[2] = 77
score[3] = 68
score[4] = 99

注意循环条件是 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;
}

输出:

这个数组有 5 个元素
90 85 77 68 99

道理很简单: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 个成绩,用空格隔开:第 1 个:90
第 2 个:85
第 3 个:77
第 4 个:68
第 5 个: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;
}

输出:

总分 = 419
平均分 = 83.80

现在可以回答 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;
}

输出:

最高分 = 99,最低分 = 68

注意 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:

请输入要找的分数:找到了,它是第 3 个(下标 2)

这个 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;
}

输出:

1 2 3 5 7 9

思路:每一趟都把当前最大的那个"冒"到末尾,所以第 i 趟之后,末尾 i 个数已经排好了,内层只需要走到 len - 1 - i。

编者注:冒泡排序其实效率并不很高,但还算比较好理解,建议掌握。


5.4 二维数组:数组的数组

数组的每个元素也可以是一个数组,这样就排成了表格(矩阵)。

int table[3][4];      /* 3 行 4 列,一共 12 个 int */
          列0   列1   列2   列3
  行0  [   1     2     3     4  ]
  行1  [   5     6     7     8  ]
  行2  [   9    10    11    12  ]

访问方式:table[1][2],先写行号,再写列号,都从 0 开始,上图里它是 7。

初始化:

int t[2][3] = {{1, 2, 3}, {4, 5, 6}};    /* 两行,每行三个 */
int z[3][3] = {0};                        /* 全部清零 */

遍历需要两层循环:外层管行,内层管列。

#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;
}

输出:

   1   2   3   4
   5   6   7   8
   9  10  11  12

(%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;
}

输出:

hello
sizeof 数格子 = 6
字符个数 = 5
第一个字符 = h
逐个打印:h-e-l-l-o-

注意三件事:

  1. %s 会从头一个个打印,遇到 '\0' 就停,所以你不需要告诉它有几个字符。
  2. sizeof(text) 是 6,因为它数的是"占了几个格子"(含 '\0'),不是字符个数。
  3. 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:

请输入你的名字(不含空格):你好,Mike!

四条要点:

  1. scanf("%s", name) 不写 &。 为什么?和指针有关,本轮不展开——现在只需要记住"字符串是例外"。
  2. %s 遇到空格、Tab、回车就停止,所以读不进 Hello World 这种带空格的句子,只会拿到 Hello。
  3. 绝对不要用 gets()。 它不检查长度,输入太长就会越界,是历史上无数安全漏洞的源头,已经被 C 标准废除了。
  4. 数组要留够空间: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 参考代码(只给出与套路四不同的那一行)

/* 从大到小:把比较符号反过来 */
if (a[j] < a[j + 1]) {
    int t = a[j];
    a[j] = a[j + 1];
    a[j + 1] = t;
}

其余部分(读入 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:

请输入一个不含空格的单词:长度 = 5
倒序 = olleh

数组是"一个名字管一排数据",它让程序第一次拥有了记忆。

从今天起,你写的不再是"算一次就忘"的脚本,而是"能攒一批数据再统一处理"的程序

作者:Mike-lin300

最后更新:2026-09-24

更新说明:无