是`*((*(& array + 1)) - 1)`可以安全地用来获取自动数组的最后一个元素吗?

时间:2015-09-12 09:55:46

标签: c arrays language-lawyer

假设我想获取大小未知的自动数组的最后一个元素。我知道我可以使用sizeof运算符来获取数组的大小并相应地获取最后一个元素。

使用*((*(&array + 1)) - 1)安全吗?

像:

char array[SOME_SIZE] = { ... };
printf("Last element = %c", *((*(&array + 1)) - 1));
int array[SOME_SIZE] = { ... };
printf("Last element = %d", *((*(&array + 1)) - 1));

7 个答案:

答案 0 :(得分:23)

不,不是。

&array是指向char[SOME_SIZE]的类型指针(在给出的第一个示例中)。这意味着&array + 1会在array结束后立即指向内存。解除引用(如在(*(&array+1))中给出未定义的行为。

无需进一步分析。一旦表达式的任何部分给出了未定义的行为,整个表达式就会这样做。

答案 1 :(得分:18)

我认为这不安全。

@dasblinkenlight quoted in his answer标准(现已删除)中我还想添加一些内容:

  

C99第6.5.6.8节 -

     

[...]
  如果表达式P指向数组对象的最后一个元素,则表达式(P)+1指向[...]
  如果结果指向数组对象的最后一个元素之后,则不应将其用作评估的一元*运算符的操作数。

正如它所说,我们不应该这样做*(&array + 1),因为它将超过数组的最后一个元素,所以不应该使用*

众所周知,指向未经授权的内存位置的解除引用指针会导致未定义的行为

答案 2 :(得分:12)

我认为,由于彼得在his answer中提到的原因,这是未定义的行为。

关于*(&array + 1)的争论很激烈。一方面,取消引用&array + 1似乎是合法的,因为它只是将类型从T (*)[]更改回T [],但另一方面,它仍然是未初始化,未使用和未初始化的指针未分配的记忆。

我的回答依赖于以下内容:

  

C99 6.5.6.7(加法运算符的语义)

     

出于这些运算符的目的,指向对象的指针   不是数组的元素与指向第一个元素的指针的行为相同   长度为1的数组的元素,其对象的类型为   元素类型。

由于&array不是指向作为数组元素的对象的指针,因此根据这一点,它意味着代码等效于:

char array_equiv[1][SOME_SIZE] = { ... };
/* ... */
printf("Last element = %c", *((*(&array_equiv[0] + 1)) - 1));

也就是说,&array是指向10个字符数组的指针,因此它的行为与指向长度为1的数组的第一个元素的指针相同,其中每个元素都是10个字符的数组。 / p>

现在,这与后面的条款一起(在其他答案中已经提到过;这个确切的摘录是从ameyCU's answer公然被盗的):

  

C99第6.5.6.8节 -

     

[...]
  如果表达式P指向数组对象的最后一个元素,则表达式(P)+1指向[...]
  如果结果指向数组对象的最后一个元素之后,则不应将其用作评估的一元*运算符的操作数。

很明显它是UB:它相当于取消引用指向array_equiv的最后一个元素的指针。

是的,在现实世界中,可能有效,因为实际上原始代码并没有真正取消引用内存位置,它主要是从T (*)[]到{{1的类型转换但是我很确定从严格的标准兼容性角度来看,它是未定义的行为。

答案 3 :(得分:2)

这可能是安全的,但有一些警告。

假设我们有

T array[LEN];

然后&array的类型为T(*)[LEN]

接下来,&array + 1再次为T(*)[LEN]类型,指向原始数组的末尾。

接下来,*(&array + 1)的类型为T[LEN],可能会隐式转换为T*,仍指向原始数组的末尾。 (因此我们没有取消引用无效的内存位置:未评估*运算符)。

接下来,*(&array + 1) - 1的类型为T*,指向最后一个数组位置。

最后,我们取消引用(如果数组长度不为零,这是合法的):*(*(&array + 1) - 1)给出最后一个数组元素,类型为T的值。

请注意,我们实际取消引用指针的唯一时间是在最后一步。

现在,潜在的警告。

首先,*(&array + 1)正式显示为尝试取消引用指向无效内存位置的指针。但实际上并非如此。这是数组指针的本质:这种正式的解引用只会改变指针的类型,实际上并不会导致尝试从引用的位置检索值。也就是说,array的类型为T[LEN],但它可以隐式转换为类型&T,指向数组的第一个元素; &array是指向类型T[LEN]的指针,指向数组的开头; *(&array+1)同样属于T[LEN],可以隐式转换为&T类型。实际上,指针实际上没有被解除引用。

其次,&array + 1实际上可能是一个无效的地址,但实际上并非如此:我的C ++ 11参考手册明确告诉我"将指针指向一个以外的元素数组的结尾保证可以工作"并且在K& R中也有类似的陈述,所以我相信它一直是标准行为。

最后,在零长度数组的情况下,表达式取消引用数组之前的内存位置,这可能是未分配/无效的。但是如果使用sizeof()使用更传统的方法而不首先测试非零长度,也会出现这个问题。

简而言之,我不相信这种表达的行为有任何未定义或依赖于实现的内容。

答案 4 :(得分:1)

Imho可能会起作用,但可能是不明智的。您应该仔细检查您的sw设计,并问自己为什么要进行数组的最后一次输入。数组的内容是否完全不为您所知,或者是否可以根据c结构和联合来定义结构。如果是这种情况,请远离char数组中的复杂指针操作,并在c代码中正确定义数据,尽可能在结构和联合中。

所以而不是:

 printf("Last element = %c", *((*(&array + 1)) - 1));

可能是:

 printf("Checksum = %c", myStruct.MyUnion.Checksum);

这澄清了您的代码。数组中的最后一个字母对于不熟悉此数组中的内容的人来说没有任何意义。 myStruct.myUnion.Checksum对任何人都有意义。研究myStruct结构可以向任何人解释整个数据结构。如果能以这种方式宣布,请使用类似的东西。如果你处于极少数情况下你不能,研究上面的答案,我认为它们很有意义

答案 5 :(得分:-1)

  • arrayint[SOME_SIZE]
  • 类型的数组
  • &arrayint(*)[SOME_SIZE]
  • 类型的指针
  • &array + 1int(*)[SOME_SIZE]
  • 类型的过去指针
  • *(&array + 1)int[SOME_SIZE]
  • 类型的左值

其他答案已经引用了该标准的相关部分,但我认为最后一个要点可能有助于消除混淆。

混淆似乎是因为解除引用&array + 1会产生一个过去的int*这听起来应该是合理的,即使标准在技术上禁止它。

但那不是发生了什么:取消引用它正在尝试为类型为{{1的不存在的对象生成左值(实际上是一个引用)一些真的听起来不合理的东西。

即使它是被定义的行为,而不是使用一个神秘的技巧,做一些易读的事情要好得多,

int[SOME_SIZE]

答案 6 :(得分:-3)

A)

  

如果指针操作数和[P + N]的结果都指向   同一个数组对象的元素,或者一个超过最后一个元素的元素   数组对象,评估不得产生溢出;
  [...]
  如果表达式P指向数组的元素   对象或一个超过数组对象的最后一个元素,以及   表达式Q指向同一个数组对象的最后一个元素,即   表达式((Q)+1) - (P)具有与((Q) - (P))+ 1和as相同的值    - ((P) - ((Q)+1)),如果表达式P指向一,则值为零   过去数组对象的最后一个元素,即使是表达式   (Q)+1不指向数组对象的元素。

这表明使用一个超过最后一个元素的数组元素的计算实际上是完全正常的。正如这里的一些人写的那样,使用不存在的对象进行计算已经是非法的,我认为我包含了那部分。

然后我们需要关注这一部分:

  

如果结果指向一个超过数组对象的最后一个元素的那个,那么   不得用作一元*运算符的操作数   评价。

有一个重要的部分,其他答案被省略,即:

  

如果指针操作数指向数组对象的元素

这不是事实。我们取消引用的指针操作数是指向数组对象元素的指针,它是指向指针的指针。所以整个条款完全无关紧要。但是,也有人说:

  

出于这些[additive]运算符的目的,指向对象的指针   不是数组的元素与指向第一个元素的指针的行为相同   长度为1的数组的元素,其对象的类型为   元素类型。

这是什么意思?

这意味着我们指向指针的指针实际上又是一个指向数组的指针 - 长度为[1]。现在我们可以关闭循环,因为正如第一段所述,我们被允许使用一个数组进行计算,因此我们可以使用数组进行计算,就好像它是一个长度数组[2]!

以更加图形化的方式:

ptr -> (ptr to int[10])[0] -> int[10]
    -> (ptr to int[10])[1]

所以,我们被允许用(ptr到int [10])[1]进行计算,即使它在技术上超出了长度[1]的数组。

b)中

发生的步骤是:

array ptr类型为int [SOME_SIZE]到第一个元素数组

&array ptr到int [SOME_SIZE]类型的ptr到数组的第一个元素

+ 1 ptr,比第一个元素数组的int [SOME_SIZE]类型的ptr多1个)到类型为int的ptr

根据C99第6.5.6.8节,这是 NOT 指向int [SOME_SIZE + 1]的指针。这是ptr + SOME_SIZE + 1

*我们取消引用指向指针的指针。 现在,在解除引用之后,我们有一个根据C99第6.5.6.8节的指针,该指针超过了数组的元素,不允许解除引用。允许该指针存在,我们可以在其上使用运算符,除了一元*运算符。但是我们还没有在那个指针上使用那个。

-1现在我们从int类型的ptr中减去一个数组的最后一个元素后面的一个,让ptr指向数组的最后一个元素。

*将ptr取消引用到数组的最后一个元素的int,这是合法的。

c)中

最后,但并非最不重要:

如果它是非法的,那么offsetof宏也是非法的,定义为:
((size_t)(&((st *)0)->m))