【数组】深析 “数组名称”
说是“深析”,我也不知道够不够深。
对于大佬来说,只能是献丑了。
例子引入
首先看如下代码:
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:
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)
更多维数组以此类推。