ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

补码符号位为何能参与运算?从模运算到负权重位讲透

补码符号位为何能参与运算?从模运算到负权重位讲透 补码的符号位为什么能参与运算这个问题我第一次啃计算机组成原理的时候也卡了很久。那时候把原码、反码、补码的转换口诀背得滚瓜烂熟取反加一四个字张口就来但只要有人追问一句最高位明明叫符号位、是用来标正负的它怎么还能跟数值位一起丢进加法器里做运算当场就哑了。后来真正想通靠的不是继续死背而是把模运算和权重这两件事串起来看一下就通了。这篇分享我想把这条思路从头到尾捋一遍从原码的坑讲到补码的数学底色再落到逐位推导和溢出判断最后说一下工程里容易踩的几个坑。不管你是刚学二进制、准备考试面试还是写代码时对位运算犯嘀咕看完应该都能拿到一个原来如此的体感。1. 计算机为什么非要发明补码1.1 原码的直观和它藏的两颗雷先说原码因为它最符合人的直觉。规则特别简单最高位拿来做符号位0 表示正、1 表示负剩下的位老老实实表示绝对值。拿 8 位举例3 写成0000 0011-3 写成1000 0011。你看一眼就知道是几人脑读起来毫无障碍这也是它一开始被采用的原因。问题是计算机不是人脑它要做的是加减乘除的电路实现。原码在硬件层面埋了两颗雷。第一颗是加减法没法统一你算3 (-3)机器得先判断两个数的符号同号就相加、异号就相减还得比较绝对值大小来决定结果带什么符号。这一套流程意味着加法器旁边还得配一套判断逻辑电路一下就复杂了。第二颗更别扭就是两个零0000 0000和1000 0000分别表示 0 和 -0。数学上 0 只有一个机器里却冒出俩任何一次判断是否等于零的操作都得多绕一步实际写底层代码的人对这种事深恶痛绝。注意原码并不是错的它在浮点数表示法里依然被使用符号位单独拎出来。只是在整数的加减运算场景里它不够经济。1.2 反码一块向上的台阶人们先想到的修补方案是反码。规则是正数跟原码一样负数的符号位保持 1 不动只把数值位按位取反。于是 3 还是0000 0011-3 变成1111 1100。这么做让取负这个动作变得机械、好电路化但两个零的老毛病一点没解决——0000 0000和1111 1111又是一对。反码真正有价值的地方是它揭示了一条规律对一个正数按位取反得到的结果恰好等于模减一再减去它本身。这句话先记住后面推导补码的时候要用。反码本身更像通往补码的一块台阶单独拿出来用的时候不多但跳过它直接理解补码很多人会卡壳。1.3 补码登场一个口诀走天下补码的规则是在反码基础上再加一正数依旧同原码负数则是数值位取反、末位加一。还是 3 和 -33 是0000 0011-3 就是1111 1100再加 1得1111 1101。这一加不要紧两个零的问题直接解决了-0 经过取反加一跟 0 撞到了同一个0000 0000上。更关键的是补码让加减法合体了。以后要算a - b机器不搞减法直接算a (b 的补码)就完事。也就是说一套加法器电路同时干加法和减法的活判断符号、比较大小这些啰嗦步骤全省了。这就是补码统治现代计算机整数运算的根本原因。至于为什么取反加一能产生这种神奇效果以及符号位为什么就这么被允许参与运算了得从模讲起。2. 补码背后的数学模运算与同余2.1 用 12 小时时钟理解模模这个词听着玄其实生活里天天见。你盯着一个 12 小时制的钟表现在是 4 点你想知道 7 小时前是几点。你可以往前拨 7 格也可以往后拨 5 格——因为 4 5 9而往前退 7 格同样到 9 点。也就是说在这个钟面上减 7和加 5完全等价因为 7 5 12恰好等于钟面的容量。这个容量 12 就叫模。凡是超过 12 的部分都会被绕回来拨上 12 格等于原地踏步。在模 12 的世界里-7 和 5 是同一个效果写成数学语言就是-7 ≡ 5 (mod 12)读作-7 与 5 对模 12 同余。2.2 同余为什么 -1 和 255 是一家人把钟表换成计算机。8 位二进制能表示 0 到 255 这 256 个状态它的模就是 2 的 8 次方也就是 256。在这个模 256 的世界里-1 和 255 就是同余的-1 ≡ 255 (mod 256)。你要算5 - 1等价于算5 255。算算看5 255 260260 对模 256 取余是多少260 - 256 4。而在正常数学里5 - 1 也等于 4。对上了。这就是补码的核心心法计算机里的减法其实是加上一个负数在模意义下的正数替身。这个替身就是补码。所以 -1 的补码写作1111 1111它在模 256 的世界里就等于 255机器拿 255 去加效果和减 1 一模一样。你不需要专门造一套减法电路加法电路加上取补这一步就够了。2.3 二进制的模就是 2 的 n 次方总结一下规律。n 位二进制数的模是 2 的 n 次方减一个数就等于加上模减去这个数。8 位的模是 25616 位的模是 6553632 位的模是 4294967296。位数越多能表示的数的范围越大但减法变加法这个逻辑不变。理解到这里你已经摸到补码的骨架了。但还有个问题没解决那个取反加一的快捷算法到底是怎么从模运算里推出来的下一页我们就把它拆开看顺便理解 0 和 -128 这两个特殊位置为什么那么关键。3. 取反加一到底在做什么3.1 从 255 减 x 推导出反码先看一个正数 x假设它是 8 位里的某个值比如 x 3。我们要找 -3 在模 256 下的替身也就是256 - 3 253。253 的二进制是1111 1101正好就是 -3 的补码。那这个 253 能不能用更机械的方法算出来呢能。注意 256 - 1 255而 255 的二进制是1111 1111全部是 1。那么255 - 3就是把 255 里对应 3 的那几位清掉结果等价于把 3 按位取反0000 0011取反得到1111 1100这个数就是 252。你看按位取反得到的是255 - x不是我们要的256 - x差了一个 1。差的那一个 1加回去就行——这就是取反加一里那个加一的来历。3.2 加一之后发生了什么把这条链子串起来反码 255 - x补码 反码 1 (255 - x) 1 256 - x。而我们一开始要的模下的替身正是 256 - x。取反加一之所以成立就是因为它凑出了 2 的 n 次方减去原数。一句口诀背后其实是一个严格的代数等式。这也顺带解释了为什么补码的转换对正数不生效——正数不需要找替身它本身就是自己的补码。只有负数才需要走取反加一这条路因为要把它换算成加上它等价于减它的那个正数。3.3 两个特殊位置0 和 -1280 是个微妙的位置。它没有正负之分取反加一之后还是0000 0000正好和 0 重合两个零因此被合并成一个。另一个特殊位置是1000 0000它对应的值是 -128。这个数很特别因为它的补码是它自己——你把它取反加一又回到1000 0000说明它和自己同余。它的绝对值 128 超出了 8 位正数能表示的范围正数最多到 127因此它没有对应的原码表示。很多题目爱问8 位补码能表示的范围是多少答案就是 -128 到 127一头一尾不对称原因就出在这个 -128 身上。提示记住补码能表示的范围是 -2 的 n-1 次方 到 2 的 n-1 次方减一这是判断溢出和取值范围的基础。4. 核心问题符号位凭什么能参与运算4.1 换个视角最高位其实是负权重位前面铺垫这么多该正面回答最初那个问题了。这里我要抛出一个颠覆常识的说法在补码体系里符号位这个叫法其实是个误导。最高位真正的身份是一个权重为负的普通二进制位它的权重是 -2 的 n-1 次方其余位的权重照旧。拿 8 位来说位从高到低依次是 b7 到 b0那么它表示的数值可以写成这样一个通式值 -b7 × 2^7 b6 × 2^6 b5 × 2^5 ... b0 × 2^0注意看只有 b7 前面带了负号权重是 -128b6 到 b0 分别是 64、32、16、8、4、2、1。这不是什么牵强的说法而是补码最本质的定义。用这个公式随便验算几个数你会立刻信服。验证0111 1111这是 127最高位 b7 0所以 0 64 32 16 8 4 2 1 127对。验证1111 1111b7 1其余全 1所以 -128 64 32 16 8 4 2 1 -128 127 -1也对。验证1000 0000只有 b7 1 -128 0 -128同样对。4.2 这个视角为什么能解释一切一旦接受了最高位是负权重位这个设定很多困惑迎刃而解。首先为什么符号位能参与运算因为整个加法器的设计逻辑就是按权重加权求和每一位该加多少加多少最高位只是权重是负的而已加法器对它一视同仁照样算进总和里。没有什么特殊的符号位专用电路最高位和普通位完全平权只是它的身价不一样。其次为什么两个不同符号的数相加还能得到正确结果因为结果的每一位权重是固定的只要参与运算的各位按自己的权重加进去最终累加出来的数值在数学上就是正确的。符号位被顶出来的那一位第 n 位权重恰好等于模 2 的 n 次方取余的时候自然被丢掉不影响结果。4.3 一个完整例子5 加 -3光说理论太空来算一遍。5 的 8 位补码是0000 0101-3 的补码是1111 1101。两个数相加0000 0101 (5) 1111 1101 (-3) ----------- 1 0000 0010结果出现了第 9 位也就是那个高高在上的 1。它被丢弃剩下的0000 0010就是 2。5 (-3) 2完全正确。那个被丢掉的第 9 位权重是 2 的 8 次方 256正好是模取余为 0丢它理所当然。再算一个带负结果的3 (-5)。3 0000 0011-5 1111 10110000 0011 (3) 1111 1011 (-5) ----------- 0 1111 1110没有第 9 位进位结果是1111 1110。按权重公式验证一下-128 64 32 16 8 4 2 0 -2。3 (-5) -2正确。整个过程机器完全没判断过符号就是硬加。4.4 位数变大原理不变有人会问那 16 位、32 位是不是也一样完全一样只是最高位的权重从 -128 变成 -32768、再变成 -2147483648。公式的骨架分毫未动只是 2 的幂次变了。这也是为什么你在 32 位系统里能看到int范围是 -2 的 31 次方到 2 的 31 次方减一它和 8 位补码是同一个道理放大版。理解了这一点你就彻底明白了符号位能参与运算不是因为它特殊到被允许而是因为它本就是一位权重为负的普通位。所谓符号只是它在正数和负数里恰好呈现出的一个方便人类阅读的副产品。5. 溢出符号位参与的代价5.1 溢出到底是怎么发生的既然符号位是负权重位那它的翻面就意味着数值大幅跳到另一头。当两个较大的正数相加和的真实值超过了最大表示范围 127数值就会顶翻最高位让 b7 从 0 变成 1结果瞬间被解释成一个负数——这就是溢出。看最经典的例子127 10111 1111 (127) 0000 0001 (1) ----------- 1000 0000没有进位到第 9 位结果1000 0000被当成 -128。127 1 本应等于 128机器却给你 -128这就是溢出闯的祸。反过来两个负数相加也可能顶翻最高位变成正数比如-128 (-1)1000 0000 (-128) 1111 1111 (-1) ----------- 1 0111 1111丢掉第 9 位后变成0111 1111即 127。本该是 -129机器给出 127又错了。5.2 三种判断溢出的方法实际写代码或者做题目判断溢出有几种常用招数我列个表方便你对照。方法判断依据优点缺点同号相加变号法两数符号相同结果符号却相反即溢出逻辑最直观一眼能看只能用于同号相加单符号位进位法最高位进位与次高位进位不同即溢出纯电路级硬件好实现需要同时观察两个进位双符号位法用两位符号位结果出现 01 或 10 即溢出判断最清晰需要额外一位存储同号相加变号法最好用也最好记。你只要抓住一句话同号相加如果结果的符号变了就是溢出。两正相加得负、两负相加得正这种情况绝对不正常。5.3 双符号位法实操演示双符号位法又叫变形补码做法是把符号位复制一位正数写00负数写11。运算后看这两位00是正常正数11是正常负数一旦出现01就是正溢出出现10就是负溢出。用 3 位数据加 2 位符号位举例范围是 -8 到 7。先算3 500 011 (3) 00 101 (5) --------- 01 000符号位变成01正溢出。真实值 8 超出了 7正确报溢出。再算-3 (-5)11 101 (-3) 11 011 (-5) --------- 1 10 000最高位丢弃剩下10 000符号位是10负溢出。真实值 -8 超出下限同样正确报警。这套方法在硬件里很受欢迎因为它把是否溢出直接编码进了符号位判断逻辑极简。5.4 -128 这个孤儿值的坑回到 -128。它在 8 位里是个特殊存在绝对值 128 比最大正数 127 还大所以它没有原码和反码表示。做题目时如果你遇到求 -128 的原码答案是无法用 8 位原码表示这题专坑没想清楚的人。还有一个常见坑-(-128)这种取负操作在 8 位里会溢出因为 128 超出了 127结果是它自己仍然 -128。写底层代码处理这种边界时一定要单独考虑。6. 常见问题与排查实录6.1 补码求原码到底怎么求这个被问的频率极高。记住一条循环规律补码的补码就是原码。具体操作分情况如果最高位是 0正数原码就等于补码不用动。如果最高位是 1负数对补码再走一遍取反加一得到的就是原码的数值部分前面冠上负号。例如1111 1101取反得0000 0010加一得0000 0011也就是 3所以原值是 -3。验证一下-3 的补码确实是1111 1101对上了。这条规则好记因为补这个操作做两次就回到原点。6.2 1024QAM 的符号位为什么是 10bit一个概念澄清网上有个热搜词1024QAM 的符号位长为啥是 10bit很多人把它和补码的符号位混在一起其实这是两码事。1024QAM 里的符号指的是通信里的调制符号也就是星座图上的一个采样点图上一共有 1024 个不同的点。要区分这 1024 个点需要多少位二进制2 的 10 次方等于 1024所以每个符号用 10 bit 就能唯一编号。这里的 10bit 是信息位数跟补码的符号位最高位表示正负没有任何关系。搜索这个词的朋友多半是被符号两个字误导了澄清一下免得走弯路。6.3 高频踩坑速查表我把新手阶段最容易翻车的几个点整理成表遇到问题可以直接对号入座。现象常见原因处理思路两个正数相加得到负数发生了正溢出检查是否超出最大值 1278位算出来的减法结果不对忘了把减数取补码记得 a-b 要先对 b 取反加一补码转原码时符号搞反直接对全数取反忘了加一取反加一一次到位别漏加-128 相关运算结果诡异-128 无原码表示单独处理边界别套通用公式位运算结果和预期不符有符号数右移是算术移位涉及符号位要小心移位类型提示写代码时如果对符号位敏感优先用无符号类型做位运算能避开一大半符号扩展的坑。7. 把符号位思维用到工程里7.1 位运算中的符号位陷阱明白了符号位是负权重位你在写位运算代码时就能少踩很多坑。最常见的是右移有符号数的右移是算术右移最高位会补符号位也就是说负数右移一位相当于除以 2 并向下取整而正数右移就是普通除以 2。如果你拿一个有符号的负数去右移得到的结果可能和你想象的逻辑右移完全不同。想避免这种意外做位运算时用无符号类型最保险此时最高位就是普通位不存在符号扩展。取绝对值也有讲究。很多人以为对负数取反加一再取正就能得到绝对值但遇到 -128 这种情况会翻车因为 128 存不进 8 位有符号数。工程代码里处理边界值永远要留一个心眼。7.2 定长字段设计时的启发从补码这套设计里能学到一个通用的工程思维用固定的位数承载信息时一定要先算清楚范围和边界。8 位补码能表示 256 个值正负各占一半但负数多一个因为零占了正数那边的名额这个不对称是刻意的也是范围计算的依据。设计任何定长字段——不管是协议报文里的长度域、数据库里的整型列还是硬件寄存器——都要先回答三个问题一共多少位能表示多少个值边界值会不会溢出把这三个问题想清楚很多上线后才发现的诡异 bug其实在字段定义阶段就能避免。补码这套几百年前的数学设计到今天依然在提醒我们二进制世界的边界永远比直觉多一个坑。7.3 一个延伸小技巧最后分享一个小技巧。如果你在做题目或者调试时对某个补码数值拿不准别急着翻计算器直接用权重公式心算把最高位当 -128其余位按正权重加。这个方法比取反加一更适合快速验算已经给出的二进制串几秒钟就能判断它到底是几。我带了这么多年新人发现凡是愿意接受最高位是负权重位这个视角的后面学溢出判断、学浮点数、学位运算都顺得多因为他们的脑子里装的不是一个口诀而是一个可以自洽运行的模型。这套模型一旦立住你会发现补码根本不需要背它就是一个在模运算框架下自然长出来的东西。符号位参与运算也不是什么需要特别批准的特权而是这个框架里最平常不过的一件事。
返回列表