不相关指针的相等比较可以评估为真吗?

时间:2017-08-30 17:46:12

标签: c pointers language-lawyer

关于==!=运营商的C standard第6.5.9节声明如下:

  

2 以下其中一项应成立:

     
      
  • 两个操作数都有算术类型;
  •   
  • 两个操作数都指向兼容类型的合格或非限定版本;
  •   
  • 一个操作数是指向对象类型的指针,另一个是指向合格或非限定版本的void的指针;或
  •   
  • 一个操作数是指针,另一个是空指针常量。
  •   
     

...

     

6 两个指针比较相等当且仅当都是空指针时,   两者都是指向同一对象的指针(包括指向对象的指针)   以及它的开头的一个子对象)或函数,都是指针   一个超过同一个数组对象的最后一个元素,或者一个是一个   指向一个数组对象末尾的指针,另一个指向   指向恰好发生的另一个数组对象的开始的指针   紧跟地址空间中的第一个数组对象。 109)

     

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

脚注109:

  

109)两个物体可能在内存中相邻,因为它们是相邻的   更大阵列的元素或没有的结构的相邻成员   填充它们之间,或因为实现选择放置   他们是这样,即使他们是无关的。如果先前无效指针   操作(例如数组边界外的访问)生成未定义   行为,后续比较也会产生不确定的行为。

这似乎表明您可以执行以下操作:

int a;
int b;
printf("a precedes b: %d\n", (&a + 1) == &b);
printf("b precedes a: %d\n", (&b + 1) == &a);

这应该是合法的,因为我们使用地址一个元素超过数组的末尾(在这种情况下是一个被视为大小为1的数组的单个对象)而不解除引用它。更重要的是,如果一个变量在内存中紧跟另一个变量,则需要输出1这两个语句中的一个。

然而,测试似乎并没有将其解决。鉴于以下测试计划:

#include <stdio.h>

struct s {
    int a;
    int b;
};

int main()
{
    int a;
    int b;
    int *x = &a;
    int *y = &b;

    printf("sizeof(int)=%zu\n", sizeof(int));
    printf("&a=%p\n", (void *)&a);
    printf("&b=%p\n", (void *)&b);
    printf("x=%p\n", (void *)x);
    printf("y=%p\n", (void *)y);

    printf("addr: a precedes b: %d\n", ((&a)+1) == &b);
    printf("addr: b precedes a: %d\n", &a == ((&b)+1));
    printf("pntr: a precedes b: %d\n", (x+1) == y);
    printf("pntr: b precedes a: %d\n", x == (y+1));

    printf("  x=%p,   &a=%p\n", (void *)(x), (void *)(&a));
    printf("y+1=%p, &b+1=%p\n", (void *)(y+1), (void *)(&b+1));

    struct s s1;
    x=&s1.a;
    y=&s1.b;
    printf("addr: s.a precedes s.b: %d\n", ((&s1.a)+1) == &s1.b);
    printf("pntr: s.a precedes s.b: %d\n", (x+1) == y);
    return 0;
}

编译器是gcc 4.8.5,系统是CentOS 7.2 x64。

使用-O0,我得到以下输出:

sizeof(int)=4
&a=0x7ffe9498183c
&b=0x7ffe94981838
x=0x7ffe9498183c
y=0x7ffe94981838
addr: a precedes b: 0
addr: b precedes a: 0
pntr: a precedes b: 0
pntr: b precedes a: 1
  x=0x7ffe9498183c,   &a=0x7ffe9498183c
y+1=0x7ffe9498183c, &b+1=0x7ffe9498183c
addr: s.a precedes s.b: 1

我们在这里可以看到int为4个字节,a的地址超过b地址的4个字节,而x保存地址当a保留y的地址时,b&a == ((&b)+1)。但是,当比较(x+1) == y计算为true时,比较-O1的计算结果为false。我希望两者都是正确的,因为被比较的地址看起来是相同的。

使用sizeof(int)=4 &a=0x7ffca96e30ec &b=0x7ffca96e30e8 x=0x7ffca96e30ec y=0x7ffca96e30e8 addr: a precedes b: 0 addr: b precedes a: 0 pntr: a precedes b: 0 pntr: b precedes a: 0 x=0x7ffca96e30ec, &a=0x7ffca96e30ec y+1=0x7ffca96e30ec, &b+1=0x7ffca96e30ec addr: s.a precedes s.b: 1 pntr: s.a precedes s.b: 1 ,我明白了:

struct

现在两个比较都评估为假,即使(如前所述)被比较的地址看起来是相同的。

这似乎指向undefined behavior,但根据我如何阅读上述段落,似乎应该允许这样做。

另请注意,row/column中相同类型的相邻对象的地址比较会在所有情况下打印出预期结果。

我在这里误读了一些关于允许的内容(意思是这是UB),或者这个版本的gcc在这种情况下是否不符合?

4 个答案:

答案 0 :(得分:22)

  

无关指针的相等比较可以评估为真吗?

是的,但......

int a;
int b;
printf("a precedes b: %d\n", (&a + 1) == &b);
printf("b precedes a: %d\n", (&b + 1) == &a);

根据我对C标准的解释,有三种可能性:

  • a紧接在b
  • 之前
  • b紧接在
  • 之前
  • a和b都不会紧接在另一个之间(它们之间可能存在间隙或其他对象)

我前段时间讨论过这个问题并得出结论,GCC正在对==运算符执行无效优化以获得指针,即使地址相同也会产生错误,因此我提交了一个错误报告:

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=63611

该错误已作为另一份报告的副本而被关闭:

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=61502

对这些错误报告做出回应的GCC维护者似乎认为两个对象的相邻性不需要一致,并且他们的地址的比较可能会显示它们是否相邻,在程序的同一运行中。从我对第二张Bugzilla门票的评论中可以看出,我强烈反对。在我看来,如果没有==运算符的一致行为,对相邻对象的标准要求是没有意义的,我认为我们必须假设这些单词不仅仅是装饰性的。

这是一个简单的测试程序:

#include <stdio.h>
int main(void) {
    int x;
    int y;
    printf("&x = %p\n&y = %p\n", (void*)&x, (void*)&y);
    if (&y == &x + 1) {
        puts("y immediately follows x");
    }
    else if (&x == &y + 1) {
        puts("x immediately follows y");
    }
    else {
        puts("x and y are not adjacent");
    }
}

当我使用GCC 6.2.0进行编译时,xy的打印地址在所有优化级别上恰好相差4个字节,但我只在{{y immediately follows x处得到-O0 1}};在-O1-O2-O3,我得到x and y are not adjacent。我认为这是不正确的行为,但显然,它不会被修复。

在我看来,

clang 3.8.1行为正确,在所有优化级别显示x immediately follows y。 Clang以前遇到过这个问题;我报告了它:

https://bugs.llvm.org/show_bug.cgi?id=21327

并且已经纠正。

我建议不要依赖于对可能相邻的对象的地址进行比较。

(请注意,关于不相关对象的指针上的关系运算符(<<=>>=)具有未定义的行为,但是相等运算符({{1} },==)通常需要表现一致。)

答案 1 :(得分:12)

int a;
int b;
printf("a precedes b: %d\n", (&a + 1) == &b);
printf("b precedes a: %d\n", (&b + 1) == &a);

是完全明确定义的代码,但可能更多的是运气而非判断。

您可以获取标量的地址并将指针设置为超过该地址。因此&a + 1有效,但&a + 2不是。您还可以使用==!=将相同类型的指针的值与任何其他有效指针的值进行比较,尽管指针算法仅在数组中有效。

您断言ab的地址告诉您有关这些内容如何放入内存的任何内容都是无意义的。要清楚,你不能达到&#34; b通过指针算法对a的地址。

至于

struct s {
    int a;
    int b;
};

该标准保证struct的地址与a的地址相同,但允许在a和{{1}之间插入任意数量的填充}}。同样,您无法通过b地址上的任何指针算法到达b的地址。

答案 2 :(得分:8)

  

无关指针的相等比较可以评估为真吗?

是。 C指定何时为真。

  

两个指针比较相等,当且仅当...或者一个是指向一个数组对象末尾的指针而另一个是指向另一个数组对象的开始的指针,该指针恰好跟随第一个数组地址空间中的对象。 C11dr§6.5.96

要明确:代码中的相邻变量不需要在内存中相邻,但可以是。

以下代码演示了可能。除了传统的int*"%p"之外,它还使用(void*)的内存转储。

但OP的代码和输出并未反映出这一点。鉴于上述规范的“比较相等且仅仅是否”,IMO, OP的编译是不合规的。相邻类型的内存变量p,q相邻,&p+1 == &q&p == &q+1必须为真。

如果对象的类型不同,则无意见 - OP不会要求IAC。

void print_int_ptr(const char *prefix, int *p) {
  printf("%s %p", prefix, (void *) p);
  union {
    int *ip;
    unsigned char uc[sizeof (int*)];
  } u = {p};
  for (size_t i=0; i< sizeof u; i++) {
    printf(" %02X", u.uc[i]);
  }
  printf("\n");
}

int main(void) {
  int b = rand();
  int a = rand();
  printf("sizeof(int) = %zu\n", sizeof a);
  print_int_ptr("&a     =", &a);
  print_int_ptr("&a + 1 =", &a + 1);
  print_int_ptr("&b     =", &b);
  print_int_ptr("&b + 1 =", &b + 1);
  printf("&a + 1 == &b: %d\n", &a + 1 == &b);
  printf("&a == &b + 1: %d\n", &a == &b + 1);
  return a + b;
}

输出

sizeof(int) = 4
&a     = 0x28cc28 28 CC 28 00
&a + 1 = 0x28cc2c 2C CC 28 00  <-- same bit pattern
&b     = 0x28cc2c 2C CC 28 00  <-- same bit pattern
&b + 1 = 0x28cc30 30 CC 28 00
&a + 1 == &b: 1                <-- compare equal
&a == &b + 1: 0

答案 3 :(得分:3)

标准的作者并没有试图使其成为“语言 - 律师证明”,因此,它有点含糊不清。当编译器作者真正努力维护最小惊讶原则时,这种模糊性通常不会成为问题,因为存在明显的非惊人行为,而任何其他行为都会产生惊人的后果。另一方面,它确实意味着那些编译器编写者更感兴趣的是,在任何标准读取下,优化是否合理,而不是它们是否与现有代码兼容,可以找到有趣的机会来证明不兼容。

标准不要求指针的表示与底层物理体系结构有任何关系。系统将每个指针表示为句柄和偏移的组合是完全合理的。以这种方式表示指针的系统可以自由地在物理存储器中移动所表示的对象,如其认为合适。在这样的系统上,对象#57的第一个字节可能紧跟在对象#23的最后一个字节之后的某个时刻,但可能在某个其他时刻处于某个完全不相关的位置。我在标准中没有看到任何禁止这样的实现将对象#23的“刚刚过去”指针报告为当两个对象恰好相邻时指向对象#57的指针,并且当它们不发生时不相等是

此外,在as-if规则下,一个以这种方式移动对象并且有一个古怪的相等运算符的实现将被允许有一个古怪的相等运算符,无论它是否物理上在存储中移动物体。

但是,如果实现指定指针如何存储在RAM中,并且此类定义与上述行为不一致,则会强制实现以与该规范一致的方式实现相等运算符。任何想要拥有一个古怪的相等运算符的编译器都必须避免指定与这种行为不一致的指针存储格式。

此外,标准似乎暗示如果代码观察到如果具有定义值的两个指针具有相同的表示,则它们必须比较相等。使用字符类型读取对象,然后将相同的字符类型值序列写入另一个对象应该产生一个等同于原始对象的对象;这种等同性是语言的基本特征。如果p是一个“刚刚过去”一个对象的指针,而q是指向另一个对象的指针,并且它们的表示分别被复制到p2q2,那么p1必须等同于pq2q。如果pq的分解字符类型表示相等,则意味着q2使用与p1相同的字符类型值序列编写,反过来,这意味着所有四个指针都必须相等。

因此,虽然允许编译器对于从未暴露于可能观察到其字节级表示的代码的指针具有奇怪的相等语义,但是这样的行为许可不会扩展到由此暴露的指针。如果一个实现定义了一个指令或设置,它邀请编译器在给定指向一个对象的末尾的指针时任意报告相等或不相等,而另一个对象的开始只能通过这种比较观察其位置,那么实现就不会有担心在观察指针表示的情况下的一致性。否则,即使在某些情况下,如果允许符合标准的实现具有古怪的比较语义,这并不意味着任何质量实现都应该这样做,除非被邀请,除非指针刚好超过一个对象的末尾自然会有不同的指向下一个开头的指针。