13 KiB
3.1 原码、反码与补码
一、先搞清楚:计算机为什么只认识 0 和 1?
你有没有想过,我们平时写下的数字 5、-3、100,计算机根本"看不懂"?
计算机的本质是电路。电路里的导线,要么通电,要么断电,只有两种状态。科学家就用 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 位全部翻转(0变1,1变0)。
例子(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 1111加1,得到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)。
- 反码:正数与原码相同;负数把除符号位以外的每一位取反(
0变1,1变0,即1 - bit)。 - 补码:正数与原码相同;负数在反码的基础上加 1,用"逐位进位"模拟加法。
代码
#include <bits/stdc++.h>
using namespace std;
// 将非负整数 n(0~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 取进位,完全模拟了手算的竖式加法。
想一想: 为什么范围限制在
-127到127,而不包括-128? 因为-128没有合法的 8 位原码(8 位原码最多表示-127),只有补码能表示它(1000 0000)。你可以尝试修改代码,增加对-128的特殊处理。
九、练习题
【第一组】进制转换热身
- 将十进制
13转换成二进制。 - 将十进制
25转换成二进制。 - 将二进制
0001 0110转换成十进制。
【第二组】求原码
用8位二进制写出下列各数的原码:
+9-9+20-20+0和-0的原码分别是什么?它们相同吗?
【第三组】求反码
用8位二进制写出下列各数的反码(需要先写原码再推导):
+9-9-20-1(提示:1的原码是0000 0001)
【第四组】求补码
用8位二进制写出下列各数的补码:
+9-9-20-1-128(这道题有些特别,想想为什么原码方法不好用?)
【第五组】补码反推原值
已知以下 8 位补码,请判断符号并求出对应的十进制数:
0000 10101111 11101111 00001000 0000(提示:这是补码范围内的特殊值)
【第六组】补码加法验证
用补码计算以下各题,并验证结果正确(用 8 位,溢出位丢弃):
6 + (-2)(-6) + (-2)10 + (-10)(-1) + 1
【第七组】思考题
- 为什么计算机不直接使用原码来做加减法?用
3 + (-3)举例说明原码的问题。 - 8 位补码最多能表示多少个不同的整数?范围是多少?
- 如果把 8 位扩展到 16 位,用补码表示整数,范围是多少?(规律:
n位补码的范围是-2^{n-1}到 $2^{n-1}-1$)
参考答案
第一组
11011 1001(即0001 1001)16 + 4 + 2 = 22
第二组
0000 10011000 10010001 01001001 0100- 分别是
0000 0000和1000 0000,不相同。
第三组
0000 1001(正数不变)1111 01101110 10111111 1110
第四组
0000 10011111 01111110 11001111 1111-128的 8 位补码为1000 0000(这是规定值,因为补码多表示了这一个负数)
第五组
- 符号位
0,正数,= +10 - 取反加一:
0000 0001 + 1 = 0000 0010,= -2 - 取反加一:
0000 1111 + 1 = 0001 0000,= -16 -128(特殊规定值)
第六组
0000 0110 + 1111 1110 = 0000 0100 = +4✓1111 1010 + 1111 1110 = 1111 1000 = -8✓0000 1010 + 1111 0110 = 0000 0000 = 0✓1111 1111 + 0000 0001 = 0000 0000 = 0✓
第七组
3 + (-3)用原码:0000 0011 + 1000 0011 = 1000 0110 = -6,答案错误。2^8 = 256个,范围-128到+127。- 范围是
-32768到+32767(即-2^{15}到 $2^{15}-1$)。