四:数组——把相同类型的数据打包在一起

一、回顾与本篇目标

上一篇你学会了 C 语言的条件判断和循环。现在你的程序已经能根据不同的条件执行不同的逻辑,也能重复执行某段代码了。

但有一个问题:如果要存储一个班 50 个学生的成绩,你怎么办?定义 50 个变量?score1score2score3……那如果要计算平均分,你要把 50 个变量一个一个写出来。这显然不现实。

你需要的是数组——一种能把多个相同类型的数据打包放在一起的数据结构。数组在几乎所有编程语言里都有,但 C 语言的数组和 JavaScript/Python 的数组有本质的不同。理解 C 语言的数组,是后续理解指针和内存管理的关键一步。

本篇的目标:

  1. 理解 C 语言数组的本质——连续的内存空间
  2. 学会声明、初始化、访问数组
  3. 掌握数组和循环的配合使用
  4. 理解数组越界的危险性
  5. 学会二维数组的基本用法

二、为什么需要数组

先看一个没有数组的反面例子:

// 没有数组:存储 5 个学生的成绩
int score1 = 85;
int score2 = 92;
int score3 = 78;
int score4 = 95;
int score5 = 88;

// 计算平均分——代码冗长
int total = score1 + score2 + score3 + score4 + score5;
float average = total / 5.0;
printf("平均分:%.1f\n", average);

如果有 50 个学生,这段代码会变得无法维护。用数组改写:

// 用数组:存储 5 个学生的成绩
int scores[5] = {85, 92, 78, 95, 88};

// 计算平均分——用循环,简洁且可扩展
int total = 0;
for (int i = 0; i < 5; i++) {
    total += scores[i];
}
float average = total / 5.0;
printf("平均分:%.1f\n", average);

如果学生人数变成 50,只需要把 5 改成 50,代码逻辑完全不变。这就是数组的力量。

三、数组的声明和初始化

声明数组

类型 数组名[大小];

大小必须是一个常量,在声明时就确定,且之后不能改变。这是 C 语言数组和 Python 列表/JavaScript 数组最大的区别——C 的数组是固定大小的。

int scores[5];         // 声明一个能存放 5 个 int 的数组
float prices[10];      // 声明一个能存放 10 个 float 的数组
char letters[26];      // 声明一个能存放 26 个 char 的数组

初始化数组

可以在声明的同时初始化:

// 方式一:完整初始化
int scores[5] = {85, 92, 78, 95, 88};

// 方式二:部分初始化(未指定的元素自动设为 0)
int numbers[5] = {10, 20};        // {10, 20, 0, 0, 0}

// 方式三:不指定大小,编译器自动推导
int scores[] = {85, 92, 78, 95, 88};  // 自动推导大小为 5

// 方式四:全初始化为 0
int zeros[100] = {0};             // 所有 100 个元素都是 0

注意:如果声明数组时没有初始化,数组里存的是垃圾值(这块内存之前残留的数据):

int data[5];    // 未初始化,里面是垃圾值
for (int i = 0; i < 5; i++) {
    printf("%d\n", data[i]);  // 输出不确定的随机数
}

建议:声明数组时始终初始化。至少写成 int arr[5] = {0}; 把所有元素初始化为 0。

四、访问数组元素

数组的索引从 0 开始。一个有 N 个元素的数组,有效索引是 0 到 N-1

int scores[5] = {85, 92, 78, 95, 88};

printf("%d\n", scores[0]);  // 85 —— 第一个元素
printf("%d\n", scores[1]);  // 92 —— 第二个元素
printf("%d\n", scores[4]);  // 88 —— 最后一个元素

// 修改数组元素
scores[0] = 100;
printf("%d\n", scores[0]);  // 100

和 Python/JavaScript 的对比

操作 JavaScript Python C
创建数组 let arr = [1, 2, 3] arr = [1, 2, 3] int arr[] = {1, 2, 3}
访问元素 arr[0] arr[0] arr[0]
数组大小 动态(可以增减) 动态(可以增减) 固定(声明时确定)
越界检查 返回 undefined 抛出异常 没有任何检查!
获取长度 arr.length len(arr) sizeof(arr) / sizeof(arr[0])

用 sizeof 计算数组长度

C 语言没有 arr.length 这种属性。你需要用 sizeof 手动计算:

int arr[] = {10, 20, 30, 40, 50};

// sizeof(arr) 是整个数组占用的字节数
// sizeof(arr[0]) 是一个元素占用的字节数
int length = sizeof(arr) / sizeof(arr[0]);

printf("数组长度:%d\n", length);  // 5

注意:这个技巧只对在同一个作用域内声明的数组有效。如果把数组传给函数(后面会讲),sizeof 会失效——因为数组会退化成指针。这是 C 语言中一个重要的知识点,我们后面会在函数篇和指针篇详细讨论。

五、数组越界:C 语言最危险的陷阱

这是 C 语言和高级语言最让人不安的区别之一:C 不会检查数组索引是否越界。

int arr[5] = {10, 20, 30, 40, 50};

// 读取越界:访问不存在的第 6 个元素
printf("%d\n", arr[5]);  // 编译不会报错!输出垃圾值(或者程序崩溃)

// 写入越界:修改不属于这个数组的内存
arr[10] = 999;           // 编译不会报错!可能覆盖了其他变量的值!

为什么 C 不检查越界?

因为 C 的设计哲学是追求性能。每次访问数组都检查索引是否合法,会消耗额外的 CPU 指令。C 把这个责任交给了程序员:你要确保自己不会越界。

数组越界的后果:

  • 读取越界:读到垃圾值,程序逻辑出错。
  • 写入越界:覆盖了其他变量的内存,导致程序行为变得莫名其妙。这种 bug 极难定位——你可能改了数组的某个元素,结果另一个变量的值突然变了,因为它们的内存恰好在数组后面。
  • 严重越界:触碰到操作系统保护的内存区域,程序崩溃(段错误 Segmentation Fault)。

防范措施

  • 始终记住数组的大小。
  • 循环遍历时,条件写 i < length,不要写 i <= length
  • 把数组长度存到一个变量中,遍历时使用这个变量。
int arr[5] = {10, 20, 30, 40, 50};
int len = 5;

for (int i = 0; i < len; i++) {  // 正确:i < len
    printf("%d\n", arr[i]);
}

// for (int i = 0; i <= len; i++) {  // 错误!i 等于 5 时越界

六、数组在内存中是如何存放的

理解数组的内存布局,对后续学习指针至关重要。

C 语言的数组在内存中是一块连续的存储空间。数组的每个元素紧挨着下一个元素,中间没有空隙。数组的名字本质上是一个指向数组首元素的指针(这是一个非常重要的概念,后面指针篇会展开讲)。

int arr[5] = {10, 20, 30, 40, 50};

// 内存布局(假设 int 占 4 字节,arr 从地址 1000 开始):
// 地址 1000:10  ← arr[0],也是 arr 指向的位置
// 地址 1004:20  ← arr[1]
// 地址 1008:30  ← arr[2]
// 地址 1012:40  ← arr[3]
// 地址 1016:50  ← arr[4]

这种连续内存布局有三个好处:

  1. 访问速度快:通过 首地址 + 索引 × 元素大小 的公式,CPU 可以在一次计算后直接访问任意元素。这就是所谓的随机访问(Random Access)。
  2. 缓存友好:CPU 读取内存时会一次读取连续的一块。访问 arr[0] 时,arr[1]arr[2] 可能已经被顺便加载到缓存中了。
  3. 指针运算:可以用指针算术来遍历数组(这是 C 语言的特色,后面会学)。

相比之下,JavaScript 和 Python 的“数组”在底层并不是简单的连续内存——它们是更复杂的数据结构,支持动态扩容、混合类型。这也解释了为什么 C 的数组更快,但更“简陋”。

七、数组和循环的配合:遍历、求和、查找

数组和循环是天生的搭档。数组提供数据存储,循环提供遍历能力。

遍历数组

#include <stdio.h>

int main() {
    int scores[] = {85, 92, 78, 95, 88};
    int len = sizeof(scores) / sizeof(scores[0]);

    printf("所有成绩:");
    for (int i = 0; i < len; i++) {
        printf("%d ", scores[i]);
    }
    printf("\n");
    return 0;
}

数组求和与平均值

int scores[] = {85, 92, 78, 95, 88};
int len = sizeof(scores) / sizeof(scores[0]);
int total = 0;

for (int i = 0; i < len; i++) {
    total += scores[i];
}

float average = (float)total / len;
printf("总分:%d,平均分:%.1f\n", total, average);

查找最大值和最小值

int scores[] = {85, 92, 78, 95, 88};
int len = sizeof(scores) / sizeof(scores[0]);
int max = scores[0];
int min = scores[0];

for (int i = 1; i < len; i++) {
    if (scores[i] > max) {
        max = scores[i];
    }
    if (scores[i] < min) {
        min = scores[i];
    }
}

printf("最高分:%d,最低分:%d\n", max, min);

查找指定元素

int scores[] = {85, 92, 78, 95, 88};
int len = sizeof(scores) / sizeof(scores[0]);
int target = 95;
int found = -1;  // -1 表示没找到

for (int i = 0; i < len; i++) {
    if (scores[i] == target) {
        found = i;
        break;  // 找到了就停止
    }
}

if (found != -1) {
    printf("找到 %d,在索引 %d 的位置\n", target, found);
} else {
    printf("未找到 %d\n", target);
}

八、二维数组

二维数组就是“数组的数组”——可以理解为一张有行有列的表格。

声明和初始化

// 声明一个 3 行 4 列的二维数组
int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

// 也可以平铺写(和上面等价)
int matrix2[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};

// 部分初始化
int matrix3[3][4] = {
    {1, 2},       // 第 0 行:1, 2, 0, 0
    {5},          // 第 1 行:5, 0, 0, 0
    {9, 10, 11}   // 第 2 行:9, 10, 11, 0
};

访问二维数组元素

int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

printf("%d\n", matrix[0][0]);  // 1 —— 第 0 行第 0 列
printf("%d\n", matrix[1][2]);  // 7 —— 第 1 行第 2 列
printf("%d\n", matrix[2][3]);  // 12 —— 第 2 行第 3 列

用嵌套循环遍历二维数组

int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

int rows = 3;
int cols = 4;

for (int i = 0; i < rows; i++) {
    for (int j = 0; j < cols; j++) {
        printf("%3d ", matrix[i][j]);
    }
    printf("\n");  // 每行结束后换行
}

输出:

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

%3d 表示每个数字占 3 个字符的宽度,让列对齐。

二维数组在内存中的布局

虽然二维数组在概念上是“行 × 列”的表格,但在物理内存中,它仍然是一块连续的内存。数据按照行优先的顺序存放——先放第 0 行的所有元素,再放第 1 行的所有元素,依此类推。

int matrix[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};

// 内存布局:
// 地址 1000:1  ← matrix[0][0]
// 地址 1004:2  ← matrix[0][1]
// 地址 1008:3  ← matrix[0][2]
// 地址 1012:4  ← matrix[0][3]
// 地址 1016:5  ← matrix[1][0]
// ...

九、综合演示:学生成绩管理系统

下面这段代码综合运用了数组、循环、条件判断、输入输出:

#include <stdio.h>

int main() {
    int num_students;

    printf("请输入学生人数:");
    scanf("%d", &num_students);

    // 声明数组(注意:这里用了变长数组,C99 支持)
    int scores[num_students];

    // 输入成绩
    printf("\n请逐个输入 %d 个学生的成绩:\n", num_students);
    for (int i = 0; i < num_students; i++) {
        printf("学生 %d:", i + 1);
        scanf("%d", &scores[i]);
    }

    // 计算总分、平均分、最高分、最低分
    int total = 0;
    int max = scores[0];
    int min = scores[0];

    for (int i = 0; i < num_students; i++) {
        total += scores[i];
        if (scores[i] > max) max = scores[i];
        if (scores[i] < min) min = scores[i];
    }

    float average = (float)total / num_students;

    // 统计各等级人数
    int excellent = 0, good = 0, pass = 0, fail = 0;
    for (int i = 0; i < num_students; i++) {
        if (scores[i] >= 90) excellent++;
        else if (scores[i] >= 80) good++;
        else if (scores[i] >= 60) pass++;
        else fail++;
    }

    // 输出统计结果
    printf("\n===== 成绩统计 =====\n");
    printf("总分:%d\n", total);
    printf("平均分:%.1f\n", average);
    printf("最高分:%d\n", max);
    printf("最低分:%d\n", min);
    printf("\n等级分布:\n");
    printf("  优秀(>=90):%d 人\n", excellent);
    printf("  良好(80-89):%d 人\n", good);
    printf("  及格(60-79):%d 人\n", pass);
    printf("  不及格(<60):%d 人\n", fail);

    return 0;
}

代码解析:

  • 变长数组int scores[num_students] 用变量来指定数组大小。这是 C99 标准引入的特性。在早期的 C 语言中,数组大小必须是编译时就能确定的常量。如果你的编译器报错,可以改用 int scores[100] 这种固定大小。
  • 成绩的输入、计算、统计各自独立成段,逻辑清晰。
  • 类型转换 (float)total 确保除法得到浮点数结果。

十、本篇动手练习

练习 1:数组反转

新建 practice4-1.c,声明一个包含 10 个整数的数组并初始化。写代码把这个数组的元素顺序反转(第一个和最后一个交换,第二个和倒数第二个交换……),然后输出反转后的数组。

练习 2:冒泡排序

新建 practice4-2.c,声明一个无序的整数数组。用冒泡排序算法把数组从小到大排序,输出排序后的结果。冒泡排序的逻辑:相邻元素两两比较,如果前面的比后面的大就交换位置。重复多轮,直到没有任何交换发生。

练习 3:矩阵转置

新建 practice4-3.c,声明一个 3×3 的二维数组,输出转置后的矩阵。转置就是把行变成列、列变成行——matrix[i][j] 变成 matrix[j][i]

练习 4:统计字符频率

新建 practice4-4.c,让用户输入一段英文(以回车结束),统计 26 个英文字母各出现了多少次。提示:用一个长度为 26 的数组,count[0] 存 a 的次数,count[1] 存 b 的次数……字符可以通过 ch - 'a' 转换成索引。

十一、本篇小结

这一篇你学会了 C 语言中最重要的数据结构之一——数组:

  • 数组的本质:一块连续的内存空间,存放相同类型的元素。大小在声明时确定,之后不能改变。
  • 声明和初始化类型 数组名[大小]。可以用 {} 初始化,部分未指定的元素自动为 0。建议始终初始化。
  • 访问元素:索引从 0 开始,arr[0]arr[N-1]。用 sizeof(arr) / sizeof(arr[0]) 计算长度。
  • 越界问题:C 语言不检查数组越界。访问或修改越界的元素可能导致程序崩溃或数据损坏。这是需要时刻注意的风险。
  • 内存布局:数组在内存中是连续的,支持快速随机访问。数组名是指向首元素的指针。
  • 二维数组类型 数组名[行数][列数]。内存中按行优先顺序存放。用嵌套循环遍历。
  • 数组和循环:天生搭档。遍历、求和、查找、统计——数组提供数据,循环提供遍历能力。

数组是 C 语言中最基础也最常用的数据结构。下一篇,我们学习 C 语言中让很多人望而却步但无比强大的特性——指针。你会理解什么叫“直接操作内存地址”,也会理解为什么数组名其实就是一个指针。

下一篇预告

下一篇——《指针——直接操作内存地址的能力》:什么是指针、为什么需要它、& 取地址和 * 解引用运算符、指针和数组的关系、指针的算术运算、指针作为函数参数传递。这是 C 语言最核心、最强大、也最需要花时间理解的一章。

C/C++ 零基础入门,每周更新。

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容