指针与多维数组:二维数组的指针表示、行指针、列指针、指针数组与数组指针的区别
说实话,二维数组和指针的关系,是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字节 |
| 常见用途 | 遍历二维数组 | 存储多个字符串或指针 |
| 内存布局 | 指向一块连续的行存储区域 | 数组元素指向分散的内存地址 |
知识体系结构图
下面这张图帮你理清本章的核心逻辑:
实际项目中的应用
我在做嵌入式系统时,经常用行指针来处理图像数据。比如一个 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