第41章:二维数组与指针——int a[3][4] 和 int **p 为什么不一样?
这个问题,我几乎每次面试新人都会问。十个人里有八个会答错。
很多人觉得,二维数组名不就是个二级指针吗?
嗯,还真不是。这俩东西,长得像,骨子里完全不同。
先看一段代码
int a[3][4]; // 二维数组
int **p; // 二级指针
// 下面这两行,哪个能编译通过?
int *q1 = a; // 行吗?
int *q2 = p; // 行吗?
我告诉你答案:q1 = a 能通过,q2 = p 也能通过。但别高兴太早——它们背后的含义天差地别。
核心区别一句话:
int a[3][4]是一块连续的内存,编译器知道它的布局int **p是一个指针,指向另一个指针,那个指针再指向数据
内存布局:一个整齐,一个松散
二维数组 int a[3][4] 在内存里长这样:
a[0][0] a[0][1] a[0][2] a[0][3]
a[1][0] a[1][1] a[1][2] a[1][3]
a[2][0] a[2][1] a[2][2] a[2][3]
一共12个int,挨个排着队。没有空隙,没有跳转。
而 int **p 呢?它指向一个指针数组,每个指针再指向一行数据:
p → [ptr0] → [d0 d1 d2 d3]
[ptr1] → [d0 d1 d2 d3]
[ptr2] → [d0 d1 d2 d3]
看到了吗?每一行的数据可能分散在内存的不同角落。
我个人习惯:把二维数组想象成「一整块地板砖」,把二级指针想象成「三块独立的小木板,用钉子连起来」。地板砖踩上去稳稳当当,小木板嘛……你懂的。
类型系统:编译器眼中的真相
咱们用 sizeof 来揭穿它们的真面目:
int a[3][4];
int **p;
sizeof(a); // 3 * 4 * sizeof(int) = 48(假设int占4字节)
sizeof(p); // 8(64位系统下指针大小)
// 再看这个:
sizeof(a[0]); // 4 * sizeof(int) = 16
sizeof(p[0]); // 8(p[0] 是一个指针)
差别一目了然。a[0] 是数组,p[0] 是指针。编译器对它们的处理方式完全不同。
| 特性 | int a[3][4] | int **p |
|---|---|---|
| 内存布局 | 连续一整块 | 可能不连续 |
| sizeof(a[0]) | 16字节(一行) | 8字节(一个指针) |
| a+1 的含义 | 跳过一行(16字节) | 跳过一个指针(8字节) |
| 能否传给函数 | 退化为 int (*)[4] | 保持 int ** |
函数传参:这里最容易翻车
我曾经在项目里见过这样的代码:
void func(int **p) {
// 处理数据...
}
int a[3][4];
func(a); // 编译警告!类型不匹配
为什么不行?因为 a 退化后是 int (*)[4],不是 int **。
正确的写法应该是:
void func(int (*p)[4]); // 或者 void func(int p[][4])
// 然后 func(a); 就OK了
我曾经踩过的坑:有一次写一个图像处理函数,参数用了 int **,传进去一个二维数组。结果函数里用 p[i][j] 访问数据,程序直接崩溃。查了半天才发现——二维数组的 a[i][j] 和二级指针的 p[i][j],编译器生成的寻址代码完全不同。
寻址方式:编译器是怎么找到 a[i][j] 的?
对于二维数组 int a[3][4]:
a[i][j] 的地址 = a + i * 4 * sizeof(int) + j * sizeof(int)
编译器直接计算偏移量,一步到位。
对于二级指针 int **p:
p[i][j] 的地址 = *(p + i) + j * sizeof(int)
// 先取 p[i] 的值(这是一个指针),再偏移 j
需要两次间接访问,效率低一些,而且要求 p[i] 必须指向有效的内存。
什么时候用哪个?
- 数据大小固定:用二维数组。比如3x4的矩阵、固定分辨率的图像。
- 每行长度不同:用二级指针。比如字符串数组、不规则表格。
- 需要动态分配:用二级指针。数组大小在运行时才能确定。
- 追求性能:用二维数组。连续内存,缓存友好。
我建议:能用数组就用数组。数组是编译器帮你管理内存,指针是你自己管理。少一份管理,少一份风险。
一张图看懂区别
下面这张SVG图,把二维数组和二级指针的内存布局画得清清楚楚:
总结一下
二维数组和二级指针,根本就不是一回事。数组是「数据容器」,指针是「地址容器」。
你想想看,int a[3][4] 这个声明,编译器直接分配了48字节的空间。而 int **p 只分配了8字节的指针空间,数据还得你自己去分配。
所以下次面试再有人问你这个问题,你可以反问他:「你觉得一块地板砖和三块用钉子连起来的小木板,能一样吗?」
一句话记住:
二维数组是「一整块内存,编译器帮你算偏移」;二级指针是「多块内存,你自己管理跳转」。
选哪个?数据大小固定、追求性能 → 数组。需要灵活、动态分配 → 指针。
公众号:蓝海资料掘金营,微信deep3321