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

关于==!=运算符的C标准的 6.5.9节陈述如下:

2以下情况之一应为:

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

6 两个指针比较相等当且仅当两者都是空指针,都是指向同一个对象的指针(包括一个指向一个对象和一个子对象的指针)或者函数,都指向一个指向同一个对象的最后一个元素数组对象,或者一个指针指向一个数组对象的末尾,另一个指向不同数组对象的起始位置,该指针恰好紧跟地址空间中的第一个数组对象。 109)

7对于这些运算符而言, 指向不是数组元素的对象的指针的行为与指向长度为1的数组的第一个元素的指针相同,该对象的types是元素types。

脚注109:

109) 两个对象可能在内存中相邻,因为它们是大型数组中相邻的元素或结构的相邻成员之间没有填充,或者因为实现select将它们放置,即使它们不相关 。 如果之前的无效指针操作(如访问数组边界外)产生未定义的行为,后续比较也会产生未定义的行为。

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

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

这应该是合法的,因为我们正在使用地址一个元素超过数组的末尾(在这种情况下是单个对象作为大小为1的数组对待),而不需要对其进行解引用。 更重要的是,如果一个variables紧跟在另一个variables的后面,那么这两个语句中的一个将被要求输出1

但是,testing似乎并没有解决这个问题。 鉴于以下testing程序:

 #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: sa precedes sb: %d\n", ((&s1.a)+1) == &s1.b); printf("pntr: sa precedes sb: %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: sa precedes sb: 1 

我们可以在这里看到int是4个字节, a的地址是b的地址之后a 4个字节,而x保存了y的地址, y保存了b的地址。 然而比较&a == ((&b)+1)在比较(x+1) == y计算结果为true时计算为false。 我希望两者都是正确的,因为被比较的地址看起来是一样的。

-O1 ,我得到这个:

 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: sa precedes sb: 1 pntr: sa precedes sb: 1 

现在两个比较的结果都是假的,即使(和以前一样)被比较的地址看起来是一样的。

这似乎是指未定义的行为 ,但基于我如何阅读上述段落似乎这应该是允许的。

还要注意,在一个struct相同types的相邻对象的地址的比较在所有情况下都打印出预期的结果。

我在这里误解了什么是允许的(意思是UB),还是这个版本的gcc在这种情况下不符合?

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

是的但是…

 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

该错误被作为另一个报告的副本closures:

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

对这些错误报告作出回应的海湾合作委员会维护者似乎认为,两个对象的邻接关系不一定是一致的,并且在同一个程序的运行过程中,他们的地址比较可能表明它们相邻或不相邻。 从您对第二张Bugzilla票的评论中可以看出,我强烈反对。 在我看来,没有==运算符的一致行为,标准对相邻对象的要求是没有意义的,我认为我们必须假设这些词不仅仅是装饰性的。

这是一个简单的testing程序:

 #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个字节,但是我只能在-O0处得到y immediately follows x ; 在-O1-O2-O3我得到x and y are not adjacent 。 我相信这是不正确的行为,但显然,这不是固定的。

叮当3.8.1,在我看来,performance正确,在所有优化级别显示x immediately follows yx immediately follows y之后。 铿锵以前有这个问题; 我报告说:

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

并得到纠正。

我build议不要依靠比较可能相邻对象的地址performance一致。

(请注意,指向不相关对象的关系运算符( <<=>>= )具有未定义的行为,但通常需要相等运算符( ==!= )才能保持一致。

 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不是。 您也可以使用==!=来比较相同types指针的值与任何其他有效指针的值,尽pipe指针算术只在数组中有效。

你断言ab的地址告诉你关于如何将这些放置在内存中的任何事情都是垃圾。 要清楚的是,你不能通过指针算术来“达到” b的地址。

至于

 struct s { int a; int b; }; 

该标准保证struct的地址与a的地址相同,但允许在ab之间插入任意数量的填充。 同样,你不能通过对地址的指针算术来达到b的地址。

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

是。 C指定这是否为真。

两个指针比较相等当且仅当…或者一个是指向一个指针的指针超过一个数组对象的末尾,另一个指针指向一个不同的数组对象的开始,该对象恰好紧跟着第一个数组对象地址空间。 C11dr§6.5.96

要清楚的是:代码中的相邻variables不需要在内存中相邻,但可以是。


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

然而,OP的代码和输出不能反映这一点。 鉴于上述规范中的“仅当且仅当”的比较,海事组织的编制是不符合规定的相同types的内存variablesp,q相邻, &p+1 == &q&p == &q+1必须为真。

没有意见,如果对象types不同 – 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 

标准的作者并没有试图使其成为“语言律师的证据”,因此,它有点含糊不清。 这种模糊不清的问题一般不会成为编写者为维护“最小惊讶原则”而作出的真正努力的问题,因为有一个明显的非惊人的行为,任何其他的行为都会带来惊人的后果。 另一方面,它确实意味着那些对于在标准阅读的情况下优化是否合理的编译器作者更感兴趣,而不是与现有代码兼容的编译器能否find有趣的机会来certificate不兼容。

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

此外,在as-if规则下,如果以这样的方式移动对象并具有一个离奇的等式操作符,那么这个实现将被允许具有一个古怪的等式操作符,而不pipe它是否在物理上移动物体在存储。

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

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

因此,尽pipe编译器对于从不暴露于可能观察它们的字节级表示的代码的指针具有古怪的平等语义,但是这样的行为许可不会延伸到由此暴露的指针。 如果一个实现定义了一个指令或设置,当给定指向一个对象的末尾的指针和另一个只能通过这种比较可以观察的位置的指针时,邀请编译器使得个体比较任意地报告相等或不相等,实现将不具有在观察指针表示的情况下担心符合性。 否则,即使在有符合实现的情况下会被允许具有古怪的比较语义的情况下,这并不意味着任何质量实现都应该这样做,除非一个指针刚好超过一个对象的末尾,从一个指针到下一个开始的表示。