原码、反码、补码彻底讲透:从模运算看懂负数存储与溢出

发布时间:2026/9/19 3:16:01
原码、反码、补码彻底讲透:从模运算看懂负数存储与溢出 1. 为什么计算机非要折腾原码、补码这一套先说个我自己带新人时经常遇到的场景。很多学生在学 C 语言、单片机或者逆向工程的时候第一次看到-5在内存里存的是一串0xFFFFFFFB第一反应都是“这什么玩意儿”再看到书上写“负数的补码是原码取反加一”能背下来但完全不知道为什么要这么干。原码、补码这东西说难其实不难但如果你只记规则、不懂原理过两天必忘而且一旦哪天调试器里蹦出一个0xFFFFFFFB你根本反应不过来它其实是-5。我建议所有搞编程、搞硬件的人都把这个知识当成“底层常识”来对待因为它是理解有符号整数、无符号整数、溢出、类型转换、位运算等一系列问题的地基。平常写业务代码可能觉得没啥用但只要一碰到底层——串口协议、网络字节序、单片机寄存器、内存调试、性能优化、逆向分析——这套知识就会反复过来找你。原码和补码本质上就是计算机内部用一个固定位数的二进制串表示整数的两种方案。原码是最符合人类直觉的编码方式最高位当符号位0 表示正1 表示负剩下的位数表示绝对值。补码是计算机实际采用的编码方式它让加减法可以统一成加法电路来实现把一个挺麻烦的“减法器”给省掉了。这篇文章我会从“为什么”讲起再讲“怎么算”最后聊实际调试中怎么用这套知识排查问题。主要面向刚学计算机基础的学生、转行做开发的程序员以及任何想彻底搞懂有符号整数原理的人。我会尽量用大白话加实际例子把原码、反码、补码的关系、负数的补码运算规则、以及“末位进1”这个细节背后的原理一次讲清楚。2. 三种编码的定义与相互转换2.1 原码最直观但电路最不想要它原码的定义特别简单。拿 8 位二进制举例最高位是符号位剩下 7 位是数值位。5的原码是00000101-5的原码是10000101。正数的原码、反码、补码完全一样所以后面凡是涉及到负数我们才需要额外讨论。原码的好处是看一眼就知道是多少符号位加绝对值人类友好。计算机刚设计的时候也确实先用过类似的思路。但问题来了如果直接用原码做减法电路没法用加法电路代替必须专门设计一套减法逻辑而且符号位还要单独判断电路复杂不说还容易出错。更尴尬的是原码会出现“正负零”的问题。8 位原码里00000000表示010000000表示-0。你说0等于-0吗数学上当然相等但计算机判断两个数相等时如果不知道这个约定就会把0和-0当成两个不同的数。别小看这个坑早期有一些硬件就因为这个设计出过兼容性问题。2.2 反码一个过渡方案也是补码的“半成品”反码的规则正数不变负数则是在原码的基础上符号位保持不变其余位全部取反。比如-5的原码是10000101反码就是11111010。反码解决了一部分问题——负号不再需要单独处理因为通过取反操作正数到负数之间有了一个对称的映射关系。但是反码依然没有解决“正负零”的问题。8 位反码里00000000是011111111也是-0。而且在反码体系里做加法如果最高位产生了进位还得补加一次这个“循环进位”的规则在硬件实现上又麻烦又慢。所以反码现在基本只作为“理解补码”的中间步骤存在实际硬件和编程语言里不怎么直接用它。但你一定要知道它因为补码的运算口诀“取反加一”第一步取反就是在算反码第二步加一就是反码变补码的关键操作。2.3 补码计算机真正采用的编码补码的规则正数不变。负数分两步——先取反码然后加 1。这就是大家背得滚瓜烂熟的“取反加一”。-5的计算过程原码10000101取反符号位不动11111010加一11111011所以-5的补码是11111011。怎么验证这个数是-5把补码当成无符号数来看11111011是 251。而256 - 5 251。这里有个非常重要的关系一个负数的补码x它的无符号数值恰好等于2^n x其中n是二进制位数。8 位数里-5映射到256 (-5) 251。这个映射关系是理解后面所有内容的关键。把补码这个概念往小了说就是“一个负数用它的模补数来表示”。往大了说补码把整个有符号数的加减运算统一到了无符号加法电路上。这也是为什么所有现代处理器都用补码来做有符号整数运算。下面这个表格把 8 位-5的三种编码放在一起对比看一眼就清楚了编码方式符号位数值位整体二进制原码1000010110000101反码1111101011111010补码11111011111110113. 负数补码的“末位进1”到底是怎么回事3.1 取反加一加的这个一到底加在哪如果你仔细看“取反加一”这个操作会发现一个非常有意思的细节负数补码的生成过程中最后那一步“加一”经常会在某些位上引起一连串的进位。传统的回答叫做“末位进1”但很多人对这四个字理解得并不深。拿-5看反码是11111010加一之后变11111011。最低位本来是 0加 1 之后变成 1没有连环进位。看起来平平无奇。再看-6原码10000110反码11111001补码11111010最低位 1 加 1变成 0向第二位进 1第二位是 0加进位变成 1于是结果是11111010。这里就出现了“末位进1后向前传导”的情况。再看一个更极端的例子-1原码10000001反码11111110补码11111111反过来看如果一个负数通过“取反加一”得到补码而这个负数本身的绝对值是 2 的整数次幂比如-128那么末位进 1 的连续进位会一路从最低位传到符号位-128的 8 位原码是10000000这里按 8 位补码的规则直接得到10000000也是 8 位有符号数的最小值它没有对应的正数原码形式因为 128 超范围了所以从原码出发硬套“取反加一”是不可行的但从规则上理解补码10000000表示-128它右边 7 位全是 0没有任何冗余空间一旦参与运算就要小心溢出。3.2 用“取模”理解补码这一节看懂了你就彻底通了要真正理解为什么“取反加一”是对的绕不开一个生活中的类比——时钟。时钟上一共有 12 个刻度如果现在是 3 点你想知道“3 小时前是几点”你会怎么做你当然可以做减法3 - 3 0也就是 12 点。但你也可以做加法3 (12 - 3) 3 9 12也是 12 点。也就是说在模 12 的体系里“减 3”等价于“加 9”。这里的 9 就是-3对模 12 的补数。同理8 位二进制就是个模 256 的体系。所有数字都在一个 0 到 255 的圆环上转圈。在这个体系里“减5”等价于“加251”因为256 - 5 251。换句话说-5的补码形式本质上就是它在模 256 体系里的补数也就是 251写成 8 位二进制就是11111011。所以补码的数学本质不是“符号位加绝对值”而是“负数的补码 模 − 该负数的绝对值”。这个公式做任何负数都能套模 256 − 5 251265 位二进制展开就是11111011。再回到“取反加一”为什么它能算出来同一个结果因为对一个二进制数取反等价于算出255 - x按位取反其实就是每一位翻转结果就是这个数对 255 的补数。再加一就变成了255 - x 1 256 - x。你看这刚好就是“模 256 − x”。所以我一直觉得真正该背的口诀不是“取反加一”而是“负数的补码就是对模取补数取反加一只是计算这个补数的快捷方式”。3.3 用加法电路做减法一次完整演示搞明白了补码的模运算本质再来看计算机里的3 - 5是怎么被“伪装”成加法的。3的补码是00000011。-5的补码是11111011。直接把这两个补码相加00000011 11111011 ---------- 100000110结果有 9 位超出 8 位最高位 1 直接丢掉这是溢出丢弃不是错误因为模 256 体系里本来就要循环回绕。剩下 8 位是11111110。这个是什么是-2的补码。因为-2的模补数是256 - 2 254 11111110。于是3 (-5) -2完全正确。整个过程里没有任何“判断符号”的步骤也没有“借位”的概念就是把两个二进制数当作普通无符号数直接加然后截断高位。减法被彻底消灭了处理器只需要加法器就够了。这就是补码能在计算机里称霸这么多年的根本原因。4. 补码的数值范围、溢出与符号位细节4.1 取值范围为什么是不对称的一个 8 位有符号数用补码表示取值范围是多少答案是-128到127。正数最大到 127负数最小到-128。注意这里的正数部分少了一个名额多出来的名额给了负数。原因看二进制就明白了。正数的补码最高位必须是 0所以正数范围是00000000到01111111也就是 0 到 127一共 128 个数。负数的补码最高位是 1范围是10000000到11111111按无符号数看是 128 到 255对应的有符号数是-128到-1也是 128 个数。中间的00000000表示 0所以没有-0这个多余品。正数它把 0 占了实际“非负数”和“负数”各占 128 个码点但正数大军里 0 也算一个所以真正的正数只有 127 个负数则有 128 个。这个不对称性最直接的后果是-128的补码是10000000它并没有一个对应的原码或反码可以正常转换——你想对-128做“取反加一”验证取反后得到01111111加一变成10000000看似绕回来了但实际上左边的“符号位”也参与了取反再按“符号位保持不变”的规则去套原码根本套不出128这个正数来。所以对-128别拿“原码”概念去死磕它只存在于补码体系。4.2 溢出补码最容易被忽略的坑溢出这东西写业务代码时不在乎但只要你处理协议、解析二进制数据、做算法优化、或者写任何跟底层打交道的东西迟早遇到。理解补码后溢出的本质就一句话运算结果超出了当前位数能表示的范围导致高位被截断结果绕到了相反符号那一边。最经典的例子是127 101111111 00000001 ---------- 1000000001111111是 12700000001是 1两者相加得到10000000。如果你把这个结果当补码解读它是-128。也就是说正数相加溢出后变成了负数。反过来-128 - 1也会绕回127。这种现象叫上溢和下溢。在任何固定位数下补码的数字在数轴上并不是无限延伸的它是一个首尾相连的环。应对溢出有几个实战建议。第一在 C/C 里做有符号整数运算时如果值域可能接近边界尽量先把变量提升到更大的类型比如int提升到long long。第二做协议解析时拿到一个“很大的正整数”先想想它是不是一个有符号负数被当成了无符号数。第三在调试的时候看到结果突然从正变负、从大变到小优先怀疑溢出而不是算法逻辑错了。4.3 符号位是“参与运算”的不是“被忽略”的很多教材在解释补码加法时会先把符号位摘出去算完再把符号位加回来。但实际硬件里根本没这么干。符号位就是普通的一位直接参与二进制加法运算。之所以能这么干是因为补码已经把所有负数映射到了无符号数区域整个环上的加减法都可以无视符号位直接进行。举个例子-3 5补码11111101 00000101二进制相加100000010丢弃最高位进位00000010结果2如果非要把符号位“单独摘出去”反而会把自己绕晕。所以请记住一点在补码体系里任何有符号数都可以先当成无符号数做加减然后重新把结果按补码解释即可。唯一需要注意的就是是否溢出。这个思路在做位运算的时候尤其有用因为它把“有符号/无符号”的差别彻底区分开了。5. 常见问题、易混淆点与排查技巧5.1 为什么补码的补码会变回原码这是新手最容易问的问题也是面试官最喜欢问的变体。其实答案就藏在前面的运算关系里。一个数的补码是其“对模的补数”那么对补码再取一次补码相当于连续求了两次补数结果自然会转回来。用-5验证一遍-5的补码是11111011对11111011取反00000100注意这里不是“符号位保持不变”的反码规则而是逐位取反加一00000101结果5的补码也就是正数 5所以“取反加一”这个操作对正数做会得到负数对负数做会得到正数正负来回折腾也是很多位运算技巧的基础。实际中比如你在学汇编或者看别人反汇编时有时会用这个性质快速把一个立即数的符号“翻过来”再检验它到底是多少。5.2 快速心算碰到一个补码怎么迅速知道它是十进制几调试的时候你在内存窗口或者寄存器窗口看到一个 8 位数比如11100100想在脑子里快速转成有符号十进制。总不可能每次掏出计算器按一遍教你一个心算流程。第一步看最高位。如果是 0直接按无符号数算即可。如果是 1说明是负数进入下一步。第二步按“取反加一”把补码转回绝对值。11100100取反00011011加一00011100转十进制28第三步加上负号得到-28。再反过来验证256 - 28 228 11100100完全对得上。这套心算流程熟练之后大概两三秒就能搞定一个 8 位数的换算对于解析串口日志、网络报文时非常有用。5.3 无符号数和有符号数混用是最大的坑实际开发中踩得最多的坑不是不懂补码而是把有符号数、无符号数混在一起用。C 语言里如果一个int和一个unsigned int做比较或运算编译器会按隐式类型转换规则把int转换成unsigned int这时负数就会变成巨大的正数。看我之前遇到的一个实际案例。有个嵌入式项目读温度传感器返回一个 16 位值手册说明这是“有符号数”。同事没仔细看直接用uint16_t temp_raw接收然后计算temp_c temp_raw * 0.0625。室内温度正常时传感器返回正数比如320乘一下是 20 度看着好像没毛病。结果把传感器放到冰水里返回变成负数对应的补码0xFF00这是-256的补码形式但用uint16_t解读是65280乘上 0.0625 变成 4080 度。那天的数据直接起飞了。这种问题排查起来特别折磨人因为数据链路看着都对就是结果离谱。后来我把接收变量改成int16_t一晚上就好了。所以说只要你在解析外部协议、寄存器值、传感器数据一定要先搞清楚对方的编码是有符号还是无符号尤其是负数这一块直接用补码解读准没错。5.4 面试题与自测清单用这些例子检验自己是否真懂光看懂了还不算完我整理几个经典问题你可以自己先答一遍再对照答案。第一个-128的 8 位补码是什么答案10000000。这个值是 8 位有符号数里唯一没有“原码”完整对应的数记忆时要小心。第二个8 位补码11111111代表几答案是-1。它是所有位全为 1 的二进制同时也是无符号数 255但在有符号场景下它只会被解释成-1。这也是为什么很多协议校验和算出来是 255 时你把它转成有符号数会看到-1。第三个0x80在 8 位有符号数和无符号数里分别是什么有符号是-128无符号是128。同一个字节解释方式变了含义天差地别这也是类型转换里最常见的坑之一。第四个用补码计算-6 7。-6补码11111010加上00000111得到00000001结果是1。这个过程里符号位直接参与进位不需要额外判断。这些例子都是我平时培训新人时必用的把这几个搞明白补码这一关基本就算过了。6. 写在最后的个人体会带过几批新人之后我发现一个规律一开始就死记“取反加一”的人过两周就忘而那些先把“模”这个概念搞明白的人哪怕很久不碰底层知识偶尔遇见一个负数补码也能自己推出来。所以我特别建议大家在学习时不要把补码当成一个“背下来的规则”而是当成一个“数轴卷成环之后自然产生的映射”。一旦你把 256 那个时钟圆圈画在纸上把负数补码一个个填进去你会发现所有规则都是顺理成章的根本不需要背。另外如果你手边有 Python、C 或者任何能打印二进制表示的语言强烈建议实验一下。写几行代码把-1到-128的 8 位补码都打印出来然后对照原码、反码手工推一遍。这个过程对建立敏感度帮助特别大比读十遍书都管用。我第一次在内存里亲眼看到0xFFFFFFFB表示-5时脑子里下意识做了个取反加一从那以后再没混淆过。