为什么编译器会假设这些看似相等的指针有所不同?

看起来GCC有一些优化认为来自不同翻译单元的两个指针永远不会相同,即使它们实际上是相同的。

码:

main.c中

#include  #include  int a __attribute__((section("test"))); extern int b; void check(int cond) { puts(cond ? "TRUE" : "FALSE"); } int main() { int * p = &a + 1; check( (p == &b) == ((uintptr_t)p == (uintptr_t)&b) ); check(p == &b); check((uintptr_t)p == (uintptr_t)&b); return 0; } 

公元前

 int b __attribute__((section("test"))); 

如果我用-O0编译它,它会打印出来

 TRUE TRUE TRUE 

但是-O1

 FALSE FALSE TRUE 

所以p&b实际上是相同的值,但编译器优化了它们的比较,假设它们永远不会相等。

我无法弄清楚,哪种优化做到了这一点。

它看起来不像严格别名,因为指针是一种类型, -fstrict-aliasing选项不会产生这种效果。

这是记录在案的行为吗? 或者这是一个错误?

您的代码中有三个方面导致一般问题:

  1. 将指针转换为整数是实现定义的 。 无法保证两个指针的转换以使所有位相同。

  2. uintptr_t保证从指针转换为相同类型然后返回不变(即比较等于原始指针)。 但仅此而已。 整数值本身不能保证比较相等。 例如,可能存在具有任意值的未使用位。 见标准, 7.20.1.4 。

  3. 并且(简要地)两个指针只能比较相等,如果它们指向同一个数组或正好在它后面(最后一个条目加一个)或至少一个是空指针 。 对于任何其他星座,他们比较不平等。 有关具体细节,请参阅标准6.5.9p6 。

最后,无法保证变量如何通过工具链放置在内存中(通常是静态变量的链接器,自动变量的编译器)。 只有数组或struct (即复合类型)才能保证其元素的排序。

例如,6.5.9p7也适用。 它基本上将指向非数组对象的指针视为比较大小为1的数组的第一个条目。 这不包括超过对象的递增指针,如&a + 1 。 相关是指针所基于的对象。 这是指针p对象a和指针&b 。 其余部分见第6段。

你的变量都不是一个数组(第6段的最后一部分),所以指针不需要比较相等,即使对于&a + 1 == &b 。 假设uintptr_t比较返回true,最后一个“TRUE”可能来自gcc。

众所周知,gcc在严格遵循标准的同时进行了积极的优化。 其他编译器更保守,但这会导致代码优化程度降低。 不要通过禁用优化或其他黑客来尝试“解决”这个问题,而是使用明确定义的行为来修复它。 这是代码中的错误。

p == &b是一个指针比较,并遵守C标准中的以下规则( 6.5.9 Equality运算符,第4点 ):

两个指针比较相等,当且仅当两个都是空指针时,两者都是指向同一对象的指针(包括指向对象的指针和在其开头的子对象)或函数,两者都是指向同一数组的最后一个元素之后的指针对象,或者一个是指向一个数组对象末尾的指针,另一个是指向不同数组对象的开头的指针,该数组对象恰好跟随地址空间中的第一个数组对象。

(uintptr_t)p == (uintptr_t)&b算术比较,并遵守以下规则( 6.5.9等式运算符,第6点 ):

如果两个操作数都具有算术类型,则执行通常的算术转换。 当且仅当它们的实部两者相等并且它们的虚部相等时,复数类型的值是相等的。 当且仅当它们转换为通常算术转换确定的(复杂)结果类型的结果相等时,来自不同类型域的任何两个算术类型值都是相等的。

这两个摘录需要与实现完全不同的东西。 很明显,在调用后一种类型的情况下,C规范没有要求实现模仿前一种比较的行为,反之亦然。 实现只需遵循此规则( 7.18.1.4能够在C99 中保存对象指针的整数类型或在C11 中保存 7.20.1.4 ):

[ uintptr_t ]类型指定一个无符号整数类型,其属性是任何有效的void指针都可以转换为此类型,然后转换回指向void的指针,结果将比较原始指针。

(附录:上述引用在这种情况下不适用,因为从int*uintptr_t不涉及void*作为中间步骤。请参阅Hadi的答案以获得解释和引用 。仍然,有问题的转换是实现定义和您尝试的两个比较不需要表现出相同的行为,这是这里的主要内容。)

作为差异的一个例子,考虑两个指向两个不同地址空间的相同地址的指针。 将它们作为指针进行比较不应该返回true,但将它们作为无符号整数进行比较可能会。

&a + 1是添加到指针的整数,符合以下规则( 6.5.6 Additive operators,point 8 ):

当一个具有整数类型的表达式被添加到指针或从指针中减去时,结果具有指针操作数的类型。 如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向与原始元素偏移的元素,使得结果元素和原始数组元素的下标的差异等于整数表达式。 换句话说,如果表达式P指向数组对象的第i个元素,则表达式(P)+ N(等效地,N +(P))和(P)-N(其中N具有值n)指向分别为数组对象的第i + n和第i-n个元素,只要它们存在。 此外,如果表达式P指向数组对象的最后一个元素,则表达式(P)+1指向一个超过数组对象的最后一个元素,如果表达式Q指向一个超过数组对象的最后一个元素,表达式(Q)-1指向数组对象的最后一个元素。 如果指针操作数和结果都指向同一个数组对象的元素,或者指向数组对象的最后一个元素,则评估不应产生溢出; 否则,行为未定义。 如果结果指向数组对象的最后一个元素之后,则不应将其用作已计算的一元*运算符的操作数。

我相信这段摘录表明,指针加法(和减法)仅针对同一数组对象中的指针或超过最后一个元素的指针定义。 并且因为(1) a不是数组而且(2) ab不是同一个数组对象的成员,在我看来你的指针数学运算调用未定义的行为,你的编译器利用它来假设指针比较返回false。 再次如Hadi的回答所指出的那样(与我原来的答案在此时所假设的相反),指向非数组对象的指针可以被认为是指向长度为1的数组对象的指针,因此在指向标量的指针中添加一个指针限定为指向数组末尾的一个。

因此,您的案例似乎属于本答案中提到的第一个摘录的最后一部分,当您并且仅当两个变量按顺序和升序链接时,才能将您的比较明确定义为评估为真。 标准是否适用于您的程序,这取决于实现。

虽然其中一个答案已经被接受,但是接受的答案(以及所有其他答案)都是严重错误的,因为我会解释然后回答这个问题。 我将引用相同的C标准,即n1570。

让我们从&a + 1 。 与@Theodoros和@Peter所说的相反,这个表达式定义了行为。 要看到这一点,请考虑第6.5.6节第7段“添加剂操作符”,其中规定:

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

和第8段(特别是强调部分):

当一个具有整数类型的表达式被添加到指针或从指针中减去时,结果具有指针操作数的类型。 如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向与原始元素偏移的元素,使得结果元素和原始数组元素的下标的差异等于整数表达式。 换句话说,如果表达式P指向数组对象的第i个元素,则表达式(P)+ N(等效地,N +(P))和(P)-N(其中N具有值n)指向分别为数组对象的第i + n和第i-n个元素,只要它们存在。 此外, 如果表达式P指向数组对象的最后一个元素,则表达式(P)+1指向一个超过数组对象的最后一个元素 ,如果表达式Q指向一个超过数组对象的最后一个元素,表达式(Q)-1指向数组对象的最后一个元素。 如果指针操作数和结果都指向同一个数组对象的元素,或者指向数组对象的最后一个元素,则评估不应产生溢出; 否则,行为未定义。 如果结果指向数组对象的最后一个元素之后,则不应将其用作已计算的一元*运算符的操作数。

表达式(uintptr_t)p == (uintptr_t)&b有两部分。 从指针到uintptr_t 不是由7.20.1.4节定义的(与@Olaf和@Theodoros所说的相反):

以下类型指定一个无符号整数类型,其属性是任何有效的void指针都可以转换为此类型,然后转换回指向void的指针,结果将等于原始指针:

uintptr_t的

重要的是要认识到此规则仅适用于void有效指针。 但是,在这种情况下,我们有一个指向int的有效指针。 有关段落可在第6.3.2.3节第1段中找到:

指向void的指针可以转换为指向任何对象类型的指针。 指向任何对象类型的指针可以转换为指向void的指针,然后再返回; 结果应该等于原始指针。

这意味着根据本段和7.20.1.4允许(uintptr_t)(void*)p 。 但是(uintptr_t)p(uintptr_t)&b由第6.3.2.3节第6段规定:

任何指针类型都可以转换为整数类型。 除了之前指定的以外,结果是实现定义的。 如果结果无法以整数类型表示,则行为未定义。 结果不必在任何整数类型的值范围内。

请注意, uintptr_t是一个整数类型,如上面第7.20.1.4节所述,因此适用此规则。

(uintptr_t)p == (uintptr_t)&b的第二部分是比较是否相等。 如前所述,由于转换结果是实现定义的,因此相等的结果也是实现定义的。 无论指针本身是否相等,这都适用。

现在我将讨论p == &b 。 @Olaf的答案中的第三点是错误的,@ Theodoros关于这个表达的答案是不完整的。 第6.5.9节“平等经营者”第7段:

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

和第6段:

两个指针比较相等,当且仅当两个都是空指针时,两者都是指向同一对象的指针(包括指向对象的指针和在其开头的子对象)或函数,两者都是指向同一数组的最后一个元素之后的指针object,或者一个指向一个数组对象末尾的指针,另一个指向另一个数组对象的开头,该数组对象恰好跟随地址空间中的第一个数组对象。)

与@Olaf所说的相反,使用==运算符比较指针永远不会导致未定义的行为(这可能仅在使用关系运算符时出现,例如<=根据第6.5.8节第5段,为简洁起见,我将在此省略) 。 现在因为p指向相对于a的下一个int ,所以只有当链接器将b放在二进制中的该位置时,它才等于&b 。 否则就有不平等。 所以这是依赖于实现的(标准未指定ab的相对顺序)。 由于ab的声明使用语言扩展,即__attribute__((section("test"))) ,因此相对位置确实依赖于J.5和3.4.2的实现(为简洁起见省略)。

我们得出结论, check(p == &b)check((uintptr_t)p == (uintptr_t)&b)是依赖于实现的。 所以答案取决于您使用的是哪个版本的编译器。 我正在使用gcc 4.8并通过使用默认选项进行编译,除了优化级别,我在-O0和-O1情况下得到的输出都是TRUE。

根据C11 6.5.9 / 6和C11 6.5.9 / 7,如果ab在地址空间中相邻,则测试p == &b必须给出1

您的示例显示GCC似乎不符合标准的此要求。


2016年4月26日更新:我的原始答案包含有关修改代码以删除其他潜在UB来源并隔离这一条件的建议。

然而,从那时起,我们就会对这个问题提出的问题进行审查 – N2012 。

他们的一个建议是p == &b应该是未指定的,并且他们承认GCC确实没有实现ISO C11要求。

所以我的答案中还有剩下的文字,因为不再需要certificate“编译器错误”,因为已经建立了不符合(无论你是否想把它称为bug)。

重新阅读您的程序我发现在优化版本中您(可以理解)感到困惑

 p == &b 

是假的,而

 (uintptr_t)p == (uintptr_t)&b; 

是真的。 最后一行表示数值确实相同; 怎么可能p == &b然后才是假的?

我必须承认我不知道。 我确信这是一个gcc bug。

在与MM讨论后,我认为如果转换为uintptr_t通过一个中间的void指针(你应该在你的程序中包含它并查看它是否有任何改变),我可以做出以下情况:

因为转换链int* – > void* – > uintptr_t中的两个步都保证是可逆的,所以不等int指针在逻辑上不会产生相等的uintptr_t值。 1 (那些相等的uintptr_t值必须转换回相等的int指针,改变其中至少一个,从而违反了保值转换规则。)在代码中(我不是为了平等,只是展示转换和比较):

 int a,b, *ap=&a, *bp = &b; assert(ap != bp); void *avp = ap, *bvp bp; uintptr_t ua = (uintptr_t)avp, ub = (uintptr_t)bvp; // Now the following holds: // if ap != bp then *necessarily* ua != ub. // This is violated by the OP's case (sans the void* step). assert((int *)(void *)ua == (int*)(void*)ub); 

1这假设uintptr_t不以填充位的forms携带隐藏信息,填充位不是在算术比较中评估,而是可能在类型转换中。 可以通过CHAR_BIT,UINTPTR_MAX,sizeof(uintptr_t)和一些小提琴来检查.-
出于类似的原因,可以想象两个uintptr_t值比较不同但转换回相同的指针(即如果uintptr_t中的位不用于存储指针值,并且转换不将它们归零)。 但这与OP的问题相反。