一码惊醒梦中人

曾经的我天真的以为C语言中的二维数组都是“指针数组套娃”(因为可以两次取值),但是上个学期的C语言课无情的打破了我的幻想——二维数组也是线性存储的(不过字符串数组确实是指针数组套娃)。
但是我真的搞明白了么?其实直到几个小时前,我依旧认为二维数组名和二级指针没什么差别,直到我尝试运行了一下下面的代码

#include<stdio.h>

int main()
{
    int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
    
    int **q;
 
    q = a;
    
    printf("%d\n", **q);
}
libxzr@libxzr-virtual-machine:~$ gcc test.c
test.c: In function ‘main’:
test.c:11:4: warning: assignment to ‘int **’ from incompatible pointer type ‘int (*)[4]’ [-Wincompatible-pointer-types]
   11 |  q = a;
      |    ^

编译时会出现警告,不过没什么关系,一个强制类型转换的事

运行一下

libxzr@libxzr-virtual-machine:~$ ./a.out 
段错误 (核心已转储)

草?
??????
为什么对二维数组名两次取值没有事,但是把它赋给一个二级指针就炸了?
我真的学过C语言吗?

是时候好好梳理一下,二维数组名、行指针、二级指针之间的关系了。

尘埃落定

我也在网上搜索了相关的资料,才知道某年菊厂面试题里也有这玩意儿(草)
网上的各种解释有很多,但是我觉得还是纯粹的从变量类型上去解释最为合适,因为变量类型作为C语言中的“天理”是不可撼动的

它是怎么发生的?

要想知道为什么会这样,其实只需要知晓下面的几个点就足够了

  • 二维数组是怎么存储的?
  • 二维数组的数组名是个什么?
  • 二维数组的数组名变量里面装了什么?
  • 二维数组的数组名在一次取值之后是个什么?
  • 二维数组的数组名在一次取值之后,变量里面装了什么?
  • 上面的二级指针里面,又装了什么?

上面的点其实都挺明显,而这个问题的答案就藏在其中
先来回答上面的问题

  • 线性存储
  • 行指针 (类型 (*名称)[第一维度的变量个数]
  • 二维数组开始的地址
  • 普通的指针
  • 二维数组第一行的开始地址

二维数组开始的地址==二维数组第一行的开始地址,这就是问题的所在

用代码来表示,就是

#include<stdio.h>

int main()
{
    int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
    
    printf("%d\n", a==*a);
}
libxzr@libxzr-virtual-machine:~$ gcc test.c
test.c: In function ‘main’:
test.c:13:18: warning: comparison of distinct pointer types lacks a cast
   13 |  printf("%d\n", a==*a);
      |                  ^~
libxzr@libxzr-virtual-machine:~$ ./a.out 
1

二维数组名和其取值的变量类型不同,但是,存储的地址相同,而这个地址,是

  • 二维数组开始的地址
  • 二维数组第一行开始的地址
  • 二维数组第一行第一个数的地址

所以如果将一个二维数组名赋值给二级指针,等同于将二维数组第一行第一个数的地址赋值给一个二级指针
而这个二级指针一次取值就得到了二维数组第一行第一个数,再取值就直接进入无权限访问的内存区域了。。。

梳理

我更愿意将这个问题的原因归结于变量类型,因为在这里扯什么地址毫无意义
上述的几个地址都是相同的
对二维数组名进行一次取值实际上是进行了一次“虚空操作”,这次操作没有改变指向的地址,仅仅只是改变了变量的类型
所以梳理变量类型才是关键

首先,二维数组名是个什么,我在上面已经提到,它是一个行指针,那么什么是行指针?
我们已经知道了行指针的一个特点,那就是对它进行一次取值,只产生了一个不同类型新变量,但是它指向的地址没有发生改变
那么,行指针和那个对它进行取值产生的东西有什么区别?
区别就在于,行指针含有一个额外的信息,那就是第一维度的变量数(二维数组中可以理解为列数)
只有掌握了这个信息,我们才能对行指针进行如a[1][2]的二维取值,否则,它怎么知道要从头指针开始向后推多少个内存地址才能找到目标?也正是因为有了这个信息,我们才能通过对行指针加减来整行移动指向位置。这个信息存在哪?变量类型里

总结一下:

  • 二维数组名是一个行指针
  • 对行指针取值产生普通指针
  • 行指针和这个普通指针变量的内容是一样的(指向相同地址)
  • 行指针与普通指针的变量类型不同,行指针的变量类型直接含有了第一维度变量数的信息

实战一下?

#include<stdio.h>

int main()
{
    int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
    
    int *q;
 
     q = a+1;
    
    printf("%d\n", *q);
}

只要明白了各变量分别存储着什么,就可以随意整活
而不会被拘禁于一维数组是一级指针,二维数组是二级指针的奇怪想法

libxzr@libxzr-virtual-machine:~$ gcc test.c
test.c: In function ‘main’:
test.c:9:5: warning: assignment to ‘int *’ from incompatible pointer type ‘int (*)[4]’ [-Wincompatible-pointer-types]
    9 |   q = a+1;
      |     ^
libxzr@libxzr-virtual-machine:~$ ./a.out 
4

最后用一张稍微有点形象的网图结束全文吧
p1