指针与二维数组:行指针、列指针与遍历
二维数组和指针的关系,说实话,是很多C语言学习者的分水岭。我自己带过不少新人,发现大家普遍能理解一维数组的指针,但一碰到二维数组,脑子就开始打结。嗯,今天我们就把它彻底理清楚。
一、二维数组的本质:一维数组的数组
先问一个问题:int a[3][4] 到底是什么?
很多人以为它是一个3行4列的矩阵。从逻辑上看没错,但从内存布局来看,它其实是「一个包含了3个元素的数组,每个元素又是一个包含4个int的数组」。说白了,就是数组的数组。
我在项目中调试过一块嵌入式板子的显示缓冲区,就是用二维数组存的像素数据。当时为了优化刷新速度,必须搞清楚每一行在内存里的实际地址偏移量。如果搞混了行和列,画面就会花掉——嗯,我确实花过,后来再也不敢马虎了。
核心理解:
a的类型是int (*)[4]—— 行指针a[0]的类型是int *—— 列指针a[0][0]的类型是int—— 具体元素
二、行指针与列指针
这两个概念,我建议你死记硬背也要记住,因为面试和实战都经常遇到。
行指针
行指针指向二维数组的「一行」。比如 a 就是行指针,a+1 跳过一整行(4个int)。
int a[3][4] = {0};
int (*p)[4] = a; // p是行指针,指向包含4个int的数组
printf("%p\n", p); // 第0行首地址
printf("%p\n", p+1); // 第1行首地址,相差 4*sizeof(int) = 16字节
列指针
列指针指向一行中的某个元素。比如 a[0] 就是列指针,a[0]+1 只跳过一个int。
int *q = a[0]; // q是列指针,指向第0行第0列
printf("%p\n", q); // 第0行第0列地址
printf("%p\n", q+1); // 第0行第1列地址,相差 4字节
我的记忆技巧: 行指针用 int (*p)[N] 声明,列指针用 int *p 声明。写代码时如果搞混了,编译器会报类型不匹配的警告——别忽略它,我曾经忽略过一次,结果在ARM Cortex-M上跑出了数组越界,系统直接hard fault。
三、二维数组的指针表示
二维数组的元素 a[i][j] 可以用指针等价表示为:
a[i][j] 等价于 *(*(a+i) + j)
为什么会这样?我们来拆解一下:
a+i:行指针,指向第i行*(a+i):解引用得到第i行的首地址,即列指针a[i]*(a+i) + j:列指针偏移j个元素,得到&a[i][j]*(*(a+i) + j):再解引用得到元素值a[i][j]
你想想看,这个表达式虽然看起来复杂,但其实就是一层层剥洋葱。我在给团队做代码审查时,经常看到有人写 *(a+i*4+j) 这种错误用法——那是把二维数组当一维数组处理了,虽然有时候碰巧能工作,但语义完全不对。
注意: a 和 a[0] 的值(地址数值)可能相同,但类型不同。前者是行指针,后者是列指针。做指针运算时,步长不一样!
四、通过指针遍历二维数组
遍历二维数组,常见的有三种方式。我个人最推荐第三种,因为它最灵活。
方式一:下标法(最直观)
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", a[i][j]);
}
}
方式二:行指针 + 列指针(最规范)
int (*row)[4] = a;
for (int i = 0; i < 3; i++) {
int *col = *row; // 或 row[0]
for (int j = 0; j < 4; j++) {
printf("%d ", col[j]);
}
row++;
}
方式三:一级指针直接遍历(最底层)
int *p = &a[0][0];
for (int i = 0; i < 3 * 4; i++) {
printf("%d ", p[i]);
}
第三种方式在嵌入式开发中很常见。我记得有一次做LCD驱动,需要把整个帧缓冲区的数据刷到屏幕上。帧缓冲区就是二维数组,但DMA传输要求连续内存块。这时候直接用一级指针遍历,效率最高。
避坑指南: 我曾经在STM32项目里用方式三遍历一个 uint8_t frame[240][320],结果发现编译器优化后,p[i] 的访问顺序和数组存储顺序不一致——因为二维数组是行优先存储的,但我的指针偏移写反了。从那以后,我每次用一级指针遍历二维数组,都会先画个内存布局图确认一下。
五、知识体系结构图
下面这张图总结了本章的核心逻辑,建议你保存下来,写代码前看一眼。
六、总结
二维数组的指针操作,说白了就是搞清楚两件事:
- 行指针:一次跳一行,类型是
int (*)[N] - 列指针:一次跳一个元素,类型是
int *
我在实际项目中,80%的情况下用下标法,因为可读性最好。但在写底层驱动或者做性能优化时,会切换到指针方式。记住一点:不管用哪种方式,理解内存布局才是根本。你想想看,如果连数组在内存里怎么排的都不知道,指针运算怎么可能不出错?
一句话记住: 二维数组的指针,行指针管「行」,列指针管「列」,*(*(a+i)+j) 就是先找到第i行,再找到第j列。
公众号:蓝海资料掘金营,微信deep3321