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 学习记录,明天继续。*
网硕互联帮助中心




评论前必须登录!
注册