39、二维数组:二维数组作为函数参数的传递方式
二维数组做参数,这事儿看着简单,但坑不少。
我记得刚工作那会儿,有个同事写了个函数,想把一个 3x4 的矩阵传进去处理。他直接写了 void func(int arr[3][4]),编译过了,跑起来却总出乱子。查了半天,原来是调用时传参方式搞混了。
嗯,今天咱们就把这个事儿彻底说清楚。
为什么二维数组传参这么特殊?
说白了,C 语言里数组名就是首元素地址。一维数组如此,二维数组也一样。
但二维数组的「首元素」是什么?不是 arr[0][0],而是 arr[0]——它本身就是一个一维数组。
所以 arr 的类型是 int (*)[4],指向一个包含 4 个 int 的数组。这个指针叫「数组指针」。
核心结论:二维数组传参,本质是传递一个指向一维数组的指针。形参必须知道第二维的大小。
三种常见的传递方式
我平时写代码,主要用下面这三种。每种都有适用场景,咱们一个一个看。
方式一:固定第二维大小
void func1(int arr[3][4]) {
for (int i = 0; i < 3; i++)
for (int j = 0; j < 4; j++)
arr[i][j] = i * 4 + j;
}
这种方式最直观。但缺点也很明显——第二维 4 是写死的。换个 3x5 的数组,这函数就用不了了。
我在项目中很少这么写,除非是固定大小的硬件寄存器映射表。
方式二:数组指针形式
void func2(int (*arr)[4], int rows) {
for (int i = 0; i < rows; i++)
for (int j = 0; j < 4; j++)
arr[i][j] = i * 4 + j;
}
这个写法跟方式一完全等价。编译器眼里,int arr[][4] 和 int (*arr)[4] 是一回事。
我个人习惯用这种。为什么?因为 int (*arr)[4] 明确告诉你:arr 是个指针,指向一个长度为 4 的 int 数组。语义更清晰。
小技巧:如果你觉得 int (*arr)[4] 看着别扭,可以用 typedef:
typedef int (*RowPtr)[4];
void func2(RowPtr arr, int rows);
这样可读性会好很多。
方式三:一维化处理(我最推荐的方式)
void func3(int *arr, int rows, int cols) {
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
arr[i * cols + j] = i * cols + j;
}
// 调用时:
int matrix[3][4];
func3(&matrix[0][0], 3, 4);
这种方式把二维数组当成一维数组处理。你想想看,数组在内存里本来就是线性存储的,matrix[2][3] 就是第 2*4+3=11 个元素。
这样做的好处是:函数完全通用,任何行列数的二维数组都能传。我在嵌入式项目里处理图像数据、矩阵运算时,几乎都用这种方式。
注意:调用时一定要传 &matrix[0][0],不能直接传 matrix。虽然两者地址值相同,但类型不同——matrix 是 int (*)[4],而 &matrix[0][0] 是 int*。类型不匹配,编译器会报警告。
我曾经踩过的坑
有一次,我需要把一个 5x5 的二维数组传给函数做卷积运算。我用了方式二,形参写的是 int (*arr)[5]。
后来需求变了,矩阵变成 5x8。我改了调用处的数组定义,却忘了改函数形参里的 5。结果编译没报错,运行结果全错——因为函数内部按 5 列去访问,实际数据是 8 列,索引全乱了。
从那以后,我只要不确定列数是否固定,一律用方式三。虽然多传两个参数,但安全、通用、不出错。
动态二维数组怎么传?
嵌入式里动态分配用得少,但偶尔也会遇到。比如用指针数组模拟二维数组:
int **create_matrix(int rows, int cols) {
int **m = malloc(rows * sizeof(int*));
for (int i = 0; i < rows; i++)
m[i] = malloc(cols * sizeof(int));
return m;
}
void func4(int **arr, int rows, int cols) {
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
arr[i][j] = i * cols + j;
}
这种写法形参是 int **arr,跟前面几种完全不同。注意:它不能接收 int arr[3][4] 这种静态二维数组,因为内存布局不一样。
总结一下:
- 静态二维数组传参,用
int (*arr)[N]或int arr[][N],N 必须已知 - 通用做法:一维化处理,传
int*加行列数 - 动态分配的指针数组,用
int **arr - 三种方式不能混用,类型必须严格匹配
一张图看懂
下面这张图总结了二维数组传参的几种方式及其关系:
避坑指南
- 不要用
int **接收静态二维数组。我曾经见过有人这么写,编译报警告也不管,结果运行时访问越界。静态二维数组的内存是连续的,而int**期望的是指针数组,两者布局完全不同。 - 形参里的第一维可以省略,第二维必须写。比如
void func(int arr[][4])是对的,void func(int arr[3][])是错的。为什么?因为编译器需要第二维大小来计算地址偏移。 - 用 sizeof 在函数内求数组大小?不行。形参里的
int arr[][4]本质上是个指针,sizeof(arr)返回的是指针大小,不是整个数组大小。我见过有人在这上面栽跟头,循环边界算错了。 - const 修饰要小心。如果函数不修改数组内容,建议加 const:
void func(const int (*arr)[4], int rows)。这样既能保护数据,也能让调用者明确知道你的意图。
我的个人习惯:
在嵌入式项目中,我几乎只用方式三(一维化处理)。原因很简单:
- 函数签名通用,不用为不同列数写多个版本
- 传参清晰,调用者一眼知道要传什么
- 配合
restrict关键字,编译器能更好地优化内存访问
当然,如果团队有统一规范,那就按规范来。代码是给人看的,一致性比花哨更重要。
二维数组传参,说白了就是搞清楚「类型」两个字。类型对了,怎么传都行;类型错了,编译能过,运行必崩。嗯,记住这一点,你就能少踩很多坑。
公众号:蓝海资料掘金营,微信 deep3321