为什么在有符号的二进制补码表示中,-INT_MIN等于INT_MIN?

8

我还没有找到一个原因,为什么最低的有符号负数没有对应的有符号正数呢?

我的意思是在一个三位二进制数中,简单起见。

100是-4吗?但我们不能用有符号格式表示正4,因为它会溢出。那么我们怎么知道二进制补码1000是-4,10000000是-128等等呢?我们没有原始的正数。


4
因为你必须要算上0。[-4,-1] 包含4个数字,[0,3] 也包含4个数字,所以总共有8个数字,而三位二进制数字有2的3次方(=8)种可能的组合方式。 - André Caron
什么?抱歉,我不理解你在2^3 = 8个可能的组合之前写的任何内容。 - Lews Therin
2
你有4个负数,3个正数和1个零。总计8个! - Mr Lister
2
[-4,-1] 是数字-4,-3,-2和-1的集合。[0,+3] 是数字0,1,2和3的集合。你的总区间的每一半都有4个数字,这是为你的3位表示分配的所有8个插槽。 - André Caron
记录一下,这个问题也在聊天中讨论过:http://chat.stackoverflow.com/transcript/message/2401301#2401301 - Flexo
10个回答

21

有一种思考方式是将带符号的二进制采用二补码表示,具体方法是给每个比特位分配一个2的幂次方,然后反转最后一个幂次方的符号。例如,我们来看一下-4的表示,它被表示为100。这意味着该值为

-1 x 2^2 + 0 x 2^1 + 0 x 2^0

如果我们想要得到这个值的正数版本,我们需要将其取反以获得

 1 x 2^2 - 0 x 2^1 - 0 x 2^0

注意这个值等于

 1 x 2^2 + 0 x 2^1 + 0 x 2^0
换句话说,这个值的正常二进制表示是100。然而,我们现在遇到了麻烦,因为我们正在使用有符号的二进制补码表示,这意味着我们已经将4位作为符号位专门保留。因此,当我们尝试将位模式100解释为有符号的三位二进制补码值时,它回到了与我们开始时相同的状态。位数不足是问题所在。
更一般地说,在n位中,第一位是两个补码表示中的符号位,尝试计算-1000...00将会返回相同的值,因为存储大正数所需的位具有分配给其的特殊含义。
为什么要这样做呢?原因是如果你只有n位,你无法存储-2n - 1到2n - 1之间的值,因为这里有2n + 1个不同的数字,但只有2^n个不同的位模式。因此排除最大的正数使得所有指定的位模式能够存储不同的数字。
但是为什么要删除高值而不是低值呢?这是为了保持与无符号整数的二进制兼容性。在无符号整数中,值0到2n-1-1都使用标准的二进制表示进行编码。因此,为了使无符号和有符号整数达成协议,无符号整数被设计为与第一个2n - 1个无符号整数按位等效,这些整数的范围从0到2n - 1-1。在这之后,无符号值需要最高有效位来编码数字,但有符号值则使用它作为符号位。
希望这可以帮助你!

2
没有提到-INT_MIN是"未定义行为",编译器可能会忽略上述所有答案。在我看来,这是一个缺失的重要部分。 - chux - Reinstate Monica

16

-INT_MIN 是在 C 语言中的整数溢出,属于未定义行为。

只有当带符号整数溢出时才能保证 -INT_MIN 等于 INT_MIN。例如,可以通过使用 gcc-fwrapv 选项来启用这种情况。

编译器通常利用整数溢出是 C 语言中的未定义行为这一事实来执行一些优化。依赖能够包装的带符号整数溢出是不安全的。

一个众所周知的编译器优化示例如下:

#define ABS(x) ((x) > 0 ? (x) : -(x)) 

void foo(int x){  
    if (ABS(x) >= 0) {
        // some code
    }
}

大多数编译器(例如gccicc)启用优化选项后,会优化掉该测试,因为其依赖于-INT_MIN是未定义行为这一事实。


1
准确来说,-fwrapv 是正确的标志,可以确保有符号整数进行模运算时会发生环绕。-fno-strict-overflow 防止优化器假设整数溢出永远不会发生。 - nucleon
@nucleon -fwrapv 是正确的选项,在这里,谢谢。我已经编辑了我的答案。 - ouah

3

A. n位二进制数有偶数种可能性,因此我们无法用相同的方法表示正数和负数的范围。

B. 我们希望以1开头的所有数字都是负数,以0开头的所有数字都不是负数。(相反的情况不行,因为我们希望在有符号和无符号的情况下,正数和零具有相同的表现形式。因此0位于正数的一半,它们就少了一位。)


3

二进制补码相反的是一补数,它具有这样的属性。
在一补数形式中,最小可能值也有一个有效的正形式。


一的补码通过翻转数字中的所有位来实现。
例如,我们知道0110 == 6,在一的补码中1001 == -6。使用一的补码,我们有正数和负数同样多。
但是对于位表示1111怎么办?仅凭外观,我们就可以判断它是零的“负”形式(0000 = 0; 1111 = -0),但这样的数字毫无意义且浪费。
相反,我们使用二的补码,它类似于一的补码,但是在翻转位之后,我们加一。因此,如果我们知道0110 = 6,则一的补码为1001,而二的补码为1001 + 1 == 1010。使用二的补码,我们没有“负零”,因为它会导致溢出。
另一种看待它的方式是“如果最高位被设置,则数字为负数”。这意味着正数范围为[0 .. 2^(bits - 1)],而负数范围则是其他所有数。正数和负数的数量相同,但是由于(在此格式中)将零视为正数,因此负数范围向左移动一个单位为[-1 .. (neg) 2^(bits - 1)]

假设我们处理的是一个3位带符号数,使用二进制补码表示。那么我们得到以下表格:

BITS  VALUE
000       0
001       1
010       2
011       3

100      -4
101      -3
110      -2
111      -1

你可以看到正数和负数的数量相同,只是负数集合不像正数集合一样从0开始。

二进制补码整数将最左边的位视为产生一个值,该值应复制到其左侧的所有位中[对于任何N,从右侧N位均为0的值中减去1,结果的右侧N位将是1],因此-1等同于无限序列的1。对于一补数,使用相同的值填充左侧和右侧。值0.1111...和...111.0 [在任一情况下都有无限多个1]分别等同于1.0和...110.111...,但只有后两种形式是规范化的。 - supercat

2
缺失的数字是0。从数学角度来看,0既不是正数也不是负数。但在二进制中,由于0没有负位,因此被视为正数。换句话说,如果你想要-128到128之间的数字,就不能有0。

是的,没错,我明白。但是128是怎样确保我们没有0的呢?我知道我们不能有128,因为它是由最高有效位(MSB)作为负号而表示为-128。 - Lews Therin
@LewsTherin:在二进制补码表示法中你做不到那样。不过你可以设计另一种没有0的表示法。 - André Caron
@LewsTherin -- 我认为这里的问题是你在以人类的方式思考数字(即如果我没有+10,我如何表示“-10”的“概念”)。计算机并不是这样看待它的。二进制映射到十进制有符号数仅仅是一种约定俗成的方式。我同样可以说000110111000映射到42,然后围绕它建立整个系统。负号和小数点只是在翻译后再添加回去的。 - Chris Eberle
尝试进行这样的思想实验:你有3个位,这意味着你有8种选择。实际上,你可以表示任何你想要的8个数字。所以我可以说000=56,001=2,010=-83,011=99,100=-422,101=101,110=23,111=10000。计算机本身不需要概念化这8位的最终“含义”。这取决于程序员。二进制也是如此。在CPU级别上,它只是一堆位。它恰好映射回十进制数后来发生的事情,是由于一些非常仔细的规划。 - Chris Eberle
2
@LewsTherin 我之前的陈述有些草率。更准确的说法是,如果有一个+128,你需要在其他地方减去一个数字。我选择了0,因为它似乎很荒谬。更现实的情况是你可能会减去-128。 - Chris Eberle
显示剩余8条评论

1

因为您必须将0计数。整数范围[-4,-1](或等效的-4,-3,-2和-1)包含4个数字,其余范围[0,3](或等效的0、1、2和3)包含4个数字,总共为8个,3位二进制数有2的3次方(=8)种可能的组合。

可以这样理解。形式为[-n,+n]的任何整数范围都必然具有奇数大小(2 * n + 1个整数)。不管使用哪种整数二进制表示,都会有不同数量的负数和正数,因为组合的数量始终是偶数(2的幂)。


0

这个答案只是一个总结。

在N位2的补码中:

  • 负数范围为[-2^{N-1}, -1],基数为2^{N}/2。
  • 正数范围为[0, 2^{N-1}-1],同样基数为2^{N}/2。

而整个范围[-2^{N-1}, 2^{N-1}-1]必须具有基数2^{N}。对此范围内的任何数字执行N位操作都会导致溢出。

请注意,当该有符号范围中的所有数字添加偏差2^{N-1}时,我们得到一个无符号范围[0, 2^{N-1}]。


0

二进制补码通过将最高位保留为负数来表示负数。这意味着您不能再将最高位用作正数。

所有其他(较低的)位都是正数,但无论如何将它们相加,总和永远无法达到最高位,因为它被视为负数。


0
那么我们如何知道二进制补码1000是-4,10000000是-128等等呢?我们没有原始的正数。
你的错误在于认为我们需要一个正数的二进制补码表示才能计算负数的二进制补码表示。
找到负数的二进制补码的过程是:
从要表示的绝对值的正常非二进制补码表示开始。因此,对于-4,取|-4|的非二进制补码表示100。
翻转所有位:100->011(或...11111011,其中1无限地向左延伸)。
加一:011->100(或...11111100)
现在截断到您正在使用的位数(这将消除进位位或无限的1字符串)。结果,100是-4的3位二进制补码表示。

要反着来,取二进制补码表示(100),翻转位(011)并加一(100),你现在有非二进制补码表示形式的|-4|。1*2^2 + 0*2^1 + 0*2^0 = 4。因此我们知道我们最初开始的表示形式,即100,是-4的3位二进制补码表示。


1
谢谢Two,你真是太好了!(http://grammar.quickanddirtytips.com/compliment-versus-complement.aspx) - NullUserException

0
我们应该知道如何将x变成-x:
1. 反转x中的所有位。例如,5是0101,在这一步中我们得到1010; 2. 在上一步所得到的结果上加1。这一次我们得到1010+1=1011。
在实际机器中,负数总是以二进制补码格式显示,因此1011代表-5(即-8 + 2 + 1=-5)。
现在回到问题上,实际机器中的INT_MIN是31个连续的0和1,其中1有31个。
因此,在第一步之后,您将获得一个数字,其中有31个连续的1和0,它在C语言中表示为INT_MAX。
在第二步中,将上一步得到的结果加1,结果是31个连续的0和1,这也是INT_MIN。

所以 INT_MIN = -INT_MIN


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