【数组】深析 “数组名称”


说是“深析”,我也不知道够不够深。
对于大佬来说,只能是献丑了。


 例子引入

首先看如下代码:

char arr[4] = { 1, 23, 4};
printf("arr:%p\n\n", arr);
printf("arr取址:%p\n\n", &arr);
printf("arr取址后寻址:%p\n\n", *(&arr));

  我们知道,数组名称 arr 是一个不可修改的变量,那么他的存放位置在哪呢?

这是我使用的教材上的描述:
在这里插入图片描述
  看起来数组名的存放位置应该是在非数组数据区的。

  现在假设,数组 arr 的首地址为 0x02,数组名的存放位置为 0x07
按照我们的正常思路,在内存中应有如下抽象表格:

地址 数据
0x01
0x02 1
0x03 2
0x04 3
0x05 4
0x06
0x07 0x02

  现在编译代码执行程序,来验证一下:
输出为:

arr:
0x02

arr取址:
0x02
	
arr取址后寻址:
0x02

  结果很奇怪,第二行输出表示 arr 并未存放在其他位置,而是就位于数组首地址处。
  这意味着,如果 arr 作为一个指针变量,那么他将指向自己。
  也就是说,我们无论对 arr 寻址多少次,输出的应该依然是它本身。
就像如下内存中的抽象表:

地址 数据
0x01
0x02 0x02
0x03 2
0x04 3
0x05 4
0x06
0x07

  然而这与我们的常识不符,1 去哪了?
  而且,arr 本应该是指向数组首成员的指针。

这里我们尝试对 arr 直接寻址试一下:

printf("arr寻址:%d\n", *arr);

输出为:

arr寻址:
1

  这样看来 arr 的地址并非是 0x02,那为什么刚才对 arr 取址后输出的却是 0x02 呢?

  这就出现了一个很神奇的矛盾,到底是为什么呢?


 一、不求甚解

  实际上,在上述例子中,我们使用 & 和 * 对arr的运算并非取址和寻址。

  先给出一个不求甚解的解释:
对数组名的 & 和 * 运算会使得该数组名对维度的引用范围“升维”或“降维”。

  怎么个升维降维?
  我们知道,数组名其实“相当于”一个行指针,或者说具有行指针的性质,而行指针可以定义一个多维数组的“框架”,然后通过指向一个数组来将定义的框架“套上去”。(“相当于”的说法是不严谨的,下一节我们详细讨论)

  在一个定义好的行指针框架中,声明了其维度和每个维度的下界。
  而对于直接声明的数组,它的数组名具有行指针的性质。

例如:

char arr[2][3][4] = { 0 }; //24个元素

  重点来了!
  这时候,对于数组名 arr ,其类型为 ( char (*arr)[2][3][4] ) ,arr + 1 意味着地址增加 3 * 4 * sizeof(char)
arr 等价于 ( char (*arr)[2][3][4] ) arr

  另外地,还有如下对应关系:
(我们刚才提到,*对数组名 arr 的运算意味着 “降维”)

*arr,其类型为 ( char (*arr)[3][4] ) ,*arr + 1 意味着地址增加 4 * sizeof(char)
*arr 等价于 ( char (*arr)[3][4] ) arr


**arr,其类型为 ( char (*arr)[4] ),**arr + 1 意味着地址增加 1 * sizeof(char)(此时已经降维至最低维)
**arr 等价于 ( char (*arr)[4] ) arr


***arr,其类型为 ( char arr ),已经引用到了第一个数组元素的实际数据

为了方便理解,我们看一下代码和输出:

#include<stdio.h>
void main()
{
	char arr[2][3][4] = { 0 };
	printf("arr = "); printf("%d\n", arr);//arr
	printf("sizeof(arr) = "); printf("%d\n", sizeof(arr));//sizeof arr
	printf("arr + 1= "); printf("%d\n\n", arr + 1);//arr + 1


	printf("*arr = "); printf("%d\n", *arr);
	printf("sizeof(*arr) = "); printf("%d\n", sizeof(*arr));
	printf("*arr + 1 = "); printf("%d\n\n", *arr + 1);

	printf("**arr= "); printf("%d\n", **arr);
	printf("sizeof(**arr) = "); printf("%d\n", sizeof(**arr));
	printf("**arr + 1 = "); printf("%d\n\n", **arr + 1);

	printf("***arr= "); printf("%d\n", ***arr);
	printf("sizeof(***arr) = "); printf("%d\n", sizeof(***arr));
	printf("***arr + 1 = "); printf("%d\n\n", ***arr + 1);
}

输出:

arr = 9698400
sizeof(arr) = 24	//作为第一维度的行指针,其数组长度 2 * 3 * 4 * sizeof(char) == 24
arr + 1= 9698412	//增加了 3 * 4 * sizeof(char) == 12
	
//降维
*arr = 9698400
sizeof(*arr) = 12	//作为第二维度的行指针,其数组长度 3 * 4 * sizeof(char) == 12
*arr + 1 = 9698404	//增加了 4 * sizeof(char) == 4
	
//降维
**arr= 9698400
sizeof(**arr) = 4	//作为第三维度的行指针,其数组长度  4 * sizeof(char) == 4
**arr + 1 = 9698401	//增加了 1 * sizeof(char) == 1

//降维
***arr= 0	//取到了首元素数据 0
sizeof(***arr) = 1
***arr + 1 = 1


  而对于 & 运算符,即为 * 的逆运算:

简单例子:将第二维的行指针 (*arr) 进行 & 运算即 &(*arr)

	printf("&(*arr) = "); printf("%d\n", &(*arr));
	printf("sizeof(&(*arr)) = "); printf("%d\n", sizeof(&(*arr)));
	printf("&(*arr) + 1 = "); printf("%d\n\n", &(*arr) + 1);

输出:

&(*arr) = 9698400
sizeof(&(*arr) ) = 4	//这里存疑,尽管其拥有三维的行指针的性质,但其长度后却是一个普通指针的长度(对于任意行指针取址后皆如此)
&(*arr)  + 1 = 9698412	//增加了 3 * 4 * sizeof(char) == 12

 二、求甚解

  问题算是基本解决了,但是我仍然有很多疑惑,比如 arr 作为指针常量时,其储存位置到底在哪?或者到底该如何理解数组名和指针?…

  我尝试从图书馆借到大名鼎鼎的《C和指针》,却发现根本找不到,传说中的《C专家编程》,《C陷阱与缺陷》更是没有影子。(吐槽一下图书馆。。。之前想借那本绿皮的 Python 也没找到,只能自掏腰包买来看)
  只能费劲从网上找了pdf版本拿来参考,翻起来真的很费劲…
  另外,我本以为《C和指针》读起来应该晦涩难懂,却没想到读着非常顺畅,并没有过于高深的难以理解的东西。
(尽管已经对着舍友发誓过 期末之前不碰代码


  1、数组名和指针的区别

  先聊一聊数组名和指针

  我大致翻阅了《C和指针》第八章(数组)的内容。

  首先要提到,所谓“行指针”,貌似只是我们中国人(或是部分教材)对多维数组的认知,实际上C标准并未对所谓“行指针”有过要求和定义。

  书中作者并未通过我们认知的“行指针”来理解多维数组,而是直接指出了指针与数组的区别。
  一般认为,数组名是指针常量(但其仍然是变量,只不过其内容不允许更改),指针是变量(内容随意更改)。

  而作者认为,指针是一个标量值,而数组名则包含很多属性(就像结构体),只不过这些属性是存在于 编译器层面的底层逻辑 里。数组名在表达式中表现出的指针常量是众多属性其中之一,还有其作为高维指针时表现出的“+ 1 跳行”也是属性之一。

我们来看这两段话:

数组具有一些和指针完全不同的特征。
例如,数组具有确定数量的元素,而指针只是一个标量值。
编译器用数组名来记住这些属性。
只有当数组名在表达式中使用时,编译器才会为他产生一个指针常量。

  也就是说,作者认为数组名并非指针常量,指针常量仅仅作为数组名的一个属性,将会在某些情况下从数组名当中体现出来。

只有在两种场合下,数组名并不用指针常量来表示——就是当数组名作为 sizeof 操作符或单目操作符&的操作数时。
sizeof 返回整个数组的长度,而不是指向数组的指针长度。
取一个数组名的地址所产生的是一个指向数组的指针,而不是一个指向某个指针常量值的指针。

  令我疑惑的是,作者在这一章并未总结对数组名进行取址寻址运算时会发生的事情,尽管他提到了&运算会返回一个指向数组的指针,但仍然没有说明白这个指针实际上是一个高维指针。


  2、数组名在哪里

  尽管我们知道了对数组名使用 * & 代表的含义,却还是无法知晓数组首元素地址到底储存在哪里?储存在数组名 arr 中?那么 arr 本身又储存在哪?
  清早起来跑步的时候,我突然想起在vs的调试器中曾经看到过什么“反汇编”这样的选项,回到宿舍之后我马上尝试去看一下这个汇编代码。
  在这里插入图片描述
  虽然我根本没学过汇编,但是对照着内存中的数据,大概可以猜到:
  在这里插入图片描述
  然而在汇编代码前后仔细翻了好几遍,把所有可能的地址全部在内存中找了一遍,没有发现哪个地方存有数组首元素的地址。
  后来包括昨天我也查了很多资料,毫无头绪,好像大家都并不关心数组首元素地址到底存在哪里了...
  
  ------------------------------------------------12/20更新------------------------------------------------
  
  问题已经解决。
  实际上,程序并未对该地址分配空间,这个问题我们马上讨论。


  3、求甚解

  我们一般这样理解数组,(数组 = 指针 + 数据) 他的数组名是一个指针,其后分配了数个连续的内存空间来存放同类型数据。然而事实并非如此,这种理解是错误的。
  那么凭什么对数组名的 * 和 & 运算就不是寻址取址,而是另有意义?
  
  既然这一节叫“求甚解”,那么在这里让我们更进一步:数组和指针完全不同
  
  前边提到了“数组名和指针的区别”,说到了《C和指针》作者对数组和指针的理解,我们即将再往深了讨论。
  
  从本质上来说,数组和指针是完全不同的派生类型,这种不同不仅仅体现在数组等于指针加数据。实际上数组里面根本没有指针,我们常说的数组名是指针变量或常量的说法是错误的,数组就是数组,数组名就是数组的名称,仅此而已。只不过数组名恰好表现出了类似指针的性质,一些教科书为了方便读者理解,对这里的内容没有深究。
  数组名所代表的是那一整块数组的内存空间,我们使用数组名就是针对这个数组,而不是针对一个地址甚至指针。之前我们讨论过 “数组名包含很多属性”,实际上这些属性就是数组这个数据类型所具有的属性。
  
  数组和指针的关系,就像 int 和 char 的关系一样,指针是指针类型,数组是数组类型,它们是两种数据类型。之所以我们可以将一个指针当做数组来用(甚至可以使用下标表达式 arr[3]),那是因为 C 恰好允许你这样做,而不是意味着我们就要将两者等同起来。(下一节将详细剖析其中的缘由)
  
  有了这些铺垫,我们回到最初的问题上,一切迎刃而解。
  我们之所以对 &arr == arr 疑惑不解,就是因为我们没有搞清楚数组和指针到底有什么区别。
  数组名代表着一段连续的内存空间,所以 对数组名取址 &arr 的含义是对数组取址,而非对指针取址,那么返回数组的首元素地址是很正常且自然的,就像我们对 int 取址也是返回首地址一样。
  
  正因为这种不同,* 和 & 作用于数组会体现出完全不同的意义也很好理解。


  4、下标表达式 和 指针表达式

  对于引用数组元素,下面我将介绍一种有些怪异的方法,我将通过它来揭示一个令人惊讶的事实。

我们知道,引用数组元素有两个途径,

char arr[4] = { 0 };

1.通过数组下标表达式:

arr[2];

2.通过指针表达式:

*(arr + 2);

  实际上,所谓“数组下标表达式”是一种伪装的写法,我们马上来说明他。
现在我们来看这个古怪的例子:

2[arr];

  注意,这样写是完全可以编译通过的!你可以现在就试一试。

  为什么这样古怪的写法并不会报错?
  这其实就是 C 编译器理解下标表达式的方法。

  很显然,上面的例子表明 2[arr]arr[2] 是等价的。

  我将 2[arr] 转换一下形态(等价的指针表达式):*( 2 + (arr) )
  不难猜到,arr[2] 等价的指针表达式为:*( (arr) + 2 )

往下拓展,对于多维数组:

char arr[2][3];

  arr[1][2] 的等价指针表达式为: *(*((arr) + 1) + 2)

  结合先前我们学到的知识,
  内层的 arr 为高纬指针:arr + 1
  对其寻址后就“降维”了:*(arr + 1) + 2
  最后我们再寻址取到数据: *(*(arr + 1) + 2)

  更多维数组以此类推。

posted @ 2019-12-18 22:38  高厉害  阅读(384)  评论(0编辑  收藏  举报