获取数组变量的地址意味着什么?

今天我读了一个让我感到困惑的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:读完答案后,我意识到这两个表达式主要让我困惑:

  1. &a + 1 ,已在SO中询问: 关于c中的表达“&anArray”

  2. *(&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是指向四个intint (*)[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,它没有被使用。