今天我读了一段让我感到困惑的C片段:
#include <stdio.h>
int
main(void)
{
int a[] = {0, 1, 2, 3};
printf("%d\n", *(*(&a + 1) - 1));
return 0;
}
在我看来,&a + 1
毫无意义,但它运行没有错误。
有人可以解释一下这意味着什么,谢谢。 K&amp; R C圣经是否涵盖了这一点?
UPDATE0: 读完答案后,我意识到这两个表达方式主要让我困惑:
&a + 1
,已在SO中询问:about the expression “&anArray” in c
*(&a + 1) -1
,与数组衰减有关。
答案 0 :(得分:35)
首先提醒一下(如果你以前不知道这个,还是新的东西):对于任何数组或指针p
和索引i
,表达式p[i]
与*(p + i)
。
现在希望能帮助您了解正在发生的事情......
程序中的数组a
存储在内存中的某个位置,这实际上并不重要。要获取存储a
的位置,即获取指向a
的指针,请使用&
之类的地址操作符&a
。这里要学习的重要一点是,指针本身并不意味着什么特别,重要的是指针的基类型。 a
的类型为int[4]
,即a
是包含四个int
元素的数组。表达式&a
的类型是指向四个int
或int (*)[4]
的数组的指针。括号很重要,因为类型int *[4]
是一个包含四个指向int
的数组,这是完全不同的事情。
现在回到初始点,p[i]
与*(p + i)
相同。我们的p
表达式与&a
相同,而非*(&a + 1)
,而不是(&a)[1]
。
现在解释*(&a + 1)
的含义及其作用。现在让我们考虑一下数组a
的内存布局。在内存中它看起来像
+---+---+---+---+ | 0 | 1 | 2 | 3 | +---+---+---+---+ ^ | &a
表达式(&a)[1]
将&a
视为一个数组数组,它肯定不是,并访问此数组中的第二个元素,这将超出范围。这当然在技术上是未定义的行为。让我们暂时运行它,并考虑 在内存中的样子:
+---+---+---+---+---+---+---+---+ | 0 | 1 | 2 | 3 | . | . | . | . | +---+---+---+---+---+---+---+---+ ^ ^ | | (&a)[0] (&a)[1]
现在请记住,a
的类型(与(&a)[0]
相同,因此意味着(&a)[1]
也必须是此类型)四个数组{{1 }} 的。由于数组自然地衰减到指向其第一个元素的指针,因此表达式int
与(&a)[1]
相同,其类型为指向&(&a)[1][0]
的指针。因此,当我们在表达式中使用int
时,编译器给出的是指向(&a)[1]
的第二个(不存在的)数组中第一个元素的指针。我们再次来到&a
等于p[i]
等式:*(p + i)
是指向(&a)[1]
的指针,它是int
p
表达式,所以完整表达式为*(p + i)
,并查看上面的内存布局从*((&a)[1] - 1)
给出的指针中减去一个int
,得到{{1}之前的元素这是(&a)[1]
中的最后一个元素,即它为(&a)[1]
提供了与(&a)[0]
相同的内容。
所以表达式(&a)[0][3]
与a[3]
相同。
它啰嗦,并且经过危险的领域(带有越界索引的东西),但是由于指针算法的力量,它最终都能解决。我不建议您编写这样的代码,但是需要人们真正了解这些转换如何能够解密它。
答案 1 :(得分:12)
让我们剖析它。
a
的类型为int [4]
(数组为4 int)。它的大小为4 * sizeof(int)
。
&a
的类型为int (*)[4]
(指向4 int数组的指针)。
(&a + 1)
也有int (*)[4]
类型。它指向一个4 int的数组,它在1 * sizeof(a)
开始后开始4 * sizeof(int)
个字节(或a
个字节)。
*(&a + 1)
的类型为int [4]
(数组为4 int)。它的存储在1 * sizeof(a)
开始后4 * sizeof(int)
个字节(或a
个字节开始。
*(&a + 1) - 1
的类型为int *
(指向int的指针),因为数组*(&a + 1)
衰减到指向此表达式中第一个元素的指针。它将指向一个在1 * sizeof(int)
开始之前开始*(&a + 1)
个字节的int。这是与&a[3]
相同的指针值。
*(*(&a + 1) - 1)
的类型为int
。由于*(&a + 1) - 1
与&a[3]
的指针值相同,因此*(*(&a + 1) - 1)
相当于a[3]
,已初始化为3
,因此这是由printf
。
答案 2 :(得分:7)
&a + 1
会在a
元素之后立即指向内存,或者更好地指向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]
(当然它应该仅应用于数组,而不应用于指针)。
答案 3 :(得分:2)
最好向自己证明:
$ cat main.c
#include <stdio.h>
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)
)
答案 4 :(得分:1)
如果您有T类型的对象,例如
T obj;
然后声明
T *p = &obj;
使用对象占用的内存地址p
初始化指针obj
表达式p + 1
指向对象obj
之后的内存。表达式p + 1
的值等于&obj plus sizeof( obj )
的值,等于
( T * )( ( char * )&obj + sizeof( obj ) )
因此,如果您在帖子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
指向真实的第一个元素int[4]
之后的a
类型的想象元素。表达式*(&a + 1)
给出了这个想象的元素。但由于该元素是一个类型为int[4]
的数组,因此该表达式将转换为指向其第一个int *
类型元素的指针
第一个元素跟在数组a
的最后一个元素之后。在这种情况下,表达式*(&a + 1) - 1
给出了数组a
的最后一个元素的地址
通过*(*(&a + 1) - 1)
中的解除引用,您可以获得数组a
的最后一个元素的值,因此将输出数字3
。
答案 5 :(得分:0)
请注意,以下内容相同,但同样令人讨厌:
printf("%d\n", (&a)[1][-1]);
在这种情况下,我认为更明确的是:
指向数组a的指针
指针的使用就好像它是一个数组:一个元素数组 a,即使用4个整数的数组,使用该数组的第1个元素。
由于a实际上不是一个数组,而只是一个元素(由...组成) 四个子元素!)这在
[-1]直接读取内存前面的整数 在a之后,这是
答案 6 :(得分:-1)
*(*(&a + 1) - 1)
是解决数组中最后一个元素的一种尴尬和危险的方法。 &amp; a是int [4]类型数组的地址。 (&amp; a + 1)在当前寻址的a之后给出下一个int [4]数组。通过使用*(&amp; 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个字节对齐。如果你这样做(&amp; a + 1)你得到下一个uint8 [5]数组的地址,该数组与4字节对齐,即它是8加8字节。如果你取这个ar的地址加上8个字节并返回一个字节,你就读到了ar加7,它没有被使用。