ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

补码加减法与位移运算:从原码反码到溢出实战

2026/9/30 1:36:07 拓冰建站 浏览量
补码加减法与位移运算:从原码反码到溢出实战 1. 补码这套规则到底想解决什么问题1.1 从硬件只会做加法说起很多同学第一次接触补码是被老师一句取反加一直接砸晕的公式背下来了题目也会算了但心里始终有个疙瘩凭啥负数非得这么表示直接把符号位拿出来单独处理不行吗要真正把补码加减法运算和位移运算弄明白得先回到那个最原始的约束条件——一个最基础的加法器电路它天生只会做一件事就是把两个二进制位加在一起并往前进位。你让它去做减法它没有这个能力除非你为它单独造一套减法电路。造一套减法电路意味着什么意味着芯片面积翻倍、功耗增加、设计复杂度飙升。在早期硬件资源极其昂贵的年代这个代价没人愿意付。于是工程师们想能不能让减法伪装成加法让同一套加法器把加法和减法都干了这个思路就是补码诞生的土壤。理解这一点非常重要因为它解释了后面所有看似反直觉的规则背后其实都是为了让二进制补码运算能够统一走加法器这条路。我们用一个生活化的类比。钟表上只有0到11这12个刻度你想知道现在3点往回拨5小时是几点。正常人会说3减5等于负2也就是10点。但换个说法往回拨5小时等价于往前拨7小时因为12减5等于73加7等于10一样得到10点。这里的7就是-5在这个12进制系统里的补数。补码的核心思想完全一致——在一个固定位宽的二进制系统里负数可以用它的补数来表示减法就变成了加法。1.2 原码、反码、补码各自扮演的角色要讲清楚补码绕不开它的两个兄弟原码和反码。很多教程把它们并列讲搞得像三个平等的方案其实它们是一条演进链上的三个阶段各自解决不同的问题。原码是最贴近人直觉的表示法最高位当符号位0表示正1表示负剩下的位表示数值大小。比如用8位表示正5是0000 0101负5是1000 0101。看着很清楚但它有两个致命伤。第一0有两种表示0000 0000是正01000 0000是负0一个数有两种编码硬件判零要判两次麻烦。第二符号位不能直接参与运算你还得单独判断两个操作数的符号来决定做加法还是减法这又绕回了需要减法电路的老问题。反码的思路是负数用原码除符号位外逐位取反来表示。负5反码就是1111 1010。它的好处是加减法开始有点规律了但0依然有两种表示正0是0000 0000负0是1111 1111而且运算时如果产生进位还得往最低位补一个1这叫循环进位处理起来依然别扭。补码才最终把这些问题一次性解决0只有一种表示符号位可以名正言顺地参与运算加减法统一用加法器完成。负数的补码就是反码末位加1也就是负数补码末位进1这个说法的来源。你有没有发现热搜里负数补码末位进1和补码原码反码这些词本质上都是在描述同一条转换链原码取反得反码反码加1得补码。把这层演进逻辑理顺那些公式就不再是死记硬背的东西了。1.3 用模的视角看补码一下就通透了我觉得理解补码最爽的角度是把它当成模运算取余运算的特例。一个n位二进制系统的模是2的n次方。8位系统的模是256就好比钟表的模是12。在这个系统里-5和251也就是256减5作用是完全等价的因为它们相差正好一个模数。我们把251写成二进制1111 1011。你再算算负5的补码是不是也是1111 1011一模一样。所以补码的本质就是负数X的补码 2的n次方 X。这个公式简洁到没有任何记忆负担你甚至不需要取反加一这套流程直接心算就能验证。提示判断一个二进制补码到底代表几最稳的做法是用最高位权值为负来算。比如8位补码1111 1011最高位是符号位权值是-128其余各位按正常位权相加-128 64 32 16 8 0 2 1 -5。这个方法比先减1再取反更不容易出错后面我会反复用它。理解了模运算这层1.2节里符号位参与运算就不再玄学了因为补码本身就是一个合法的、能被加法器直接吞进去操作的无符号数运算结果自然落在同一个模系统里最高位的进位直接丢掉就行丢掉的正好是溢出到模数以外的部分。2. 补码互转正数、负数分别怎么动手2.1 正数的补码就是它自己正数这部分没什么好纠结的正数的原码、反码、补码三者完全相同。正数78位表示就是0000 0111原码反码补码都是它最高位固定是0。之所以这么统一是因为正数本来就符合补码的定义一个正数X的补码就是X本身没有任何符号位上的特殊处理。我知道有同学会问那正数会不会也有取反加一的情况不会。取反加一是专门为负数设计的换算规则正数你如果也去取反加一得到的根本不是你想要的数。我见过不少初学者在刷题时把正数也套一遍流程结果全错这个坑踩一次就长记性了。另外要留意一个边界0的补码只有一种就是全0。8位下是0000 0000它是正数还是负数严格来说它既不是正也不是负就是个零。正因为补码消除了负0才让硬件判零变得干净利落这也是补码相比原码、反码的一大优势。2.2 负数补码取反加一以及末位进1的细节负数才是重头戏。以8位、-5为例标准流程是这样的先写出-5的原码1000 0101符号位1数值位000 0101。然后符号位保持不变数值位逐位取反得到反码1111 1010。最后末位进1也就是整体加1得到补码1111 1011。这就是热搜词负数补码末位进1的完整含义。这里有个很多教程一笔带过、但实操中经常翻车的点加1是给整个反码加1进位有可能一路波及到符号位。比如求-1的补码原码1000 0001反码1111 1110加1得1111 1111。再比如求-128的补码它比较特殊8位下根本没有对应的正128原码正数最大只有127所以不能用常规流程推只能直接记1111 1111加1之前的那个特殊值——实际8位补码里-128就是1000 0000。这个数是唯一一个不对称的存在记牢它。注意取反加一时符号位不参与取反这一步但加1产生的进位可以影响符号位。这两句话看似矛盾其实说的是两件事前者是取反操作的范围后者是加1操作的影响范围。很多人栽在把这两者混为一谈。还有一个更快的技巧我平时手算常用从右往左遇到第一个1保持不变把这个1右边的所有0变1、再往左的位全部取反得到的就是补码。这个方法叫快速补码法验证-51111 1011从右往左第一个1在倒数第一位……你试着推导一下就明白它本质是取反加一的简化。对爱刷题的人来说能省不少时间。2.3 已知补码怎么还原成原码补码求原码方法也是高频搜索词原理很简单对一个补码再求一次补码就回到原码。因为补码的补码等于原码这是个自反性质。具体做法和求补码一样如果补码最高位是0那它本来就是正数直接读如果最高位是1说明是负数那就把补码再取反加一得到原码然后翻译成十进制。举个例子给你补码1111 0011问它表示几。最高位是1是负数。取反得0000 1100加1得0000 1101这是原码的数值部分为13加上负号答案是-13。你也可以用最高位权值为负的方法直接算-128 64 32 16 0 0 2 1 -13。两条路殊途同归我建议两种都练熟考试或调试时互相验证。2.4 位宽一变数值范围就变补码的取值范围是考试和工程里的必考项n位补码能表示的范围是负2的(n-1)次方 到 2的(n-1)次方减1。我把常见位宽整理成表方便你随时对照。位宽表示范围最小值补码最大值补码8位-128 ~ 1271000 00000111 111116位-32768 ~ 327671000 0000 0000 00000111 1111 1111 111132位-2147483648 ~ 21474836471后跟31个00后跟31个1注意一个规律负数比正数能多表示一个也就是那个最小值。这不是笔误而是补码系统的固有特性——因为0占用了正数区间的一个位置负区间就多出来一个。这个不对称在写代码判断边界时非常关键比如你写-x时如果x是那个最小值结果会溢出回它自己很多bug就是这么来的后面第5章会细说。3. 补码加减法的完整推演3.1 加法两条 二进制直接对齐相加补码加法简单到有点无聊符号位一起参与逐位相加最高位产生的进位直接丢弃。不用管正负不用分类讨论加法器一视同仁。我们来算一个7加负5。0000 0111 (7) 1111 1011 (-5) ----------- 1 0000 0010 丢弃最高进位 ----------- 0000 0010 (2)结果0000 0010就是2正确。注意最左边那个多出来的1是第9位的进位放不进8位直接扔掉。这个扔掉不是偷懒而是模运算的必然结果——我们本来就在一个模256的系统里操作256的倍数自然要被舍弃。再来一个负数加负数的。-3加-6应该等于-9。8位下-3是1111 1101-6是1111 1010。1111 1101 (-3) 1111 1010 (-6) ----------- 1 1111 0111 丢弃最高进位 ----------- 1111 0111用权值法验证1111 0111-1286432160421-9正确。你看加的过程中正负完全不影响操作这就是补码最迷人的地方。3.2 减法把减数变成它的补码再加减法在补码体系里被彻底改造了A减B 等价于 A加负B。而负B的补码就是把B的补码取反加一对整个B包括符号位取反再加一。所以减法根本不需要独立电路一个加法器加一个取反器就搞定了。算一个15减6等价于15加负6。15是0000 11116是0000 0110负6的补码是对0000 0110全取反得1111 1001再加1得1111 1010。0000 1111 (15) 1111 1010 (-6) ----------- 1 0000 1001 丢弃最高进位 ----------- 0000 1001 (9)结果9正确。实操中我建议你养成一个习惯做减法前先在心里把减数转换成补码形态别两套逻辑混着用否则符号位很容易搞错。尤其是整体取反加一含符号位和数值位取反加一不含符号位这两个操作前者用于求一个数的相反数后者用于原码转补码用途不同千万别互换。3.3 溢出什么时候结果会悄悄地错补码好用归好用但它有个必须警惕的副作用——溢出。当两个同号数相加结果超出了当前位宽能表示的范围时就会溢出。可怕的是溢出不会报错它会给你一个看起来像那么回事、其实完全错误的结果。经典的例子8位下127加1。127是0111 11111是0000 0001。0111 1111 (127) 0000 0001 (1) ----------- 1000 0000结果1000 0000最高位变成1了按补码解释它是-128。可127加1明明是128怎么就变-128了这就是典型的正溢出。反过来-128加-1也会翻成正数这叫负溢出。怎么判断溢出我给你两个方法。方法一双符号位把操作数扩成双符号位再运算如果结果的两位符号位是01说明正溢出是10说明负溢出是00或11则正常。方法二同号判定如果两个操作数符号相同而结果符号与之相反那就溢出了。两个异号数相加永远不会溢出这个结论可以帮你快速筛掉一部分情况。操作数A符号操作数B符号结果符号是否溢出正正负正溢出负负正负溢出正负任意不溢出负正任意不溢出提示CPU里有个溢出标志位OF就是靠这个逻辑自动设置的。你写代码时做加法如果关心溢出得靠语言或框架提供的检查机制底层硬件是不会帮你拦的。3.4 手算验证的三道题我把三道我当年练习时最有代表性的题放这儿你可以跟着算一遍算完对答案。第一题-7加4。负7补码1111 10014是0000 0100相加得1111 1101用权值法算-1286432168401-3。对了答案是-3。第二题-100减50。负100补码是1001 1100负50补码是1100 1110两者相加1001 1100 1100 1110 1 0110 1010丢弃进位得0110 1010。用权值法643208020106可-100减50应该是-150这说明什么溢出了8位表示不了-150所以结果106是个错误值正好验证了负溢出的存在。第三题0减1。0是0000 0000负1补码1111 1111相加得1111 1111正好就是-1没问题。这道题能帮你确认-1的补码形态要背下来。4. 位移运算正数负数待遇不一样4.1 逻辑移位和算术移位不是一回事位移运算这块逻辑移位和算术移位必须分清。逻辑移位把二进制序列当成纯位串左移右移都从一边补0算术移位把序列当成有符号数右移时要保持符号不变往左边补的是符号位。很多语言里默认做算术右移而部分语言才有才是逻辑右移这个区别是坑的重灾区。用生活类比逻辑移位像把一排硬币整体往一边挪空出来的位置用空位0填算术移位像挪一排有正负意义的筹码右移时为了不让这个数变了正负号得把最左边的符号位复制过去补上。4.2 左移低位补0相当于乘2左移运算不管正负数规则都是整体向左移动低位统一补0移出去的位丢弃。这和乘2的关系非常紧密在补码体系下左移一位就是把数乘以2左移k位就是乘以2的k次方——前提是没溢出。比如正数50000 0101左移一位得0000 1010是10等于5乘2。负数呢-3是1111 1101左移一位得1111 1010用权值法算-1286432168020-6正好是-3乘2。所以左移对正负数都统一是乘2这点很省心。但溢出问题跟着就来。正数640100 0000左移一位得1000 0000变成了-128可64乘2应该是128又溢出了。所以用左移代替乘法时心里得清楚只有当结果落在当前位宽范围内时这个等价关系才成立否则会得到匪夷所思的值。4.3 右移正数补0负数补1右移是正负数分道扬镳的地方。正数右移高位补0等价于除以2取整负数右移为了保持符号位不变高位补的是1算术右移效果上也等价于往负无穷方向取整的除法。先看正数10是0000 1010右移一位得0000 0101是5等于10除以2。再看负数-6是1111 1010算术右移一位高位补1得1111 1101用权值法-1286432168401-3。注意-6除以2正好是-3没问题。再来个有小数的情况你就明白向负无穷取整了-5是1111 1011算术右移一位得1111 1101也就是-3。可数学上-5除以2是-2.5向上取整是-2向下取整才是-3。补码右移得到的是-3即向下取整。而如果是正数50000 0101右移一位得0000 0010是25除以2是2.5向下取整也是2。所以统一规律是算术右移等价于除以2后向负无穷取整。这个细节在算法题里经常被利用来简化处理。4.4 移位实现乘除的边界与陷阱用移位代替乘除2的幂是写高性能代码的常见技巧编译器也经常自动帮你做这个优化。但有几个边界必须清楚我先给你一张对照表。运算结果说明5 110正数左移乘2-3 1-6负数左移同样乘264 18位-128溢出结果错误10 15正数右移除以2-6 1-3负数算术右移-5 1-3向负无穷取整还有两个容易忽略的坑。第一个是移位数超过位宽时不同语言的规则不一样。有些语言会取移位数对位宽取模比如8位系统里左移9位相当于左移1位有些语言则直接返回不确定值。这个我踩过跨语言移植代码时特别容易出错务必查清目标语言的规定。第二个是移位数是负值这在大多数语言里是未定义行为别去试探老老实实判断边界。再补一个实战心得如果你要把一个数乘以8写x 3比x * 8快但现代编译器基本会自动识别x * 8并优化成移位所以除非你在写极其底层的代码比如驱动、汇编、加密算法否则可读性优先用乘法就行。移位用过头反而会降低代码可读性得不偿失。5. 踩坑记录与常见问题排查5.1 高频疑问速查表补码和移位这块的疑问来来回回就那么几个我整理成速查表你遇到问题先对着找。问题现象根本原因解决思路负数取反后结果不对只对数值位取反忘了符号位规则明确是数值位取反1还是整体取反1127加1变成-1288位正溢出换用更大位宽或做溢出检查-5右移结果比除法小1算术右移是向负无穷取整需要向零取整时手动加补偿求补码时0出现了两种形态混淆了原码和补码补码的0只有全0一种负数的相反数算不对用了数值位取反而非整体取反求相反数必须整体取反加一5.2 几条我从实际调试里攒下的经验第一条永远用权值法交叉验证。取反加一算完再用最高位为负权相加的方式算一遍两个方法结果一致基本就没跑了。这个习惯帮我抓出过好几次符号位处理错误。第二条涉及负数的取整运算要格外小心。很多业务代码里除以2和右移一位结果可能不一样如果输入可能是负数用移位前先想清楚你要的是哪种取整语义。我就见过财务报表计算出错的案例根源就是用移位替换了除法。第三条面试或考试里遇到补码加减法别急着套公式先在草稿纸上把两个操作数的补码写全对齐位数再算。位数不对齐是最高频的错误来源8位对8位16位对16位混着来必错。另外记得最后检查一下是否溢出点没点到那个关键值上。第四条理解-x对最小值的特殊行为。8位下最小负数是-128它的相反数应该是128但128超出范围所以-(-128)在补码运算下会得到-128自己这就是著名的溢出陷阱。写代码做取绝对值、求相反数时一定要对最小值单独判断否则线上会冒出你完全想不到的负数。把这几章过下来你会发现补码加减法和位移运算的所有规则其实都能从模运算加加法器统一处理这一个源头推出来。下次再有人问你负数补码为什么要末位进1你可以从钟表往回拨的例子讲起一路说到溢出标志位这比背公式有意思多了。我自己是反复在纸上画位、用权值法验算慢慢才把直觉练出来的纸上功夫比刷题软件更管用。