【数组】深析 “数组名称”
0w0
- 例子引入
- 一、不求甚解
- 二、求甚解
- 1、数组名和指针的区别
- 2、数组名在哪里
- 3、求甚解
- 4、下标表达式 和 指针表达式
说是“深析”,我也不知道够不够深。
对于大佬来说,只能是献丑了。
例子引入
首先看如下代码:
char arr[4] = { 1, 2,3, 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:
0x02arr取址:
0x02arr取址后寻址:
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);//arrprintf("sizeof(arr) = "); printf("%d\n", sizeof(arr));//sizeof arrprintf("arr + 1= "); printf("%d\n\n", arr + 1);//arr + 1printf("*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)
更多维数组以此类推。
【数组】深析 “数组名称”相关推荐
- 【C 语言】数组 ( 多维数组本质 | n 维数组名称本质 是 n-1 级数组指针 )
文章目录 总结 一.多维数组本质 二.完整代码示例 总结 n 维数组名称本质 是 n-1 级数组指针 一.多维数组本质 给定多维数组 : // 声明一个多维数组int array[2][3]; 打印二 ...
- Linux C编程---指针数组简析(二维数组、多级指针)
讲到指针和数组,先给大家看一道例题: 题目:填空练习(指向指针的指针) 1.程序分析: 2.程序源代码: main() { char *s[]={"man","woman ...
- php 数组去重_数组去重(JavaScript 为例)
数组去重,就是在数组中查找相同的元素,保留其中一个,去除其他元素的程. 从这句话揭示了数组去重的两个关键因素: 找到重复项 去除重复项 本文告诉你在遇到去重问题时该如何思考,并以 JavaScript ...
- java数组解析_Java - 数组解析
一.什么是数组 数组?什么是数组?在我印象中的数组是应该这样的:通过new关键字创建并组装他们,通过使用整形索引值访问它的元素,并且它的尺寸是不可变的! 但是这只是数组的最表面的东西!深一点?就是这样 ...
- 【C 语言】指针 与 数组 ( 指针 | 数组 | 指针运算 | 数组访问方式 | 字符串 | 指针数组 | 数组指针 | 多维数组 | 多维指针 | 数组参数 | 函数指针 | 复杂指针解读)
相关文章链接 : 1.[嵌入式开发]C语言 指针数组 多维数组 2.[嵌入式开发]C语言 命令行参数 函数指针 gdb调试 3.[嵌入式开发]C语言 结构体相关 的 函数 指针 数组 4.[嵌入式开发 ...
- 指针数组、数组指针、数组的区别与联系
指针数组.数组指针.数组的区别与联系! 一:基本定义 1.指针数组 char *arr[4] = {"hello", "world", "shannx ...
- 不确定大小的数组_原来数组是容器喔
和我一起学C吧 努力是可以提升智力的5.1.2 数组的使用:如何定义和使用数组,数组的下标和下标的范围 前面有数组的简单介绍,可以去回顾一下其相关定义 变量名称[元素数量]: 例: ...
- java 二维数组倒置_Java数组反转及二维数组
数组的反转 反转就是把最远端的元素和最近端的元素位置互换(反转前数据:1,2,3,4,5,6反转后:6,5,4,3,2,1) //准备一个需要反转的数组 int[] arr = {1,2,3,4,5, ...
- java定义数组_java中数组的三种定义方式_java中数组的定义及使用方法(推荐)...
java中数组的三种定义方式 java中,数组是一种很常用的工具,今天我们来说说数组怎么定义 [java] view plain copy /** * 数组的三种定义方法 * 1.数组类型[] 数组名 ...
最新文章
- Linux系统搭建FTP服务器
- DL之DNN:利用MultiLayerNet模型【6*100+ReLU+SGD,weight_decay】对Mnist数据集训练来抑制过拟合
- 测试Spring的“会话”范围
- Mybatis的缓存机制Cache
- 吉林建筑大学电气与计算机学院院长,吉林建筑大学电气与计算机学院研究生导师:张玉红...
- 无可用源 没有为任何调用堆栈加载任何符号_看完这篇后,别再说你不懂JVM类加载机制了~...
- Linux网络协议栈【转载】
- radius服务器认证系统,TekRadius(RADIUS服务器)
- 项目使用微信公众平台图片显示此图片来自微信公众平台 解决方法
- 我的wow血精灵圣骑士,晒晒
- 浩辰3D软件中应用程序菜单如何使用?
- html基础之好看的header
- 手把手教你webpack3(14)HMR模块热加载
- P 问题、NP 问题、NPC 问题(NP 完全问题)、NPH 问题和多项式时间复杂度
- sd和sem啥区别_SD与SEM有区别吗
- GPS纠偏 WGS84转GCJ02 Java版本
- 工厂用计算机自动控制技术,一种基于计算机技术的工厂车间灯光控制系统的制作方法...
- spring相关资料
- 锁相环技术,单边带信号,信号的调制
- 《人民的名义》---简单的文本分析
热门文章
- jQuary中delegate()函数的作用
- STM32F4主板硬件设计与接口
- 2020 校招,我是如何拿到小米、京东、字节大厂前端offer
- 最新浴血凤凰2020年DNF自动化辅助开发教程
- 浅谈DDS IP核之频率控制字与相位控制字
- 【转】VC++的窗口句柄和窗口ID
- 《你好,数智新世界》系列访谈
对话数睿数据总裁刘超|企业级无代码赋能软件产业变革...
- Flash 第一章 课堂笔记
- 联想小新一键恢复小孔_联想小新一键恢复小孔 联想小新笔记本怎么一键恢复...
- 好久不见,我回来了!