为什么对于带符号数,使用二进制补码比符号-大小更好?

238

我只是好奇为什么要使用二进制的补码来表示-1:翻转位并加上1?

-1用11111111(二进制补码)表示,而不是(对我来说更直观的)10000001,这是二进制1,第一个位作为负标志。

免责声明:我不依靠二进制算术来工作!


7
就您提到的内容而言,“直觉”方法使用符号位在某些情况下是常见的,例如,大多数计算机在表示浮点数时使用符号位。 - Adisak
2
@Adisak 这被称为有符号数。 - Cole Tobin
2
我一直将符号-大小表示法与整数联系在一起,因为浮点数包含三个组成部分:符号、指数和尾数(通常带有一个隐式的“1”)。但我想只要意识到它们不是严格线性的,就足够容易将指数和尾数视为大小。 - Adisak
这里有一篇文章讨论了浮点数如何以二进制形式存储,供那些对@Adisak的评论感到好奇的人参考。文章链接为:http://kipirvine.com/asm/workbook/floating_tut.htm - GDP2
刚刚看了一个很好的视频,解释了这个问题。 https://www.youtube.com/watch?v=dHB7jFjESLY - allenlinli
19个回答

378

这样做是为了让加法不需要处理负数的特殊逻辑。请参阅维基百科上的文章

假设你有两个数字,2和-1。用“直观”的表示数字的方式,它们分别是00101001(我保持大小为4位)。用二进制补码的方式,它们是00101111。现在,假设我想把它们相加。

二进制补码加法非常简单。您按照正常方式相加,末尾的任何进位位都将被丢弃。因此,它们被添加如下:

  0010
+ 1111
=10001
= 0001 (discard the carry)
0001是1,这是"2+(-1)"的预期结果。 但在你的"直观"方法中,加法更加复杂:
  0010
+ 1001
= 1011

那么答案是-3,对吧?简单的加法在这种情况下不起作用。你需要注意到其中一个数字是负数,并使用不同的算法, 如果遇到这种情况。

对于这种“直观”的存储方法,减法是一种不同的操作,需要在将数字相加之前进行额外的检查。由于希望最基本的操作(如加法,减法等)尽可能快,因此需要以一种可以让您使用最简单的算法的方式存储数字。

此外,在这种“直观”的存储方法中,有两个零:

0000  "zero"
1000  "negative zero"

这两个数字在直观上看起来相同,但存储时具有不同的值。每个应用程序都需要采取额外的步骤来确保非零值也不是负零。

以这种方式存储整数还有另一个好处,即当您需要扩展存储值的寄存器的宽度时。使用二进制补码,在8位寄存器中存储4位数字只需重复其最高有效位即可。

    0001 (one, in four bits)
00000001 (one, in eight bits)
    1110 (negative two, in four bits)
11111110 (negative two, in eight bits)

只需要查看较小的单词的符号位,并重复它直到填充较大的单词的宽度。

使用您的方法,您需要清除现有的位,这是除了填充之外的额外操作:

    0001 (one, in four bits)
00000001 (one, in eight bits)
    1010 (negative two, in four bits)
10000010 (negative two, in eight bits)

在这两种情况下,您仍需要设置那些额外的4位,但是在“直观”情况下,您还需要清除第5位。这是在每个应用程序中都存在的最基本和常见的操作之一中的一个微小额外步骤。


22
我同意。2的补码是有效的。但是,我们最初是如何得出这个方法的呢?假设我需要得出这种表示法,思考的过程会是什么样的?我认为得到2的补码不仅仅是侥幸,对吗? - Lazer
1
另外,为什么浮点数没有二进制补码的对应形式? - Lazer
13
@Lazer请查看这篇文章,了解我们是如何得出二进制补码的。 http://www.cs.cornell.edu/~tomf/notes/cps104/twoscomp.html - Ankit
1
据我所知,Java仅具有带符号整数类型,因此它始终将其视为二进制补码解释。在其他语言中,值的处理方式取决于代码如何处理它。没有任何东西告诉您一个内存块是带符号或无符号整数、双精度浮点数、字符串或其他内容。原始数据是您选择解释的任何类型。 - Welbog
4
@Suraj,我建议你查看维基百科对二进制补码的完整解释:https://en.wikipedia.org/wiki/Two%27s_complement。简短的回答是,最高位为`1`表示负数`-8`,其余三个`1`分别表示`4`、`2`和`1`,因此`-8+4+2+1=-1`。 - Welbog
显示剩余19条评论

20

维基百科 给出了以下说明:

二进制补码系统的优势在于不需要加法和减法电路检查操作数的符号来判断是加法还是减法。这种特性使得系统实现更加简单,能够轻松处理高精度算术。此外,零只有唯一一种表示方式,消除了出现在补码反码系统中的负零的微妙之处。

换句话说,无论数字是否为负数,加法运算都是相同的。


先生,如果我写了 char a = 12; 和 unsigned char b = 12,底层的二进制模式是相同的吗?到底会发生什么? - Suraj Jain
当进行加法或减法时,读写操作不会改变任何内容。 - Talespin_Kit

18

即使这个问题早已过时,让我也来发表一下我的看法。

在我解释这个问题之前,让我们回到基础知识。二进制补码是一的补码加1得到的。 那么什么是一的补码,以及它在加法中的意义是什么。

任何n位数和它的一补数之和都会得到可以由这n位表示的最高数。 例如:

 0010 (2 in 4 bit system)
+1101 (1's complement of 2)
___________________________
 1111  (the highest number that we can represent by 4 bits)

现在如果我们尝试将结果再加1会发生什么。这将导致溢出。

结果将为1 0000,这是0(因为我们使用4位数,左侧的1是溢出)。

所以,

Any n-bit number + its 1's complement = max n-bit number
Any n-bit number + its 1'complement + 1 = 0 ( as explained above, overflow will occur as we are adding 1 to max n-bit number)

有人决定把1的补数加1叫做2的补数。所以上述语句变成了:

任何n位数字和它的2的补数=0,这意味着一个数字的2的补数=(-该数字)。

这引出了另一个问题,为什么我们只能使用n位中的(n-1)位来表示正数,而最左边的第n位表示符号(左侧最高位为0表示正数,1表示负数)。例如,在Java中,我们为什么要使用int的前31位来表示正数,如果第32位是1,则表示这是一个负数。

 1100 (lets assume 12 in 4 bit system)
+0100(2's complement of 12)
___________________________

1 0000 (结果为零,进位1溢出)

因此,(n + 2的补码)= 0的系统仍然有效。这里唯一的模棱两可之处是12的2的补码是0100,除了在2的补码系统中表示-12之外,还模棱两可地表示+8。

如果正数总是在其最左边的位上有一个0,则此问题将得到解决。在这种情况下,它们的2的补码将始终在最左边的位上具有1,并且我们不会遇到相同位集表示2的补码数字以及+ve数字的歧义。


1
+1。这是有关信息的,但最终我不确定为什么您想采用最高有效位表示正负数的方法。它有许多问题,例如0将有2个表示形式-0000(+)和1000(-)..此外,加法和减法不能使用相同的算法进行。当您说一个普通的0100时,它是+8,而当您说二进制补码0100时,它是-12.. - hagrawal7777

9

二进制补码 允许按照正常的方式进行加减运算(就像对无符号数一样)。它还可以防止-0的出现(这是一种单独的表示0的方式,使用比特逐位比较数字的正常方法将无法将其与0相等)。


7

二进制补码允许负数和正数相加而不需要任何特殊逻辑。

如果您尝试使用自己的方法将1和-1相加
10000001 (-1)
+00000001 (1)
你会得到
10000010 (-2)

相反,通过使用二进制补码,我们可以相加

11111111 (-1)
+00000001 (1) 你会得到
00000000 (0)

减法也是这样的。

此外,如果您尝试从6中减去4(两个正数),则可以使用二进制补码对4进行操作,并将它们相加6 +(-4)= 6-4 = 2

这意味着CPU中的同一电路可以处理正数和负数的加减法。


6

这是为了简化数字的加减而设计的。在2的补码中,一个负数和一个正数相加的结果与以常规方式相加的结果相同。


6
通常实现这个操作的方法是“翻转位并加1”,但还有另一种定义它的方式,这可能使原理更清晰。2的补码是您得到的形式,如果您采用了通常的无符号表示,其中每个位控制下一个2的幂,并且只是将最高有效项变为负数。
取一个8位值a7 a6 a5 a4 a3 a2 a1 a0
通常的无符号二进制解释是: 2^7*a7 + 2^6*a6 + 2^5*a5 + 2^4*a4 + 2^3*a3 + 2^2*a2 + 2^1*a1 + 2^0*a0 11111111 = 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255
二进制补码解释是: -2^7*a7 + 2^6*a6 + 2^5*a5 + 2^4*a4 + 2^3*a3 + 2^2*a2 + 2^1*a1 + 2^0*a0 11111111 = -128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = -1
其他位的含义完全不改变,并且进位到a7是“溢出”,不应该工作,因此几乎所有算术操作都可以在不修改的情况下工作(正如其他人所指出的)。按位取反一般会检查符号位并使用不同的逻辑。

5

进一步解释其他人的答案:

在二进制补码中

  • 加法和普通正整数加法机制相同。
  • 减法也不会改变。
  • 乘法也是如此!

除法需要使用不同的机制。

所有这些都是正确的,因为二进制补码只是正常的模算术,在其中我们选择通过减去模数将某些数字看作负数。


请注意,仅限于非扩展乘法是相同的。但由于大多数高级语言不支持隐式转换的扩展乘法,因此在这些语言中结果将是相同的。 - phuclv
@LưuVĩnhPhúc:扩展乘法通常是相同的,但有符号和无符号乘法的结果只有在结果适合有符号整数范围内时才保证相同。一些编译器(如gcc)给出类似于unsigned mul(unsigned short x, unsigned short y) { return x*y; } [16位短整型;32位整型]的代码时,如果乘积大于2147483647,则会偶尔生成会出现故障的代码。 - supercat

3
阅读这个问题的答案时,我看到了这条评论[编辑]。
2的补码为0100(4)将是1100。现在如果我说普通话1100,那么它就是12。所以当我说2的补码1100时,它是-4吗?另外,在Java中,当存储1100(现在假设为4位)时,如何确定它是+12还是-4? - Hagrawal于7月2日下午16:53
我认为,这条评论中提出的问题非常有趣,因此我首先想重新表述它,然后提供答案和示例。
问题-系统如何确定一个或多个相邻字节的解释方式?特别是,系统如何确定给定的字节序列是纯二进制数还是2的补码数?
答案-系统通过类型来确定如何解释一系列字节。类型定义
多少字节需要考虑
如何解释这些字节
例如-在下面的示例中,我们假设
  • char的长度为1个字节
  • short的长度为2个字节
  • intfloat的长度为4个字节

请注意,这些大小是针对我的系统而言的。虽然很常见,但它们在不同的系统上可能会有所不同。如果您想知道您的系统上它们的大小,请使用sizeof运算符

首先,我们定义一个包含4个字节的数组,并将它们全部初始化为二进制数10111101,对应于十六进制数BD

// BD(hexadecimal) = 10111101 (binary)
unsigned char   l_Just4Bytes[ 4 ]   =   { 0xBD, 0xBD, 0xBD, 0xBD };

然后,我们使用不同类型读取数组内容。

unsigned charsigned char

// 10111101 as a PLAIN BINARY number equals 189
printf( "l_Just4Bytes as unsigned char  -> %hi\n", *( ( unsigned char* )l_Just4Bytes ) );

// 10111101 as a 2'S COMPLEMENT number equals -67
printf( "l_Just4Bytes as signed char    -> %i\n", *( ( signed char* )l_Just4Bytes ) );

unsigned short and short

// 1011110110111101 as a PLAIN BINARY number equals 48573
printf( "l_Just4Bytes as unsigned short -> %hu\n", *( ( unsigned short* )l_Just4Bytes ) );

// 1011110110111101 as a 2'S COMPLEMENT number equals -16963
printf( "l_Just4Bytes as short          -> %hi\n", *( ( short* )l_Just4Bytes ) );

unsigned int, int and float

// 10111101101111011011110110111101 as a PLAIN BINARY number equals 3183328701
printf( "l_Just4Bytes as unsigned int   -> %u\n", *( ( unsigned int* )l_Just4Bytes ) );

// 10111101101111011011110110111101 as a 2'S COMPLEMENT number equals -1111638595
printf( "l_Just4Bytes as int            -> %i\n", *( ( int* )l_Just4Bytes ) );

// 10111101101111011011110110111101 as a IEEE 754 SINGLE-PRECISION number equals -0.092647
printf( "l_Just4Bytes as float          -> %f\n", *( ( float* )l_Just4Bytes ) );

RAM中的4个字节(l_Just4Bytes [0..3])始终保持完全相同。唯一变化的是我们如何解释它们。
同样,我们通过类型告诉系统如何解释它们。
例如,我们使用以下类型来解释l_Just4Bytes数组的内容:
  • unsigned char:1个字节的二进制数
  • signed char:1个字节的2补码
  • unsigned short:2个字节的纯二进制表示法
  • short:2个字节的2补码
  • unsigned int:4个字节的纯二进制表示法
  • int:4个字节的2补码
  • float:4个字节的IEEE 754单精度表示法

这篇文章在用户s4581301的评论后进行了编辑。感谢您抽出时间留下这几句有用的话!

那段代码需要进行编辑,这样读者就不必来回滚动了。更好的做法是将顶部的大量注释变成普通文本,让渲染器处理格式。此外,在讨论大小和格式的结尾处,您还应该添加一个警告,因为这些大小并非固定的。 - user4581301
+1. 你可能考虑将这个问题/答案对作为社区维基条目单独发布,因为它对于那些对于原始字节解释感兴趣但不涉及二进制补码数学的人非常有用。 - Welbog
我只是想知道二进制补码是否总是遵循规则。比如说,如果我有一个 int x = -4,然后执行 printf("%d", x),那它会被如何解释呢?另外,unsigned intsigned int 以及 %d%u 有什么区别……这个问题困扰我已经很长时间了。谢谢。 - Suraj Jain
@Suraj Jain 在使用“int”类型时,“signed”修饰符是默认的。这意味着“int”和“signed int”是完全相同的类型。因此,“int i = -4;”和“signed int i = -4;”这两个定义具有相同的含义。 - mw215
@Suraj Jain 关于 printf 格式,您可以在 intunsigned int 上使用 %d%u。实际上,如果您使用 %d,“整数参数将以带有符号的十进制形式[-]dddd转换”,并且使用 %u 发生类似的转换。在C标准中,du被称为“转换说明符”。 - mw215
显示剩余2条评论

2

使用二进制补码是因为它在电路实现上更简单,而且不允许出现负零。

如果有x位,则二进制补码的范围为+(2^x/2+1)到-(2^x/2)。一进制补码将从+(2^x/2)到-(2^x/2)运行,但将允许出现负零(在4位1进制补码系统中,0000等于1000)。


是的,拥有一个负零是浪费。 - Geremia

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