2. 数组进阶:二维数组与多维数组,数组作为函数参数传递的陷阱与正确姿势

好,咱们接着聊数组。上一章我们把一维数组的基本功打扎实了,现在该上点硬菜了。

二维数组、多维数组,还有那个让无数新手(甚至老手)翻车的“数组传参”。

说实话,我当年刚入行时,就在数组传参这个坑里栽过跟头。调试了一下午,最后发现是形参写错了。嗯,这种痛,我希望你别经历。

2.1 二维数组:从“一排”到“一个面”

一维数组就像一排储物柜,每个柜子有个编号。那二维数组呢?

说白了,就是一张表格。有行,有列。

比如你要存一个班30个学生、每个学生5门课的成绩。用一维数组?你得定义30个数组,或者自己算偏移量,麻烦得很。用二维数组就清爽了:

int scores[30][5];  // 30行5列

访问第i个学生、第j门课的成绩:scores[i][j]。直观吧?

2.1.1 内存里到底怎么排的?

你想想看,计算机的内存是一维的,只有一条地址线。那二维数组怎么存?

答案是:按行优先

什么意思?就是先把第0行的所有列存完,再存第1行,再存第2行……

我画个图你就明白了:

二维数组 int a[3][4] 的内存布局(按行优先) 行0 行1 行2 a[0][0] a[0][1] a[0][2] a[0][3] a[1][0] a[1][1] a[1][2] a[1][3] a[2][0] a[2][1] a[2][2] a[2][3] 地址递增 → 连续内存块 sizeof(a) = 3 × 4 × sizeof(int) = 48字节(假设int=4字节)

关键点:二维数组在内存中是连续存放的。a[0][3] 的后面紧跟着 a[1][0]。这意味着你可以用指针遍历整个数组,但要注意别越界。

2.1.2 初始化技巧

二维数组初始化有几种写法,我个人习惯用花括号嵌套,清晰:

// 完全初始化
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};

// 部分初始化(未指定的补0)
int matrix[2][3] = {{1, 2}, {4}};  
// 结果:{{1, 2, 0}, {4, 0, 0}}

// 也可以省略行数(编译器自动推断)
int matrix[][3] = {{1, 2, 3}, {4, 5, 6}};  // 自动推断为2行

小技巧:省略行数时,列数必须显式指定。因为编译器需要知道每行有多少个元素,才能正确计算偏移量。

2.2 多维数组:不止二维

C语言支持任意维度的数组。三维、四维……理论上你想几维就几维。

但说实话,我在实际项目中,三维以上几乎没用过。二维已经能解决90%的问题了。

三维数组长啥样?比如你要存一个立方体的数据:

int cube[5][4][3];  // 5层 × 4行 × 3列

访问方式:cube[层][行][列]。内存里还是按行优先,最右边的下标变化最快。

警告:多维数组的维度越多,内存消耗越大。一个 int cube[100][100][100] 就是 100×100×100×4 = 4,000,000 字节 ≈ 4MB。在嵌入式系统中,这可能是灾难。我在做单片机项目时,就见过同事因为定义了一个超大三维数组,导致栈溢出,程序直接跑飞。

2.3 数组作为函数参数:陷阱与正确姿势

好,重点来了。这部分我建议你打起精神,因为这是面试高频考点,也是实际开发中容易出bug的地方。

2.3.1 一维数组传参:数组名就是指针

先看个最简单的:

void print_array(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
}

int main() {
    int a[5] = {1, 2, 3, 4, 5};
    print_array(a, 5);  // 传数组名
    return 0;
}

这里有个陷阱:int arr[]int *arr 在函数参数中是等价的。你写 int arr[],编译器会把它当成指针处理。

为什么会这样?因为C语言设计时,为了效率,数组传参不会复制整个数组,而是传递首地址。所以 sizeof(arr) 在函数内部得到的是指针的大小(4或8字节),不是数组的大小。

这就是为什么你必须额外传一个 size 参数。 函数内部无法通过 sizeof 知道数组长度。

2.3.2 二维数组传参:必须指定列数

二维数组传参,坑就更多了。看代码:

// 正确写法
void print_matrix(int mat[][4], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", mat[i][j]);
        }
        printf("\n");
    }
}

// 错误写法(编译报错)
void print_matrix_bad(int mat[][], int rows) {  // ❌ 列数不能省略
    // ...
}

// 另一种正确写法:用指针数组
void print_matrix_ptr(int (*mat)[4], int rows) {  // 指向包含4个int的数组的指针
    // 用法同上
}

为什么必须指定列数?因为编译器需要知道每行有多少个元素,才能计算 mat[i][j] 的地址偏移量。

偏移量公式:地址 = 基地址 + i * 列数 * sizeof(int) + j * sizeof(int)

你看,列数在公式里是必须的。不告诉编译器列数,它就没法算地址。

我曾经在一个图像处理项目中,把二维数组传参时的列数写错了。结果图像数据全部错位,显示出来像万花筒一样。调试了整整一个下午,最后发现是函数声明和定义中列数不一致。嗯,从那以后,我养成了一个习惯:用 #define 定义列数,保证全局统一。

2.3.3 更灵活的方案:用一维数组模拟二维

如果你觉得二维数组传参太麻烦,有个更灵活的办法:用一维数组模拟二维。

void print_matrix_flat(int *mat, int rows, int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", mat[i * cols + j]);  // 手动计算偏移
        }
        printf("\n");
    }
}

int main() {
    int a[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
    print_matrix_flat(&a[0][0], 3, 4);  // 传首元素地址
    return 0;
}

这种方式的优点:

  • 函数签名清晰,行列数都作为参数
  • 可以处理任意大小的二维数据
  • 内存布局完全由你控制

缺点嘛,就是每次访问都要手动计算下标,稍微麻烦一点。但我觉得,为了灵活性和安全性,这点代价值得。

2.4 避坑指南:数组传参常见错误

我把这些年见过的坑总结一下,你对照着检查自己的代码:

错误类型 错误示例 后果 正确做法
函数内用sizeof求数组长度 void f(int a[]) { int n = sizeof(a)/sizeof(int); } 得到的是指针大小,不是数组大小 额外传size参数
二维数组传参省略列数 void f(int a[][]) 编译错误 必须指定列数,如 int a[][4]
传参类型不匹配 int a[3][4]; f(a); 但函数声明为 void f(int **a) 编译警告或运行时错误 int (*a)[4] 或一维模拟
越界访问 for (i=0; i<=rows; i++) 内存污染,程序崩溃 严格检查边界条件

我的个人习惯:对于需要传递二维数组的函数,我倾向于用一维模拟的方式。虽然多写几行下标计算代码,但函数签名清晰,调用者一目了然,也不容易出错。在团队协作中,这种“笨办法”往往比花哨的指针技巧更受欢迎。

2.5 本章小结

二维数组就是一张表格,内存里按行连续存放。多维数组同理,但别贪多,三维以上基本用不上。

数组传参的核心就一句话:数组名会退化为指针,函数内部不知道数组大小,你得自己告诉它。

二维数组传参必须指定列数,要么用 int a[][N],要么用 int (*a)[N],要么干脆用一维数组模拟。

好了,数组这块硬骨头,咱们啃得差不多了。下一章,我们聊聊字符串——那个让无数C程序员又爱又恨的东西。


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