上一篇文章我们学会了一维数组——它像一排连续的抽屉,整齐地排成一条直线。但现实中的数据往往比“一排”更复杂:课程表有行有列,棋盘有纵横坐标,图像是由像素组成的矩形网格。这时候,一维数组就有些力不从心了。
好在 C 语言允许我们把这些“行”再堆叠起来,形成多维数组。二维数组用得最多,三维偶尔露脸,四维以上更多是理论存在。今天就主要攻克二维,再浅探多维,让你以后碰到矩阵运算、图像处理、游戏棋盘之类的问题能心中有数。
一、二维数组的声明与理解
1. 声明方式
类型 数组名[行数][列数];
比如,存储一个班级 3 门课、5 个学生的成绩表:
int scores[3][5]; // 3 行 5 列
它对应这样一张表:
学生0 学生1 学生2 学生3 学生4
课程0: ? ? ? ? ?
课程1: ? ? ? ? ?
课程2: ? ? ? ? ?
逻辑上我们把它理解成“行”和“列”,但在物理内存中,它依然是一维连续排列的。C 语言用的是行优先存储:先完整存第 0 行,然后紧接着存第 1 行,再存第 2 行……像把一张表格一行行拆开,拼成一条长链。
内存顺序: [0][0] [0][1] [0][2] [0][3] [0][4] [1][0] [1][1] … [2][4]
这个知识点非常重要。因为行优先,访问 scores[i][j] 时,编译器实际上计算的是:
地址偏移 = i * 列数 + j
知道这个原理,你以后写遍历循环时,就会把行循环放外层、列循环放内层——尽量让内存访问连续,能大幅提升性能(缓存友好)。
2. 初始化二维数组
完全初始化(嵌套花括号):
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
结果为:
第0行: 1, 2, 3
第1行: 4, 5, 6
部分初始化(未指定的自动为 0):
int matrix[2][3] = {
{1, 2} // 第0行:1, 2, 0;第1行:全0
};
省略内层花括号(不推荐,可读性差):
int matrix[2][3] = {1, 2, 3, 4, 5, 6}; // 按行优先依次填充
省略第一维大小(编译器根据初始化列表推断):
int matrix[][3] = {
{1, 2, 3},
{4, 5, 6}
}; // 自动推断为 2 行 3 列
注意:只有最左边的维度可以省略,其他维度必须写清楚,否则编译器算不出地址偏移公式。
二、访问二维数组元素
通过 数组名[行下标][列下标] 访问,下标都从 0 开始。
int matrix[2][3] = {
{10, 20, 30},
{40, 50, 60}
};
printf("%d\\n", matrix[0][1]); // 20
matrix[1][2] = 99; // 修改第1行第2列
遍历二维数组通常用嵌套的 for 循环:
#include <stdio.h>
int main(void) {
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
for (int i = 0; i < 2; i++) { // 外层遍历行
for (int j = 0; j < 3; j++) { // 内层遍历列
printf("%d ", matrix[i][j]);
}
printf("\\n"); // 每行结束后换行
}
return 0;
}
输出:
1 2 3
4 5 6
遍历顺序很重要:外层是行、内层是列,才能让内存访问是连续的(先 [0][0]、[0][1]、[0][2],然后 [1][0]…)。如果反过来外层列、内层行,CPU 缓存会频繁落空,在大数据量下性能差距可达数倍。记住一个原则:让内层循环对应最右边的下标。
三、二维数组作为函数参数
把二维数组传给函数时,必须告诉编译器除了最左边维度以外的所有维度大小。
void print_matrix(int rows, int cols, int mat[][cols]) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", mat[i][j]);
}
printf("\\n");
}
}
注意参数 int mat[][cols]:
- 最左边的维度(行)可以省略,写成空的 []。
- 列的大小必须给定,因为编译器需要它来计算 i * cols + j 的偏移。
调用时:
int matrix[2][3] = {{1,2,3}, {4,5,6}};
print_matrix(2, 3, matrix);
四、经典案例:矩阵运算
矩阵加法是最直观的二维数组应用——两个相同大小的矩阵,对应元素相加。
#include <stdio.h>
#define ROWS 2
#define COLS 3
void add_matrices(int a[ROWS][COLS], int b[ROWS][COLS], int result[ROWS][COLS]) {
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
result[i][j] = a[i][j] + b[i][j];
}
}
}
void print_matrix(int rows, int cols, int mat[][cols]) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%3d ", mat[i][j]);
}
printf("\\n");
}
}
int main(void) {
int A[ROWS][COLS] = {
{1, 2, 3},
{4, 5, 6}
};
int B[ROWS][COLS] = {
{6, 5, 4},
{3, 2, 1}
};
int C[ROWS][COLS] = {0};
add_matrices(A, B, C);
printf("矩阵 A:\\n");
print_matrix(ROWS, COLS, A);
printf("\\n矩阵 B:\\n");
print_matrix(ROWS, COLS, B);
printf("\\nA + B = \\n");
print_matrix(ROWS, COLS, C);
return 0;
}
输出:
矩阵 A:
1 2 3
4 5 6
矩阵 B:
6 5 4
3 2 1
A + B =
7 7 7
7 7 7
%3d 让每个数字占 3 个字符宽度,排版整齐。二维数组在数值计算、图像处理中无处不在——图像本身就是一个像素值的二维数组,核心操作几乎都是对行列的遍历。
五、多维数组:三维及更高
二维之上,可以扩展出三维、四维乃至更高维度的数组。三维数组可以理解为“多页表格”——每一“页”是一个二维矩阵。
// 2页,每页3行4列
int cube[2][3][4] = {
{ // 第0页
{1, 2, 3, 4},
{5, 6, 7, 8},
{9,10,11,12}
},
{ // 第1页
{13,14,15,16},
{17,18,19,20},
{21,22,23,24}
}
};
遍历三重循环:
for (int p = 0; p < 2; p++) {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%2d ", cube[p][i][j]);
}
printf("\\n");
}
printf("—\\n");
}
更高维度同理,层层嵌套。但维度越高,对内存的消耗呈指数增长,实际开发中很少超过三维。保持对高维数组“层层嵌套”的感知即可。
六、字符串数组与命令行参数预告
二维 char 数组的一个典型用途是存储多个字符串:
char names[3][20] = {"Alice", "Bob", "Charlie"};
这里 names[0] 是一个长度为 20 的字符数组,存放 "Alice"(带 \\0 结尾)。后面我们讲到字符串、指针数组时,会深入这个主题,包括 argv 命令行参数的本质。
七、常见错误与陷阱
下标顺序搞反
int mat[3][5];
mat[0][1] = 10; // 正确
mat[1][0] = 20; // 正确
// 不要写成 mat[1,0],那是逗号运算符,结果是 0
给二维数组赋值忘记列下标
int mat[2][3];
mat[0] = {1,2,3}; // 错误!数组名不能赋值
只能逐个元素赋值,或者在声明时整体初始化。
传递二维数组时漏写列数
void func(int mat[][]) // 错误!必须指定列数
void func(int mat[3][]) // 也错!列数必须给出
void func(int mat[][5]) // 正确
把 sizeof 用在函数参数里的二维数组
void func(int mat[][5]) {
int rows = sizeof(mat) / sizeof(mat[0]); // 错误!mat 是指针
}
和一维数组一样,函数参数中的数组名退化为指针,sizeof 失效。
遍历时行列循环写反导致性能骤降
内层循环应遍历最右边的下标,保证内存访问连续。
八、小结
今天你把一维的“队伍”扩展成了二维的“矩阵”,还学会了更高维度的基本概念。二维数组是图像处理、游戏开发、科学计算中的核心数据结构,矩阵的加法、乘法、转置都建立在它之上。你学到的“行优先存储”和遍历原则,以后也会直接影响程序的运行速度。
现在我们已经能管理大量的数据了,但代码依然堆在 main 函数里。当程序逐渐庞大,单靠 main 是远远不够的——你需要把功能拆分成可复用的模块。下一篇文章,我们就进入函数的世界:如何把代码封装成函数,如何传递参数,如何获得返回值。它会让你的代码从“长篇大论”进化成“团队协作”。
课后小练习
我们下期见!





