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。

我画了一张图,帮你理解这个布局:

二维数组 int a[3][4] 内存布局 a[0] a[0][0] a[0][1] a[0][2] a[0][3] a[1] a[1][0] a[1][1] a[1][2] a[1][3] a[2] a[2][0] a[2][1] a[2][2] a[2][3] 地址: 0x1000 0x1010 0x1020

你看,每一行其实就是一个一维数组。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