获取数组变量的地址意味着什么?
今天我读了一个让我感到困惑的C片段:
#include int main(void) { int a[] = {0, 1, 2, 3}; printf("%d\n", *(*(&a + 1) - 1)); return 0; }
在我看来, &a + 1
毫无意义,但它运行没有错误。
有人可以解释一下这意味着什么,谢谢。 K&R C圣经是否涵盖了这一点?
UPDATE0:读完答案后,我意识到这两个表达式主要让我困惑:
-
&a + 1
,已在SO中询问: 关于c中的表达“&anArray” -
*(&a + 1) -1
,与数组衰减有关。
让我们剖析它。
a
具有int [4]
类型(4 int的数组)。 它的大小是4 * sizeof(int)
。
&a
具有类型int (*)[4]
(指向4 int数组的指针)。
(&a + 1)
也有类型int (*)[4]
。 它指向一个4 int的数组,它在a开始后启动1 * sizeof(a)
字节(或4 * sizeof(int)
字节)。
*(&a + 1)
的类型为int [4]
(4 int的数组)。 它的存储在a开始后开始1 * sizeof(a)
字节(或4 * sizeof(int)
字节。
*(&a + 1) - 1
的类型为int *
(指向int的指针),因为数组*(&a + 1)
衰减到指向此表达式中第一个元素的指针。 它将指向一个int,它在*(&a + 1)
开始之前启动1 * sizeof(int)
字节。 这&a[3]
指针值相同。
*(*(&a + 1) - 1)
的类型为int
。 因为*(&a + 1) - 1
是与&a[3]
相同的指针值, *(*(&a + 1) - 1)
相当于a[3]
,它已被初始化为3
,因此这是printf
打印的数字。
首先是一个小提醒(如果你以前不知道这个,还是新的东西):对于任何数组或指针p
和索引i
,表达式p[i]
与*(p + i)
完全相同。
现在希望能帮助您了解正在发生的事情……
程序中的数组a
存储在内存中的某个地方,确切地说并不重要。 要获取存储位置,即获取指向a的指针,可以使用address-of运算符&
like &a
。 这里要学习的重要一点是,指针本身并不意味着什么特别,重要的是指针的基本类型 。 a的类型是int[4]
,即a
是由四个int
元素组成的数组。 表达式的类型&a
是指向四个int
或int (*)[4]
的数组的指针。 括号很重要,因为int *[4]
类型是一个由四个指针组成的数组,这是一个完全不同的东西。
现在回到初始点, p[i]
与*(p + i)
。 而不是p
我们有&a
,所以我们的表达式*(&a + 1)
与(&a)[1]
。
现在解释*(&a + 1)
含义及其作用。 现在让我们考虑一下有关arraysa
的内存布局。 在内存中它看起来像
+ --- + --- + --- + --- + | 0 | 1 | 2 | 3 | + --- + --- + --- + --- + ^ | &一个
表达式(&a)[1]
处理&a
因为它是一个数组数组,它肯定不是,并访问此数组中的第二个元素,这将超出范围。 这当然在技术上是未定义的行为 。 让我们暂时运行它,并考虑它在内存中的样子:
+ --- + --- + --- + --- + --- + --- + --- + --- + | 0 | 1 | 2 | 3 | 。 | 。 | 。 | 。 | + --- + --- + --- + --- + --- + --- + --- + --- + ^ ^ | | (&a)[0](&a)[1]
现在记住a
的类型(与(&a)[0]
,因此意味着(&a)[1]
也必须是这种类型)是四个int
数组 。 由于数组自然地衰减到指向其第一个元素的指针,因此表达式(&a)[1]
与&(&a)[1][0]
,其类型是指向int
指针 。 因此,当我们在表达式中使用(&a)[1]
,编译器给出的是指向第二个(不存在的) &a
数组中第一个元素的指针。 再次我们来到p[i]
等于*(p + i)
等式: (&a)[1]
是指向int
的指针 ,它是*(p + i)
表达式中的*(p + i)
,所以完整表达式为*((&a)[1] - 1)
,并查看上面的内存布局从(&a)[1]
给出的指针中减去一个int
给出了(&a)[1]
之前的元素,它是(&a)[1]
中的最后一个元素(&a)[0]
,即它给出了我们(&a)[0][3]
,它与a[3]
相同。
所以表达式*(*(&a + 1) - 1)
与a[3]
相同。
它是啰嗦,并且经过危险的领域(越界索引),但由于指针算术的力量,它最终都能解决。 我不建议您编写这样的代码,但是需要人们真正了解这些转换如何能够解密它。
&a + 1
将指向紧跟在a
元素之后的内存或更好地在数组之后说,因为&a
具有int (*)[4]
(指向四个int
的数组的指针)。 标准允许构造此类指针,但不允许解除引用。 因此,您可以将其用于后续的算术。
因此, *(&a + 1)
是未定义的。 但是*(*(&a + 1) - 1)
更有趣。 实际上它被评估为a中的最后一个元素。有关详细说明,请参阅https://stackoverflow.com/a/38202469/2878070 。 只是一个评论 – 这个hack可能被更可读和更明显的结构所取代: a[sizeof a / sizeof a[0] - 1]
(当然它应该只应用于数组,而不是指针)。
最好向自己certificate:
$ cat main.c #include main() { int a[4]; printf("a %p\n",a); printf("&a %p\n",&a); printf("a+1 %p\n",a+1); printf("&a+1 %p\n",&a+1); }
以下是地址:
$ ./main a 0x7fff81a44600 &a 0x7fff81a44600 a+1 0x7fff81a44604 &a+1 0x7fff81a44610
前两个是相同的地址。 第三个是4
个(这是sizeof(int)
)。 第4个是0x10 = 16
个(这是sizeof(a)
)
例如,如果您有T类型的对象
T obj;
宣言
T *p = &obj;
用对象obj
占用的内存地址初始化指针p
表达式p + 1
指向对象obj
之后的内存。 表达式p + 1
的值等于&obj plus sizeof( obj )
,它等效于
( T * )( ( char * )&obj + sizeof( obj ) )
因此,如果您在post中显示数组,则int a[] = {0, 1, 2, 3};
您可以使用typedef以下列方式重写其声明:
typedef int T[4]; T a = { 0, 1, 2, 3 };
在这种情况下, sizeof( T )
等于sizeof( int[4] )
而等于4 * sizeof( int )
表达式&a
给出了数组占用的内存范围的地址。 表达式&a + 1
给出数组后面的内存地址,表达式的值等于&a + sizeof( int[4] )
另一方面,表达式中使用的数组名称 – 极少数例外,例如在sizeof
运算符中使用数组名称 – 被隐式转换为指向其第一个元素的指针。
因此,表达式&a + 1
指向真实的第一个元素a
之后的int[4]
类型的想象元素。 表达式*(&a + 1)
给出了这个想象的元素。 但由于该元素是一个类型为int[4]
的数组,因此该表达式转换为指向其第一个int *
类型元素的指针
第一个元素跟在数组a
的最后一个元素之后。 在这种情况下,表达式*(&a + 1) - 1
给出了数组a
最后一个元素的地址
通过在*(*(&a + 1) - 1)
取消引用,您将获得数组a
的最后一个元素的值,因此将输出数字3
。
请注意,以下内容相同,但同样令人讨厌:
printf("%d\n", (&a)[1][-1]);
在这种情况下,我认为更明确的是:
采用指向数组a的指针
-
指针的使用就好像它是一个数组:一个像a的元素数组,即4个整数的数组,使用这个数组的第一个元素。
-
由于a实际上不是一个数组,而只是一个元素(由四个子元素组成!),因此这将直接索引一段内存。
-
[-1]直接在a之后读取直接在内存之前的整数,这是a的最后一个子元素
*(*(&a + 1) - 1)
解决数组中最后一个元素是一种尴尬和危险的方法。 &a是int [4]类型的数组的地址。 (&a + 1)在当前寻址的a之后给出下一个int [4]数组。 通过使用*(&a + 1)取消引用它,你可以使它成为* int,而使用额外的-1,你可以指向a的最后一个元素。 然后取消引用最后一个元素,从而返回值3(在您的示例中)。
如果数组元素的类型与目标CPU的对齐长度相同,则此方法很有效。 考虑你有一个类型为uint8和长度为5的数组的情况:uint8 ar [] = {1,2,3,4,5}; 如果你现在也这样做(在32位架构上),你在5之后寻址一个未经填充的填充字节。所以ar [5]的地址与4个字节对齐。 ar中的各个元素与单个字节对齐。 即,ar [0]的地址与ar本身的地址相同,ar [1]的地址是ar之后的一个字节(而不是ar之后的4个字节),…,ar的地址[4]是ar加5个字节,因此不与4个字节对齐。 如果你这样做(&a + 1),你得到下一个uint8 [5]数组的地址,该数组与4字节对齐,即,它是8加8字节。 如果你取这个ar的地址加上8个字节并返回一个字节,那你就读到了ar加7,它没有被使用。