拓冰建站拓冰建站
首页 / 资讯中心 / 正文

原码反码补码移码详解:从408考研到工程实战

之前备考 408 的时候最让我头疼的一个点就是“原码、反码、补码、移码”这四位“兄弟”。当时感觉定义都背得清清楚楚一做到选择题就开始怀疑人生一会是补码的符号位参与运算一会是移码直接用于阶码稍不注意还会把无符号数和有符号数的比较结果搞反。工作以后写嵌入式协议解析、处理二进制数据包才发现当年那些看起来很“应试”的考点其实都是非常实在的工程基础。这篇文章不打算只列一张表就结束。咱们把原码、反码、补码、移码的来龙去脉讲透再结合 408 考研和一些项目实战中常见的考法、坑点、对比逻辑一起拆开看。文章会包含完整的概念讲解、示例表格、Python/C 验证代码、常见误区以及一套可以记在笔记里的复习思路。无论是准备考研、期末复习还是做底层开发需要重新捡起计组知识都可以用这篇文章作为系统梳理。1. 背景与核心概念计算机为什么要搞出这么多种码1.1 机器数计算机只认 0 和 1但数据有正负计算机内部一切数据最终都要用二进制来表示。但是在数学世界里整数是有正负之分的我们能不能直接在一个二进制数的最高位放一个符号位比如用 0 表示正数用 1 表示负数这个想法很朴素也确实是很多教材介绍“原码”的起点。我们把一个带符号的二进制数叫作“机器数”把这个机器数真正代表的数值叫作“真值”。举个例子真值 7如果用 8 位二进制表示它的原码是 0000 0111。真值 -7原码则是 1000 0111。这里最高位就是符号位剩下的 7 位是数值位。这个方案最直观但也带来了几个问题符号位能不能参与运算比如原码的 1 (-1)如果用最简单的想法去做结果是 0000 0001 1000 0001 1000 0010也就是 -2这显然不对。0 的表示不唯一。8 位原码里0 是 0000 0000-0 是 1000 0000这给比较和运算都带来了麻烦。让加法器处理负数时需要额外判断符号硬件实现非常别扭。正因为原码在加减法运算和硬件实现上有明显缺陷人们才继续设计了反码、补码、移码。1.2 模运算与补码思想同余是理解补码的钥匙学习补码之前必须先理解“模”的概念。时钟是最经典的例子如果现在时针指向 9想让它指向 2可以逆时针拨 7 格也可以顺时针拨 5 格。因为 9 5 14而 14 去掉一个 12 后仍然是 2所以 -7 和 5 在“模 12”的意义下是等价的。计算机里的 n 位二进制数同样存在“溢出”问题加法结果超过了 n 位能表示的范围最高位的进位就直接丢弃。因此在 n 位机器数的情况下减法就可以转换成加法利用模运算把负数变成“它的补数”。补码的核心价值在于把减法统一成加法硬件上只需要加法器。符号位可以和数值位一起参与运算不再需要单独判断符号。0 的表示唯一解决了原码中正负零并存的问题。让 n 位二进制能表示的范围多出一个负数比如 8 位补码可以表示到 -128。1.3 那移码又是什么移码主要用于浮点数的阶码表示也就是 IEEE 754 标准里的“偏置指数”概念。移码的思路也很简单给真值加上一个固定的偏置值让所有真值都变成非负数。比如真值范围是 -128 到 127偏置值取 128也常写成 2^(n-1)那么真值 0 的移码就是 128二进制 1000 0000。真值 -128 的移码就是 0二进制 0000 0000。真值 127 的移码就是 255二进制 1111 1111。为什么浮点数要用移码而不用补码呢因为移码有一个非常好的性质移码越大对应的真值也越大。浮点数比较大小的时候只需要用无符号整数的方式直接比较阶码大小就能判断指数的大小这对硬件排序和比较非常友好。2. 四种机器数详解定义、计算规则与表示范围2.1 原码Sign-Magnitude原码的定义很简单最高位是符号位符号位为 0 表示正数为 1 表示负数其余位表示数值的绝对值也就是真值的绝对值按二进制展开。以 8 位机器数为例真值原码70000 0111-71000 011100000 0000-01000 00001270111 1111-1271111 1111原码的表示范围是 -(2^(n-1) - 1) 到 (2^(n-1) - 1)。8 位原码的范围就是 -127 到 127。原码的优点是非常直观人类阅读和转换都方便早期做乘除法运算时也容易理解。缺点是加减法运算复杂0 的表示不唯一。2.2 反码Ones Complement反码通常不会单独出现在最终结果里它更多是作为原码到补码的中间过程。反码的规则如下正数的反码 原码本身。负数的反码 原码符号位不变其余数值位全部取反。8 位反码示例真值原码反码70000 01110000 0111-71000 01111111 100000000 00000000 0000-01000 00001111 1111发现没有反码的 0 和 -0 仍然是不唯一的。它虽然比原码更适合做减法但依然存在正负零的问题。现在的计算机基本不用反码作为存储格式但理解它的存在对推导补码很有帮助。2.3 补码Twos Complement补码是当前绝大多数计算机表示有符号整数的标准形式。补码的转换规则正数的补码 原码 反码。负数的补码 反码 1。这里有一个从数学角度更好理解的思路n 位补码中负数 x 的补码等于2^n x。本质上是在“模 2^n”体系下用正数表示负数因此减法可以变成加法。以 8 位补码为例给出常见真值对应的补码真值原码反码补码70000 01110000 01110000 0111-71000 01111111 10001111 100100000 00000000 00000000 0000-128无法表示无法表示1000 00001270111 11110111 11110111 1111-1271111 11111000 00001000 00018 位补码表示范围是 -128 到 127比原码和反码多了一个 -128。为什么能多一个因为补码的 -0 被用来表示 -128 了。看一下 1000 0000 这个机器数原码角度它代表 -0但补码规定它就是 -128。2.4 移码Biased Representation / Excess-N移码的应用场景主要是浮点数的阶码。它的定义是移码 真值 偏置值通常取偏置值为 2^(n-1)。8 位移码如果取偏置 128那么真值和移码的对应关系是真值十进制移码二进制-1280000 0000-10111 111101000 000011000 00011271111 1111关键特征移码的最小值 0000 0000 对应最小真值。移码的最大值 1111 1111 对应最大真值。移码可以直接按无符号整数比较大小这在浮点数比较中非常有用。注意移码和补码看起来很像尤其符号位是相反的。比如补码的 1 是 0000 0001而移码的 1 是 1000 0001两者符号位刚好相反。这个关系在解题时经常出现可以做一种速记移码 补码符号位取反前提是偏置值为 2^(n-1)且补码是固定位数。3. 经典考法拆解从 408 真题套路到工程应用3.1 考法一原码、反码、补码、移码的表示范围对比这道题几乎是 408 考研选择题的常客。给你 n 位机器数问四种编码各自能表示多少个整数或者能表示的最大最小真值分别是多少。解题的关键是记住这几张范围表编码8 位表示范围最小真值最大真值0 的个数原码-127 到 1271111 1111 (-127)0111 1111 (127)两个0 和 -0反码-127 到 1271000 0000 (-127)0111 1111 (127)两个0 和 -0补码-128 到 1271000 0000 (-128)0111 1111 (127)一个0000 0000移码-128 到 1270000 0000 (-128)1111 1111 (127)一个1000 0000做题时最常出现的混淆点是8 位补码的最小负数到底是多少答案是 -128补码是 1000 0000。原码和补码中的 1000 0000 分别代表什么原码是 -0补码是 -128。移码 1000 0000 代表 0但它的符号位是 1很多人第一次看到会不习惯。3.2 考法二补码加减运算与溢出判断补码把减法统一成加法后硬件实现很简单A - B可以拆成A (-B)也就是先对 B 求补码的相反数按位取反再加 1再做加法。两个无符号整数做补码加法时结果 两个补码相加直接丢弃进位举个例子8 位补码计算7 - 57 的补码0000 0111 5 的补码0000 0101 -5 的补码1111 1011 7 (-5) 0000 0111 1111 1011 ----------- 10000 0010 最高位进位丢弃 结果0000 0010 2说明减法运算确实可以统一为加法。接下来是重要的溢出判断。所谓溢出是指运算结果超出了机器数能表示的范围。两个正数相加变成负数或者两个负数相加变成正数都说明发生了溢出。具体判断方法有三种直接看符号位。如果两个操作数符号相同但结果符号与操作数不同说明溢出。利用最高位进位和次高位进位。如果符号位进位与最高数值位进位不同说明溢出。这个在数字逻辑课程里叫“异或判断法”。利用双符号位。即变形补码用两个符号位表示正负运算后两个符号位不一致则溢出。下面用双符号位来看一个溢出例子8 位补码计算64 6464 的补码0100 0000 两个 64 相加 0100 0000 0100 0000 ----------- 1000 0000结果变成了负数显然是错误的发生了正溢出。因为 64 64 128已经超出了 8 位补码能表示的最大值 127。3.3 考法三无符号数的补码陷阱热词里出现了“无符号数的补码”这确实是一个很经典的坑。很多人以为只有有符号数才有补码但实际上无符号数也存在补码运算只不过它没有符号位所有位都直接参与数值计算。在无符号数视角下一个 8 位二进制数1111 1111不再是 -1 的补码而是十进制的 255。因此同样的二进制位串在不同视角下代表的值可能完全不同。这在 C 语言中尤其危险。举一个典型例子#include stdio.h int main() { unsigned int a 1; int b -1; if (a b) { printf(a is greater ); } else { printf(b is greater ); } return 0; }直觉上1 -1 应该成立。但在 C 语言中当有符号数和无符号数混用时编译器会把有符号数隐式转换为无符号数。-1转成无符号数是 0xFFFFFFFF也就是 4294967295所以实际判断的是 1 4294967295结果为假。程序输出结果是b is greater。这就是无符号数的补码在工程里最容易踩的坑。在处理二进制协议、文件格式、网络字节序时只要混用了signed和unsigned就很容易出现这种反直觉的比较结果。3.4 考法四移码与 IEEE 754 浮点表示在 IEEE 754 标准中单精度浮点数float的存储结构为符号位1 位0 表示正1 表示负。阶码8 位使用移码偏置值 127。尾数23 位隐含整数位 1。单精度浮点数的阶码偏置值不是 128而是 127。这一点经常和 8 位移码默认偏置 128 混淆。具体来说IEEE 754 规定存储的阶码值是“实际指数 127”这样实际指数 -126 到 127 就映射到 1 到 254。例如十进制数1.51.5 1.1(二进制) × 2^0 符号位0 阶码0 127 127 0111 1111 尾数1.1 去掉隐含的 1得到 1000 0000 0000 0000 0000 000因此1.5的 IEEE 754 单精度表示是0 0111 1111 10000000000000000000000移码的作用在这里体现得非常直接阶码从 1 到 254单调递增地对应实际指数 -126 到 127因此比较两个浮点数大小时可以先按无符号整数比较阶码位再做进一步判断。3.5 考法五强制类型转换与位运算408 计组和 C 语言程序设计经常结合考察“位级转换”问题。比如unsigned int x 0xFFFFFFFF; int y (int)x;在大多数平台上int是 32 位补码表示所以0xFFFFFFFF被解释成 -1。这里没有发生任何位级变化只是解释方式变了。再看另一个经典面试题如何判断一个整数是否是 2 的整数次幂int is_power_of_two(int x) { return x 0 (x (x - 1)) 0; }这个技巧用的就是补码和位运算的性质。因为 2 的幂次方在二进制中只有一个 1比如0100减 1 以后变成0011两者相与为 0。如果 x 不是 2 的幂则结果不会为 0。这类题目并不需要背诵完整算法但需要熟练掌握补码的位级视角二进制位串本身没有正负正负是解释方式决定的而位运算作用的是位串本身。4. 实战验证用 Python 与 C 语言摸清机器数转换4.1 Python 实现输出任意整数的 8 位二进制表示很多时候我们光看表格容易记混不如自己写个小工具输入一个有符号整数输出它的原码、反码、补码。下面给出一段完整的 Python 脚本模拟 8 位二进制机器数。def to_bin8(value): 将有符号整数转换为8位二进制字符串 if value 0: value value 0xFF return format(value, 08b) def original_code(value): 模拟8位原码 if -127 value 127: sign 1 if value 0 else 0 abs_val abs(value) # 正数部分转换成8位二进制再去掉最高位符号位 bin_str format(abs_val, 07b) return sign bin_str return N/A (超出8位原码范围) def ones_complement(value): 模拟8位反码 if 0 value 127: return original_code(value) if -127 value 0: sign 1 abs_val abs(value) bin_str format(abs_val, 07b) # 数值位取反 inverted .join(1 if c 0 else 0 for c in bin_str) return sign inverted return N/A (超出8位反码范围) def twos_complement(value): 模拟8位补码 if -128 value 127: return to_bin8(value) return N/A (超出8位补码范围) def biased_128(value): 模拟8位移码偏置值为128 if -128 value 127: return to_bin8(value 128) return N/A (超出8位移码范围) if __name__ __main__: test_values [7, -7, 0, -128, 127, -1] for v in test_values: print(f真值: {v:5} 原码: {original_code(v):10} 反码: {ones_complement(v):10} 补码: {twos_complement(v):10} 移码: {biased_128(v):10})运行结果真值: 7 原码: 00000111 反码: 00000111 补码: 00000111 移码: 10000111 真值: -7 原码: 10000111 反码: 11111000 补码: 11111001 移码: 01111001 真值: 0 原码: 00000000 反码: 00000000 补码: 00000000 移码: 10000000 真值: -128 原码: N/A 反码: N/A 补码: 10000000 移码: 00000000 真值: 127 原码: 01111111 反码: 01111111 补码: 01111111 移码: 11111111 真值: -1 原码: 10000001 反码: 11111110 补码: 11111111 移码: 01111111从这个输出可以直观地看到正的 7 和负的 7 在原码、反码、补码上的不同表现。-128 无法用 8 位原码和反码表示但补码可以。移码和补码存在符号位取反的关系比如 -1 的补码是 1111 1111移码是 0111 1111。4.2 Python 实现补码加法验证接下来用 Python 模拟 8 位补码加法验证减法转加法的过程。def add_twos_complement(a, b, bits8): 模拟bits位补码加法丢弃最高位进位 mask (1 bits) - 1 result (a b) mask return result def to_signed(value, bits8): 将bits位二进制数转换为有符号整数 sign_bit 1 (bits - 1) if value sign_bit: return value - (1 bits) return value # 测试: 7 - 5 x 7 y -5 result_bits add_twos_complement(x, y) print(f{x} ({y}) 的补码结果位串: {format(result_bits, 08b)}) print(f解释为有符号数: {to_signed(result_bits)}) print(f预期结果: {x y}) # 测试: 100 50 会发生什么 x 100 y 50 result_bits add_twos_complement(x, y) print(f{x} {y} 的补码结果位串: {format(result_bits, 08b)}) print(f解释为有符号数: {to_signed(result_bits)}) print(f预期结果: {x y} (这里发生了溢出))运行结果7 (-5) 的补码结果位串: 00000010 解释为有符号数: 2 预期结果: 2 100 50 的补码结果位串: 10010110 解释为有符号数: -106 预期结果: 150 (这里发生了溢出)第一组结果验证了正常情况第二组结果展示了溢出100 50 已经超过 127机器数结果被解释成了负数。这正是面试和考研题里常说的“正溢出”。4.3 C 语言验证看 unsigned 和 signed 的二进制视角差异写一段 C 代码观察同一个位串在不同类型视角下是什么结果。#include stdio.h #include stdint.h int main() { int32_t signed_val -1; uint32_t unsigned_val (uint32_t)signed_val; printf(signed_val %d, hex 0x%08X\n, signed_val, signed_val); printf(unsigned_val %u, hex 0x%08X\n, unsigned_val, unsigned_val); int32_t x 100; int32_t y 50; uint32_t result_bits (uint32_t)(x y); printf(100 50 (int32 溢出视角): %d\n, (int32_t)result_bits); if (1 (int32_t)-1) { printf(1 -1 is true\n); } else { printf(1 -1 is false\n); } if (1u (uint32_t)-1) { printf(1u (uint32_t)-1 is true\n); } else { printf(1u (uint32_t)-1 is false\n); } return 0; }这段代码的核心价值在于(uint32_t)-1并不会改变任何位只是把原来解释为 -1 的 0xFFFFFFFF 改解释成 4294967295。因此比较结果会发生反转。5. 常见问题与排查思路下面把初学者和考研党最常遇到的概念混淆、做题错误整理成表格方便快速查阅。问题现象常见原因解决思路8 位补码的 1000 0000 被当作 -0把补码与原码的解读混用了记住补码中 1000 0000 表示最小负数 -128-128 没有对应的原码和反码8 位原码/反码范围只有 -127 到 127遇到范围题先判断编码类型再判断是否越界补码加法结果为负数怀疑计算错误发生了溢出结果超出可表示范围用双符号位或符号位判断方法检查溢出移码永远看不懂没有抓住“移码 真值 偏置值”先确定偏置值再套公式不要把移码和补码直接画等号C 语言中 unsigned 和 signed 比较结果反直觉隐式类型转换把 signed 变成 unsigned避免混用必要时显式强转并注释说明浮点数阶码为什么用移码不用补码对 IEEE 754 的偏置设计不熟记住移码便于无符号比较阶码单调递增反码似乎没什么用为什么还要学补码推导和某些校验算法会用到反码把它当作“补码的前置步骤”不与补码混淆强制类型转换后数据变了位串相同但解释方式不同用十六进制打印位串观察位级变化6. 最佳实践与工程建议6.1 在 C/C 中统一类型避免隐式符号转换C/C 里的unsigned和signed混用是隐藏炸弹。工程建议是循环计数、数组索引、位掩码等天然非负的场景统一使用size_t或uint32_t。需要表示负数的业务变量统一使用int32_t或int64_t。如果必须混用显式加入static_cast或int32_t()转换并在注释中说明原因。编译时开启-Wsign-compare之类的警告选项尽早发现问题。这样做的好处是避免 6.3 节中展示的类型隐式转换陷阱同时让代码的可读性和可维护性更好。6.2 二进制协议解析时先厘清字节序与符号位在嵌入式开发、网络协议解析、音视频封装解析中我们经常需要把一个 4 字节缓冲区转换成整数值。此时必须明确三件事缓冲区是高字节在前还是低字节在前也就是大端和小端。数据类型是有符号还是无符号。字段位宽是多少是否可能有溢出。如果协议定义的是有符号整数却用了无符号类型去解析那么负数值读出来会变成一个很大的正数。反过来也一样。建议在解析函数中封装统一的方法把字节序转换和符号转换收敛到一处避免到处直接移位拼接。6.3 做题时先看范围再动手算408 考研和期末考试里不少补码题目表面是计算题实际是范围题。看到“8 位补码”“16 位补码”这样的字眼第一反应应该是列出表示范围再去判断结果是否溢出。比如计算64 64如果知道 8 位补码最大值是 127那么一眼就能看出结果越界。再比如真值 -129 用 8 位补码表示答案是“无法表示”而不是某个奇怪位串。很多同学容易把溢出后的位串写出来却丢了“无法表示”这个关键结论。6.4 使用工具辅助验证而不是硬算在线进制转换、Python 的struct模块、C 语言的联合体都是验证机器数的好工具。下面给出一个使用 Pythonstruct观察浮点数二进制表示的例子import struct def float_to_bits(f): 获取单精度浮点数的 IEEE 754 位串 bits struct.unpack(I, struct.pack(f, f))[0] return format(bits, 032b) print(float_to_bits(1.5)) print(float_to_bits(-1.5)) print(float_to_bits(0.0))运行结果00111111110000000000000000000000 10111111110000000000000000000000 00000000000000000000000000000000这和前面手动推导的1.5表示完全一致可以用来验证自己对 IEEE 754 的理解是否正确。7. 总结与学习路线原码、反码、补码、移码这四种机器数本质上是从“人类直观”到“硬件友好”的演进过程。原码最符合直觉但运算困难反码是过渡方案补码让符号位参与运算并且统一了加减法移码则解决了浮点数阶码的比较问题。如果要在计组和 408 考试中进一步提高建议按下面这条线继续复习先把四种编码的定义和 8 位表示范围背熟能默写范围表。练习补码加减法和溢出判断至少掌握符号位判断法和双符号位法两种思路。把 IEEE 754 单精度和双精度的存储结构拆开做一遍手工换算重点理解偏置值为什么是 127 和 1023。回到 C 语言用printf打印各种类型转换前后十六进制位串体会位串与解释方式的区别。最后做几道 408 真题或者王道计组强化课里的题目检验自己是否真正掌握了“根据位串快速判断真值”“根据真值快速写出机器数”的能力。机器数这部分内容并不难难的是第一次接触时容易混淆以及做题时被“到底按有符号还是无符号解释”这个问题卡住。只要自己动手用 Python 或 C 语言验证过几次把范围表和转换公式变成条件反射后续再做浮点数、强制类型转换、补码加减相关的题就会顺很多。如果本文对你有帮助可以收藏备用也可以随手转发给一起备考计组的朋友。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门