7、指针与二维数组:二维数组的指针表示、行指针与列指针、通过指针遍历二维数组
二维数组,说白了就是「数组的数组」。很多初学者一碰到二维数组和指针的组合就头大。我当年刚学的时候也绕了好一阵子,后来在做一个图像处理的项目时,才真正把这块给吃透了。今天咱们就把这个硬骨头啃下来。
7.1 二维数组的内存布局
先看一个最简单的二维数组:
int a[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
这个数组在内存里是怎么存的?按行优先,也就是先存第0行的4个元素,再存第1行的4个元素,最后存第2行的4个元素。说白了就是连续的一块内存,总共12个int。
我画了一张图,帮你理解这个布局:
你看,每一行其实就是一个一维数组。a[0] 是第0行的首地址,a[1] 是第1行的首地址,以此类推。它们之间相差 4 * sizeof(int) = 16 个字节。
7.2 行指针 vs 列指针
这里有个关键概念,我当年花了很长时间才搞明白——行指针和列指针的区别。
7.2.1 列指针
列指针就是指向单个元素的指针。比如:
int *p = &a[0][0]; // 指向第0行第0列
// 或者
int *p = a[0]; // a[0] 就是第0行的首地址,等价于 &a[0][0]
用列指针遍历二维数组,你得手动计算偏移量:
int *p = &a[0][0];
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 4; j++) {
printf("%d ", *(p + i*4 + j)); // 手动计算偏移
}
}
说白了,这就是把二维数组当成一维数组来访问。我早期做嵌入式LCD驱动时,经常用这种方式操作帧缓冲区——因为底层内存就是线性的。
7.2.2 行指针
行指针就高级一点了。它指向的是「一整行」,而不是单个元素。
int (*p)[4]; // 声明一个行指针,指向含有4个int的一维数组
p = a; // p 指向第0行
注意这里的写法:int (*p)[4],括号不能少。少了就变成 int *p[4],那是指针数组了——完全两码事。
我曾经在面试题里看到过这个陷阱:int *p[4] 和 int (*p)[4] 的区别。前者是「有4个int指针的数组」,后者是「指向有4个int的数组的指针」。写错一个括号,编译器不会报错,但逻辑全乱套了。
用行指针遍历二维数组就优雅多了:
int (*p)[4] = a;
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 4; j++) {
printf("%d ", *(*(p + i) + j)); // 或者 p[i][j]
}
}
你想想看,*(p + i) 拿到第i行的首地址,然后 *(*(p + i) + j) 就是第i行第j列的元素。这其实就是编译器处理 a[i][j] 时的底层逻辑。
7.3 通过指针遍历二维数组的三种方式
我总结了三种常用的遍历方式,每种都有它的适用场景。
| 方式 | 代码示例 | 适用场景 |
|---|---|---|
| 下标法 | a[i][j] |
最直观,可读性最好 |
| 列指针法 | *(p + i*cols + j) |
底层驱动、内存操作 |
| 行指针法 | *(*(p + i) + j) |
函数参数传递、矩阵运算 |
来看一个完整的例子:
#include <stdio.h>
int main() {
int a[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 方式1:下标法
printf("方式1:下标法\n");
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 4; j++) {
printf("%2d ", a[i][j]);
}
printf("\n");
}
// 方式2:列指针法
printf("\n方式2:列指针法\n");
int *p = &a[0][0];
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 4; j++) {
printf("%2d ", *(p + i*4 + j));
}
printf("\n");
}
// 方式3:行指针法
printf("\n方式3:行指针法\n");
int (*q)[4] = a;
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 4; j++) {
printf("%2d ", *(*(q + i) + j));
}
printf("\n");
}
return 0;
}
我在做嵌入式系统时,经常需要把二维数组作为函数参数传递。这时候行指针就特别有用。比如写一个矩阵乘法函数:void mat_mul(int (*a)[4], int (*b)[4], int (*c)[4], int n)。这样传参,函数内部可以直接用 a[i][j] 访问,代码清晰又高效。
7.4 二维数组作为函数参数
这里有个坑,我见过不少同事踩过。二维数组作为函数参数时,第二维的大小必须指定。
// 正确写法
void func1(int a[][4], int rows); // 第二维必须写
void func2(int (*a)[4], int rows); // 行指针形式
// 错误写法
void func3(int a[][]); // 编译错误
void func4(int **a); // 语义完全不同
为什么会这样?因为编译器需要知道每行有多少个元素,才能计算 a[i][j] 的地址。你想想看,如果不知道每行多长,a[1][0] 的地址怎么算?
如果你需要传递任意大小的二维数组,可以这样:
void func(void *data, int rows, int cols) {
int *p = (int *)data;
for(int i = 0; i < rows; i++) {
for(int j = 0; j < cols; j++) {
printf("%d ", *(p + i*cols + j));
}
}
}
这样就把二维数组「拍平」成一维来访问了。我在做图像处理时经常用这个技巧,因为图像数据在内存里本来就是连续存储的。
7.5 总结一下
二维数组的指针操作,核心就三点:
- 内存是连续的——按行优先存储,这是所有操作的基础
- 行指针 vs 列指针——行指针指向一整行,列指针指向单个元素
- 函数传参要指定第二维——否则编译器没法计算地址偏移
嗯,这块内容确实有点绕,但只要你动手写几个例子,把内存图画一画,很快就能掌握。我当年就是在纸上画了十几遍内存布局图,才彻底搞明白的。
公众号:蓝海资料掘金营,微信deep3321