第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图,把二维数组和二级指针的内存布局画得清清楚楚:

二维数组 vs 二级指针 内存布局对比 int a[3][4](连续内存) a[0] a[1] a[2] [0] [1] [2] [3] 连续内存,共 12 个 int int **p(分散内存) p ptr0 ptr1 ptr2 d0 d1 d2 d3 e0 e1 e2 e3 f0 f1 f2 f3 关键区别 二维数组:编译器直接计算偏移 → 一步到位 二级指针:先取指针值,再偏移 → 两步操作

总结一下

二维数组和二级指针,根本就不是一回事。数组是「数据容器」,指针是「地址容器」。

你想想看,int a[3][4] 这个声明,编译器直接分配了48字节的空间。而 int **p 只分配了8字节的指针空间,数据还得你自己去分配。

所以下次面试再有人问你这个问题,你可以反问他:「你觉得一块地板砖和三块用钉子连起来的小木板,能一样吗?」

一句话记住:

二维数组是「一整块内存,编译器帮你算偏移」;二级指针是「多块内存,你自己管理跳转」。

选哪个?数据大小固定、追求性能 → 数组。需要灵活、动态分配 → 指针。


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