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



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


     例子引入

    首先看如下代码:

    char arr[4] = { 1, 23, 4};
    printf("arr:%p
    
    ", arr);
    printf("arr取址:%p
    
    ", &arr);
    printf("arr取址后寻址:%p
    
    ", *(&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
    ", *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
    ", arr);//arr
    	printf("sizeof(arr) = "); printf("%d
    ", sizeof(arr));//sizeof arr
    	printf("arr + 1= "); printf("%d
    
    ", arr + 1);//arr + 1
    
    
    	printf("*arr = "); printf("%d
    ", *arr);
    	printf("sizeof(*arr) = "); printf("%d
    ", sizeof(*arr));
    	printf("*arr + 1 = "); printf("%d
    
    ", *arr + 1);
    
    	printf("**arr= "); printf("%d
    ", **arr);
    	printf("sizeof(**arr) = "); printf("%d
    ", sizeof(**arr));
    	printf("**arr + 1 = "); printf("%d
    
    ", **arr + 1);
    
    	printf("***arr= "); printf("%d
    ", ***arr);
    	printf("sizeof(***arr) = "); printf("%d
    ", sizeof(***arr));
    	printf("***arr + 1 = "); printf("%d
    
    ", ***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
    ", &(*arr));
    	printf("sizeof(&(*arr)) = "); printf("%d
    ", sizeof(&(*arr)));
    	printf("&(*arr) + 1 = "); printf("%d
    
    ", &(*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)

      更多维数组以此类推。

  • 相关阅读:
    如何缓解DDOS攻击
    centos-linux热拔插scsi硬盘
    AWS CLI以及AWS S3 SYNC命令行使用
    CentOS安装VMware Tools
    在VM克隆CENTOS以后,网卡的处理过程
    Centos 的计划任务 crontab
    Windows7无法访问共享文件夹(0x800704cf,0x80070035)解决方法
    Javascript遍历页面控件
    总结一些js自定义的函数
    JS应用(资料很全)
  • 原文地址:https://www.cnblogs.com/gaolihai/p/13149764.html
Copyright © 2020-2023  润新知