原码反码补码详解:从机器数到真值,彻底搞懂计算机整数表示
做技术这些年我发现一个很有意思的现象不管是在大学课堂上第一次接触计算机组成原理还是在公司里调试一些看起来很诡异的 bug最后都会绕回到同一组概念上——机器数、真值、原码、反码、补码。很多同事大学时靠考前突击把这个知识点背下来了可一到实际工作看到内存里的0xFFFFFFFF脑子里浮现的只是“这好像是个非负数”其实它不是这就是 -1 的补码。这篇文章我想把这些概念从定义到原理、从手算到实战完完整整讲一遍。目标是让零基础的同学能看懂让已经工作的人能顺手查漏补缺也让准备考研或者面试的朋友能有一条清晰的复习线索。内容不绕弯子可以直接照着算、照着练。1. 机器数与真值计算机内部到底怎么表示一个数1.1 为什么计算机非要用二进制而不是十进制先问一个问题为什么我们平时用十进制到了计算机里就全变成 0 和 1 了很多人会背答案说“因为晶体管只能表示开和关”这个说法没错但还可以更深入一点。从硬件制造来看想让一个物理元器件稳定地表示十种状态远没有想象中那么容易。你当然可以设计出十个电压等级来对应 0 到 9但电路在工作时会受到温度、噪声、电压波动的影响十个等级之间的区分度太低稍微有点干扰就分不清到底是多少。反观二进制只有高电平和低电平两种情况判断阈值很宽哪怕信号有点抖动也能靠恢复电路重新拉回 0 或 1。也就是说二进制的物理实现简单、抗干扰能力强、成本低。从逻辑层面看二进制的优势更明显。布尔代数只有真和假正好对应 1 和 0这意味着算术运算和逻辑运算可以在同一套电路结构上统一实现。加减乘除本质上都可以拆成与、或、非、异或这类基本逻辑操作CPU 里的算术逻辑单元就是这么干活的。如果你非要在硬件上实现十进制逻辑门那复杂度会高得离谱而且毫无性价比。所以在计算机内部任何数字、字符、图片、声音最终都会变成一串二进制位。我们把这种在机器内部用二进制编码表示的数值形式叫作机器数。机器数是计算机真正认识的东西但人不能直接看懂于是就有了“真值”这个概念。1.2 真值和符号位正负号怎么塞进二进制里“真值”这个词听起来有点高深其实特别简单就是我们平时写在纸上的、带正负号的真实数值。比如 5、-5、127、-128这些都是真值。真值是给人看、给人算的而机器数是在计算机内部存储和运算的二进制形式。问题来了二进制里只有 0 和 1那正负号怎么表示总不能存一个“”号或“-”号进去吧。计算机领域的通用做法是把一个二进制数的最高位拿出来当符号位0 代表正数1 代表负数剩下的位表示数值大小。举个 8 位二进制的例子5 的二进制是00000101最高位是 0表示正数后面的0000101表示数值 5-5 就把最高位换成 1变成10000101后面的0000101还是数值 5。这看起来非常自然最高位就是符号位其他位就是绝对值。问题也跟着出现了符号位到底参不参与运算如果把10000101当成一个普通二进制数它等于 133而不是 -5。计算机在存储一个数的时候不可能自己脑补出“哦我最高位是符号位”这种规则所有运算逻辑都是在电路层面写死的。为了回答“符号位怎么处理”历史上发展出了三种编码方式原码、反码、补码。它们在早期都有人用最后补码胜出成为现代计算机的主流方案。下面逐个拆开看你就会明白为什么补码能赢。2. 原码和反码直观理解容易但硬伤也很明显2.1 原码就是把符号和绝对值拼在一起为什么没人用它算数原码的定义一句话就能说清符号位用 0 表示正数、1 表示负数后面的数值位用二进制表示这个数的绝对值。所以 5 的原码是00000101-5 的原码是10000101。这个编码最大的优点就是直观。你要是问一个没学过计算机的人“怎么用二进制表示 -5”他大概率会给出原码这个思路把负号换成最高位的 1把 5 转成二进制放后面。人一眼就能从原码看出它代表的是几做“机器数转真值”这种题也特别快。但原码的缺点同样致命用它做加减法非常麻烦。因为符号位被人为地排除在数值运算之外计算机在做加法时必须先判断两个数的符号位是否相同相同就直接加数值位不同则要比较绝对值大小然后用大数减小数最后还要决定结果的符号。这一套判断逻辑在硬件上做起来不仅繁琐而且慢。举个例子如果直接用原码做 5 (-5)00000101 (5) 10000101 (-5) ------------ 10001010 (-10)5 加 -5 的结果居然算成了 -10完全不对。原因就在于符号位把数值位隔开了二进制加法自然把最高位的 1 也加了进去结果彻底乱套。所以在早期计算机设计中如果用原码减法器就必须单独做一套还要额外处理“比较绝对值大小”这个步骤。硬件电路复杂不说运算速度也上不去。这个问题的根源在于符号位没有参与运算数值位只是被机械地相加。要解决它只能换一种编码方式。2.2 反码是减法转加法的“半成品”也是补码的垫脚石反码的定义同样不复杂正数的反码和原码一样负数的反码是符号位不变其余各位按位取反也就是 0 变 1、1 变 0。用 8 位二进制再看一遍 -5原码是10000101符号位 1 不动数值位0000101全部取反变成1111010合起来就是11111010。那么 5 的反码还是00000101-5 的反码是11111010。反码出现的历史动机就是想解决原码做减法太麻烦的问题。它的思路是能不能让负数也参与常规的二进制加法从而把减法统一成加法于是有人设计了“反码 循环进位”的方案两个数相加时如果最高位溢出就把溢出的进位再加回到最低位这个操作叫循环进位。这样算下来某些情况下确实能把减法变成加法计算过程比原码顺畅不少。但反码有一个特别尴尬的问题它会出现两个零。0 的原码是00000000按定义0 的反码还是00000000而 -0 的原码是10000000取反之后变成11111111。一个数有两种表示方式这对计算机来说非常不友好。因为它意味着做相等判断时要额外处理“00000000和11111111其实是同一个数”这种情况很别扭。另一个问题是反码在运算时循环进位这个额外步骤依然要依赖特殊电路。虽然比原码好一点但硬件设计者还是不满足。反码更像是一个过渡方案它在补码出现之前短暂存在过今天的计算机几乎不会用反码直接存储数据。但我们依然要学它因为补码的定义是“反码加 1”有反码做铺垫补码的规则才显得顺理成章。今天你写 C 语言看到~这个运算符它叫按位取反把一个数的所有位都反过来。很多人会把这个操作和负数的反码搞混。实际上~是按整个二进制位取反而“负数的反码”是符号位不变、只取反数值位。别小看这个区别面试和考试里经常有人在这里翻车。3. 补码计算机最终选它的真正原因以及“取反加一”的来龙去脉3.1 一个时钟的类比帮你理解补码背后的模运算在讲补码规则之前我想先讲一个时钟的例子因为它能最直观地解释“取反加一”到底是怎么回事。想象你面前有一个 12 小时的钟表现在指针指向 9 点。如果问“9 点往前拨 4 小时是几点”你肯定会做 9 - 4 5答案是 5 点。但换一种方式同样能把指针拨到 5 点往后拨 8 小时也就是 9 8 17而 17 点在 12 小时制里等价于 5 点。为什么 -4 和 8 会得到同样的效果因为 12 小时制是一个模 12 的系统任何数字加到 12 或者超过 12都会把 12 的整数倍丢掉。-4 在这个系统里就等价于 12 - 4 8。这就是“补数”的概念在模 n 的系统中减去一个数可以等价于加上它的补数。计算机里的二进制也是同一个道理。8 位二进制能表示 256 种状态从 0 到 255它就是一个模 256 的系统。如果我用 8 位二进制表示 -5最直接的想法是-5 等价于 256 - 5 251。而 251 的二进制刚好是11111011。这就是 -5 的补码它不是凭空被设计成11111011的而是来自“在模 256 系统里-5 用 251 表示”这个思想。现在再回头看教科书上的规则“负数的补码 反码 1”你就知道为什么了-5 的反码是11111010也就是 250250 1 251也就是11111011。所以“取反加一”的本质就是为了得到“模 - 绝对值”这个补数而不是什么玄学操作。3.2 补码规则、8位数值范围和补码求原码的三种方法补码的规则正式写出来是这样正数的补码与原码相同负数的补码是符号位不变其余各位按位取反再加 1。这个规则你已经懂了我再补几个具体例子方便对照真值8位原码8位反码8位补码5000001010000010100000101-51000010111111010111110110000000000000000000000000-0100000001111111100000000-1100000011111111011111111-128无法表示无法表示10000000看最后一行原码和反码都表示不了 -128因为 8 位原码中数值位只有 7 位最大绝对值是 127但如果用补码10000000可以合法地表示 -128。这里有一个容易绕晕的点为什么补码允许最高位是 1 还表示一个负数因为补码的符号位和数值位是统一编码的10000000并不是“符号位 1 数值 0”它整体表示的是模 256 下的 128但因为最高位被解读成负数所以它等于 128 - 256 -128。这也是补码数值范围比原码反码多一个负数的原因。8 位补码的范围是 -128 到 127而原码和反码的范围是 -127 到 127那一个 -128 是它们永远表达不了的。这个不对称很多人初学时会觉得奇怪但正是它让 0 得到了唯一表示。补码里 0 和 -0 的编码都是00000000不会再出现原码反码那种“两个零”的尴尬。再来说说“补码求原码”的方法这是搜这个词的人特别多的问题。其实规律特别简单对一个补码再求一次补得到的结果就是它的原码。也就是说取反加一的逆操作还是取反加一自己反自己。举一个例子已知某个数是 8 位补码11111011想求它的原码。按规矩来先取反00000100再加 100000101。得到00000101这是 5 的原码说明原来的补码就是 -5。这个方法验证起来毫不费力而且从头到尾只需要“取反加一”这一个动作特别好记。如果你非要先减一再取反结果也一样但多记一条规则就容易在紧张的时候搞混。我个人强烈推荐只记“补码的补码等于原码”这一条做题时又快又稳。还有一个笔试里很喜欢考的小技巧求一个负数的补码可以不用先写原码再取反加一而是直接看它绝对值对应的二进制从右往左找到第一个 1这个 1 及其右边保持不变左边的所有位全部取反。比如 -88先看 88 是01011000从右往左第一个 1 在第 3 位那么左边01010取反变成10101最终得到10101000。这个方法和取反加一结果完全一致熟练之后口算负数补码的效率特别高。稍微延伸一下还有一个叫“符号扩展”的操作经常被忽略。把一个 8 位补码扩展成 16 位时正数在高位补 0负数在高位补 1。原因很简单补码表示中高位扩展必须保持数值不变。如果 -5 的 8 位补码是11111011扩展到 16 位应该是11111111 11111011而不是在最高位前硬塞 0否则它就会从 -5 变成某个正数。符号扩展在类型提升、字节拼接、协议解析里特别常见是补码知识在实际工程中最容易踩坑的地方之一。4. 实战一下用手算和真实代码把补码彻底用起来4.1 用8位补码完整算一遍加减法包括溢出的判断纸上谈兵再多不如实际算两道题。第一道用 8 位补码计算 5 - 3也就是 5 (-3)。5 的补码是00000101。再求 -3 的补码3 的原码是00000011取反变成11111100加 1 变成11111101。所以 -3 的 8 位补码是11111101。现在把两个补码直接相加00000101 (5) 11111101 (-3) ------------ 100000010结果最高位前面多出一个 1这个 1 是进位在 8 位运算中直接被丢弃剩下00000010正好是 2。5 - 3 2答案正确。注意看溢出进位被丢弃这件事不是出错而是补码设计的精髓。因为 8 位系统能表示的数值之和是模 256多出来的进位本质上就是 256把它丢掉相当于完成了“模 256”的取模操作。再算一个经典的越界题127 1。127 的补码是01111111加上 1 得到10000000。按照 8 位补码的解读10000000不是 128而是 -128。所以 127 1 在 8 位补码里会算出 -128。这就是溢出结果超出了表示范围硬件不会报错它只是忠实地把二进制结果解释成补码对应的值。在很多编程语言里这种整数溢出会表现为“正数加出负数”“负数减出正数”非常迷惑人。判断溢出的规则其实很清晰两个正数相加得到负数或者两个负数相加得到正数一定是溢出了。如果两个数的符号位不同那永远不会溢出因为结果一定落在两个加数之间。再看进位层面更严格的判定是最高数值位向符号位产生了进位但符号位向上没有产生进位或者反过来都属于溢出。这个规则在组成原理大题里经常出现你可以拿 127 1 反复验证记忆。实际做题时还要注意一个细节补码做减法时符号位是参与运算的千万别把符号位摘出去单独处理。我见过很多同学手算补码加法时先把符号位挪到一边只加数值位算完再把符号位拼回去这是错误的做法。补码之所以能统一加减法靠的就是符号位和数值位一起按二进制加法规则运算一旦把它拆开整个体系就垮了。4.2 真实开发里的补码坑位速查表最后把实战中最常遇到的几个问题整理成一张速查表方便你以后遇到类似现象时快速定位。现象原因判断或处理方法打印一个负数的十六进制是 0xFFFFFFFF-1 的补码是全 1任何位宽下负数补码都会填充符号位用%x打印有符号数时常见正常现象不是 bugint 的范围是 -2147483648 到 2147483647正数少一个补码中 0 唯一并且多出一个 -2147483648用Integer.MIN_VALUE/INT_MIN表示边界别硬记byte b (byte) 0x80;打印出来是 -1288 位补码10000000对应 -128正数 128 超出 byte 范围转换时发生截断把一个 byte 的负数转成 int怎么高位全是 1符号扩展负数高位补 1如果只想保留无符号值用b 0xFF两个很大的正数相加结果变成负数补码溢出用更大范围类型或使用语言提供的溢出检查机制位运算里~x 1可以得到 -x补码的取反加一正好是原数的相反数这是补码自反性的应用常用于手写取负这张表覆盖了日常写代码时和补码最相关的几个坑。我特别想强调一下0xFFFFFFFF这个值如果你在调试网络协议或者文件格式解析时看到一串 F别急着判断“这是个很大的正数”它很可能是一个负数。比如在 32 位系统中0xFFFFFFFF就是 -1这在二进制协议里非常常见是标准的“-1 哨兵值”。顺便说一句浮点数不要用补码的思路去理解。IEEE 754 那种符号位加指数位加尾数位的结构和整数的原码反码补码完全是两套体系。很多人在复习时会把它们混在一起结果越学越乱。回到开头说的场景为什么我会说这些概念在工作中依然重要因为你不是在跟数学题打交道而是在跟二进制位打交道。协议解析、序列化、位标志、加密压缩、图像处理、游戏存档这些领域到处都藏着补码。懂了补码遇到“负数怎么打印成这样”“为什么强转之后数字对不上”这类问题一眼就能看穿本质不需要靠打印日志一点点猜。如果你正在准备面试或考试我的建议是先把 8 位补码的加减法手算练熟再重点掌握“补码的补码是原码”和溢出判断两条规则。这两件事想通了相关的题目基本都能拿下。以后再看到0x开头的二进制数据你心里就会浮现出它代表的真值那种感觉就是对这组概念真正吃透了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →