一、回顾与本篇目标
上一篇你学会了 C 语言的条件判断和循环。现在你的程序已经能根据不同的条件执行不同的逻辑,也能重复执行某段代码了。
但有一个问题:如果要存储一个班 50 个学生的成绩,你怎么办?定义 50 个变量?score1、score2、score3……那如果要计算平均分,你要把 50 个变量一个一个写出来。这显然不现实。
你需要的是数组——一种能把多个相同类型的数据打包放在一起的数据结构。数组在几乎所有编程语言里都有,但 C 语言的数组和 JavaScript/Python 的数组有本质的不同。理解 C 语言的数组,是后续理解指针和内存管理的关键一步。
本篇的目标:
- 理解 C 语言数组的本质——连续的内存空间
- 学会声明、初始化、访问数组
- 掌握数组和循环的配合使用
- 理解数组越界的危险性
- 学会二维数组的基本用法
二、为什么需要数组
先看一个没有数组的反面例子:
// 没有数组:存储 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]
这种连续内存布局有三个好处:
- 访问速度快:通过
首地址 + 索引 × 元素大小的公式,CPU 可以在一次计算后直接访问任意元素。这就是所谓的随机访问(Random Access)。 - 缓存友好:CPU 读取内存时会一次读取连续的一块。访问
arr[0]时,arr[1]、arr[2]可能已经被顺便加载到缓存中了。 - 指针运算:可以用指针算术来遍历数组(这是 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++ 零基础入门,每周更新。












暂无评论内容