Files
hustoj/web/doc/3.1 原码反码补码.md

13 KiB
Raw Permalink Blame History

3.1 原码、反码与补码


一、先搞清楚:计算机为什么只认识 0 和 1

你有没有想过,我们平时写下的数字 5-3100,计算机根本"看不懂"

计算机的本质是电路。电路里的导线,要么通电,要么断电,只有两种状态。科学家就用 1 表示通电,用 0 表示断电。这就是二进制的由来。

类比: 想象一排电灯开关,每个开关只能「开」或「关」。用这一排开关,我们可以用不同的开/关组合来代表不同的数字——这就是二进制的核心思想。

十进制 二进制
0 0000
1 0001
2 0010
3 0011
5 0101
7 0111

为什么不用十进制? 如果用十进制,每根导线需要精确区分 10 种不同的电压0V、1V、2V……9V。电路稍微受到干扰电压抖动一点数字就读错了。而二进制只需区分「高电压」和「低电压」两种情况极难出错制造成本也低得多。


二、正数好说,负数怎么办?

表示 5 很简单,写成 0101 就行。但 -5 呢?

计算机存数字时,会分配固定长度的空间,比如 8位8个 0 或 1。科学家想出了一个办法

把最左边(最高位)那一位专门用来表示正负,叫"符号位"

  • 0 → 正数
  • 1 → 负数

剩下的 7 位用来表示数值的大小。

这就是最朴素的方案——原码


三、原码:最直观的方案

规则

  • 正数: 符号位写 0,后面写数值的二进制。
  • 负数: 符号位写 1,后面写数值绝对值的二进制。

例子8位

十进制 原码
+5 0000 0101
-5 1000 0101
+0 0000 0000
-0 1000 0000

直观,容易理解!但……

原码的两个大麻烦

麻烦一:零有两个写法

+0 写成 0000 0000-0 写成 1000 0000,但它们其实是同一个数 0!这会让计算机很困惑——判断一个数是不是 0 时,要检查两种情况。

麻烦二:加减法会算错

用原码做 5 + (-5),把两个原码直接相加:

  0000 0101   (+5 的原码)
+ 1000 0101   (-5 的原码)
-----------
  1000 1010   ← 这是 -10 的原码,答案错了!

正确答案应该是 0,结果算出了 -10。这意味着计算机必须专门写一套特殊的判断逻辑来处理负数加法,硬件电路会变得很复杂。


四、反码:改进的尝试

规则

  • 正数: 反码与原码完全相同
  • 负数: 符号位保持 1 不变,其余 7 位全部翻转0110)。

例子8位

十进制 原码 反码
+5 0000 0101 0000 0101
-5 1000 0101 1111 1010

用反码再试试 5 + (-5)

  0000 0101   (+5 的反码)
+ 1111 1010   (-5 的反码)
-----------
  1111 1111   ← 这是反码,对应的真值是 -0

结果是 1111 1111,这个反码代表 -0。虽然不是理想的 0000 0000,但至少方向对了——运算逻辑有所改进。

反码还剩下的问题

依然有两种写法

  • 0000 0000 = +0 的反码
  • 1111 1111 = -0 的反码

问题没有根本解决,只是往前走了一步。


五、补码:现代计算机真正使用的方案

规则

  • 正数: 补码与原码完全相同
  • 负数: 先求反码,再在最末位加 1

一步步求 -5 的补码8位

第一步:写出 -5 的原码
        1000 0101

第二步:符号位不变,其余位取反(得到反码)
        1111 1010

第三步:反码末位加 1得到补码
        1111 1010
      +         1
      -----------
        1111 1011   ← 这就是 -5 的补码

例子8位

十进制 原码 反码 补码
+5 0000 0101 0000 0101 0000 0101
-5 1000 0101 1111 1010 1111 1011
+0 0000 0000 0000 0000 0000 0000
-0 1000 0000 1111 1111 0000 0000

神奇的事情: -0 的反码 1111 11111,得到 1 0000 0000,共 9 位!但我们只保留 8 位,最高位的 1 自动丢弃,结果就是 0000 0000。这样正负零合并为同一个编码了!

用补码验证 5 + (-5)

  0000 0101   (+5 的补码)
+ 1111 1011   (-5 的补码)
-----------
1 0000 0000   ← 产生了进位,但只保留低 8 位

结果 = 0000 0000 = 0  ✓ 正确!

再验证 7 + (-3)(答案应为 4

  0000 0111   (+7 的补码)
+ 1111 1101   (-3 的补码)
-----------
1 0000 0100   ← 低 8 位是 0000 0100 = +4  ✓

补码让减法可以用加法来做,计算机只需要一个加法器就够了,硬件设计大大简化!


六、三种编码方式总结

原码 反码 补码
正数规则 符号位 0 + 绝对值 同原码 同原码
负数规则 符号位 1 + 绝对值 原码各位取反(符号位除外) 反码 + 1
零的表示 两种(正零/负零) 两种(正零/负零) 一种(唯一)
能否直接做加减法 ✗ 不能 部分改善 ✓ 可以
8位表示范围 -127 ~ +127 -127 ~ +127 -128 ~ +127

补码能多表示一个负数,原因是消灭了 -0,空出来的编码 1000 0000 就用来表示 -128


七、补码的"逆运算":已知补码求原来的数

如果拿到一个负数的补码,怎么还原成十进制?

方法:对补码再做一次"取反加一"即可。

例: 已知某数的 8 位补码为 1111 1011,求它是多少?

第一步:末位加 1取反加一 = 再做一次补码运算)
        1111 1011  →(取反)→  1000 0100  →加1→  1000 0101

第二步:读结果:符号位为 1负数数值部分为 0000 0101 = 5

结论1111 1011 这个补码表示的是 -5。

八、用 C++ 实现原码、反码、补码的输出

学完了手算方法,我们来用 C++ 写一个程序,让计算机自动完成这些步骤。

核心思路

用一个长度为 8 的 int 数组来表示 8 位二进制,下标 0 存最低位,下标 7 存符号位(最高位)

  • 原码:先把绝对值不断除以 2 取余数,填入数组;再把下标 7 设为符号位(正数为 0负数为 1
  • 反码:正数与原码相同;负数把除符号位以外的每一位取反(0110,即 1 - bit)。
  • 补码:正数与原码相同;负数在反码的基础上加 1用"逐位进位"模拟加法。

代码

#include <bits/stdc++.h>
using namespace std;

// 将非负整数 n0~127的二进制填入数组 bits[]
// bits[0] 是最低位bits[7] 是最高位(符号位)
void fillBits(int n, int bits[8]) {
    for (int i = 0; i < 8; i++) {
        bits[i] = n % 2;
        n = n / 2;
    }
}

// 打印 8 位数组,格式为 "XXXX XXXX"(高位在前)
void printBits(int bits[8]) {
    for (int i = 7; i >= 0; i--) {
        cout << bits[i];
        if (i == 4) cout << " ";
    }
    cout << endl;
}

// 输出整数 n范围 -127 ~ 127的原码、反码、补码
void showCodes(int n) {
    int original[8];   // 原码
    int inverse[8];    // 反码
    int complement[8]; // 补码

    if (n >= 0) {
        fillBits(n, original);
        // 正数:三种编码完全相同
        for (int i = 0; i < 8; i++) {
            inverse[i]    = original[i];
            complement[i] = original[i];
        }
    } else {
        // 原码:绝对值的二进制,符号位设为 1
        fillBits(-n, original);
        original[7] = 1;

        // 反码:符号位不变,其余位取反
        inverse[7] = 1;
        for (int i = 0; i < 7; i++) {
            inverse[i] = 1 - original[i];
        }

        // 补码:反码加 1模拟手动进位
        int carry = 1;
        for (int i = 0; i < 8; i++) {
            int sum      = inverse[i] + carry;
            complement[i] = sum % 2;
            carry         = sum / 2;
        }
    }

    cout << "原码: "; printBits(original);
    cout << "反码: "; printBits(inverse);
    cout << "补码: "; printBits(complement);
}

int main() {
    int n;
    cin >> n;

    if (n < -127 || n > 127) {
        cout << "请输入 -127 到 127 之间的整数!" << endl;
        return 0;
    }

    cout << "n = " << n << endl;
    showCodes(n);
    return 0;
}

运行示例

输入 -5,输出:

n = -5
原码: 1000 0101
反码: 1111 1010
补码: 1111 1011

输入 5,输出:

n = 5
原码: 0000 0101
反码: 0000 0101
补码: 0000 0101

关键代码讲解

fillBits:把整数转为二进制数组

n = 5
  5 % 2 = 1 → bits[0] = 1,  n = 5/2 = 2
  2 % 2 = 0 → bits[1] = 0,  n = 2/2 = 1
  1 % 2 = 1 → bits[2] = 1,  n = 1/2 = 0
  其余位全为 0

这就是"短除法"的代码版本,与手算完全一致。

inverse[i] = 1 - original[i]:取反

  • original[i] 是 0 时,1 - 0 = 1
  • original[i] 是 1 时,1 - 1 = 0

不需要任何特殊符号,普通减法就能完成翻转。

③ 反码加 1 的进位模拟

  假设反码是 1111 1010加 1
  carry = 1
  i=0: 0+1=1, bits=1, carry=0
  i=1: 1+0=1, bits=1, carry=0
  ...carry 已为 0后续不变
  结果1111 1011  ← 这正是 -5 的补码

sum % 2 取本位,sum / 2 取进位,完全模拟了手算的竖式加法。

想一想: 为什么范围限制在 -127127,而不包括 -128 因为 -128 没有合法的 8 位原码8 位原码最多表示 -127),只有补码能表示它(1000 0000)。你可以尝试修改代码,增加对 -128 的特殊处理。


九、练习题

【第一组】进制转换热身

  1. 将十进制 13 转换成二进制。
  2. 将十进制 25 转换成二进制。
  3. 将二进制 0001 0110 转换成十进制。

【第二组】求原码

8位二进制写出下列各数的原码:

  1. +9
  2. -9
  3. +20
  4. -20
  5. +0-0 的原码分别是什么?它们相同吗?

【第三组】求反码

8位二进制写出下列各数的反码(需要先写原码再推导):

  1. +9
  2. -9
  3. -20
  4. -1(提示:1 的原码是 0000 0001

【第四组】求补码

8位二进制写出下列各数的补码:

  1. +9
  2. -9
  3. -20
  4. -1
  5. -128(这道题有些特别,想想为什么原码方法不好用?)

【第五组】补码反推原值

已知以下 8 位补码,请判断符号并求出对应的十进制数:

  1. 0000 1010
  2. 1111 1110
  3. 1111 0000
  4. 1000 0000(提示:这是补码范围内的特殊值)

【第六组】补码加法验证

用补码计算以下各题,并验证结果正确(用 8 位,溢出位丢弃):

  1. 6 + (-2)
  2. (-6) + (-2)
  3. 10 + (-10)
  4. (-1) + 1

【第七组】思考题

  1. 为什么计算机不直接使用原码来做加减法?用 3 + (-3) 举例说明原码的问题。
  2. 8 位补码最多能表示多少个不同的整数?范围是多少?
  3. 如果把 8 位扩展到 16 位,用补码表示整数,范围是多少?(规律:n 位补码的范围是 -2^{n-1} 到 $2^{n-1}-1$

参考答案

第一组

  1. 1101
  2. 1 1001(即 0001 1001
  3. 16 + 4 + 2 = 22

第二组

  1. 0000 1001
  2. 1000 1001
  3. 0001 0100
  4. 1001 0100
  5. 分别是 0000 00001000 0000不相同

第三组

  1. 0000 1001(正数不变)
  2. 1111 0110
  3. 1110 1011
  4. 1111 1110

第四组

  1. 0000 1001
  2. 1111 0111
  3. 1110 1100
  4. 1111 1111
  5. -128 的 8 位补码为 1000 0000(这是规定值,因为补码多表示了这一个负数)

第五组

  1. 符号位 0,正数,= +10
  2. 取反加一:0000 0001 + 1 = 0000 0010= -2
  3. 取反加一:0000 1111 + 1 = 0001 0000= -16
  4. -128(特殊规定值)

第六组

  1. 0000 0110 + 1111 1110 = 0000 0100 = +4
  2. 1111 1010 + 1111 1110 = 1111 1000 = -8
  3. 0000 1010 + 1111 0110 = 0000 0000 = 0
  4. 1111 1111 + 0000 0001 = 0000 0000 = 0

第七组

  1. 3 + (-3) 用原码:0000 0011 + 1000 0011 = 1000 0110 = -6,答案错误。
  2. 2^8 = 256 个,范围 -128+127
  3. 范围是 -32768+32767(即 -2^{15} 到 $2^{15}-1$)。