【问题标题】:Is &array[i] always equivalent to (array + i)? [duplicate]&array[i] 总是等价于 (array + i) 吗? [复制]
【发布时间】:2021-06-18 03:59:24
【问题描述】:

最近看到一个C代码是这样的:

#include <stdio.h>

int main(void) {
    int array[5] = {1, 2, 3, 4, 5};

    for (int* ptr = &array[0]; ptr != &array[5]; ptr++)
        printf("%d\n", *ptr);

    return 0;
}

由于在 C 中运算符[] 的优先级高于运算符&amp;,我认为&amp;array[5] 等效于&amp;(*(array + 5)),这会导致未定义的行为(我们不允许取消引用array + 5)。这就是为什么我怀疑上面的代码格式不正确。 (顺便说一句,我知道ptr != array + 5可以。)

我使用带有-O0 -fsanitize=address,undefined 编译器标志的GCC 11.1.0 和Clang 12.0.0 测试了这段代码,但是两个编译器都将&amp;array[5] 解释为array + 5,并且没有发生意外行为。

&amp;array[i] 是否总是等同于array + i(即使array[i] 无效)?提前谢谢你。

【问题讨论】:

  • 你是对的,&amp;array[5] 等同于&amp;(*(array + 5)),但我认为两者都是有效的。两者实际上都没有取消引用指针。他们只是用它来做指针算术。
  • 一般来说,你需要一个sizeof() 来确定每个元素的偏移量。
  • 可能重复:stackoverflow.com/questions/38915128/… 用标准的引用回答您的问题:§6.5.3.2 “...如果操作数是一元 * 运算符的结果,则该运算符和 &运算符被评估,结果好像两者都被省略了”所以没有未定义的行为。
  • 如果我是你,我会使用array + i 以防万一。 “上面的代码格式错误” 格式错误的意思是“需要编译错误”,所以不是这样。
  • @pjs,我不明白你的说法。指针运算已经在元素大小的步骤中执行,那么sizeof 会完成什么?能举个例子吗?

标签: c pointers language-lawyer undefined-behavior


【解决方案1】:

首先是6.5.2.1/2:

下标运算符[]的定义是E1[E2]等同于(*((E1)+(E2)))

然后在 (6.5.3.2/3) 中定义,一元 &amp; 运算符:

[...] 同样,如果操作数是 [] 运算符的结果,则 &amp; 运算符和 对 [] 隐含的一元 * 求值,结果与 &amp; 运算符一样 已删除,[] 运算符已更改为 + 运算符。

这明确表示&amp;x[y] 意味着(x) + (y)

【讨论】:

    【解决方案2】:

    即使数组[i] 无效

    从消毒剂的角度回答:

    &amp;array[i]array+i 总是给出相同的指针,但只有&amp;array[i] 会通过地址清理器引发运行时错误(至少在 gcc 中)。所以,在这方面它们是不等价的。

    请注意,如果i=5 在您的情况下,如果指针未取消引用,地址清理程序不会出错,因此上面的代码将起作用(即使清理程序已打开)。但是,如果 i 大于 5,sanitizer 会立即给出错误。关于上面的代码,建议使用指针算法(如果你坚持使用指针):

    for (int* ptr = array; ptr < array+5; ptr++)
    

    【讨论】:

      【解决方案3】:

      尽管 C 标准根据指针添加和取消引用运算符 +*+ 和一元 * 运算符定义了 [] 运算符的行为,因此 array[i] 意味着*(array+i),clang 和 gcc 都没有真正以这种方式处理它。在两个运算符都会产生已定义行为的情况下,这些行为是相同的,但它们与编译器对“严格别名规则”的解释的交互方式不同。

      例如,标准不允许使用成员类型左值访问联合对象。给定如下声明:

      union blob { unsigned short hh[4]; unsigned ww[2]; } u;
      

      建议访问这些成员的唯一方法是使用字符指针或函数,如memcpy,这将是荒谬的,特别是因为标准明确允许通过联合对象进行类型双关。另一方面,如果在 gcc 中测试函数:

      int test1(int i, int j)
      {
          u.hh[i] = 1;
          u.ww[j] = 2;
          return u.hh[i];
      }
      int test2(int i, int j)
      {
          *(u.hh+i) = 1;
          *(u.ww+j) = 2;
          return *(u.hh+i);
      }
      

      两个编译器都会为test1 生成代码以适应类型双关语的可能性,但将为test2 生成无条件返回1 的代码。这是允许的,因为标准将这两种形式都描述为未定义行为以便允许实现以 - 在实施质量的基础上 - 支持他们的客户认为有用的任何形式。

      虽然我不知道您列出的特定表达式中的行为如何比较,但它们对 [] 的解释不同于 +* 的组合这一事实意味着标准对前者的定义后者的术语不应被视为表明它们将被相同地处理。

      【讨论】:

      • 这似乎只是在描述一个编译器错误,test2(0, 0) 不是 UB(虽然由于字节序是实现定义的)
      • @MM:clang 和 gcc 的维护者都认为联合类型双关语和通用初始序列保证仅适用于通过联合类型的左值“直接”进行的访问,并且不适用于它们是通过从联合左值派生的指针生成的情况,编译器必须故意盲目地不注意。该标准的作者并没有特别努力避免将其描述为 UB 操作,他们希望大多数实现会始终如一地处理而没有很好的理由不这样做,而是期望...
      • ...质量实现作为“符合语言扩展”的一种形式,在比标准规定的更多情况下表现得更有意义。虽然这种松懈使得“严格符合”程序的概念不如其他程序有用,但绝大多数 C 程序将符合但无论如何都不是严格符合。
      • 我怀疑您声称该标准的作者打算将 test2 设为 UB 。鉴于标准明确将 test1 和 test2 定义为相同,编译器供应商似乎是一个糟糕的决定来打破test2。不是您所说的“质量实施”
      • @MM:我不会说他们“打算”它是 UB,因为他们对它是否是漠不关心,因为会期望质量实现来有意义地处理它 标准是否要求他们这样做。如果标准从本质上说,如果代码从另一种指针或左值派生出一种类型的指针或左值,则许多问题都可以解决,使用该指针的操作在派生点和使用点之间是无序的.它们需要稍微复杂一点才能处理函数和循环边界......
      猜你喜欢
      • 2015-02-10
      • 2017-10-16
      • 1970-01-01
      • 2011-05-05
      • 2021-09-28
      • 2015-08-26
      • 2012-09-07
      • 2020-12-28
      • 1970-01-01
      相关资源
      最近更新 更多