指针与二维数组:行指针、列指针与遍历

二维数组和指针的关系,说实话,是很多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)

为什么会这样?我们来拆解一下:

  1. a+i:行指针,指向第i行
  2. *(a+i):解引用得到第i行的首地址,即列指针 a[i]
  3. *(a+i) + j:列指针偏移j个元素,得到 &a[i][j]
  4. *(*(a+i) + j):再解引用得到元素值 a[i][j]

你想想看,这个表达式虽然看起来复杂,但其实就是一层层剥洋葱。我在给团队做代码审查时,经常看到有人写 *(a+i*4+j) 这种错误用法——那是把二维数组当一维数组处理了,虽然有时候碰巧能工作,但语义完全不对。

注意: aa[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 a[3][4] 行指针:int (*p)[4] p+1 跳过一行(16字节) 列指针:int *q = a[0] q+1 跳过一个元素(4字节) a[i][j] = *(*(a+i) + j) 先取行,再取列 下标法:a[i][j] 最直观 行+列指针 最规范 一级指针遍历 最底层/高效 注意:三种方式本质相同,只是抽象层次不同

六、总结

二维数组的指针操作,说白了就是搞清楚两件事:

  • 行指针:一次跳一行,类型是 int (*)[N]
  • 列指针:一次跳一个元素,类型是 int *

我在实际项目中,80%的情况下用下标法,因为可读性最好。但在写底层驱动或者做性能优化时,会切换到指针方式。记住一点:不管用哪种方式,理解内存布局才是根本。你想想看,如果连数组在内存里怎么排的都不知道,指针运算怎么可能不出错?

一句话记住: 二维数组的指针,行指针管「行」,列指针管「列」,*(*(a+i)+j) 就是先找到第i行,再找到第j列。


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