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

dbu*_*ush 37 c pointers language-lawyer

关于和运营商的C标准第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);
Run Code Online (Sandbox Code Playgroud)

这应该是合法的,因为我们使用地址一个元素超过数组的末尾(在这种情况下是一个被视为大小为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;
}
Run Code Online (Sandbox Code Playgroud)

编译器是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
Run Code Online (Sandbox Code Playgroud)

我们在这里可以看到a int是4个字节并且地址a是4个字节超过地址b,并且x保存地址的awhile y保存地址b.但是,当比较&a == ((&b)+1)评估为true时,比较结果(x+1) == y为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: s.a precedes s.b: 1
pntr: s.a precedes s.b: 1
Run Code Online (Sandbox Code Playgroud)

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

这似乎指向未定义的行为,但根据我如何阅读上述段落,似乎应该允许这样做.

另请注意,struct在所有情况下,相同类型的相邻对象的地址的比较打印出预期结果.

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

Kei*_*son 23

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

是的,但......

int a;
int b;
printf("a precedes b: %d\n", (&a + 1) == &b);
printf("b precedes a: %d\n", (&b + 1) == &a);
Run Code Online (Sandbox Code Playgroud)

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

  • a紧接在b之前
  • b紧接着a
  • 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");
    }
}
Run Code Online (Sandbox Code Playgroud)

当我用GCC 6.2.0编译它时,所有优化级别的打印地址xy4个字节相差不多,但我y immediately follows x只得到-O0; 在-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

它被纠正了.

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

(请注意,关系运算符(<,<=,>,>=上指向不相关的对象)具有不确定的行为,但相等运算符(==,!=)一般都要求行为一致.)

  • 有趣的是,`a+1 == b` 可能是真的,但根据我对 §6.5.9 6 和 §6.5.8 5 的阅读,`a+1 &gt;= b` 是 UB。 (2认同)

Bat*_*eba 12

int a;
int b;
printf("a precedes b: %d\n", (&a + 1) == &b);
printf("b precedes a: %d\n", (&b + 1) == &a);
Run Code Online (Sandbox Code Playgroud)

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

您可以获取标量的地址并将指针设置为超过该地址.所以&a + 1是有效的,但&a + 2事实并非如此.您也允许比较同类型使用任何其他有效的指针的值的指针的值==!=,虽然指针运算只阵列内有效.

你的断言是关于如何将它们放在内存中的地址ab告诉你什么是双层的.要清楚,你不能通过b地址上的指针算术"到达" a.

至于

struct s {
    int a;
    int b;
};
Run Code Online (Sandbox Code Playgroud)

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

  • 但是OP的问题(6.5.9)中的第6点呢?也就是说,一个是指向一个数组对象**的结尾**的指针,另一个是指向另一个数组对象的**开始的指针**恰好跟随地址空间中的第一个数组对象.结合第7点,它似乎符合这种情况. (3认同)
  • @groo 定义 `int a; 时 int b;` 不能假设这两个变量的内存位置...... (2认同)

chu*_*ica 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;
}
Run Code Online (Sandbox Code Playgroud)

产量

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
Run Code Online (Sandbox Code Playgroud)

  • 这在过去已经讨论了很多,但是 gcc 故意不遵守 C11,因为他们认为在这种情况下遵守会使代码变得悲观(我同意);已建议 C2X 此更改未指定 (2认同)

sup*_*cat 5

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

该标准不要求指针的表示与底层物理架构有任何关系。系统将每个指针表示为句柄和偏移量的组合是完全合法的。以这种方式表示指针的系统可以自由地在物理存储中随意移动由此表示的对象,因为它认为合适。在这样的系统上,对象#57 的第一个字节可能在某个时刻紧跟在对象#23 的最后一个字节之后,但在其他时刻可能位于某个完全不相关的位置。我在标准中看不到任何内容会阻止这样的实现在两个对象碰巧相邻时将对象 #23 的“刚刚过去”的指针报告为等于对象 #57 的指针,

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

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

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

因此,虽然允许编译器对从未暴露于可能观察其字节级表示的代码的指针具有古怪的相等语义,但这种行为许可不会扩展到因此暴露的指针。如果一个实现定义了一个指令或设置,当给定指向一个对象的结尾和另一个对象的开头的指针时,邀请编译器任意报告单个比较的相等或不相等,该对象的位置只能通过这种比较观察到,则实现将不会有在观察到指针表示的情况下担心一致性。否则,即使在某些情况下允许符合的实现具有古怪的比较语义,那也不会