二维数组
引入:从一维到二维
一维数组像一排格子,适合存一条线上的数据,比如一个班的成绩。但如果要存一个班级多次考试的成绩,或者一个二维表格,就需要二维数组。
二维数组可以理解为“数组的数组”,或者说是带行和列的表格。
二维数组的声明、初始化与访问
声明
int scores[3][4]; // 3 行 4 列,共 12 个 int
第一个数字是行数,第二个数字是列数。
初始化
int a[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
也可以按一维方式写:
int a[2][3] = {1, 2, 3, 4, 5, 6};
但第一种写法更清晰,推荐初学时使用。
访问元素
#include <stdio.h>
int main(void) {
int a[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
printf("a[0][0] = %d\n", a[0][0]);
printf("a[1][2] = %d\n", a[1][2]);
return 0;
}
输出:
a[0][0] = 1
a[1][2] = 6
行优先存储
二维数组在内存中其实是连续存放的,C 语言采用行优先(row-major order)方式:先存完第 0 行,再存第 1 行,以此类推。
对于 int a[2][3],内存中的顺序是:
a[0][0], a[0][1], a[0][2], a[1][0], a[1][1], a[1][2]
初学阶段你不用太关心底层细节,但知道这个有助于理解为什么二维数组可以按一维方式初始化,以及为什么作为函数参数时列数必须指定。
矩阵遍历
二维数组通常用两层 for 循环遍历:
#include <stdio.h>
int main(void) {
int a[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
for (int i = 0; i < 2; i++) { // 外层控制行
for (int j = 0; j < 3; j++) { // 内层控制列
printf("%d ", a[i][j]);
}
printf("\n");
}
return 0;
}
输出:
1 2 3
4 5 6
二维数组作为函数参数
把二维数组传给函数时,列数必须写明,行数可以省略:
#include <stdio.h>
void print_matrix(int rows, int cols, int a[][3]) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", a[i][j]);
}
printf("\n");
}
}
int main(void) {
int a[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
print_matrix(2, 3, a);
return 0;
}
为什么列数必须指定?因为函数内部计算元素位置时,需要知道每一行有多少个元素,才能正确跳过一整行。如果省略列数,编译器就不知道怎么偏移了。
更通用的写法
如果想让函数接受任意列数的二维数组,需要用到指针,这个我们后面再讲。初学阶段先掌握固定列数的写法。
锯齿数组:每行长度不同
二维数组要求每行的列数相同。如果你需要"每一行长度不同"的数据结构(比如存储不同长度的字符串列表),可以用指针数组来实现:
const char* fruits[] = {
"apple",
"banana",
"cherry"
};
这里 fruits[0] 指向字符串 "apple",fruits[1] 指向 "banana",每行的长度自然不同。这本质上是"数组的指针",和真正的二维数组在内存布局上不同,后面学完指针后会更容易理解。
常见错误与注意事项
- 混淆行列顺序:访问时先写行下标,再写列下标,
a[i][j]不要写成a[j][i]。 - 越界访问:比如
int a[2][3]; a[2][0] = 1;就越界了,有效行下标是 0 和 1。 - 函数参数省略列数:
void f(int a[][])是错误的。 - 把二维数组当一维用时要小心:虽然内存连续,但用
a[i * cols + j]这种写法需要强制转换,初学阶段不建议。
二维数组是“数组的数组”
int matrix[3][4];
它包含 3 个元素,每个元素的类型都是 int[4]。因此 matrix + 1 跨过的是整整一行,而不是一个 int。
int (*row)[4] = matrix;
printf("%d\n", row[1][2]);
函数参数必须让编译器知道每行宽度,才能计算 matrix[i][j] 的偏移:
void print_matrix(size_t rows, int matrix[][4]);
C99 还可以把列数写成先出现的参数:
void print_matrix(size_t rows, size_t cols, int matrix[rows][cols]);
动态二维数据有两种常见布局
- 一整块连续存储:
rows * cols个元素,通过data[i * cols + j]访问; - 行指针数组,每行分别分配。
连续布局只需一次主要分配,缓存友好,也更容易一次释放;分行布局允许每行长度不同,但需要处理多次分配失败和逐行释放。int ** 只适合第二种“指针的指针”布局,不能直接代表连续二维数组。
小结与下一篇预告
今天我们学习了二维数组的声明、初始化、访问和遍历,理解了行优先存储,还练习了把二维数组作为函数参数。二维数组非常适合处理表格和矩阵数据。
下一篇我们来学习字符数组和字符串,这是 C 语言中处理文本的基础,也是很多初学者容易出错的地方。