483 lines
13 KiB
Markdown
483 lines
13 KiB
Markdown
# 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,用"逐位进位"模拟加法。
|
||
|
||
### 代码
|
||
|
||
```cpp
|
||
#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` 的特殊处理。
|
||
|
||
---
|
||
|
||
## 九、练习题
|
||
|
||
### 【第一组】进制转换热身
|
||
|
||
1. 将十进制 `13` 转换成二进制。
|
||
2. 将十进制 `25` 转换成二进制。
|
||
3. 将二进制 `0001 0110` 转换成十进制。
|
||
|
||
---
|
||
|
||
### 【第二组】求原码
|
||
|
||
用**8位**二进制写出下列各数的原码:
|
||
|
||
4. `+9`
|
||
5. `-9`
|
||
6. `+20`
|
||
7. `-20`
|
||
8. `+0` 和 `-0` 的原码分别是什么?它们相同吗?
|
||
|
||
---
|
||
|
||
### 【第三组】求反码
|
||
|
||
用**8位**二进制写出下列各数的反码(需要先写原码再推导):
|
||
|
||
9. `+9`
|
||
10. `-9`
|
||
11. `-20`
|
||
12. `-1`(提示:`1` 的原码是 `0000 0001`)
|
||
|
||
---
|
||
|
||
### 【第四组】求补码
|
||
|
||
用**8位**二进制写出下列各数的补码:
|
||
|
||
13. `+9`
|
||
14. `-9`
|
||
15. `-20`
|
||
16. `-1`
|
||
17. `-128`(这道题有些特别,想想为什么原码方法不好用?)
|
||
|
||
---
|
||
|
||
### 【第五组】补码反推原值
|
||
|
||
已知以下 8 位补码,请判断符号并求出对应的十进制数:
|
||
|
||
18. `0000 1010`
|
||
19. `1111 1110`
|
||
20. `1111 0000`
|
||
21. `1000 0000`(提示:这是补码范围内的特殊值)
|
||
|
||
---
|
||
|
||
### 【第六组】补码加法验证
|
||
|
||
用补码计算以下各题,并验证结果正确(用 8 位,溢出位丢弃):
|
||
|
||
22. `6 + (-2)`
|
||
23. `(-6) + (-2)`
|
||
24. `10 + (-10)`
|
||
25. `(-1) + 1`
|
||
|
||
---
|
||
|
||
### 【第七组】思考题
|
||
|
||
26. 为什么计算机不直接使用原码来做加减法?用 `3 + (-3)` 举例说明原码的问题。
|
||
27. 8 位补码最多能表示多少个不同的整数?范围是多少?
|
||
28. 如果把 8 位扩展到 **16 位**,用补码表示整数,范围是多少?(规律:$n$ 位补码的范围是 $-2^{n-1}$ 到 $2^{n-1}-1$)
|
||
|
||
---
|
||
|
||
## 参考答案
|
||
|
||
**第一组**
|
||
|
||
1. `1101`
|
||
2. `1 1001`(即 `0001 1001`)
|
||
3. `16 + 4 + 2 = 22`
|
||
|
||
**第二组**
|
||
|
||
4. `0000 1001`
|
||
5. `1000 1001`
|
||
6. `0001 0100`
|
||
7. `1001 0100`
|
||
8. 分别是 `0000 0000` 和 `1000 0000`,**不相同**。
|
||
|
||
**第三组**
|
||
|
||
9. `0000 1001`(正数不变)
|
||
10. `1111 0110`
|
||
11. `1110 1011`
|
||
12. `1111 1110`
|
||
|
||
**第四组**
|
||
|
||
13. `0000 1001`
|
||
14. `1111 0111`
|
||
15. `1110 1100`
|
||
16. `1111 1111`
|
||
17. `-128` 的 8 位补码为 `1000 0000`(这是规定值,因为补码多表示了这一个负数)
|
||
|
||
**第五组**
|
||
|
||
18. 符号位 `0`,正数,`= +10`
|
||
19. 取反加一:`0000 0001 + 1 = 0000 0010`,`= -2`
|
||
20. 取反加一:`0000 1111 + 1 = 0001 0000`,`= -16`
|
||
21. `-128`(特殊规定值)
|
||
|
||
**第六组**
|
||
|
||
22. `0000 0110 + 1111 1110 = 0000 0100 = +4` ✓
|
||
23. `1111 1010 + 1111 1110 = 1111 1000 = -8` ✓
|
||
24. `0000 1010 + 1111 0110 = 0000 0000 = 0` ✓
|
||
25. `1111 1111 + 0000 0001 = 0000 0000 = 0` ✓
|
||
|
||
**第七组**
|
||
|
||
26. `3 + (-3)` 用原码:`0000 0011 + 1000 0011 = 1000 0110 = -6`,答案错误。
|
||
27. $2^8 = 256$ 个,范围 `-128` 到 `+127`。
|
||
28. 范围是 `-32768` 到 `+32767`(即 $-2^{15}$ 到 $2^{15}-1$)。
|