第13章:二维数组与多维数组
数组这东西,咱们之前聊的都是一维的,就像一排储物柜。但现实世界哪有这么简单?你想想看,一张Excel表格、一个棋盘、一张图片——这些玩意儿天然就是二维的。所以,二维数组就派上用场了。
13.1 二维数组的定义
二维数组,说白了就是「数组的数组」。定义方式很简单:
// 语法:类型 数组名[行数][列数];
int matrix[3][4]; // 3行4列的二维数组
我个人习惯把行数想象成「有几排」,列数想象成「每排有几个格子」。上面这个matrix,就是3排,每排4个格子,总共12个int元素。
内存里怎么存的?C语言采用行优先存储。什么意思?就是先存完第0行的所有列,再存第1行,再存第2行……
内存布局示意:
matrix[0][0] → matrix[0][1] → matrix[0][2] → matrix[0][3] →
matrix[1][0] → matrix[1][1] → matrix[1][2] → matrix[1][3] →
matrix[2][0] → matrix[2][1] → matrix[2][2] → matrix[2][3]
嗯,这里要注意:matrix本身是一个二维数组名,它代表整个数组的首地址。但matrix[0]代表第0行的首地址,matrix[1]代表第1行的首地址。这两个值虽然数值上可能相等(如果第0行没有偏移),但类型不同——一个是「指向一维数组的指针」,一个是「指向int的指针」。这个区别,我当年踩过坑。
13.2 二维数组的初始化
初始化方式有好几种,我按常用程度列一下:
// 方式1:按行初始化(最推荐,清晰)
int arr1[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
// 方式2:线性初始化(C语言允许,但可读性差)
int arr2[2][3] = {1, 2, 3, 4, 5, 6};
// 方式3:部分初始化(未指定的元素自动补0)
int arr3[2][3] = {
{1, 2},
{4}
};
// 结果:{{1, 2, 0}, {4, 0, 0}}
// 方式4:省略行数(必须指定列数)
int arr4[][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
}; // 编译器自动推断行数为3
我的建议:永远使用方式1,按行用花括号括起来。代码是写给人看的,不是写给编译器看的。我曾经接手过一个项目,前辈用方式2初始化了一个5x8的数组,我数括号里的数字数到眼瞎。
13.3 二维数组的遍历
遍历二维数组,标准做法是双重循环:外层控制行,内层控制列。
#include <stdio.h>
int main() {
int matrix[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("%3d ", matrix[i][j]);
}
printf("\n"); // 每行结束换行
}
return 0;
}
输出效果:
1 2 3 4
5 6 7 8
9 10 11 12
为什么外层用行、内层用列?因为C语言是行优先存储。如果你反过来——外层列、内层行——虽然逻辑上没错,但CPU缓存命中率会下降。我在做图像处理项目时,一个简单的循环顺序调换,性能差了3倍。嗯,底层的东西,有时候就是差这么一点。
13.4 二维数组作为函数参数
这里有个坑,我必须说清楚。二维数组作为函数参数时,必须指定列数,行数可以省略。
// 正确写法
void printMatrix(int arr[][4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
// 错误写法(编译报错)
void badPrint(int arr[][], int rows) { // ❌ 列数不能省略
// ...
}
为什么会这样?因为编译器需要知道每行有多少个元素,才能计算arr[i][j]的内存地址。公式是:地址 = 首地址 + i * 列数 * 元素大小 + j * 元素大小。列数不知道,这个公式就没法算。
避坑指南:我曾经在写一个矩阵运算库时,把二维数组用int**传进去,然后直接用arr[i][j]访问——程序直接崩了。二维数组名和二级指针,根本不是一回事!二维数组是连续内存块,二级指针是指向指针的指针。别搞混。
13.5 多维数组的概念
二维之上,还有三维、四维……C语言支持任意维度的数组。定义方式就是继续加方括号:
// 三维数组:2个平面,每个平面3行4列
int cube[2][3][4];
// 四维数组(实际项目中极少用)
int hyper[2][3][4][5];
三维数组的内存布局:先存第0个平面的所有元素(按行优先),再存第1个平面的所有元素。说白了,就是「二维数组的数组」。
遍历三维数组需要三重循环:
int cube[2][3][4] = {0};
// 初始化一些值
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
for (int k = 0; k < 4; k++) {
cube[i][j][k] = i * 100 + j * 10 + k;
}
}
}
// 遍历
for (int i = 0; i < 2; i++) {
printf("平面 %d:\n", i);
for (int j = 0; j < 3; j++) {
for (int k = 0; k < 4; k++) {
printf("%3d ", cube[i][j][k]);
}
printf("\n");
}
printf("\n");
}
实际项目中,多维数组用得多吗?
二维数组:非常常见。图像处理(像素矩阵)、游戏地图、矩阵运算、表格数据……
三维数组:偶尔见到。比如3D游戏中的体素数据、医学影像(CT扫描)、视频帧序列(宽x高x通道)。
四维及以上:我工作十几年,几乎没见过。如果你需要四维数组,大概率是设计上可以优化的。
13.6 本章知识体系
下面这张图,帮你理清二维数组和多维数组的核心脉络:
13.7 避坑总结
最后,我把二维数组常见的坑列一下,都是我亲手踩过的:
| 常见错误 | 错误示例 | 正确做法 |
|---|---|---|
| 传参省略列数 | void func(int arr[][]) |
void func(int arr[][4]) |
| 混淆二维数组和二级指针 | int** p = matrix; |
int (*p)[4] = matrix; |
| 遍历顺序导致缓存不友好 | 外层列、内层行 | 外层行、内层列 |
| 初始化时行数写错 | int a[3][4] = {...} 但只给了2行数据 |
用int a[][4]让编译器自动推断 |
二维数组这东西,刚开始觉得绕,用多了就习惯了。记住一条:行优先,列指定,双重循环慢慢来。多维数组嘛,理解原理就好,实际工作中用到三维以上的场景真不多。
公众号:蓝海资料掘金营,微信deep3321