☰
原码反码补码从原理到实战:手算-105的16位补码与常见陷阱
2026/10/11 6:24:09 网站建设 项目流程

三年前我第一次在汇编课上被原码反码补码按在地上摩擦时,怎么也想不通:明明叫"原码"是给人看的,计算机却偏偏要用"补码"来算。后来自己写了一个模拟ALU的小项目,用门电路搭加减法器,才彻底明白这三兄弟的关系根本不是什么教学顺序,而是一段硬件设计师被现实逼出来的进化史。这篇我就把当时的理解过程完整捋一遍,从"为什么原码不行"到"补码怎么算",最后把 -105 的 16 位补码手算全程和常见的翻车点都摊开来说。

1. 一条 1 + (-1) 的歪路:原码为什么第一个被淘汰

1.1 原码的第一印象:好看,但只是好看

原码的定义几乎不需要解释:最高位当符号位,0 代表正数,1 代表负数,剩下的位直接放绝对值的二进制。8 位原码里,+5 就是 0000 0101,-5 就是 1000 0101。你要是把它写在纸上给人看,完全符合直觉,这就是我们平时写"正负号 + 数值"的习惯被翻译成二进制的样子。

我第一次学到这里是舒服的,毕竟没有任何弯弯绕绕。但舒服只是暂时的,因为只要真的把两个原码数字丢进加法器,问题立刻就炸了。

拿 1 + (-1) 举例,按小学加法直接逐位加,原码是这样的:

0000 0001 (+1) + 1000 0001 (-1) ----------- 1000 0010

最高位是 1,后面是 000 0010,翻译回来是 -2。这结果错得离谱,关键是加法器压根不知道自己错了,它只是老老实实把每一位加起来。原因也清楚:原码里的符号位虽然叫"符号位",但在硬件眼里它就是普通的一位比特,它和数值位一样参与逐位相加,于是产生了这种莫名其妙的进位污染。

1.2 原码做减法时人类和机器都崩溃

如果以为加法出错是符号位惹的祸,那干脆把符号位单独拎出来判断行不行?行,但代价是你得写一套"先判断符号 + 比较绝对值大小 + 决定谁减谁 + 再定结果符号"的流程。把这条流程翻译成逻辑电路,就是一大串比较器和选择器,运算速度慢得离谱。

举例:5 - 3,在十进制里人类心算直接得 2。可如果只用原码且不引入补码概念,那得先发现 5 的绝对值大于 3 的绝对值,确定结果为正,然后再做 5 的绝对值减 3 的绝对值。要是遇到 3 - 5,流程还要反过来,先借位再做借位修正。这套逻辑用软件模拟能跑,但硬件工程师看到这种设计会直接摔键盘:为了一个减法,追加的电路规模比加法器本身还大好几倍,实在不划算。

1.3 一个数两个零,比较器都犯迷糊

还有一个很少有人注意但特别致命的问题:0 的原码有两种写法。+0 是 0000 0000,-0 是 1000 0000。某次我调一个写得很烂的排序算法,数据里有正零和负零两种"皮"的数字,程序里写if (x == 0)想过滤零值,结果负零没被过滤掉,导出来一看负数排序全乱套。原因就是从原码角度,-0 和 +0 确实是两个不同的二进制串。

两条完全一样的数值,却对应了两套编码,这在数学上就是灾难。你没法用简单的按位比较判断两个整数是否相等,因为 1000 0000 和 0000 0000 得先"翻译"成人能读的规则再做一次逻辑推断。

正是因为加法错误、减法繁琐、双零混乱,原码在实际做运算时根本没法直接用。但它的价值在于,它是理解反码和补码的起点,因为后面两个定义都建立在符号位加绝对值这个框架上。

2. 反码的巧思:按位取反背后的数学直觉

2.1 反码到底是什么

反码的规则我记得很清楚:正数的反码等于原码,负数的反码是符号位不变、其余每一位取反。比如 8 位里 -5 的原码是 1000 0101,按位取反得到 1111 1010,这就是 -5 的反码。

当时我的第一反应是,这不就是把 1 换成 0、0 换成 1 吗?看起来毫无道理,直到我把它当成一个数学函数去理解:对一个 n 位数 x 做按位取反,实际上算的是(2^n - 1) - x。

举 8 位的例子,x = 0000 0101(5),按位取反得到 1111 1010,而2^8 - 1 = 255,255 - 5 = 250,正是 1111 1010 的十进制值。所以"按位取反"不是一拍脑袋想出来的花活,它对应的是用一个全 1 的数去减原数。

2.2 反码能救减法吗:试一下就知道一半

带着这个数学理解去看反码做减法,思路就通了:既然取反等价于用全 1 去减,那 5 - 3 或许可以改写成 5 + (255 - 3) 的形式,而 255 - 3 恰好就是 3 的反码,不需要真正的减法器,只需要按位取反一根根导线接出来就行。

8 位里手动验证 5 - 3:

0000 0101 (+5) + 1111 1100 (-3 的反码,因为 0000 0011 取反得 1111 1100) ----------- 0000 0001 (最高位进位溢出,8 位结果看 0000 0001)

直接看 8 位结果是 1,但正确结果应该是 2。问题出在溢出的最高位进位丢了。反码解决这个问题的方法是"端回进位":把最高位的进位再加到最低位上去。上面的结果 0000 0001 加回进位 1,得到 0000 0010,这才是正确的 2。

看到这里我觉得反码比原码聪明多了,至少把"真减法"变成了"取反加修正",但"修正"还是要额外判断有没有进位、要不要再加一次。硬件上为了这个端回进位还得加一条回送线路和一次额外的加法,效率损失不小。

2.3 反码的残留问题:零还是两个,进位还是难缠

反码和原码有个共同的毛病:0 依然有两种表示。8 位反码里 +0 是 0000 0000,-0 是 1111 1111。

而且端回进位这个东西,在并行加法器里非常讨厌。它意味着你没法一次把加法做完,必须先得到一个中间结果,看最高位有没有进位,有进位再回加一次。如果回加之后又产生了进位呢?理论上不会,因为两个 n 位数相加最多产生一次最高位进位,但这一次额外回加已经从"纯组合逻辑电路"变成"带反馈的时序逻辑"了,控制时序一下子复杂起来。

所以反码在历史上不是完全没用,很多老式机器确实用过反码,但它是个典型的过渡方案——比原码强,却没强到让人省心。

3. 补码的翻身仗:模运算视角下的唯一解

3.1 钟表类比:为什么减法可以变成加法

补码之所以是最终答案,核心思想来自模运算。一个 8 位二进制数能表示 0 到 255,如果只在 8 位范围内看,256 就等于 0,因为再多一位就溢出了,这部分必须丢掉。更准确地说,8 位系统里的所有加减法其实都是在模 256 的意义下进行的。

这个概念我后来用一个钟表才彻底想通:钟面上只有 12 个数字,从 3 点开始拨回 8 个小时到 7 点,和从 3 点开始往前拨 4 个小时到 7 点,效果完全一样。为什么?因为 13 和 1 在模 12 的意义下是同一个数。减 8 就等于加 4,差一个模数 12。

放到 8 位二进制里,模数是 256,减一个数 b,等价于加256 - b。这个256 - b就成了 b 的"补数",而负数在补码里的编码,本质上就是它对应的那个补数。

3.2 "取反加一"不是魔法,是公式

教材上总说负数补码 = 原码取反加一,但对刚入门的人而言,这句话像天降神谕。从模运算角度推导一下就清楚了。

对于 n 位系统,模 M = 2^n。对一个数 x 按位取反,前面说了,得到的是(M - 1) - x。再在这个结果上加 1,就得到M - x。而M - x恰恰是 x 关于模 M 的补数。

所以"取反加一"不是一个约定俗成的小技巧,它是在算M - x。例如 8 位下 -5 的补码:先算 5 的二进制 0000 0101,取反得 1111 1010,加 1 得 1111 1011。验证一下,256 - 5 = 251,而 251 的二进制正好是 1111 1011,对上了。

3.3 补码带来的三大红利

第一个红利是零唯一。模 256 下256 - 0还是 0,所以 -0 不存在了。空出来的 1000 0000 也不能浪费,它被定义为 -128。这就是为什么 8 位有符号整数的范围是 -128 到 127,而不是对称的 -127 到 127。

第二个红利是减法彻底统一成加法。减法X - Y变成X + (256 - Y),而256 - Y正好是 Y 的相反数补码,于是硬件只需要一个加法器,减号在电路层面直接消失了。

第三个红利是符号位可以光明正大地参与运算,不需要额外判断。因为补码的符号位本质上也参与模运算,进位自然丢弃,结果天然正确。1 + (-1) 用补码算一遍:

0000 0001 + 1111 1111 ----------- 0000 0000

溢出进位丢掉,剩 0,干净利落。

后来我自己在逻辑仿真软件里搭了一个四位加法器,接上补码电路之后才发现,整套做加减法的核心器件除了加法器就是取反器和增量器,没有复杂的比较器,原来硬件工程师舍得用补码就是这么个朴素原因。

4. 公式解密:为什么 X - Y 的补码等于 X补 + Y补取反 + 1

4.1 拆开这条经常让人眼花的式子

网上搜补码减法经常看到这么一条写法:X - Y = X补 + Y补 + 1,但第一次看的人特别容易懵,因为按公式直接代,Y 的补码加 1 到底是什么?这里其实有个简写造成的误会。

准确讲,要计算 X - Y 的补码,最直接的办法是:

  1. 先求 Y 的相反数的补码,也就是对 Y 的补码按位取反再加 1,记为(~Y补) + 1
  2. 再把 X 的补码加到这个结果上

所以完整的公式是:

(X - Y)补 = X补 + (~Y补) + 1

网上那句"X补 + Y补 + 1"里的 Y补,通常指的是"Y 的补码按位取反"之后的缩写,也就是~Y补,并不是 Y 本身那个补码。你要是直接拿 Y 的补码原值去加 1,举 5 - 3 的例子就会算出莫名其妙的结果。

把 3 的补码取反得到 1111 1100,再加 1 得到 1111 1101,这才是 -3 的补码。最后让 5 的补码和它相加:

0000 0101 + 1111 1101 ----------- 0000 0010

结果 2,正确。

4.2 这个公式为什么能同时处理正 Y 和负 Y

我当时有个疑问:如果 Y 本来就是负数,这个公式还成立吗?答案是成立,但思路要反过来看。

假设算 5 - (-3),也就是 5 + 3。用上面的流程:

  • 3 的补码是 0000 0011
  • 取反得 1111 1100
  • 加 1 得 1111 1101,这是 -3 的补码
  • 但这里 Y = -3,要算的是 5 - (-3),所以应该取 (-Y) 的补码,也就是 3 的补码 0000 0011
  • 结果 0000 0101 + 0000 0011 = 0000 1000 = 8,正确

细看会发现,"对 Y 的补码按位取反再加 1"这个操作,求的永远都是"Y 的相反数"。如果 Y 是 3,取反加一得到 -3;如果 Y 是 -3,取反加一又回到 3。所以无论 Y 的正负,把公式里的 Y 当作"已知正数"去套也没问题,因为取反加一这个操作天然具备"求相反数"的对称性。

4.3 实战:用公式算 100 - 67

16 位环境里,100 的补码是 0000 0000 0110 0100,67 的补码是 0000 0000 0100 0011。

先对 67 的补码取反:1111 1111 1011 1100,再加 1:1111 1111 1011 1101。这是 -67 的补码,十六进制是 FFB D。

然后直接加:

0000 0000 0110 0100 + 1111 1111 1011 1101 ---------------------- 0000 0000 0010 0001

最高位进位溢出被丢掉,结果为 0x0021,即 33。100 - 67 = 33,完全一致。

这类公式用熟了之后,你会发现根本不需要记一堆分支规则:不管被减数是正是负,统一执行取反、加一、相加、丢进位四步,答案一定对。位数要是不一致,记得先把两个数符号扩展成同样宽度。

5. 给十六位较真一次:-105 的补码全程手算

5.1 先把 105 写成 16 位二进制

要求 -105 的 16 位补码,第一步永远是先求 105 的二进制。我习惯用短除法,简单且不容易出错:

105 ÷ 2 = 52 余 1
52 ÷ 2 = 26 余 0
26 ÷ 2 = 13 余 0
13 ÷ 2 = 6 余 1
6 ÷ 2 = 3 余 0
3 ÷ 2 = 1 余 1
1 ÷ 2 = 0 余 1

从下往上读余数,得到 110 1001。用四位一组切分是 110 1001,补成 16 位,前面补 9 个 0,得:

0000 0000 0110 1001

分四位看就是 0x0069。

5.2 取反,加一,得到 FF97

负数补码是对绝对值二进制整体取反再加 1,注意是"全部 16 位"取反,不是只处理数值位:

0000 0000 0110 1001 取反: 1111 1111 1001 0110 加一: 1111 1111 1001 0111

换成十六进制就是 FF97。所以你以后看到 16 位有符号整数 FF97,第一个想法就应该是它等于 -105。

验证方法也很简单:把 FF97 当成无符号数,它是 0xFF97 = 65431,用模 65536 减去它,65536 - 65431 = 105,负号一加回来就是 -105。这其实是另一个计算补码的捷径:负数补码的无符号值等于2^n - 绝对值。

5.3 手写的原码反码对照表

为了看得更清楚,我把 -105 在 16 位下的三种编码放一起:

编码类型二进制表示十六进制
原码1000 0000 0110 10018069
反码1111 1111 1001 0110FF96
补码1111 1111 1001 0111FF97

原码是符号位为 1,后面跟 105 的 15 位绝对值;反码是符号位保持不变、数值位取反;补码是在反码基础上加 1。这张表建议贴在显示器旁边,因为很多位运算题考的就是在三种编码之间互相转换,最容易出错的反而是原码的绝对值位数。

5.4 为什么 16 位够用,8 位也够用,但别混着用

很多人会顺手把 -105 也换算成 8 位补码看看,这是允许的,因为 8 位有符号范围是 -128 到 127,-105 落在里面。8 位计算如下:

105 的 8 位二进制是 0110 1001,取反得 1001 0110,加 1 得 1001 0111,十六进制 0x97。

但同样的十六进制 0x97,在 16 位视角下是无符号 151,在 8 位有符号视角下是 -105。所以一旦跨宽度推理,必须明确当前数据的位宽,否则 0x97 到底是正数还是负数根本说不清楚。我调程序的时候遇到过很多次类似问题,最后都归因于把 8 位负数直接塞进 16 位变量,符号位被错误地当成了普通数值补零。

正确的做法叫符号扩展:把 8 位的 1001 0111(-105)转成 16 位,要在前面补 8 个 1,得到 1111 1111 1001 0111,也就是 FF97,而不是 0000 0000 1001 0111 这种正数值。

6. 补码实战里最容易翻车的四个细节

6.1 陷阱一:取反时把符号位也翻了个底朝天

计算负数补码时,第一步是"对绝对值二进制整体取反再加 1",这个"整体"包括最高位。但做原码转反码时,符号位是保持不变的。这两个规则经常被混在一起。

举个例子,-105 的原码是 1000 0000 0110 1001,求反码时符号位仍然为 1,数值位取反,得到 1111 1111 1001 0110。可如果你带着"取反就是把每一位都反过来"的惯性,从原码直接逐位取反,得到 0111 1111 1001 0110,这就等于把一个负数变成了正数是 32662,完全不是我们要的东西。

记一个死规则:原码转反码,符号位不动;反码转补码,才是整体加 1,包括符号位也参与进位。但补码的符号位在加法运算中是自然参与运算的,不需要人干预。

6.2 陷阱二:补码求相反数也是取反加一,别再绕回原码

求一个负数补码的相反数,有一个特别高效的办法:对它整体取反加一。比如 -5 的 8 位补码是 1111 1011,取反得 0000 0100,加 1 得 0000 0101,正好是 +5。这是因为补码是一个对称结构,取反加一操作就是数学上的取负函数。

很多初学者算 -( -5 ) 时会走弯路,先翻译成原码,再判断符号,再转换,折腾半天。实际上补码体系里根本不需要"人脑判断正负",直接取反加一就完事。这也是补码适合硬件的原因之一,它连"求相反数"都只有一种固定操作。

6.3 陷阱三:溢出判断不能只看最高位

补码虽然好用,但溢出问题总是冒出来。以 8 位为例,-128 的补码是 1000 0000,再减去 1 就超过了下边界。计算过程如下:

-128 的补码 1000 0000,加上 -1 的补码 1111 1111,得到 1 0111 1111,8 位截断后是 0111 1111,即 127。

你看结果竟然是正数 127,符号位完全变了。但真正可怕的不是符号位变没变,而是两个负数相加居然产生了正结果,或者两个正数相加产生了负结果,这种情况铁定溢出。正数加负数则永远不可能溢出,因为结果被限制在两数之间的那个范围内。

硬件上的判断思路是看最高位进位和符号位进位是否一致。如果最高位向更高位产生了进位,而符号位没有进位,说明结果的符号被数据位"顶穿"了;反过来如果符号位进位了而最高位没进位,同样说明溢出。这两个标志在大多数 CPU 里对应进位标志 C 和溢出标志 V,编程时可以用它们配合检查。

6.4 陷阱四:零的唯一性与它带来的范围不对称

补码解决了双零问题,代价是表示范围变得不对称。8 位范围是 -128 到 127,正数最多只能表示 127,负数却能多一个 -128。这不是错误,是模运算的自然结果。

这个不对称带来的一个实用影响是,当你用一个无符号变量读一个补码负数时,读出来的无符号数会非常大。比如 16 位下 -1 的补码是 0xFFFF,如果强制转成无符号 short,就变成 65535。某次我调试数据协议时,收到 0xFFFF 以为是合法的大数据,后来才发现是对方发来的 -1,这种坑在嵌入式串口通信和网络字节序处理里非常常见。

建议在处理任何二进制协议时,先明确规定字段是有符号还是无符号。各领域常犯的错误里,十有八九都能追溯到"把有符号数当无符号数解析"或者反过来。

7. 我自己验证补码正确性时用的两种土办法

7.1 模数回推法:算完再减回去

每次手算补码结果不确定时,我会用模数回推验证。对 n 位补码,把结果的十六进制当作无符号数 U,如果 U 大于等于 2^(n-1),那它就是一个负数,真实值等于U - 2^n。

比如刚才的 16 位 -105,补码 0xFF97,无符号值 65431,减去 65536 得到 -105,立刻校验通过。这种方法不依赖原码反码转换,只需要一次减法,非常快。

7.2 运算法交叉验证:用加法验证减法

另一种土办法是拿补码做一道减法题再原路返回。比如算出 -105 的补码是 0xFF97 之后,心里立刻算一下 0xFF97 + 0x0069:

1111 1111 1001 0111 + 0000 0000 0110 1001 ---------------------- 0000 0000 0000 0000

进位溢出丢弃,结果是 0,说明 -105 + 105 = 0,互为相反数,补码关系没算错。这个方法能顺便检验取反加一的流程有没有漏掉最高位进位,实际操作中非常好用。

从我个人的教训来看,学原码反码补码最忌讳的就是死背规则。只要抓住"模数、相反数、取反加一"这条主线,所有计算都能在一个框架里完成,也不容易出错。最后提醒一句:以后写代码处理有符号数时,看到 0xFFFF 这种满屏 F 的数值,先想想它是不是 -1,补码这个设计虽然伟大,但坑起人来也是一点都不含糊。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询