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。虽然两者地址值相同,但类型不同——matrixint (*)[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 arr[3][4] 方式二:数组指针 int (*arr)[4] 方式三:一维化处理 int *arr + rows + cols 第二维固定 可读性好 灵活性差 第二维固定 语义清晰 可传行数 完全通用 行列任意 需手动索引 ⚠ 三种方式不能混用,类型必须严格匹配 静态数组 → 方式一/二 | 动态指针数组 → int** | 通用场景 → 方式三

避坑指南

  • 不要用 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)。这样既能保护数据,也能让调用者明确知道你的意图。

我的个人习惯:

在嵌入式项目中,我几乎只用方式三(一维化处理)。原因很简单:

  1. 函数签名通用,不用为不同列数写多个版本
  2. 传参清晰,调用者一眼知道要传什么
  3. 配合 restrict 关键字,编译器能更好地优化内存访问

当然,如果团队有统一规范,那就按规范来。代码是给人看的,一致性比花哨更重要。

二维数组传参,说白了就是搞清楚「类型」两个字。类型对了,怎么传都行;类型错了,编译能过,运行必崩。嗯,记住这一点,你就能少踩很多坑。


公众号:蓝海资料掘金营,微信 deep3321