2. 数组进阶:二维数组与多维数组,数组作为函数参数传递的陷阱与正确姿势
好,咱们接着聊数组。上一章我们把一维数组的基本功打扎实了,现在该上点硬菜了。
二维数组、多维数组,还有那个让无数新手(甚至老手)翻车的“数组传参”。
说实话,我当年刚入行时,就在数组传参这个坑里栽过跟头。调试了一下午,最后发现是形参写错了。嗯,这种痛,我希望你别经历。
2.1 二维数组:从“一排”到“一个面”
一维数组就像一排储物柜,每个柜子有个编号。那二维数组呢?
说白了,就是一张表格。有行,有列。
比如你要存一个班30个学生、每个学生5门课的成绩。用一维数组?你得定义30个数组,或者自己算偏移量,麻烦得很。用二维数组就清爽了:
int scores[30][5]; // 30行5列
访问第i个学生、第j门课的成绩:scores[i][j]。直观吧?
2.1.1 内存里到底怎么排的?
你想想看,计算机的内存是一维的,只有一条地址线。那二维数组怎么存?
答案是:按行优先。
什么意思?就是先把第0行的所有列存完,再存第1行,再存第2行……
我画个图你就明白了:
关键点:二维数组在内存中是连续存放的。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