为什么0 < -0x80000000?

255
我这里有一个简单的程序:
#include <stdio.h>

#define INT32_MIN        (-0x80000000)

int main(void) 
{
    long long bal = 0;

    if(bal < INT32_MIN )
    {
        printf("Failed!!!");
    }
    else
    {
        printf("Success!!!");
    }
    return 0;
}

条件if(bal < INT32_MIN )总是成立。这怎么可能呢?
如果我将宏更改为,它就能正常工作:
#define INT32_MIN        (-2147483648L)

有人能指出问题吗?

3
CHAR_BIT * sizeof(int) 的值是多少? - 5gon12eder
1
你尝试过打印 bal 吗? - Ryan Fitzpatrick
11
依我之见,更有趣的是这仅对于“-0x80000000”是正确的,但对于“-0x80000000L”、“-2147483648”和“-2147483648L”是错误的(在gcc 4.1.2中),所以问题是:为什么int字面值“-0x80000000”与int字面值“-2147483648”不同? - Andreas Fester
2
@Bathsheba 我刚在在线编译器http://www.tutorialspoint.com/codingground.htm上运行程序。 - Jayesh Bhoi
2
如果你曾经注意到(某些版本的)<limits.h>INT_MIN定义为(-2147483647 - 1),现在你知道原因了。 - zwol
显示剩余6条评论
6个回答

367

这有点微妙。

在您的程序中,每个整数字面量都有一个类型。它具有什么类型是由6.4.4.1中的一张表规定的:

Suffix      Decimal Constant    Octal or Hexadecimal Constant

none        int                 int
            long int            unsigned int
            long long int       long int
                                unsigned long int
                                long long int
                                unsigned long long int

如果一个字面量数字无法放入默认的 int 类型中,它将尝试上表中指示的下一个更大的类型。所以对于常规的十进制整数字面量,步骤如下:

  • 尝试 int
  • 如果无法容纳,尝试 long
  • 如果无法容纳,尝试 long long

但十六进制字面量行为不同! 如果字面量无法容纳在像 int 这样的带符号类型中,它将首先尝试 unsigned int,然后再尝试更大的类型。请参见上表中的差异。

因此,在 32 位系统上,您的字面量 0x80000000 是类型 unsigned int

这意味着您可以对该字面量应用一元运算符 -,而不会引发实现定义的行为,就像当溢出有符号整数时那样。相反,您将得到值 0x80000000,即正值。

bal < INT32_MIN 调用通常的算术转换,表达式 0x80000000 的结果从 unsigned int 升级为 long long。值 0x80000000 被保留下来,而 0 小于 0x80000000,因此得到的结果是真。

当您使用 2147483648L 替换该字面量时,您使用了十进制表示法,因此编译器不会选择 unsigned int,而是尝试将其放入一个 long 中。此外,L 后缀表示您希望使用一个 long,如果可能的话。实际上,如果您继续阅读 6.4.4.1 中提到的表格,L 后缀实际上有类似的规则:如果数字无法放入请求的 long 中,那么在 32 位情况下,编译器将给您一个 long long,它将很好地容纳该数字。


3
如果你使用"-2147483648L"代替字面量,你会明确得到一个有符号的长整型(long),这是因为在32位的long系统中,"2147483648L"无法适应long类型,因此会变成"long long"类型,之后再应用"-"符号。 - chux - Reinstate Monica
2
@A.S.H 因为 int 类型的最大值是 0x7FFFFFFF。你可以自己试一下:#include <limits.h> printf("%X\n", INT_MAX); - Lundin
5
不要混淆源代码中整数字面量的十六进制表示和带符号数字的底层二进制表示。当在源代码中写入字面量“0x7FFFFFFF”时,它总是一个正数,但是您的int变量当然可以包含高达0xFFFFFFFF的原始二进制数字。 - Lundin
2
@A.S.H ìnt n = 0x80000000 强制将无符号字面量转换为有符号类型。具体会发生什么取决于编译器 - 这是实现定义的行为。在这种情况下,它选择将整个字面值显示到int中,覆盖了符号位。在其他系统上,可能无法表示该类型,从而引发未定义行为 - 程序可能会崩溃。如果您执行int n = 2147483648;,则会得到完全相同的行为,因此与十六进制表示法无关。 - Lundin
3
对于无符号整数如何应用一元运算符“-”,需要进行进一步的解释。我曾经一直认为(尽管幸运地没有依赖这种假设),无符号值会被“提升”为有符号值,或者可能结果是未定义的。(实际上,这应该是编译错误;“- 3u”究竟意味着什么?) - Kyle Strand
显示剩余11条评论

29

0x80000000是一个无符号字面量,值为2147483648。

对此应用一元负运算仍然会得到一个无符号类型且具有非零值。(实际上,对于一个非零值x,你最终得到的值是UINT_MAX - x + 1。)


在这种情况下,-0x80000000 是无符号的 0x80000000,因为 UINT_MAX+1 等于 0xFFFFFFFF+1 = 1ULL<<32。(或者实际上是 0,因为如果按照 C 规则重新排列为 UINT_MAX+1 - x 来评估该表达式,由于加法在没有有符号溢出 UB 的情况下是可结合的,所以结果为 0)。有趣的事实是:有符号的 -INT_MIN 会导致有符号溢出 UB,而其他任何 int 值都不会。最负数是其在二进制补码系统中的补码。 - Peter Cordes

23

这个整型字面值0x80000000的类型为unsigned int

根据C标准(6.4.4.1 整型常量)

5 整型常量的类型是相应列表中可以表示其值的第一个类型。

而且这个整型常量可以用unsigned int类型表示。

因此表达式

-0x80000000具有相同的unsigned int类型。此外,在补码表示法中,它使用如下方式计算相同的值0x80000000

-0x80000000 = ~0x80000000 + 1 => 0x7FFFFFFF + 1 => 0x80000000

这会有一个副作用,例如要写

int x = INT_MIN;
x = abs( x );

结果将再次为 INT_MIN

因此在这种情况下

bal < INT32_MIN

根据通常算术转换的规则将0x80000000转换为长整型,并将其与无符号值0进行比较。

显然,0小于0x80000000


13
在考虑“-”是否是数字常数的一部分时会出现混淆。
在下面的代码中,0x80000000是数字常量。它的类型仅由此确定。 "-"应用之后,但不会改变类型
#define INT32_MIN        (-0x80000000)
long long bal = 0;
if (bal < INT32_MIN )

原始的未经装饰的数字常量是正数。

如果是十进制,则分配的类型是能够容纳它的第一个类型: int, long, long long

如果常量是八进制或十六进制,它将获得能够容纳它的第一个类型: int, unsigned, long, unsigned long, long long, unsigned long long

在OP的系统上,0x80000000会被视为unsignedunsigned long类型。无论哪种情况,它都是某个无符号类型。

-0x80000000也是某个非零值,并且由于是一些无符号类型,它大于0。当代码将其与long long进行比较时,两端的不会改变,因此0 < INT32_MIN为真。


另一种定义可以避免这种奇怪的行为。

#define INT32_MIN        (-2147483647 - 1)

让我们暂时进入幻想之地,假设intunsigned类型都是48位。

那么0x80000000适合于int类型,并且int类型为有符号整数。因此-0x80000000是一个负数,打印输出结果也就不同了。

[回到现实世界]

由于0x80000000可以适应某些无符号类型,在某些已知的有符号类型最大值some_signed_MAX之上但在some_unsigned_MAX之内,因此它是一种无符号类型。


12

数字常量0x80000000的类型为unsigned int。如果我们取-0x80000000并对其进行二进制补码运算,得到的结果如下:

~0x80000000 = 0x7FFFFFFF
0x7FFFFFFF + 1 = 0x80000000
所以-0x80000000 == 0x80000000。并且比较(0 < 0x80000000)(因为0x80000000是无符号的),结果为真。

这假设是32位的int。虽然这是一个非常普遍的选择,但在任何给定的实现中,int可能会更窄或更宽。然而,对于这种情况,这是正确的分析。 - John Bollinger
这与 OP 的代码无关,-0x80000000 是无符号算术。~0x800000000 是不同的代码。 - M.M
对我来说,这似乎是最好和正确的答案。@M.M.他正在解释如何进行二进制补码。这个答案明确说明了负号对数字的影响。 - Octopus
@Octopus 负号不是对数字应用2补码(!)尽管这似乎很清楚,但它并没有描述代码“-0x80000000”中发生的情况! 实际上,2补码与此问题完全无关。 - M.M

8

在C语言中,整数字面量可以是有符号的(signed)或无符号的(unsigned),这取决于它是否适合有符号还是无符号的整数提升规则。在一个32位机器上,字面量0x80000000将被视为无符号。在32位机器上,-0x80000000的补码是0x80000000。因此,比较bal < INT32_MIN是有符号和无符号之间的比较,在按照C规则进行比较之前,无符号整数将被转换为long long。

C11: 6.3.1.8/1:

[...] 否则,如果具有有符号整数类型的操作数的类型可以表示无符号整数类型的所有值,则具有无符号整数类型的操作数将转换为具有有符号整数类型的操作数的类型。

因此,bal < INT32_MIN始终为true。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接