为啥 -(-2147483648) = - 2147483648 在 32 位机器中?
Posted
技术标签:
【中文标题】为啥 -(-2147483648) = - 2147483648 在 32 位机器中?【英文标题】:Why is -(-2147483648) = - 2147483648 in a 32-bit machine?为什么 -(-2147483648) = - 2147483648 在 32 位机器中? 【发布时间】:2017-07-16 16:26:43 【问题描述】:我认为这个问题是不言自明的,我想它可能与溢出有关,但我仍然不太明白。到底发生了什么,按位计算?
为什么-(-2147483648) = -2147483648
(至少在用 C 编译时)?
【问题讨论】:
注意:-INT_MIN 的值在 C 语言中是未定义的。我猜大多数实现,大多数时候,会返回 INT_MIN,但他们不必这样做。 这是 UB,您只是看到了 NEG 指令在 Intel/AMD 处理器上的行为。如果你将这个数字除以 -1 会更有趣。 面试题:abs(INT_MIN)==?
在 64 位机器上也是如此。重要的是int
的大小,而不是你在什么机器上
@MartinBonner 请不要超出 OP 的预期。他只是想知道 2 的补码,而且他在“32 位机器”上
【参考方案1】:
这取决于 C 的版本、实现的细节以及我们是在谈论变量还是文字值。
首先要了解的是,C 中没有负整数字面量,“-2147483648”是一元减法运算,后跟一个正整数字面量。
假设我们在一个典型的 32 位平台上运行,其中 int 和 long 都是 32 位,long long 是 64 位,并考虑表达式。
(-(-2147483648) == -2147483648)
编译器需要找到一个可以容纳 2147483648 的类型,在兼容的 C99 编译器上它将使用“long long”类型,但 C90 编译器可以使用“unsigned long”类型。
如果编译器使用 long long 类型,则不会溢出,比较结果为假。如果编译器使用 unsigned long ,那么 unsigned 环绕规则就会起作用,并且比较是正确的。
【讨论】:
【参考方案2】:否定一个(无后缀的)整数常量:
-(-2147483648)
表达式在 C 中被完美定义,但可能并不明显为什么会这样。
当您编写-2147483648
时,它被形成为应用于整数常量的一元减号运算符。如果2147483648
不能表示为int
,则表示为long
或long long
*(以先符合者为准),其中后一种类型由C保证涵盖该值的标准†。
要确认这一点,您可以通过以下方式对其进行检查:
printf("%zu\n", sizeof(-2147483648));
在我的机器上产生8
。
下一步是应用第二个-
运算符,在这种情况下,最终值为2147483648L
(假设它最终表示为long
)。如果你尝试将它分配给int
对象,如下:
int n = -(-2147483648);
那么实际行为是实现定义的。参考标准:
C11 §6.3.1.3/3 有符号和无符号整数
否则,新类型有符号,值无法表示 在里面;结果是实现定义的或 引发了实现定义的信号。
最常见的方法是简单地切断高位。比如 GCC documents 它为:
为了转换为宽度为 N 的类型,值以 2^N 为模减少 在类型的范围内;没有发出信号。
从概念上讲,转换为宽度为 32 的类型可以通过位与运算来说明:
value & (2^32 - 1) // preserve 32 least significant bits
按照two's complement的算法,n
的值由全零和MSB(符号)位组成,代表-2^31
的值,即-2147483648
。
否定int
对象:
如果您尝试否定 int
对象,该对象的值是 -2147483648
,然后假设二进制补码机,程序将表现出未定义的行为:
n = -n; // UB if n == INT_MIN and INT_MAX == 2147483647
C11 §6.5/5 表达式
如果在评估一个异常情况 表达式(也就是说,如果结果不是数学定义的,或者 不在其类型的可表示值范围内),行为 未定义。
其他参考:
INT32-C. Ensure that operations on signed integers do not result in overflow*) 在撤回的 C90 标准中,没有long long
类型,规则不同。具体来说,无后缀小数的序列是int
、long int
、unsigned long int
(C90 §6.1.3.2 整数常量)。
†) 这是由于LLONG_MAX
,它必须至少为+9223372036854775807
(C11 §5.2.4.2.1/1)。
【讨论】:
这个答案应该强调它只适用于整数文字;特别是,它不适用于否定包含值-2147483648
的 int
对象。
它仅在现代编译器(C99 或 C++11 及更高版本)中被提升为 long long
。在旧的编译器上,它会给出令人惊讶的结果Why it is different between -2147483648 and (int)-2147483648,Casting minimum 32-bit integer (-2147483648) to float gives positive number (2147483648.0)
@Hurkyl 请注意,在 C 中,2147483648
被指定为 整数常量,而不是整数文字。与2147483648
. 不同,C 中的文字可以像 字符串文字 和 复合文字 一样使用它们的地址
@Random832:我认为仅这个案例就值得单独提问,但总之看看DR #298。底线是它可能会导致 违反约束(C11 §6.4.4/2),假设:1)9223372036854775808
不能用long long
类型表示(所以事实上, 它超过了LLONG_MAX
), 2) 实现不支持扩展整数类型(例如 GCC 不支持)。
2147483648
没有被提升为任何东西。它的类型为int
、long
或long long
(以它可以容纳的最小者为准)。 "promote" 指的是一个实际上确实具有比int
更窄的类型的值,在表达式中使用时被更改为不同类型的值【参考方案3】:
我将使用 4 位数字,只是为了让数学变得简单,但想法是一样的。
在 4 位数字中,可能的值介于 0000 和 1111 之间。应该是 0 到 15,但是如果要表示负数,则第一位用于表示符号(0 表示正数,1 表示负)。
所以 1111 不是 15。因为第一位是 1,所以它是一个负数。要知道它的值,我们使用前面答案中已经描述的二补法:“反转位并加 1”:
反转位:0000 加1:0001二进制的0001是十进制的1,所以1111是-1。
双补码方法是双向的,所以如果你将它与任何数字一起使用,它将为你提供该数字的二进制表示,并带有倒号。
现在让我们看看 1000。第一位是 1,所以它是一个负数。使用二补法:
反转位:0111 加 1:1000(十进制为 8)所以 1000 是 -8。如果我们做-(-8)
,二进制表示-(1000)
,这实际上意味着在1000中使用二补法。正如我们在上面看到的,结果也是1000。
因此,在 4 位数字中,-(-8)
等于 -8。
在一个 32 位数字中,二进制的-2147483648
是1000..(31 zeroes)
,但是如果你使用二补法,你最终会得到相同的值(结果是相同的数字)。
这就是为什么在 32 位数字中 -(-2147483648)
等于 -2147483648
【讨论】:
【参考方案4】:这不是一个 C 问题,因为在具有类型 int
的 32 位二进制补码表示的 C 实现上,将一元否定运算符应用于具有值 -2147483648
的 int
的效果是 未定义。也就是说,C 语言明确拒绝指定评估此类操作的结果。
然而,更一般地考虑一下,一元 -
运算符是如何在二进制补码算术中定义的:正数的倒数 x 是通过翻转其二进制表示的所有位并添加1
。同样的定义也适用于任何至少有一个位不是其符号位集的负数。
但是,对于没有设置值位的两个数字会出现一些小问题:0,它根本没有设置任何位,以及仅设置其符号位的数字(32 位表示中的 -2147483648)。当您翻转其中任何一个的所有位时,您最终会设置所有值位。因此,当您随后加 1 时,结果会溢出值位。如果您想像数字无符号一样执行加法,将符号位视为值位,那么您会得到
-2147483648 (decimal representation)
--> 0x80000000 (convert to hex)
--> 0x7fffffff (flip bits)
--> 0x80000000 (add one)
--> -2147483648 (convert to decimal)
类似的情况也适用于反转零,但在这种情况下,加 1 时的溢出也会溢出以前的符号位。如果忽略溢出,则生成的 32 个低位全为零,因此 -0 == 0。
【讨论】:
恐怕 Grzegorz Szpetkowski 搞定了:-(-2147483648)
的表达是完美定义的。
@chqrlie:仅当您假设 OP 正在谈论整数文字时,而不是询问当您否定包含值 -2147483648
的 int
变量时会发生什么。
完美定义,因为-2147483648
is a long long
in modern compilers and unsigned long
in older ones。两种情况下的结果都不同,但它们仍然被定义
@chqrlie,你是对的,当然,但这没有抓住问题的重点。我已经改写了我答案的那部分以纠正该技术性问题。【参考方案5】:
注意:此答案不适用于许多编译器仍在使用的过时 ISO C90 标准
首先,在C99、C11上,-(-2147483648) == -2147483648
这个表达式其实是false:
int is_it_true = (-(-2147483648) == -2147483648);
printf("%d\n", is_it_true);
打印
0
那么这怎么可能计算为真呢?
机器使用 32 位 two's complement 整数。 2147483648
是一个完全不适合 32 位的整数常量,因此它将是 long int
或 long long int
取决于它适合的第一个位置。这个否定将导致-2147483648
- 再次,即使数字-2147483648
可以适合32 位整数,表达式-2147483648
包含一个> 32 位正整数,前面是一元-
!
您可以尝试以下程序:
#include <stdio.h>
int main()
printf("%zu\n", sizeof(2147483647));
printf("%zu\n", sizeof(2147483648));
printf("%zu\n", sizeof(-2147483648));
这种机器上的输出很可能是 4、8 和 8。
现在,-2147483648
negated 将再次导致 +214783648
,它仍然是 long int
或 long long int
类型,一切都很好。
在 C99、C11 中,整型常量表达式 -(-2147483648)
在所有符合要求的实现上都有很好的定义。
现在,当将此值分配给具有 32 位和二进制补码表示的 int
类型的变量时,该值无法在其中表示 - 32 位 2 的补码上的值范围从 -2147483648 到 2147483647 .
C11 标准6.3.1.3p3 说明了以下整数转换:
[当]新类型有符号且值不能在其中表示时;结果要么是implementation-defined,要么是一个implementation-defined信号。
也就是说,C 标准实际上并没有定义这种情况下的值是什么,或者不排除程序执行由于发出信号而停止的可能性,而是将其留给实现(即编译器)来决定如何处理它(C11 3.4.1):
实现定义的行为
每个实现都记录了如何做出选择的未指定行为
和(3.19.1):
实现定义的值
每个实现记录如何做出选择的未指定值
在您的情况下,实现定义的行为是该值是 32 个最低位 [*]。由于 2 的补码,(long)long int 值0x80000000
设置了第 31 位,并清除了所有其他位。在 32 位二进制补码整数中,第 31 位是符号位 - 表示该数字为负数;所有值位为零表示该值是最小可表示数字,即INT_MIN
。
[*] GCC documents its implementation-defined behaviour in this case as follows:
当值不能在该类型的对象中表示时,将整数转换为有符号整数类型(C90 6.2.1.2、C99 和 C11 6.3.1.3)的结果或引发的信号。
为了转换为宽度
N
的类型,该值以2^N
为模减少到该类型的范围内;没有发出信号。
【讨论】:
【参考方案6】:出于同样的原因,将磁带机计数器从 000 向前缠绕 500 步(通过 001 002 003 ...)将显示 500,从 000 向后缠绕 500 步(通过 999 998 997 ...)将显示也显示 500。
这是二进制补码表示法。当然,由于 2 的补码约定是将最高位视为符号位,所以结果溢出了可表示的范围,就像 2000000000+2000000000 溢出了可表示的范围一样。
因此,处理器的“溢出”位将被设置(看到这需要访问机器的算术标志,在汇编程序之外的大多数编程语言中通常不是这种情况)。这是唯一值,它会在否定 2 的补码时设置“溢出”位:任何其他值的否定都在 2 的补码可表示的范围内。
【讨论】:
以上是关于为啥 -(-2147483648) = - 2147483648 在 32 位机器中?的主要内容,如果未能解决你的问题,请参考以下文章
为啥最大负整数-2147483648的绝对值还是-2147483648?