云计算百科
云计算领域专业知识百科平台

二维数组用int *p为什么不行?那个括号到底怎么读?

1. 一个编译器的"嫌弃"

自从昨天(Day12)搞懂了 int *p = a; 能遍历一维数组,我今天就有点"飘了"。练习时遇到一个 int a[3][4],我想都没想,下意识就敲出了同一招:

int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
int *p = a; // 昨天不都这么用吗?没问题吧?

结果,编译器"啪"一下甩给我一个警告:"assignment from incompatible pointer type" —— 类型不兼容!当时就给我整清醒了。今天就来死磕这个"类型不兼容"背后到底藏着什么。

2. 暂停一下,先想清楚:int *p = a 凭啥能管一维数组?

在搞懂二维数组的报错前,我必须先死磕一个关键点:指针的类型,是由"首元素"的类型决定的。

比如一维数组 int a[4],它的首元素 a[0] 是个普普通通的 int。所以,&a[0] 就是 int * 类型。那 int *p = a; 当然天经地义,p 知道自己一次走 4 个字节(一个 int 的大小),靠 *(p+i) 就能顺藤摸瓜。

好了,核心思想有了:指针,要和"首元素"的类型对齐。 带着这个标准,我们再去审视二维数组,看看它的"首元素"到底是个啥。

3. 我的错误写法以及为什么错

3.1 错误写法

回到一开始那个让我抓狂的代码:

int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
int *p = a; // 编译警告:不兼容的指针类型

我心里还在想:数组名 a 不就是首元素地址吗?在一维数组里就是这么用的,怎么到这里就行不通了?

3.2 根本原因:二维数组的首元素到底长什么样

问题就出在对“首元素”的理解上。一维数组 int a[4] 的首元素是 a[0],类型是 int,所以 &a[0] 的类型是 int *,直接赋给 int *p 完全没问题。

但是二维数组 int a[3][4] 的本质是一维数组的数组。你可以把它拆开看:int[4] a[3],也就是说,这个二维数组由 3 个元素组成,每个元素本身是一个包含 4 个 int 的一维数组。因此:

  • 首元素:a[0],它的类型是 int[4](一个有 4 个 int 的数组)
  • 首元素的地址:&a[0],类型就是 int[4] *,也就是指向“包含 4 个 int 的数组”的指针。

而在 C 语言里,指向数组的指针必须写成一个很特别的语法:int (*p)[4]。注意,int *p[4] 是完全不同的东西(那是指针数组,后面再聊)。所以,int *p 只能指向单个 int,根本指不了一个 int[4] 的整体,编译器当然会警告你类型不兼容。

3.3 人话总结

一维数组的首元素是普通变量,二维数组的首元素是一整行的一维数组。想用指针指二维数组,指针的类型必须能准确描述这一行的大小,也就是要用数组指针。

4. 正确写法:数组指针 int (*p)[4] 的含义与读法

搞清楚首元素的类型之后,正确写法就很自然了:

int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
int (*p)[4] = a; //等价于 int (*p)[4] = &a[0];

注意: 看到 a 和 &a[0] 等价,你是不是想起了昨天的内容?对,这个规则没变!只是在二维数组里,a[0] 不再是一个 int 变量,而是一个 int[4] 数组。所以 &a[0] 的类型"升级"成了指向数组的指针。规则没变,变的是规则作用的对象。这点想通,后面就不会乱。

这里 int (*p)[4] 读法是:p 是一个指针,指向一个包含 4 个 int 的数组。括号必须加上,否则就变成 int *p[4](指针数组),这就是典型的“括号读不懂”的问题。

现在 p 指向了二维数组的首元素 a[0](即第 0 行),接下来就可以用指针运算走到任意一行:

p + i; // p 加上 i,实际跳过了 i 个 int[4],所以指向第 i 行

5. 用 p 访问二维数组元素的全过程

有了 int (*p)[4],怎么取出第 i 行第 j 列那个具体的值?下面一步一步拆解:

// 1. p+i → 指向第 i 行(整个一维数组),类型仍然是 int(*)[4]
// 2. *(p+i) → 解引用,相当于第 i 行的一维数组名 a[i],
// 此时类型已经变成 int *(指向这一行第 0 个元素)
// 3. *(p+i)+j → 在第 i 行内部偏移 j 个 int 位置,得到第 i 行第 j 列元素的地址
// 4. *(*(p+i)+j) → 再解引用,拿到最终的 int 元素

完整演示代码:

#include <stdio.h>
int main() {
int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
int (*p)[4] = a; // 数组指针,指向整行
// 遍历所有元素
for (int i = 0; i < t; 3; i++) {
for (int j = 0; j < t; 4; j++) {
printf("a[%d][%d] = %d\\n", i, j, *(*(p + i) + j));
}
}
return 0;
}

最终你会发现:*(*(p+i)+j) 和 a[i][j] 以及 p[i][j] 完全等价。虽然 p[i][j] 这种下标写法最直观,但理解底层 *(*(p+i)+j) 的转换过程,才能真正明白数组指针在内存里是怎么工作的。

6. 字符型二维数组与指针数组的区别

二维数组的麻烦还没完,换到字符类型一样存在坑。比如要存多个字符串:

char s[3][10] = {"hello", "world", "china"}; // 固定宽度 10 的二维字符数组

同样可以用数组指针来操作它:

char (*p)[10] = s; // p 指向每个 char[10] 行
for (int i = 0; i < 3; i++) {
printf("%s\\n", *(p + i)); // *(p+i) 是 char * 指向当前行字符串
}

但实际中,这些字符串长度差别很大,用固定长度的二维数组很浪费空间。于是还有另一种方式——指针数组:

char *s[3] = {"hello", "world", "china"}; // 每个元素是 char * 指针

这里 s 是一个数组,每个元素是指向字符串常量区某个位置的 char * 指针。那么想用指针操作这个数组时,需要指向 char * 的指针,也就是 char **:

char **p = s; // 或者写成 char *(*p) 也可以,但一般直接用二级指针
for (int i = 0; i < 3; i++) {
printf("%s\\n", *(p + i)); // p[i] 就是第 i 个字符串
}

小结一下容易搞混的三种类型:

  • int (*p)[4]:数组指针,指向包含 4 个 int 的数组,用于指向二维数组的整行。
  • int *p[4]:指针数组,一个包含 4 个 int * 元素的数组,每个元素是指向 int 的指针。
  • char **p:二级指针,指向一个 char * 变量,常用于管理字符串指针数组。

区分的关键永远是先看括号,再看修饰,从里往外读,才能把指针的类型说清楚。

7. 今日避坑指南

坑点别这么干,因为…
用 int *p 指向二维数组 人家二维数组的"第一个小弟"是一整行(int[4]),不是单个 int。你用 int * 去指,就像用摩托车驾照开大巴,肯定不行。
混淆 int (*p)[4] 和 int *p[4] 这是今天括号读法的核心。括号在哪,意义就在哪。记死:(*p) 先结合说明 p 是指针,所以是数组指针;p[4] 先结合说明 p 是数组,所以是指针数组。
忘了 *(p+i) 拿到的是"行地址" *(p+i) 解引用一次后,你拿到的是这一行的"数组名",类型是 int*,还不是具体元素。要再走一步 *(*(p+i)+j) 才到终点。
字符型二维数组和指针数组混用 char s[5][10] 需要 char (*p)[10];char *s[5] 需要 char **p,两者内存布局完全不同。
不清楚 *(*(p+i)+j) 等价于 p[i][j] 实际编码时可以用下标简化,但理解指针解引用链才能搞懂二维数组与指针的对应关系。

8. 今日学习感悟

今天最爽的一刻,不是写出了 int (*p)[4] 这个声明,而是我终于能从里往外读懂它了。以前看到这种带括号的声明,心里就发怵,只敢复制粘贴改改。今天跟着报错一步步推导,从 a[0] 的类型推到 int[4]*,再推到 int (*p)[4],这个括号在我眼里终于不再是乱码,而是一个有逻辑的"指向数组的声明结构"。这种从"死记"到"理解"的转变,可能就是学 C 语言最上瘾的时刻。

*Day13 学习记录,明天继续。*

赞(0)
未经允许不得转载:网硕互联帮助中心 » 二维数组用int *p为什么不行?那个括号到底怎么读?
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!