为什么在32位机器中 - ( - 2147483648)= - 2147483648?

时间:2017-02-25 22:30:25

标签: c 32-bit twos-complement

我认为这个问题是自我解释的,我想它可能与溢出有关,但我仍然不太明白。发生了什么,按位,在引擎盖下?

为什么{% include _ishout_includes.html%}(至少在用C编译时)?

6 个答案:

答案 0 :(得分:74)

否定(未混合的)整数常量:

表达式makefile在C中是完美定义的,但是为什么这样做可能并不明显。

当你写-(-2147483648)时,它形成为应用于整数常量的一元减运算符。如果-2147483648无法表示为2147483648,则表示为intlong * (以先到者为准),其中后者的类型由C标准保证涵盖该值

要确认,您可以通过以下方式检查:

long long

在我的机器上产生printf("%zu\n", sizeof(-2147483648));

下一步是应用第二个8运算符,在这种情况下,最终值为-(假设它最终表示为2147483648L)。如果您尝试将其分配给long对象,如下所示:

int

然后实际行为是实现定义的。参考标准:

  

C11§6.3.1.3/ 3有符号和无符号整数

     

否则,新类型已签名且无法表示该值   在里面;结果是实现定义的还是   实现定义的信号被提出。

最常见的方法是简单地切断较高位。例如,GCC documents为:

  

对于转换为宽度N的类型,该值以2 ^ N为模减少   在该类型的范围内;没有信号被提出。

从概念上讲,转换为宽度类型32可以通过按位AND运算来说明:

int n = -(-2147483648);

根据two's complement算法,value & (2^32 - 1) // preserve 32 least significant bits 的值由全0和MSB(符号)位组成,表示n的值,即{{1} }。

否定-2^31对象:

如果你试图否定-2147483648对象,它保持int的值,然后假设两个补码机器,程序将展示未定义的行为

int
  

C11§6.5/ 5表达式

     

如果在评估期间发生异常情况   表达式(即,如果结果未在数学上定义或   不在其类型的可表示值的范围内),行为   未定义。

其他参考文献:

*)在撤销的C90标准中,没有-2147483648类型,规则也不同。具体来说,未加十进制的十进制序列为n = -n; // UB if n == INT_MIN and INT_MAX == 2147483647 long longint(C90§6.1.3.2整数常量)。

†)这是long int所致,unsigned long int必须至少为LLONG_MAX(C11§5.2.4.2.1/ 1)。

答案 1 :(得分:16)

注意:此答案不适用于许多编译器仍在使用的过时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 intlong 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 intlong long int类型,一切都很好。

在C99,C11中,整数常量表达式-(-2147483648)在所有符合要求的实现上都是明确定义的。

现在,当此值分配给类型int的变量时,具有32位和2的补码表示,该值无法在其中表示 - 32位2和#39上的值;补码范围从-2147483648到2147483647。

C11标准6.3.1.3p3表示以下整数转换:

  
      
  • [何时]签署新类型并且无法在其中表示值;要么结果是实现定义,要么引发实现定义的信号。
  •   

也就是说,C标准实际上并没有定义这种情况下的值是什么,或者也没有排除由于信号被提升而导致程序执行停止的可能性,但是它到实现(即编译器)来决定如何处理它(C11 3.4.1)

  

实施定义的行为

     

未指明的行为,其中每个实现记录了如何做出选择

(3.19.1)

  

实施定义值

     

未指定的值,其中每个实现都记录了如何做出选择

在您的情况下,实现定义的行为是该值是32个最低位[*]。由于2的补码,(长)长整数值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以便在类型的范围内;没有信号被提出。

答案 2 :(得分:6)

这不是一个C问题,对于具有类型int的32位二进制补码表示的C实现,将一元否定运算符应用于具有值{{int的{​​{1}}的效果1}} 未定义。也就是说,C语言特别拒绝指定评估此类操作的结果。

然而,更一般地考虑如何在二进制补码算法中定义一元-2147483648运算符:通过翻转其二进制表示的所有位来形成正数 x 的逆并添加-。这个相同的定义也适用于除了其符号位设置之外至少有一位的任何负数。

但是,对于没有设置值位的两个数字,会出现小问题:0,根本没有设置位,以及仅设置其符号位的数字(以32位表示形式的-2147483648)。当您翻转其中任何一个的所有位时,最终会设置所有值位。因此,当您随后添加1时,结果会溢出值位。如果您想象执行添加就像数字是无符号的那样,将符号位视为值位,那么就得到

1

类似适用于反转零,但在这种情况下,添加1时的溢出也会溢出以前的符号位。如果忽略溢出,则产生的32个低位全部为零,因此-0 == 0。

答案 3 :(得分:1)

我将使用一个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位数字中,二进制-21474836481000..(31 zeroes),但如果使用二补码方法,则最终会得到相同的值(结果是相同的数字) )。

这就是为什么32位数-(-2147483648)等于-2147483648

答案 4 :(得分:0)

这取决于C的版本,实现的细节以及我们是否在讨论变量或文字值。

要理解的第一件事是C中没有负整数文字“-2147483648”是一元减号操作后跟一个正整数文字。

让我们假设我们运行在一个典型的32位平台上,其中int和long都是32位,long long是64位,并考虑表达式。

( - ( - 2147483648)== -2147483648)

编译器需要找到一个可以容纳2147483648的类型,在一个符合C99编译器的编译器中,它将使用“long long”类型,但C90编译器可以使用“unsigned long”类型。

如果编译器使用long long类型,则没有任何溢出,并且比较为false。如果编译器使用unsigned long,则无符号环绕规则将起作用,并且比较结果为真。

答案 5 :(得分:-1)

出于同样的原因,将磁带卡座计数器从000向前卷绕500步(通过001 002 003 ...)将显示500,并将其从000向后绕回500步(通过999 998 997 ......)将也显示500。

这是两个补充表示法。当然,由于2的补码符号约定是考虑符号位的最高位,结果溢出可表示的范围,就像2000000000 + 2000000000溢出可表示的范围。

结果,处理器&#34;溢出&#34;将设置位(看到这需要访问机器的算术标志,在汇编器之外的大多数编程语言中通常不是这种情况)。这是 only 值,它将设置&#34;溢出&#34;否定2的补数:任何其他值的否定位于2的补码所代表的范围内。