有没有过这样的场景早上刚背完原码、反码、补码三者的定义晚上刷题碰到负5的8位补码是几还是愣了一下然后开始怀疑自己是不是把取反和加1的先后顺序记反了我之前在帮学弟学妹复习《计算机组成原理》时发现几乎所有人都在同一句话上卡壳负数在计算机里用补码存储。这句话本身不难难的是很少有人解释为什么偏偏是补码原码和反码到底失败在哪补码的底层逻辑又是什么。这篇文章不是教科书式复述。我想把一条完整线索讲清楚计算机里为什么要发明符号位为什么有了原码还要搞反码反码又是怎么一步步逼出补码的补码为什么能做到带符号数也能直接相加以及笔试、面试和实际写代码时最容易踩的坑。适合正在学计组、准备校招笔试、或者工作中突然被有符号数与无符号数转换绕晕的朋友。读完之后你不需要再死背口诀因为你会自己推出来。1. 从问题出发为什么二进制世界里会出现三种码1.1 符号位最朴素的负数表示计算机底层只有0和1这个大家都清楚。问题是正数可以直接用二进制存负数怎么办总不能用字符负号去存那得额外设计电路。最朴素的思路就是拿出二进制数的最高位来当正负标记约定最高位是0表示正数最高位是1表示负数剩下低位照常存数值的绝对值。这就是原码。比如8位情况下5就是0000 0101-5就是1000 0101。这种设计人类看着最舒服符号位一眼就能识别正负绝对值也很直观。但计算机硬件并不喜欢看着舒服。硬件只喜欢干一件事把两个数往加法器一丢然后拿结果。原码能直接满足这件事吗不能。原因很简单符号位一旦参与运算结果就完全乱套。1.2 硬件加法器不想要分类讨论我们用8位原码算一次5(-3)5的原码是 0000 0101-3的原码是 1000 0011如果直接把这两串比特当作普通二进制数相加得到的是1000 1000。在8位原码规则下1000 1000表示的是-8不是2。结果完全错误。如果想算出正确结果电路必须执行一套分类讨论逻辑先比较符号符号相同就直接加绝对值符号不同还要比较绝对值谁大谁小最后用大的减小的再给结果填上符号。换句话说一个加法器还得内置一套比较器、减法器和符号判定电路。这在1940年代计算机刚起步的时候意味着晶体管数量和电路复杂度急剧上升成本和故障率也跟着涨。工程师最想要的方案是不管正数负数我只要做一个二进制加法操作输出结果天然就是对的。为此就必须改变负数在机器里的编码方式。反码和补码本质上都是在重新编码负数。1.3 两个0的尴尬除了加法器的问题原码还有一个绕不开的尴尬0被定义了两次。8位原码里0000 0000是01000 0000是-0。这俩数值明明都是0二进制编码却不一样。你可能会觉得0多个负号也没啥大不了。但对于硬件来说这意味着判断两个数是否相等时必须无条件地把0和-0视为相等计数循环时每次从-1加到0都要面对两种可能的0编码转换成整数时还得专门处理这个负零。这不是不能做而是处处添麻烦。更关键的是反码同样继承了这个问题。真正把0归一的是补码。这一点后文还会展开。2. 原码、反码、补码到底长什么样2.1 原码符号位加绝对值原码就一句话最高位是符号位0表示正1表示负剩余位存放数值的绝对值。以8位为例十进制二进制绝对值原码5000 01010000 0101-5000 01011000 01010000 00000000 0000-0000 00001000 0000127111 11110111 1111-127111 11111111 1111从这个表能看出来原码的可表示范围是-127到127。符号位不参与数值大小计算正数和负数都有各自独立的一套编码互相对称。2.2 反码把负数的原码逐位取反反码的定义也不复杂正数的反码和原码一样负数的反码是符号位保持不变其余位全部取反。还是8位例子5的原码是 0000 0101反码也是 0000 0101-5的原码是 1000 0101反码是 1111 1010为什么会出现取反这个操作我们可以从模运算的角度理解但先记住最直接的做法反码原码除符号位外按位取反。这个转换有一个好处反码的符号位依然能保留而且正负数的运算开始有一定的对称性。比如5和-5相加按位相加得到1111 1111也就是负零从数值上看确实等于0。但反码的缺点很致命它依然有两个0。8位反码里0000 0000是01111 1111是-0。更麻烦的是反码做减法时会出现循环进位问题后面我用具体运算说明。2.3 补码取反之后末位进1补码的定义教材上通常这样写正数的补码等于原码负数的补码等于原码除符号位外取反然后末位加1。这个末位加1就是很多人搜过的负数补码末位进1。注意这里的进1不是随便加一下而是把反码向正方向整体推进一步。换句话说负数的补码 负数的反码 1符号位保持不变。8位例子-5的原码 1000 0101除符号位取反得到 1111 1010这是反码末位加1得到 1111 1011这就是-5的补码这个简单的1直接解决了反码的两个大问题一个是负零被吞掉了另一个是减法不再需要循环进位。下面这张表能一眼看清三种编码的区别十进制原码反码补码50000 01010000 01010000 0101-51000 01011111 10101111 101100000 00000000 00000000 0000-01000 00001111 1111不存在1270111 11110111 11110111 1111-1271111 11111000 00001000 0001-128无法表示无法表示1000 0000最后一行很有意思原码和反码都表示不了-128补码却可以。多出来的这个-128正是因为负零被合并掉后省出来的一种编码。这个细节在笔试里经常出现。2.4 一个不用死记的速算技巧很多教材只教你取反加1但实际做题时尤其是手算二进制逐位取反再末位加1容易算错。我分享一个自己常用的速算法从二进制最低位开始往高位看遇到第一个1时这个1以及它右边的所有低位照抄它左边的所有位除符号位外全部取反。符号位保持原样。举个例子-10的8位原码是1000 1010。从最低位开始看bit0是0bit1是1这是从右往左遇到的第一个1。于是bit1和bit0照抄为10bit2到bit6取反原码里是00010取反后是11101符号位仍为1。拼起来得到1111 0110这就是-10的补码。验证一下-10的补码按取反加1来算原码1000 1010除符号位取反得1111 0101末位加1得1111 0110。两个方法结果一致。这个技巧尤其适合在纸上手算大量补码时节省时间。3. 深入补码内部为什么取反加一不是巧合3.1 从时钟模型理解模运算想要真正理解补码不能停留在负数怎么转换的层面要理解它背后的数学结构模运算。拿一个12小时时钟举例。在时钟上10点往前走4个小时是2点。如果你把10点看作-2点往前走4小时同样得到2点。也就是说在模12的世界里-2和10占据同一个位置是等价的。一句话概括超出模范围的部分被丢掉就像时针转完一圈又回到起点。二进制也一样。8位二进制一共能表示2的8次方也就是256个状态。它的模就是256。任何一个8位二进制数只要加上或减去256就会回到同一个状态。比如1111 1111是255如果把它看作模256下的-1完全成立因为256-1255。3.2 补码就是模减去绝对值有了模的概念负数的补码就有了最干净的定义对n位补码负数x的补码等于2的n次方减去x的绝对值再取低n位。拿-5来算8位情况下-5的补码256-5251转成8位二进制是1111 1011。和前面用取反加1算出的结果完全一致。这就是为什么补码比原码、反码更自然它本质上是在做模运算而不是在给二进制码强行规定符号规则。这也是为什么负数的补码做加减法时符号位可以直接参与运算。因为在模256的世界里负数已经被表示成一个等价的正数了。加法运算是同一种运算符号位不过是普通的一位比特。3.3 从全1到取反加1的证明很多人好奇为什么恰好是取反加1而不是取反加2或者取反减1我们来推一下。假设有一个n位二进制数x对它逐位取反得到~x。无论x是什么x (~x)的结果一定是一串全1也就是n位都是1。在8位情况下这个值是1111 1111等于255刚好是256-1也就是模256下的-1。所以x (~x) ≡ -1 (mod 256)。等式两边同时加1就得到x (~x) 1 ≡ 0 (mod 256)。括号里那一坨(~x)1就是x在模256下的相反数也就是-x。换句话说取反加1得到的东西恰好让x和它相加变成0。它对每个负数来说都是那个加法逆元。这也是为什么取反加1永远正确不是因为规定如此而是因为补码就是这么定义出来的数学成果。3.4 为什么0唯一为什么多出-128前面说过原码和反码都有两个0。补码靠末位加1把-0那个编码给推进了-1的位置。具体看8位反码的-0是1111 1111加1后得到1 0000 0000超出8位部分被截断留下来的低8位是0000 0000。于是-0被自然合并进了00的编码只剩一种全0。既然负零没了原来负零对应的编码位就空了出来。8位二进制256种状态里正数和0一共用了128种0000 0000到0111 1111剩下的128种全归负数。其中从1000 0000到1111 1111一共128个负数。但前面算过原码时代负数范围只有-1到-127现在多出了一个-128。它对应的就是1000 0000也就是原本应该表示负零的位置。所以记住一个结论8位补码能表示的数值范围是-128到127比原码、反码的-127到127多出一个最小值。这个不对称是很多面试题的来源。4. 用补码做加减法完整推演和溢出判断4.1 加法符号位直接参与先看最简单的情况两个正数相加。535的补码是0000 01013的补码是0000 0011相加得到0000 1000结果是8正确。这里没有任何悬念。再看负数加正数这是补码最拿手的地方。5-3换成补码加法就是5(-3)0000 0101 1111 1101 1 0000 0010。结果多出一位进位超出了8位直接把最高位的进位丢掉剩下0000 0010也就是2。答案正确而且你不需要关心哪个数绝对值大不需要分类讨论加法器一把梭就完了。4.2 减法就是加上相反数的补码补码的一大优势是减法可以被改写成加法。3-5可以先求出-5的补码1111 1011然后计算3(-5)0000 0011 1111 1011 1111 1110。这个结果在补码里表示-2。怎么验证对1111 1110再求一次补码就能得到它的绝对值先取反得0000 0001加1得0000 0010即2所以原值是-2正确。负数加负数也一样。(-1)(-1)1111 1111 1111 1111 1 1111 1110截断最高位后剩下1111 1110也就是-2。整个过程里符号位从一开始就参与运算不需要额外操作。这就是补码和原码最本质的区别原码的符号位是装饰品运算前要检查、运算后要修正补码的符号位是普通零件直接跟着加法器一起工作。4.3 溢出判断两个正数加起来突然变成负数补码解决了正负号统一运算的问题但没有解决所有问题最典型的就是溢出。8位补码能表示的最大正数是127也就是0111 1111。如果计算12710111 1111 0000 0001 1000 0000。这个结果对应的是-128显然是错的。问题出在哪里127和1都是正数正确的和应该是128但8位补码根本表示不了128。这个错误的本质是结果超出了8位补码的数值范围。判断溢出有个最简单的方法两个同号数相加结果的符号如果变了就说明溢出了。两个正数相加变成负数或者两个负数相加变成正数基本可以断定溢出。严格一点的判断法看两个进位加法器最高位产生的进位和符号位向外的进位如果这两个进位不一致就是溢出。具体操作时把符号位参与运算的最高位进位和最终丢弃的那个最高位进位比较相同就无溢出不同就有溢出。这个方法在硬件里非常直观所以很多教材要求掌握。4.4 用几行代码验证补码运算如果觉得纸上推演不够直观可以用代码验证。Python里负数做bin()时会显示负号不太方便。一个常用技巧是用按位与操作把负数掩成指定宽度的补码def to8bit(x): return x 0xFF # 取低8位得到补码形态 print(bin(to8bit(-5))) # 0b11111011 print(bin(to8bit(5))) # 0b101 # 验证 5 (-3) 2 result (0b00000101 0b11111101) 0xFF print(bin(result), result) # 0b10 2看到没有计算时只需要做普通加法最后按位与0xFF截断8位结果就自动是补码运算的正确结果。这个技巧在分析位运算问题、写底层模拟代码时非常实用。5. 常见问题与实操避坑记录5.1 求负和取反为什么总差1很多人学到后面会混淆取反和求负。取反是按位取反C语言里写作~x求负是求相反数写作-x。对于补码来说~x -x - 1。换句话说取反之后还要再加1才是求负。用5举例~5 -6-5 -5。相差1。这个关系从前面全1的推导就能看出来x (~x) -1所以~x -x - 1。这个式子最好记牢很多位运算题、嵌入式开发里找最低有效位1的套路都会用到。比如计算一个数最低位的1常用x (-x)这里-x的本质就是~x1。5.2 符号扩展与截断的坑实际编程中最常见的坑不是加减法而是宽度转换。一个8位有符号数-1补码是1111 1111。如果把它扩展成32位有符号整数正确的做法是符号扩展也就是用符号位的值去填充高位得到1111 1111 1111 1111 1111 1111 1111 1111仍然表示-1。如果你在C语言里把有符号char赋给int编译器默认做符号扩展但如果先赋给unsigned char再赋给int高位就被填0-1就变成了255。很多从串口、文件里读字节的程序员都在这上面栽过跟头。反过来高位截断也有类似问题一个32位整数0x000001FF如果强转成8位char低8位是0xFF若这个char被当作有符号数打印出来就是-1而不是511。这也是为什么读写二进制数据时一定要明确每个字段是有符号还是无符号。5.3 反码是不是彻底没用了现代CPU基本清一色用补码原码、反码只在教材和面试题里出现。但反码这个思路并没有完全消失。比如网络协议里经常提到的ones complement checksum也就是二进制反码求和校验就和按位取反相加的思路有关。那里的反码求和不是用来表示负数而是用来做差错检测核心思想是所有数据按16位一组相加溢出位循环回加到最低位最后结果取反作为校验值。这个场景说明反码在特定场景下仍有应用只是和负数怎么存储已经关系不大了。还有一个冷知识C语言标准早期并不强制要求有符号整数用补码理论上允许原码、反码、补码三种实现。直到近年新标准C23才正式把补码规定为唯一选择。所以你平时看到的负数就是补码背后其实也是现代软硬件生态收敛的结果。5.4 自查练习一组必会的题目学完概念之后刷几道题比看十遍书都管用。下面这组题覆盖了最常见的坑你可以先在纸上算再看答案。写出-7的8位原码、反码、补码。写出-128的8位补码并说明为什么原码、反码表示不了它。补码1110 0100对应的十进制是多少8位补码中1000 0000这个数如何解释计算-126 (-4)8位补码运算是否溢出答案和思路-7的原码是1000 0111反码是1111 1000补码是1111 1001。-128的补码是1000 0000因为原码和反码都要先表示128的绝对值但8位绝对值部分最多7位存不下128。补码1110 0100先判断符号位为1是负数求补得0001 1100即28所以是-28。8位补码中1000 0000表示-128是全补码里唯一的负零被转化后的产物。-126的补码是1000 0010-4的补码是1111 1100相加得到1 0111 1110截断后是0111 1110符号位变成0正数126。两个负数相加却得到正数明显溢出。最后再分享一个我个人教学时的小经验别去背正数三码相同这种笼统口诀建议每次拿到一个负数都从时钟模型推一遍值加补码等于0的关系。只要你能解释清楚为什么补码加它的补码能归零原码和反码就都不容易再混淆。这个思路比考前突击刷一百道转换题都管用。