指针与多维数组:二维数组的指针表示、行指针、列指针、指针数组与数组指针的区别

说实话,二维数组和指针的关系,是C语言里最容易让人绕晕的地方之一。我当年刚入行时,也在这上面栽过跟头。有一次调试一个图像处理算法,数据是二维的像素矩阵,我愣是用一维指针的方式去访问,结果数据全乱了。后来才明白——二维数组在内存里其实是一维的,只是编译器帮我们做了“分行”的假象。

今天咱们就把这块硬骨头啃下来。我会从二维数组的本质讲起,然后一步步拆解行指针、列指针、指针数组和数组指针。你跟着我的思路走,保证能理清楚。

二维数组的本质:一维数组的数组

先看一个简单的二维数组:

int arr[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

这个 arr 是什么类型?很多人会脱口而出“int[3][4]”。没错,但更本质的理解是:arr 是一个长度为3的数组,每个元素又是一个长度为4的int数组。

说白了,二维数组就是“数组的数组”。

内存布局上,这12个int是连续存放的:

地址: 0x00  0x04  0x08  0x0C  0x10  0x14  0x18  0x1C  0x20  0x24  0x28  0x2C
数据:  1     2     3     4     5     6     7     8     9    10    11    12
      └── 第0行 ──┘   └── 第1行 ──┘   └── 第2行 ──┘

嗯,这里要注意:二维数组在内存中是行优先存储的。也就是说,先存完第0行的所有列,再存第1行,以此类推。

行指针:指向一维数组的指针

行指针,也叫数组指针。它的类型是 int (*p)[4],意思是“指向一个长度为4的int数组的指针”。

int (*p)[4] = arr;  // p指向arr的第0行(一个长度为4的int数组)

为什么 arr 可以直接赋值给 int (*)[4]?因为 arr 在表达式中会退化为指向其首元素的指针。而 arr 的首元素是 arr[0],类型是 int[4],所以 arr 退化为 int (*)[4]

我个人习惯用行指针来遍历二维数组:

for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 4; j++) {
        printf("%d ", *(*(p + i) + j));  // 等价于 p[i][j]
    }
    printf("\n");
}

这里 p + i 跳过了 i 行,*(p + i) 得到第 i 行的首地址(类型是 int*),再加 j 就是第 i 行第 j 列的地址,再解引用就是值。

核心理解:行指针的加减运算,是以“行”为单位的。p+1 跳过一整行(4个int),而不是一个int。

列指针:指向单个元素的指针

列指针其实就是普通的 int* 指针。它指向二维数组中的某个具体元素。

int *q = &arr[0][0];  // 指向第0行第0列

用列指针遍历整个二维数组:

for (int i = 0; i < 3 * 4; i++) {
    printf("%d ", *(q + i));  // 按内存顺序访问所有元素
}

我在项目中遇到过一个问题:用列指针访问二维数组时,如果越过了行边界,不会自动换行。比如 q + 4 指向的是第1行第0列,但编译器不会帮你做任何检查。所以用列指针时,一定要自己控制好边界

避坑指南:我曾经在写矩阵乘法时,用列指针访问二维数组,结果索引算错了,访问到了数组之外的内存。调试了一下午才发现是偏移量多算了1。从那以后,我只要用列指针访问二维数组,都会先算清楚总元素个数,再循环。

指针数组:数组里存的是指针

指针数组,顾名思义,是一个数组,它的每个元素都是指针。

int a = 1, b = 2, c = 3;
int *arrPtr[3] = {&a, &b, &c};  // arrPtr[0] 是 int*,指向 a

指针数组常用于处理“不规则二维数组”——每行的列数不同。比如:

int row0[] = {1, 2, 3};
int row1[] = {4, 5};
int row2[] = {6, 7, 8, 9};

int *jagged[3] = {row0, row1, row2};

这样,jagged[0] 指向 row0,jagged[1] 指向 row1,每行的长度可以不同。这在处理字符串数组时特别常见:

char *names[] = {"Alice", "Bob", "Charlie"};  // 每个字符串长度不同

小技巧:指针数组和二维字符数组的区别在于内存布局。指针数组的每个字符串可以存放在内存的不同位置,而二维字符数组的所有字符是连续存放的。如果你需要频繁修改字符串内容,用二维字符数组更合适;如果只是读取,指针数组更灵活。

数组指针 vs 指针数组:别再搞混了

这两个东西的名字太像了,我当年也经常搞混。其实记住一个口诀就行:

  • 数组指针int (*p)[4] —— 它是一个指针,指向一个数组。
  • 指针数组int *p[4] —— 它是一个数组,里面存的是指针。

你看,括号的位置决定了优先级:

  • int (*p)[4]:先解引用 *p,说明 p 是指针;然后 [4] 说明指向的是长度为4的数组。
  • int *p[4]:先 p[4],说明 p 是数组;然后 * 说明元素类型是 int 指针。

你想想看,是不是括号一加,意思完全不一样了?

特性 数组指针 (int (*p)[4]) 指针数组 (int *p[4])
本质 指针 数组
元素类型 指向 int[4] 的指针 int*
sizeof(p) 指针大小(8字节,64位系统) 4 * 8 = 32字节
常见用途 遍历二维数组 存储多个字符串或指针
内存布局 指向一块连续的行存储区域 数组元素指向分散的内存地址

知识体系结构图

下面这张图帮你理清本章的核心逻辑:

指针与多维数组知识体系 二维数组 本质:数组的数组 行指针 int (*p)[4] 列指针 int *q 指针数组 int *p[4] 数组指针 int (*p)[4] 两者区别与对比 核心:理解指针的加减运算单位 行指针 +1 跳过一行,列指针 +1 跳过一个元素

实际项目中的应用

我在做嵌入式系统时,经常用行指针来处理图像数据。比如一个 320x240 的灰度图像,可以定义为:

uint8_t image[240][320];  // 240行,每行320个像素
uint8_t (*rowPtr)[320] = image;  // 行指针

// 处理第i行
for (int j = 0; j < 320; j++) {
    rowPtr[i][j] = some_filter(rowPtr[i][j]);
}

用行指针的好处是,代码可读性强,而且编译器能生成高效的地址计算代码。你想想看,如果我用 int* 去访问,每次都要手动计算 i * 320 + j,不仅容易出错,代码也不够直观。

个人建议:如果你要传递二维数组给函数,优先用行指针作为参数类型。这样函数内部可以直接用 arr[i][j] 的方式访问,清晰又安全。

好了,关于二维数组的指针表示,咱们就聊到这儿。记住一句话:搞清楚指针的“步长”——行指针的步长是一行,列指针的步长是一个元素。搞懂了这一点,二维数组的指针操作就难不倒你了。


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