为什么 -(-2147483648) = - 2147483648 在 32 位机器上?
Why is -(-2147483648) = - 2147483648 in a 32-bit machine?
我认为这个问题是不言自明的,我猜它可能与溢出有关,但我还是不太明白。引擎盖下按位发生了什么?
为什么 -(-2147483648) = -2147483648
(至少在用 C 编译时)?
这不是 C 问题,因为在 C 实现中,int
类型具有 32 位二进制补码表示,将一元否定运算符应用于具有值的 int
的效果-2147483648
是 未定义 。也就是说,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.
注意:此答案不适用于许多编译器仍在使用的过时 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
否定将再次导致 +214783648
,它仍然是 long int
或 long long int
类型,一切正常。
在 C99、C11 中,整型常量表达式 -(-2147483648)
在所有符合标准的实现中都有明确定义。
现在,当将此值分配给具有 32 位和二进制补码表示的 int
类型的变量时,该值无法在其中表示 - 32 位二进制补码的值范围为-2147483648 到 2147483647.
C11 标准 6.3.1.3p3 规定了以下整数转换:
- [When] the new type is signed and the value cannot be represented in it; either the result is implementation-defined or an implementation-defined signal is raised.
也就是说,C 标准实际上并没有定义这种情况下的值,或者不排除程序的执行因发出信号而停止的可能性,而是将其留给决定如何处理它的实现(即编译器)(C11 3.4.1):
implementation-defined behavior
unspecified behavior where each implementation documents how the choice is made
和(3.19.1):
implementation-defined value
unspecified value where each implementation documents how the choice is made
在您的情况下,实现定义的行为是该值是 32 个最低位 [*]。由于 2 的补码,(long)long int 值 0x80000000
设置了第 31 位,并清除了所有其他位。在 32 位二进制补码整数中,第 31 位是符号位——意味着该数字是负数;所有值位都归零意味着该值是最小可表示数,即 INT_MIN
.
[*] 海湾合作委员会 documents its implementation-defined behaviour in this case as follows:
The result of, or the signal raised by, converting an integer to a signed integer type when the value cannot be represented in an object of that type (C90 6.2.1.2, C99 and C11 6.3.1.3).
For conversion to a type of width N
, the value is reduced modulo 2^N
to be within range of the type; no signal is raised.
取反一个(无后缀的)整数常量:
表达式 -(-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 Signed and unsigned integers
Otherwise, the new type is signed and the value cannot be represented
in it; either the result is implementation-defined or an
implementation-defined signal is raised.
最常见的方法是简单地截掉高位。例如,GCC documents 为:
For conversion to a type of width N, the value is reduced modulo 2^N
to be within range of the type; no signal is raised.
从概念上讲,转换为宽度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 Expressions
If an exceptional condition occurs during the evaluation of an
expression (that is, if the result is not mathematically defined or
not in the range of representable values for its type), the behavior
is undefined.
其他参考资料:
*) 在被撤销的 C90 标准中,没有 long long
类型并且规则不同。具体来说,无后缀小数的序列为 int
、long int
、unsigned long int
(C90 §6.1.3.2 整数常量)。
†) 这是由于 LLONG_MAX
,必须至少为 +9223372036854775807
(C11 §5.2.4.2.1/1)。
出于同样的原因,磁带卡座计数器从 000(通过 001 002 003 ...)向前缠绕 500 步将显示 500,而从 000(通过 999 998 997 ...)向后缠绕 500 步将显示 500 ) 也会显示 500.
这是二进制补码。当然,由于2的补码约定是把最高位作为符号位,所以结果溢出了可表示范围,就像2000000000+2000000000溢出了可表示范围。
因此,处理器的 "overflow" 位将被设置(看到这需要访问机器的算术标志,在汇编程序之外的大多数编程语言中通常不是这种情况)。这是 only 值,它将在取反 2 的补码时设置 "overflow" 位:任何其他值的取反都在 2 的补码可表示的范围内。
我将使用 4 位数字,只是为了简化数学运算,但思路是一样的。
在一个 4 位数字中,可能的值在 0000 到 1111 之间。那就是 0 到 15,但是如果你想表示负数,则第一位用于表示符号(0 表示正数, 1 为负)。
所以1111不是15,因为第一位是1,所以是负数。要知道它的值,我们使用前面答案中已经描述的二补法:"invert the bits and add 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
这取决于 C 的版本、实现的细节以及我们谈论的是变量还是文字值。
首先要明白,C语言中没有负整数字面量 "-2147483648"是一个一元负运算,后面跟着一个正整数字面量。
假设我们 运行 在一个典型的 32 位平台上,其中 int 和 long 都是 32 位,long long 是 64 位,并考虑表达式。
(-(-2147483648) == -2147483648 )
编译器需要找到一个可以容纳 2147483648 的类型,在兼容的 C99 编译器上它将使用类型 "long long" 但 C90 编译器可以使用类型 "unsigned long".
如果编译器使用 long long 类型,则不会溢出,比较结果为 false。如果编译器使用 unsigned long,则 unsigned wraparound 规则开始起作用,比较结果为真。
我认为这个问题是不言自明的,我猜它可能与溢出有关,但我还是不太明白。引擎盖下按位发生了什么?
为什么 -(-2147483648) = -2147483648
(至少在用 C 编译时)?
这不是 C 问题,因为在 C 实现中,int
类型具有 32 位二进制补码表示,将一元否定运算符应用于具有值的 int
的效果-2147483648
是 未定义 。也就是说,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.
注意:此答案不适用于许多编译器仍在使用的过时 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
否定将再次导致 +214783648
,它仍然是 long int
或 long long int
类型,一切正常。
在 C99、C11 中,整型常量表达式 -(-2147483648)
在所有符合标准的实现中都有明确定义。
现在,当将此值分配给具有 32 位和二进制补码表示的 int
类型的变量时,该值无法在其中表示 - 32 位二进制补码的值范围为-2147483648 到 2147483647.
C11 标准 6.3.1.3p3 规定了以下整数转换:
- [When] the new type is signed and the value cannot be represented in it; either the result is implementation-defined or an implementation-defined signal is raised.
也就是说,C 标准实际上并没有定义这种情况下的值,或者不排除程序的执行因发出信号而停止的可能性,而是将其留给决定如何处理它的实现(即编译器)(C11 3.4.1):
implementation-defined behavior
unspecified behavior where each implementation documents how the choice is made
和(3.19.1):
implementation-defined value
unspecified value where each implementation documents how the choice is made
在您的情况下,实现定义的行为是该值是 32 个最低位 [*]。由于 2 的补码,(long)long int 值 0x80000000
设置了第 31 位,并清除了所有其他位。在 32 位二进制补码整数中,第 31 位是符号位——意味着该数字是负数;所有值位都归零意味着该值是最小可表示数,即 INT_MIN
.
[*] 海湾合作委员会 documents its implementation-defined behaviour in this case as follows:
The result of, or the signal raised by, converting an integer to a signed integer type when the value cannot be represented in an object of that type (C90 6.2.1.2, C99 and C11 6.3.1.3).
For conversion to a type of width
N
, the value is reduced modulo2^N
to be within range of the type; no signal is raised.
取反一个(无后缀的)整数常量:
表达式 -(-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 Signed and unsigned integers
Otherwise, the new type is signed and the value cannot be represented in it; either the result is implementation-defined or an implementation-defined signal is raised.
最常见的方法是简单地截掉高位。例如,GCC documents 为:
For conversion to a type of width N, the value is reduced modulo 2^N to be within range of the type; no signal is raised.
从概念上讲,转换为宽度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 Expressions
If an exceptional condition occurs during the evaluation of an expression (that is, if the result is not mathematically defined or not in the range of representable values for its type), the behavior is undefined.
其他参考资料:
*) 在被撤销的 C90 标准中,没有 long long
类型并且规则不同。具体来说,无后缀小数的序列为 int
、long int
、unsigned long int
(C90 §6.1.3.2 整数常量)。
†) 这是由于 LLONG_MAX
,必须至少为 +9223372036854775807
(C11 §5.2.4.2.1/1)。
出于同样的原因,磁带卡座计数器从 000(通过 001 002 003 ...)向前缠绕 500 步将显示 500,而从 000(通过 999 998 997 ...)向后缠绕 500 步将显示 500 ) 也会显示 500.
这是二进制补码。当然,由于2的补码约定是把最高位作为符号位,所以结果溢出了可表示范围,就像2000000000+2000000000溢出了可表示范围。
因此,处理器的 "overflow" 位将被设置(看到这需要访问机器的算术标志,在汇编程序之外的大多数编程语言中通常不是这种情况)。这是 only 值,它将在取反 2 的补码时设置 "overflow" 位:任何其他值的取反都在 2 的补码可表示的范围内。
我将使用 4 位数字,只是为了简化数学运算,但思路是一样的。
在一个 4 位数字中,可能的值在 0000 到 1111 之间。那就是 0 到 15,但是如果你想表示负数,则第一位用于表示符号(0 表示正数, 1 为负)。
所以1111不是15,因为第一位是1,所以是负数。要知道它的值,我们使用前面答案中已经描述的二补法:"invert the bits and add 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
这取决于 C 的版本、实现的细节以及我们谈论的是变量还是文字值。
首先要明白,C语言中没有负整数字面量 "-2147483648"是一个一元负运算,后面跟着一个正整数字面量。
假设我们 运行 在一个典型的 32 位平台上,其中 int 和 long 都是 32 位,long long 是 64 位,并考虑表达式。
(-(-2147483648) == -2147483648 )
编译器需要找到一个可以容纳 2147483648 的类型,在兼容的 C99 编译器上它将使用类型 "long long" 但 C90 编译器可以使用类型 "unsigned long".
如果编译器使用 long long 类型,则不会溢出,比较结果为 false。如果编译器使用 unsigned long,则 unsigned wraparound 规则开始起作用,比较结果为真。