为啥更喜欢二进制补码而不是有符号数的符号和大小?

Posted

技术标签:

【中文标题】为啥更喜欢二进制补码而不是有符号数的符号和大小?【英文标题】:Why prefer two's complement over sign-and-magnitude for signed numbers?为什么更喜欢二进制补码而不是有符号数的符号和大小? 【发布时间】:2010-11-10 15:37:38 【问题描述】:

我只是想知道为什么为了用二进制表示 -1 而使用二进制补码:翻转位并加 1 是否有原因?

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

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

【问题讨论】:

FWIW,您使用符号位的“直观”方法是偶尔使用的——例如,大多数计算机在表示浮点数时使用符号位。 @Adisak 叫做有符号幅度 我一直将符号和大小表示与整数相关联,因为浮点数包含三个组件:符号、指数和尾数(通常带有隐式“1”)。但我想只要意识到它们不是严格线性的,就很容易将指数和尾数视为量级。 Here's an article 讨论浮点数如何以二进制形式存储,供那些对@Adisak 言论感兴趣的人参考。 刚刚看到一个很好的视频来解释这个youtube.com/watch?v=dHB7jFjESLY 【参考方案1】:

我有一个在某些情况下很重要的小补充:考虑到这些限制,二的恭维是唯一可能的表示:

无符号数和补码是commutative rings with identity。它们之间存在同态。 它们共享相同的表示,但负数的分支切割不同,(因此,为什么它们之间的加法和乘法相同。) 高位决定符号。

要了解原因,它有助于降低基数;例如,Z_4。

符号和量级ones'commitment都不构成元素个数相同的环;症状是双零。因此很难在边缘工作;为了在数学上保持一致,它们需要检查溢出或trap representations。

【讨论】:

【参考方案2】:

这样做是为了让加法不需要任何特殊的逻辑来处理负数。查看the article on Wikipedia。

假设您有两个数字,2 和 -1。在您表示数字的“直观”方式中,它们将分别是 00101001(我坚持使用 4 位的大小)。在two's complement 方式中,它们是00101111。现在,假设我要添加它们。

补码加法非常简单。您通常添加数字,最后的任何进位位都会被丢弃。所以添加如下:

  0010
+ 1111
=10001
= 0001 (discard the carry)

0001 为 1,即“2+(-1)”的预期结果。

但在您的“直观”方法中,添加更复杂:

  0010
+ 1001
= 1011

-3,对吗?在这种情况下,简单的添加不起作用。您需要注意其中一个数字是负数,如果是这种情况,请使用不同的算法。

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

另外,在“直观”的存储方式中,有两个零:

0000  "zero"
1000  "negative zero"

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

以这种方式存储整数还有另一个好处,那就是当您需要扩展存储值的寄存器的宽度时。使用二进制补码,将 4 位数字存储在 8 位寄存器中是一件容易的事重复其最重要的位:

    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 位。这是每个应用程序中存在的最基本和最常见的操作之一中的一个微小的额外步骤。

【讨论】:

我同意。 2 的补码有效。但是我们最初是如何做到的呢?如果假设我需要达到这个符号,那么思考过程会是什么。我认为获得 2 的补码不仅仅是运气,不是吗? 另外,为什么浮点数没有 2 的补码? @Lazer 看这篇文章就知道了how we arrived at 2s compliment the first place.cs.cornell.edu/~tomf/notes/cps104/twoscomp.html 据我所知,Java 仅具有带符号的整数类型,因此它始终将其视为二进制补码解释。在其他语言中,如何处理值取决于代码如何处理它。没有什么可以告诉您内存块是有符号或无符号整数或双精度数或字符串或其他内容。原始数据是您选择将其解释为的任何类型。 @Suraj,我建议查看关于二进制补码的***文章以获得完整答案:en.wikipedia.org/wiki/Two%27s_complement。简而言之,MSB 1 表示-8,其余三个1s 分别表示421,所以-8+4+2+1 = -1【参考方案3】:

有不同类型的表示:

    无符号数表示 有符号数表示 一个补码表示 二进制补码表示

-用于仅表示正数的无符号数表示

- 用于表示正数和负数的有符号数表示。在有符号数表示中,MSB 位表示符号位,其余位表示数字。 MSB为0时表示正数,MSB为1时表示负数。

有符号数表示的问题是 0 有两个值。

补码表示的问题是 0 有两个值。

但如果我们使用二进制补码表示,那么 0 将只有一个值,这就是我们以二进制补码形式表示负数的原因。

来源:Why negative numbers are stored in two's complement form bytesofgigabytes

【讨论】:

【参考方案4】:

该操作的通常实现是“翻转位并加 1”,但还有另一种定义它的方法可能会使基本原理更清楚。如果您采用通常的无符号表示形式,其中每个位控制 2 的下一个幂,并且只使最重要的项为负,则 2 的补码是您得到的形式。

取一个 8 位值 a7 a6 a5 a4 a3 a2 a1 a0

通常的无符号二进制解释是: 27*a7 + 26*a6 + 25* a5 + 24*a4 + 23*a3 + 22*a2 + 21*a1 + 20*一个0 11111111 = 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255

两者的补码解释为: -27*a7 + 26*a6 + 25 *a5 + 24*a4 + 23*a3 + 22*a2 + 21*a1 + 20 *a0 11111111 = -128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = -1

其他位根本没有改变含义,并且携带到 a7 是“溢出”并且不会起作用,因此几乎所有算术运算都可以在没有修改的情况下工作(因为其他人有著名的)。符号幅度一般检查符号位并使用不同的逻辑。

【讨论】:

【参考方案5】:

二进制补码表示的一个主要优点是,二进制补码和、差或乘积的低位取决于操作数。 -1 的 8 位有符号值是 11111111 的原因是,从最低 8 位为 0000000 的任何其他整数中减去最低 8 位为 00000001 的任何其他整数将产生一个最低 8 位为11111111 的整数。在数学上,值 -1 将是一个无限的 1 字符串,但在特定整数类型范围内的所有值在某个点之后要么全为 1,要么全为 0,因此计算机可以方便地“符号扩展”一个数字的最高有效位,就好像它代表了无数个 1 或 0。

Two's-complement 是唯一一种在处理大于二进制机器的自然字长的类型时效果很好的符号数表示,因为在执行加法或减法时,代码可以获取每个操作数的最低块,计算结果的最低块,并将其存储,然后加载每个操作数的下一个块,计算结果的下一个块,并将其存储,等等。因此,即使是需要所有加法和减法的处理器都经过一个 8 -bit 寄存器可以相当有效地处理 32 位有符号数(当然比 32 位寄存器慢,但仍然可行)。

当使用 C 标准允许的任何其他有符号表示时,结果的每一位都可能受到操作数的任何位的影响,因此有必要一次将整个值保存在寄存器中,或者遵循计算有一个额外的步骤,至少在某些情况下,需要读取、修改和重写每个结果块。

【讨论】:

请将您的答案格式化为段落并将代码标记为代码,它会更具可读性,您将获得投票。 @SurajJain:这样更好吗? 是的,比之前更好,我想问你一件事,signed char a = 1 和 unsigned char a = 1 有什么区别,它们在内存中是如何表示的。跨度> @SurajJain:在“char”小于“int”的二进制补码系统上[即绝大多数系统],有符号和无符号 char 类型的行为相同除了,有符号类型将在读取时进行符号扩展,而无符号类型则不会。在这样的系统上,将值 194 或 -62 存储到有符号字符中将写入与将 194 或 -62 存储到无符号字符(即 11000010)中相同的位模式。从有符号字符读取该位模式将产生 -62,从无符号字符读取它将产生 194。 符号扩展的意思?【参考方案6】:

为什么使用 Two2 的补码而不是 One 的补码系统来表示负数的一个令人满意的答案是 Two's Complement系统解决了0的多重表示和One's Complement系统中存在的表示负数的end-around-carry的问题。

欲了解更多信息,请访问https://en.wikipedia.org/wiki/Signed_number_representations

对于随身携带访问 https://en.wikipedia.org/wiki/End-around_carry

【讨论】:

实际上,如果您有小数点并且明确说明所有位是什么:“0..0000.1111..1”表示所有最左侧未说明的位均为 0,并且所有最右侧未说明的位为 1,因此“..1”表示触发进位。因此它(机械地)是“0.0001.0000..0”。这意味着“1..1111.1111..1”等于零!这也意味着要否定一个整数,你真的只是翻转它的位。但它现在适用于可表示的分数。【参考方案7】:

阅读这个问题的答案时,我看到了这条评论 [已编辑]。

2 的 0100(4) 的补码将是 1100。如果我正常地说,现在 1100 是 12。所以, 当我说正常的 1100 时它是 12,但是当我说 2 的补码 1100 时 它是-4?此外,在 Java 中,当存储 1100(现在假设为 4 位)时 如何确定它是+12还是-4? ——哈格拉瓦尔 7 月 2 日 16:53

在我看来,这个评论中提出的问题很有趣,所以我想首先重新表述它,然后提供一个答案和一个例子。

问题 – 系统如何确定必须如何解释一个或多个相邻字节?特别是,系统如何确定给定的字节序列是普通二进制数还是 2 的补码?

ANSWER – 系统确定如何通过类型解释字节序列。 类型定义

需要考虑多少字节 必须如何解释这些字节

示例 - 下面我们假设

char 的长度为 1 个字节 short 的长度为 2 个字节 intfloat 的长度为 4 个字节

请注意,这些尺寸特定于我的系统。虽然很常见,但它们可能因系统而异。如果您对它们在您的系统上的内容感到好奇,请使用sizeof operator。

首先我们定义一个包含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 shortshort

// 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 intintfloat

// 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:2 的补码中的 1 个字节 unsigned short: 2 个字节的普通二进制符号 short:2 的补码中的 2 个字节 unsigned int: 4 个字节的普通二进制符号 int:2 的补码中的 4 个字节 float:IEEE 754 单精度表示法中的 4 个字节

[编辑] 此帖已在 user4581301 发表评论后编辑。感谢您抽出宝贵的时间删除这几行有用的信息!

【讨论】:

该代码块需要编辑,因此读者不必一直来回滚动。更好的是,顶部的大量注释应该变成普通的旧文本,并让渲染器处理格式。您还应该在讨论尺寸和格式的末尾附近添加一个警告,因为尺寸不是固定的。 +1。您可能会考虑做的一件事,@mw215,就是将此问题/答案对单独作为社区 Wiki 条目,因为它对于可能对二进制补码数学上下文之外的原始字节解释感兴趣的人很有用。 我只想知道 2 的补码总是跟随,我的意思是如果我有 int x = -4 ,然后我做 printf("%d" , x) 它是如何被解释的?还有unsigned intsigned int%d%u 之间有什么区别...这已经困扰我很长时间了。谢谢。 @Suraj Jain 当使用int 类型时,signed 修饰符是默认的。这意味着intsigned int 是完全相同的类型。因此int i = -4;signed int i = -4; 这两个定义具有相同的含义。 @Suraj Jain 系统建立了如何通过类型来解释字节序列。类型定义:必须考虑多少字节以及必须如何解释这些字节。 int 在 2's complement 中是 4 个字节,unsigned int 在 plain binary 表示法中是 4 个字节(使用 sizeof 运算符检查系统上的实际类型大小)。【参考方案8】:

即使这个问题很老,让我投入我的 2 美分。

在我解释这个之前,让我们回到基础。 2' 补码是 1's 补码 + 1 。 现在什么是1的补码,加法的意义是什么。

任何 n 位数字及其 1 的补码的总和为您提供可以由这些 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'bit 数 + 其 2 的补码 = 0 这意味着一个数字的 2 的补码 = - (那个数字的)

所有这一切又产生了一个问题,为什么我们只能使用 n 位中的 (n-1) 位来表示正数,为什么最左边的第 n 位表示符号(最左边的位上的 0 表示 +ve 数,和 1 表示 -ve number ) 。例如,如果第 32 位是 1,那么为什么我们只使用 java 中 int 的前 31 位来表示正数,它是一个 -ve 数。

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

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

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

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

【讨论】:

+1'ed。这是信息,但最后我不确定你为什么想要使用最高有效位的方法来表示它是正数还是负数。它有很多问题,例如 0 会有 2 种表示形式 - 0000(+) 和 1000(-) .. 加法和减法也不能使用相同的算法完成。当你说正常的 0100 时,它是 +8,当你说二进制补码 0100 时,它是 -12 ..【参考方案9】:

用补法进行减法的好处是减少了硬件 复杂性。不需要不同的数字电路进行加法和减法。两者 加法和减法仅由加法器执行。

【讨论】:

【参考方案10】:

值得注意的是,在一些早期的加法机器上,在数字计算机时代之前,减法是通过让操作员在每个键上使用一组不同颜色的图例输入值来执行的(因此每个键将输入 9 减去要减去的数字),然后按下特殊按钮将假定进位进入计算。因此,在六位数机器上,要从一个值中减去 1234,操作员将按下通常指示“998,765”的键并按下按钮将该值加一加到正在进行的计算中。二进制补码算术只是早期“十补码”算术的二进制等价物。

【讨论】:

【参考方案11】:

我们只对加法和减法执行加法运算。我们将第二个操作数添加到第一个操作数以进行加法。对于减法,我们将第二个操作数的 2 的补码添加到第一个操作数。

使用 2 的补码表示,我们不需要单独的数字组件进行减法 - 只使用加法器和补码器。

【讨论】:

【参考方案12】:

好吧,您的意图并不是真正反转二进制数的所有位。它实际上是从 1 中减去它的每个数字。从 1 中减去 1 得到 0,从 1 中减去 0 得到 1,这只是一个幸运的巧合。因此,翻转位有效地执行了这种减法。

但是你为什么要找到每个数字与 1 的差值呢?好吧,你不是。您的实际意图是计算给定二进制数与另一个具有相同位数但仅包含 1 的二进制数的差异。例如,如果您的数字是 10110001,当您翻转所有这些位时,您实际上是在计算 (11111111 - 10110001)。

这解释了计算二进制补码的第一步。现在让我们在图片中加入第二步——加 1。

对上述二元方程加1:

11111111 - 10110001 + 1

你得到了什么?这个:

100000000 - 10110001

这是最终的等式。通过执行这两个步骤,您将试图找到最终的区别:从另一个二进制数中减去二进制数,并带有一个额外的数字,并且除了在最重要的位位置之外包含零。

但是为什么我们真的渴望这种差异呢?好吧,从这里开始,我想如果你阅读Wikipedia article会更好。

【讨论】:

【参考方案13】:

扩展其他答案:

二进制补码

加法与普通正整数加法的机制相同。 减法也不会改变 乘法也是!

除法确实需要不同的机制。

所有这些都是正确的,因为二进制补码只是普通的模运算,我们选择通过减去模数将某些数字视为负数。

【讨论】:

不是only non-widening multiplication is the same。但是由于大多数高级语言不支持没有显式转换的扩展乘法,因此这些语言的结果将是相同的。 @LưuVĩnhPhúc:加宽乘法通常是相同的,但有符号和无符号乘法的结果只有在结果适合有符号整数的范围时才能保证相同。一些编译器,如 gcc,给出类似 unsigned mul(unsigned short x, unsigned short y) return x*y; [16-bit short; 32-bit int] 有时会生成大于 2147483647 的乘积会发生故障的代码。【参考方案14】:

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

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

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

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

减法也是如此。

另外,如果您尝试从 6(两个正数)中减去 4,您可以将 2 的补码 4 加在一起 ​​6 + (-4) = 6 - 4 = 2

这意味着正负数的减法和加法都可以由cpu中的同一电路完成。

【讨论】:

【参考方案15】:

使用二进制补码是因为它在电路中实现起来更简单,并且也不允许负零。

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

【讨论】:

【参考方案16】:

Wikipedia 说明一切:

二进制补码系统的优点是不需要加法和减法电路检查操作数的符号以确定是加还是减。此属性使系统更易于实现并且能够轻松处理更高精度的算术。此外,零只有一个表示,消除了与负零相关的微妙之处,后者存在于一个补码系统中。

也就是说,加法是一样的,不管数是否为负数。

【讨论】:

先生,如果我写 char a = 12 ;和 unsigned char b = 12 ,底层的位模式是否相同,到底发生了什么? 写入或读取时没有任何变化。仅适用于加法或减法。【参考方案17】:

这是为了简化数字的和和差。用 2 的补码编码的负数和正数之和与以正常方式相加相同。

【讨论】:

【参考方案18】:

Two's complement 允许以正常方式进行加法和减法(就像你为无符号数字缠绕一样)。它还可以防止 -0(一种单独的表示 0 的方式,使用普通的逐位比较数字的方法不等于 0)。

【讨论】:

以上是关于为啥更喜欢二进制补码而不是有符号数的符号和大小?的主要内容,如果未能解决你的问题,请参考以下文章

verilog中有符号数的运算

汇编语言无符号数与有符号数转换

理解有符号数和无符号数

关于有符号整数的补码编码的一点经验

有符号数和无符号数

有符号数处理