C-无关指针的平等比较可以评估为true



C标准的第6.5.9节有关==!=操作员指定以下内容:

2 以下一个应保持:

  • 两个操作数具有算术类型;
  • 两个操作数都是兼容类型的合格或不合格版本的指示;
  • 一个操作数是指向对象类型的指针,另一种是指向合格或不合格版本的void的指针。或
  • 一个操作数是指针,另一个是一个无效的指针常数。

...

6 两个指针在且仅当两个指数都是零指针时, 两者都是指向同一对象的指针(包括指向对象的指针 以及一个从开始时)或功能,都是指的指示 一个超过同一数组对象的最后一个元素,或一个是一个 指向一个阵列对象的一个末端,另一个是一个 指向发生的不同数组对象的开始 立即按地址空间中的第一个数组对象。 109)

7 出于这些操作员的目的,指向对象的指针 阵列的元素不是与第一个指针相同 长度为一段的元素,其对象的类型为其 元素类型。

脚注109:

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

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

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

这应该是合法的,因为我们在数组末尾使用的一个元素(在这种情况下是一个被视为尺寸1的数组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)=%zun", sizeof(int));
    printf("&a=%pn", (void *)&a);
    printf("&b=%pn", (void *)&b);
    printf("x=%pn", (void *)x);
    printf("y=%pn", (void *)y);
    printf("addr: a precedes b: %dn", ((&a)+1) == &b);
    printf("addr: b precedes a: %dn", &a == ((&b)+1));
    printf("pntr: a precedes b: %dn", (x+1) == y);
    printf("pntr: b precedes a: %dn", x == (y+1));
    printf("  x=%p,   &a=%pn", (void *)(x), (void *)(&a));
    printf("y+1=%p, &b+1=%pn", (void *)(y+1), (void *)(&b+1));
    struct s s1;
    x=&s1.a;
    y=&s1.b;
    printf("addr: s.a precedes s.b: %dn", ((&s1.a)+1) == &s1.b);
    printf("pntr: s.a precedes s.b: %dn", (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时评估为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

现在两个比较都评估为false,即使(如前所述)所比较的地址似乎是相同的。

这似乎指出了不确定的行为,但是根据我阅读上述段落的方式,似乎应该允许。

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

我在这里误读了有关允许的内容(这意味着这是UB),还是在这种情况下此版本的GCC不合格?

无关指针的平等比较可以评估为true吗?

是,但是...

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

通过我对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 = %pn&y = %pn", (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。我相信这是不正确的行为,但显然,它不会修复。

我认为

clang 3.8.1的行为正确,在所有优化级别显示x immediately follows y。Clang以前有问题。我报告了:

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

并得到纠正。

我建议不要依靠可能始终如一的相邻物体的地址的比较。

(请注意,关系运算符(<<=>,CC_29,>=)在无关对象的指示器上具有不确定的行为,但是通常需要相等的运算符(==!=)。

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

是明确定义明确的代码,但可能比通过判断更多。

您被允许取标量的地址,并设置一个指针一个地址。因此,&a + 1是有效的,但&a + 2无效。您还可以使用==!=将同一类型指针的值与任何其他有效指针的值进行比较,尽管指针算术仅在数组中有效。

您的断言ab的地址告诉您有关将它们放置在内存中的任何内容。需要明确的是,您不能在a的地址上通过指针算术"到达" b

至于

struct s {
    int a;
    int b;
};

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

无关指针的平等比较可以评估为true吗?

是。c指定何时为。

两个指针在且仅当...或一个是一个指针的指针,一个指向一个数组对象的末端,而另一个是一个指针,是一个指向不同数组对象的开始地址空间中的对象。C11DR§6.5.96

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


以下代码表明它是可能的。除了传统的"%p"(void*)

但是,OP的代码和输出没有反映这一点。给定"在且仅当"仅当"上述规格的一部分时,IMO, OP的汇编是不合格的。相同类型的内存变量p,q中的相邻&p+1 == &q&p == &q+1必须为true。

没有意见的对象是否在类型上不同-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) = %zun", 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: %dn", &a + 1 == &b);
  printf("&a == &b + 1: %dn", &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

标准的作者并没有试图使其"防止律师",因此,这有些模棱两可。当编译器作家做出真正的努力以维护最少惊讶的原则时,这种歧义通常不会是一个问题,因为存在明显的不受声望的行为,并且任何其他行为都会带来惊人的后果。另一方面,这确实意味着那些对在标准读取是否可以在任何读取标准中可以证明优化合理的编译器作家要比是否与现有代码兼容的是合理的,这可以找到有趣的机会来证明不相容的合理性。

标准不需要指针的表示与基本物理体系结构有任何关系。对于系统,将每个指针表示为手柄和偏移的组合将是完全合法的。以这种方式代表指针的系统将可以自由移动在物理存储中所示的物体,因为它认为合适。在这样的系统上,对象#57的第一个字节可能会立即在对象#23的最后一刻之后立即跟随,但可能在其他某个时刻处于完全无关的位置。我在标准中什么也看不到这种实现将无法报告对象#23的"过去"指针,这是当两个对象恰好相邻的对象#57的指针等于

此外,根据AS-IF规则,以这种方式在移动对象中可以证明的实现并因此具有古怪的平等运算符,因此,无论是物理上是否具有古怪的平等运算符在存储中移动对象。

但是,如果实现指定指示器如何存储在RAM中,则该定义与上述行为不一致,但是,这将迫使实现以符合该规范一致的方式实施平等操作员。任何想要具有古怪平等运算符的编译器都必须避免指定与这种行为不一致的指针储存格式。

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

因此,虽然编译器可以为指示器提供古怪的平等语义,而这些指示器永远不会暴露于可能观察其字节级表示的代码,但这种行为许可不会扩展到如此暴露的指针。如果一个实现定义了邀请编译器的指令或设置,则在给出一个对象的末尾的指示时,请任意报告单独的比较,而另一个对象的启动只能通过这种比较观察到另一个对象的开始,则该实现将没有担心在观察到指针表示的情况下的一致性。否则,即使在某些情况下,允许符合实现的情况具有古怪的比较语义,这并不意味着任何质量实施都应该这样做从指针到下一个开始的表示形式。

最新更新