为什么计算机用“补码”存储整数?

你有没有想过,计算机是如何表示负数的?

我们都知道,计算机世界里只有 0 和 1。表示正数很简单,把十进制转换成二进制就行了。比如 5 就是 0101。

但 -5 呢?总不能在前面加个负号吧,因为计算机只认识 0 和 1。

为了解决这个问题,工程师们想出了几种方案,最终,"补码"脱颖而出,成为了几乎所有现代计算机存储和运算数字的标准。

为什么是它?难道就因为它名字听起来比较"专业"吗?

当然不是。选择补码,是因为它是一个极其精妙的设计,用最简单、最高效的方式解决了计算机做减法和表示负数的两大难题。

我们从头说起,看看工程师们是怎么一步步打怪升级,最终选定"补码"这位天选之子的。

第一关:最初的尝试------原码(Sign-Magnitude)

我们最容易想到的方法,就是"原码"。

思路很直接:我们不是要表示正负号吗?那就专门留一个比特位(bit)当做"符号位"好了。

  • 规则:最高位是符号位,0 代表正数,1 代表负数。剩下的位表示这个数的绝对值。

举个例子,我们用 8 个比特位来表示一个数:

  • +5:符号位是 0,数值是 5 (0000101)。所以 +5 的原码是 0000 0101。
  • -5:符号位是 1,数值是 5 (0000101)。所以 -5 的原码是 1000 0101。

看起来是不是很完美?很符合人类的直觉?

但计算机科学家们很快发现了它的两大"天坑":

坑1:出现了两个"0"

  • +0 的原码是 0000 0000。
  • -0 的原码是 1000 0000。

"+0" 和 "-0" 在数学上是同一个东西,但在计算机里却有两种不同的表示法。这会带来很多麻烦,比如判断一个数是不是 0,就需要比较两次。这对于追求效率的计算机来说,是不能接受的。

坑2:加法和减法规则极其复杂

计算机最核心的功能就是计算。如果用原码来计算,会发生什么?

我们想算 5 + (-3)。 原码表示是:0000 0101 + 1000 0011。

如果直接按位相加,会得到 1000 1000,转换成十进制是 -8。这显然是错的,正确答案应该是 2。

所以,CPU 如果要用原码计算,就必须内置一套复杂的逻辑:

  1. 先判断两个数的符号位。
  2. 如果符号相同,就做加法。
  3. 如果符号不同,就要先比较两个数的绝对值大小,然后用大数减小数,最后结果的符号还要跟大数保持一致。

想象一下,每次做个加减法都要这么折腾,CPU 得多心累?这会大大增加电路的复杂性,降低运算速度。

原码,卒。

第二关:改进的方案------反码(One's Complement)

为了解决原码的计算问题,反码登场了。

  • 规则:

  • 正数的反码和原码一样。

  • 负数的反码,是在其原码的基础上,符号位不变,其余各位取反(0 变 1,1 变 0)。

再看 -5:

  1. 原码是 1000 0101。
  2. 符号位 1 不变,其余各位取反,得到反码 1111 1010。

反码厉害在哪里?它把减法变成了加法!

我们再来算 5 - 3,也就是 5 + (-3):

  • +5 的反码是 0000 0101。
  • -3 的原码是 1000 0011,反码是 1111 1100。

现在我们把它们加起来:

yaml 复制代码
0000 0101  (5)
+ 1111 1100  (-3)
-----------------
  1 0000 0001

结果多出来一个"溢出"的 1。在反码运算中,这个溢出的 1 需要加回到结果的末尾(循环进位)。

yaml 复制代码
0000 0001
+         1
-----------------
  0000 0010

0000 0010 转换成十进制,正好是 2!

通过"取反"这个简单的操作,我们成功地让计算机只用加法电路就能同时处理加法和减法了。这是一个巨大的进步!

但反码依然没能解决原码的第一个坑:"0" 的问题。

  • +0 的反码是 0000 0000。
  • -0 的原码是 1000 0000,反码是 1111 1111。

"0" 还是有两个编码。这个问题不解决,始终是个隐患。

反码,淘汰。

第三关:终极王者------补码(Two's Complement)

终于,轮到我们的主角------补码出场了。它吸收了反码的优点,并完美地修复了它的缺陷。

  • 规则:

  • 正数的补码和原码、反码都一样。

  • 负数的补码,是在其反码的末尾加 1。

我们再来看 -5:

  1. 原码是 1000 0101。
  2. 反码是 1111 1010。
  3. 反码加 1,得到补码 1111 1011。

补码究竟神在哪里?

优点1:只有一个 "0"

我们来看看 +0 和 -0 在补码里是什么样的:

  • +0:补码是 0000 0000。
  • -0:
  1. 原码 1000 0000

  2. 反码 1111 1111

  3. 反码加 1 1 0000 0000。因为我们只有 8 位,最高位的溢出直接丢掉,结果是 0000 0000。

看到了吗?在补码的世界里,+0 和 -0 的表示方法统一了,都是 0000 0000。那个讨厌的 "-0" (1000 0000) 被腾了出来。工程师们没有浪费它,而是用它来表示 -128,这使得 8 位有符号数的表示范围从 [-127, 127] 变成了 [-128, 127],还多了一个表示空间。

优点2:完美统一加减法

我们再来用补码算一次 5 + (-3):

  • +5 的补码是 0000 0101。
  • -3 的原码是 1000 0011,反码是 1111 1100,补码是 1111 1101。

把它们加起来:

yaml 复制代码
0000 0101   (5)
+ 1111 1101   (-3)
-----------------
  1 0000 0010

最高位的溢出 1 直接丢弃,不用像反码那样再加回来。结果是 0000 0010,转换成十进制,就是 2。

简单、干脆、漂亮!

一个绝妙的数学思想:模运算

补码的精髓在于它利用了"模运算"的思想,可以把它想象成一个时钟。

在一个 12 小时的时钟上,当前是 5 点,要回到 3 小时前(5 - 3),是 2 点。 你也可以往前拨 9 小时(5 + 9 = 14),因为 14 mod 12 = 2,结果也是 2 点。

在这里,12 就是"模", -3 和 +9 在模 12 的系统下是等价的。9 就是 -3 关于 12 的"补数"。

对于 8 位二进制数,它的"模"是 2^8 = 256。所以 -3 的补码 1111 1101 转换成无符号数就是 253。而 256 - 3 = 253。

5 + (-3) 在计算机里就变成了 5 + 253 = 258。因为 8 位存不下 258,最高位溢出, 258 mod 256 = 2。结果完全正确。

通过补码,计算机巧妙地将所有减法都转化成了加法,并且不需要任何额外的判断和处理。硬件电路可以做得极其简单,大大提升了运算效率。

总结一下

我们来回顾一下这场"数字编码淘汰赛":

  • 原码:最直观,但有 +0 和 -0 两个零,且加减法运算复杂,硬件实现困难。
  • 反码:解决了"减法变加法"的问题,但 +0 和 -0 的问题依然存在,且运算时可能需要"循环进位"。
  • 补码:
  1. 完美解决了 +0 和 -0 的问题,只有一个 0 的编码。

  2. 完美统一了加法和减法,让 CPU 可以用一套最简单的加法电路搞定所有运算,硬件设计大大简化。

  3. 顺便还多了一个表示范围。

因此,补码凭借其高效、简单、无歧义的巨大优势,最终成为了计算机世界存储和运算数字的唯一标准。它不是一个随意的选择,而是一个充满了工程智慧的、极其优雅的解决方案。

相关推荐
完美世界的一天2 小时前
Golang 面试题「中级」
开发语言·后端·面试·golang
潘小安4 小时前
『译』React useEffect:早知道这些调试技巧就好了
前端·react.js·面试
原生高钙6 小时前
一文了解 WebSocket
前端·面试
uhakadotcom6 小时前
next.js和vite的关系傻傻分不清,一文讲解区别
前端·面试·github
薛定谔的算法7 小时前
面试官问hooks函数,如何高效准确的回答?
前端·react.js·面试
yanlele7 小时前
给 35+ 程序员的绝地求生计划书
前端·后端·面试
似水流年流不尽思念7 小时前
Session、Cookie 的工作原理以及优缺点
后端·面试
蒋星熠9 小时前
Python API接口实战指南:从入门到精通
开发语言·分布式·python·设计模式·云原生·性能优化·云计算
triticale9 小时前
【计算机组成原理】LRU计数器问题
cache·计算机组成原理·lru