Menu Close

什么是二进制补码?计算机如何表示负数

什么是二进制补码?计算机如何表示负数

什么是二进制补码?计算机如何表示负数

计算机内部只存储 0 和 1。

如果只处理正整数,这很简单。比如 8 位二进制一共有 256 种组合,可以表示:


但是计算机还需要处理负数。

例如:


问题来了:

硬件内部只有 0 和 1,负号应该怎样表示?

现代计算机通常使用一种叫做 二进制补码,Two’s Complement 的方法来表示有符号整数。

补码最重要的优势是:

正数和负数可以使用同一套二进制加法硬件进行运算。

这也是为什么二进制补码成为现代 CPU 的标准整数表示方式。


1. 为什么需要特殊的负数表示方法

假设我们只有 4 个比特。

一共可以产生:


种不同组合。

如果全部按无符号整数解释,那么范围就是:


例如:


但是这里完全没有负数。

如果 CPU 需要计算:


结果应该是:


因此,我们必须拿出一部分二进制模式,用来表示负数。


2. 为什么不能直接加一个符号位

一个很自然的想法,是把最左边的一位作为符号位。

例如:


那么 8 位整数就可以写成:


例如:


这种表示方法叫做:

Sign-and-Magnitude,符号加绝对值。

看起来很直观,但是它有明显的问题。

首先,它会产生两个零:


计算机实际上并不需要两个零。

另外,加减法硬件也会变得复杂,因为 CPU 必须先判断符号,再决定怎样计算。

二进制补码解决了这些问题。


3. 二进制补码的基本思想

补码中,正数和普通二进制完全一样。

例如 8 位整数:


负数通常以 1 开头:


这里要特别注意:

虽然最高位可以帮助我们判断正负,但补码并不是简单的“一个符号位加一个数值”。

整个二进制模式共同决定这个数的值。


4. 怎样把一个正数变成负数

补码最常见的规则非常简单:


也就是:


例如,我们把:


转换成:


先写出 8 位的正 5:


第一步,所有比特取反:


第二步,加 1:


所以:


这就是 8 位二进制补码中的负 5。


5. 为什么 11111111 表示 -1

再看一个非常重要的例子。

正 1 是:


全部取反:


再加 1:


所以:


这也是低级编程中经常看到的情况。

十六进制:


对应二进制:


如果按 8 位无符号整数解释:


如果按 8 位有符号补码解释:


比特完全没有变化。

变化的是解释方式。


6. 同样的比特,为什么既可以是 255,也可以是 -1

这是理解计算机数据表示非常重要的一点。

内存只保存比特。

例如:


内存并不知道它是 255,还是 -1。

如果程序把它解释成 unsigned,也就是无符号整数:


如果程序把它解释成 signed two’s complement,也就是有符号补码


所以:

比特本身没有 signed 或 unsigned 属性。

真正决定意义的是:

  • 数据类型
  • CPU 指令
  • 程序上下文

同样一组二进制数据,可以表示完全不同的值。


7. 为什么二进制补码能够工作

8 个比特一共有:


种状态。

8 位二进制运算天然具有一种“环绕”特性。

例如:


如果 CPU 只保留最低 8 位,那么最高的进位会被丢掉。

结果就变成:


也就是说:


这就是固定宽度二进制运算的 wraparound,也就是环绕。

补码正是利用了这个性质。

可以把:


理解成比 0 小一步,也就是:


然后:


依次向下。


8. 另一种理解负数的方法

对于一个 n 位补码,如果最高位是 1,可以先把它按无符号数读取,然后计算:


例如 8 位的:


作为无符号数是:


因为:


所以:


再看:


作为无符号数是:


于是:


所以:


这种方法在分析机器码和底层数据时非常方便。


9. 为什么 8 位有符号整数范围是 -128 到 127

8 位一共有 256 种二进制组合。

如果按无符号整数解释:


如果按补码有符号整数解释,则分成两部分。

非负数:


负数:


因此 8 位有符号整数的范围是:


一般来说,n 位有符号补码的范围是:


例如 8 位:


也就是:


10. 为什么负数比正数多一个

你可能已经注意到:

8 位补码有:


但是只有:


另外还有一个 0。

原因很简单:

补码只有一个零。


不像 sign-and-magnitude 那样存在:


因此,多出来的一个二进制模式可以用来表示:


所以 8 位补码有:


但是没有:


最大的正数只能是:


11. 怎样读取一个负补码

假设我们看到:


并且知道它是一个 8 位有符号整数。

最高位是 1,因此它是负数。

可以用“取反加一”的方式找到它的绝对值。

先取反:


再加 1:


得到:


所以原来的数是:


也可以使用前面介绍的快速方法。

11110110 作为无符号数是:


所以:


结果完全一样。


12. 补码为什么让加法变得简单

补码最大的硬件优势,是同一个二进制加法器可以同时处理正数和负数。

例如:


8 位的 5 是:


负 3 的补码是:


直接做普通二进制加法:


丢掉最高位进位后:


也就是:


所以:


加法器并不需要知道哪个数是负数。

它只需要正常做二进制加法。


13. 减法为什么可以变成加法

补码还可以把减法转换成加法。

例如:


可以变成:


例如:


可以写成:


8 位二进制:


相加:


丢掉最高位进位:


也就是:


因此:


这意味着 CPU 不需要完全独立的加法和减法硬件。

减法可以建立在加法器基础上实现。


14. 最高位为什么可以看成负权重

还有一种非常直观的数学理解方式。

普通 8 位无符号整数的权重是:


而 8 位补码的权重可以看成:


注意最高位从:


变成了:


例如:


按照补码权重计算:


得到:


所以也可以直接通过权重计算补码数值,而不必每次都取反加一。


15. 什么是 Signed Overflow

固定宽度整数的表示范围有限。

8 位有符号整数只能表示:


如果计算:


127 是:


加 1:


但是:


在 8 位补码中代表:


而不是 128。

原因是:


已经超出了 8 位有符号整数能够表示的范围。

这种情况叫:

Signed Overflow,有符号溢出。

很多 CPU 都提供 overflow flag,用来帮助程序判断这种情况。


16. Carry 和 Overflow 不是一回事

Carry,也就是进位,和 overflow 都和算术有关,但两者不是同一个概念。

Carry 更常用于判断无符号运算是否超出范围。

Overflow 则关注:

有符号运算的真实结果是否超出了当前位宽能够表示的范围。

例如:


会产生 signed overflow。

所以在学习 CPU 状态寄存器和算术指令时,一定要区分:


和:


17. 什么是 Sign Extension

假设 8 位的负 5 是:


现在需要把它扩展成 16 位。

如果简单在左边补 0:


它就会变成一个正数。

正确的方法叫:

Sign Extension,符号扩展。

做法是复制原来的最高位。

因为负数最高位是 1,所以:


扩展成 16 位后是:


它仍然表示:


正数最高位是 0,所以扩展时高位补 0。


18. Zero Extension 和 Sign Extension

再看:


如果它是 8 位无符号整数:


扩展成 16 位时使用 zero extension:


结果仍然是:


但是如果原来的八个比特表示:


就必须使用 sign extension:


这样它仍然表示:


又一次可以看到:

同样的原始比特,因为 signed 和 unsigned 不同,处理方式也会不同。


19. C 语言中的 signed 和 unsigned

编程语言直接暴露了这种区别。

例如 C 语言中:


int8_t 是 8 位有符号整数。

范围通常是:


uint8_t 是 8 位无符号整数。

范围是:


假设存储的二进制模式是:


作为:


它代表:


作为:


它代表:


这就是为什么在 C 语言、嵌入式系统和底层编程中,理解 signed 和 unsigned 非常重要。


20. 为什么现代 CPU 使用二进制补码

二进制补码能够成为现代计算机的主流表示方式,主要有几个原因。

只有一个零


不需要单独的负零。

正数和负数可以共用加法器

CPU 可以用相同的二进制加法硬件处理正数和负数。

减法可以转换成加法


可以转化为:

符号扩展非常简单

只需要复制最高位。

硬件实现更加简单

很多算术操作都可以共享底层电路。

这也是补码非常适合 CPU 的根本原因。


21. 32 位和 64 位整数也是同样的原理

补码并不只适用于 8 位整数。

无论是:


基本原理完全一样。

32 位有符号整数的范围是:


而同样的 32 个比特,如果解释成无符号整数,则范围是:


比特没有变化。

变化的仍然只是解释方式。


22. 二进制补码最重要的规则

最后,可以记住下面几条规则。

对于 n 位有符号补码

表示范围

把正数变成负数

把负补码还原成正数绝对值

快速计算负补码

扩展有符号数

使用:


复制最高位。


总结

二进制补码,Two’s Complement,是现代计算机表示有符号整数的标准方法。

正数和普通二进制没有区别。

负数可以通过:


得到。

例如:


取反:


加 1:


所以:


补码只有一个零,并且允许同一个二进制加法器同时处理正数和负数。

8 位有符号整数的范围是:


而同样的 8 个比特,如果按无符号整数解释,则范围是:


这说明了计算机体系结构中一个非常重要的原则:

比特本身并不知道自己是 signed 还是 unsigned。

同一组比特,可以因为数据类型、CPU 指令或者程序上下文不同,而代表完全不同的数值。

理解二进制补码以后,再学习 CPU 算术、整数类型、溢出、符号扩展、汇编语言和 ALU,就会容易很多。

下一步,我们可以继续研究:

二进制加法、半加器、全加器,以及逻辑门如何一步步组成 CPU 内部真正的算术电路。

Posted in CPU结构