2024-10-22 11:09:19 +08:00

549 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 第一章:前言
## 1.1 概述
* 计算机的底层只有`二进制`,即计算机中`运算``存储``所有数据`都需要转换为`二进制`,包括:数字、字符、图片、视频等。
![](./assets/1.jpg)
## 1.2 冯·诺依曼体系结构
* 之前,我们也提到现代的计算机(量子计算机除外)几乎都遵循`冯·诺依曼`体系结构,其理论要点如下:
***存储程序**`程序指令``数据`都存储在计算机的内存中,这使得程序可以在运行时修改。
***二进制逻辑**:所有数据和指令都以`二进制`形式表示。
***顺序执行**:指令按照它们在内存中的顺序执行,但可以有条件地改变执行顺序。
***五大部件**:计算机由`运算器``控制器``存储器``输入设备``输出设备`组成。
***指令结构**:指令由操作码和地址码组成,操作码指示要执行的操作,地址码指示操作数的位置。
***中心化控制**计算机的控制单元CPU负责解释和执行指令控制数据流。
* 所以,再次论证了为什么计算机只能识别二进制。
# 第二章:进制
## 2.1 常见的进制
* 在生活中,我们最为常用的进制就是`十进制`,其规则是`满 10 进 1` ,即:
![](./assets/2.jpeg)
* 在计算机中,常见的进制有`二进制``八进制``十六进制`,即:
* 二进制:只能 0 和 1 ,满 2 进 1 。
* 八进制0 ~ 7 ,满 8 进 1 。
* 十六进制0 ~ 9 以及 A ~ F ,满 16 进 1 。
> [!NOTE]
>
> 在十六进制中,除了 `0` 到 `9` 这十个数字之外,还引入了字母,以便表示超过 `9` 的值。其中,字母 `A` 对应十进制的 `10` ,字母 `B` 对应十进制的 `11` ,字母 `C` 对应十进制的 `12`,字母 `D` 对应十进制的 `13`,字母 `E` 对应十进制的 `14`,字母 `F` 对应十进制的 `15`。
* 进制的换算举例,如下所示:
| 十进制 | 二进制 | 八进制 | 十六进制 |
| ------ | ------ | ------ | -------- |
| 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 1 |
| 2 | 10 | 2 | 2 |
| 3 | 11 | 3 | 3 |
| 4 | 100 | 4 | 4 |
| 5 | 101 | 5 | 5 |
| 6 | 110 | 6 | 6 |
| 7 | 111 | 7 | 7 |
| 8 | 1000 | 10 | 8 |
| 9 | 1001 | 11 | 9 |
| 10 | 1010 | 12 | a 或 A |
| 11 | 1011 | 13 | b 或 B |
| 12 | 1100 | 14 | c 或 C |
| 13 | 1101 | 15 | d 或 D |
| 14 | 1110 | 16 | e 或 E |
| 15 | 1111 | 17 | f 或 F |
| 16 | 10000 | 20 | 10 |
| ... | ... | ... | ... |
* 二进制和十六进制的关系:十六进制是以 16 为基数的进制系统16 在二进制中表示为 ( 2^4 ),即:一个十六进制可以表示 4 位二进制。
> [!NOTE]
>
> 十六进制的范围是0 ~ F 0 ~ 15对应的二进制数的范围是0000 ~ 1111 0 ~ 15
* 每个十六进制数都可以映射到一个唯一的 4 位二进制数,即:
| 十六进制 | 二进制 |
| -------- | ------ |
| 0 | 0000 |
| 1 | 0001 |
| 2 | 0010 |
| 3 | 0011 |
| 4 | 0100 |
| 5 | 0101 |
| 6 | 0110 |
| 7 | 0111 |
| 8 | 1000 |
| 9 | 1001 |
| A | 1010 |
| B | 1011 |
| C | 1100 |
| D | 1101 |
| E | 1110 |
| F | 1111 |
>[!NOTE]
>
>由此可见,每个十六进制数字确实由 4 位二进制数表示。
* 二进制和八进制的关系:八进制是以 8 为基数的进制系统8 在二进制中表示为 ( 2^3 );即:一个八进制位可以表示 3 个二进制位。
> [!NOTE]
>
> 八进制的范围是0 ~ 7 对应的二进制数的范围是000 ~ 111。
* 每个八进制数位都可以映射到一个唯一的 3 位二进制数,即:
| 八进制 | 二进制 |
| ------ | ------ |
| 0 | 000 |
| 1 | 001 |
| 2 | 010 |
| 3 | 011 |
| 4 | 100 |
| 5 | 101 |
| 6 | 110 |
| 7 | 111 |
> [!NOTE]
>
> 由此可见,每个八进制数字确实由 3 位二进制数表示。
## 2.2 C 语言中如何表示不同进制的整数?
* 规则如下:
* 在 C 语言中,如果是`二进制`(字面常量),则需要在二进制整数前加上 `0b``0B`
* 在 C 语言中,如果是`八进制`(字面常量),则需要在八进制整数前加上 `0`
* 在 C 语言中,如果是`十进制`(字面常量),正常数字表示即可。
* 在 C 语言中,如果是`十六进制`(字面常量),则需要在十六进制整数前加上 `0x``0X`
* 示例:
```c
#include <stdio.h>
int main() {
int num1 = 0b10100110; // 二进制
int num2 = 0717563; // 八进制
int num3 = 1000; // 十进制
int num4 = 0xaf72; // 十六进制
printf("num1 = %d\n", num1); // num1 = 166
printf("num2 = %d\n", num2); // num2 = 237427
printf("num3 = %d\n", num3); // num3 = 1000
printf("num4 = %d\n", num4); // num4 = 44914
return 0;
}
```
## 2.3 输出格式
* 在 C 语言中,可以使用不同的`格式占位符``输出`不同`进制`的整数,如下所示:
* `%d`:十进制整数。
* `%o` :八进制整数。
* `%x`:十六进制整数。
* `%#o` :显示前缀 `0` 的八进制整数。
* `%#x` :显示前缀 `0x` 的十六进制整数。
* `%#X` :显示前缀 `0X` 的十六进制整数。
> [!CAUTION]
>
> C 语言中没有输出二进制数的格式占位符!!!
* 示例:
```c
#include <stdio.h>
int main() {
int num = 100;
printf("%d 的十进制整数: %d\n", num, num); // 100 的十进制整数: 100
printf("%d 的八进制整数: %o\n", num, num); // 100 的八进制整数: 144
printf("%d 的十六进制整数: %x\n", num, num); // 100 的十六进制整数: 64
printf("%d 的八进制(前缀)整数: %#o\n", num, num); // 100 的八进制(前缀)整数: 0144
printf("%d 的十六进制(前缀)整数: %#x\n", num, num); // 100 的十六进制(前缀)整数: 0x64
printf("%d 的十六进制(前缀)整数: %#X\n", num, num); // 100 的十六进制(前缀)整数: 0X64
return 0;
}
```
# 第三章: 进制的运算规则
## 3.1 概述
* `十进制`的运算规则,如下所示:
*`十``一`(针对加法而言)。
*`一``十`(针对减法而言)。
* `二进制`的运算规则,如下所示:
*`二``一`(针对加法而言)。
*`一``二`(针对减法而言)。
* `八进制`的运算规则,如下所示:
*`八``一`(针对加法而言)。
*`一``八`(针对减法而言)。
* `十六进制`的运算规则,如下所示:
*`十六``一`(针对加法而言)。
*`一``十六`(针对减法而言)。
## 3.2 二进制的运算
* 二进制的加法:`1 + 0 = 1``1 + 1 = 10``11 + 10 = 101``111 + 111 = 1110`
![](./assets/3.svg)
* 二进制的减法:`1 - 0 = 1``10 - 1 = 1``101 - 11 = 10``1100 - 111 = 101`
![](./assets/4.svg)
## 3.3 八进制的运算
* 八进制的加法:`3 + 4 = 7``5 + 6 = 13``75 + 42 = 137``2427 + 567 = 3216`
![](./assets/5.svg)
* 八进制的减法:`6 - 4 = 2``52 - 27 = 33``307 - 141 = 146``7430 - 1451 = 5757`
![](./assets/6.svg)
## 3.4 十六进制的运算
* 十六进制的加法:`6 + 7 = D``18 + BA = D2``595 + 792 = D27``2F87 + F8A = 3F11`
![](./assets/7.svg)
* 十六进制的减法:`D - 3 = A``52 - 2F = 23``E07 - 141 = CC6``7CA0 - 1CB1 = 5FEF`
![](./assets/8.svg)
# 第四章:进制的转换
## 4.1 概述
* 不同进制的转换,如下所示:
![](./assets/9.png)
* 在计算机中,数据是从右往左的方式排列的;其中,最右边的是低位,最左边的是高位,即:
![](./assets/10.svg)
## 4.2 二进制和十进制的转换
### 4.2.1 二进制转换为十进制
* 规则:从最低位开始,将每个位上的数提取出来,乘以 2 的 (位数 - 1 )次方,然后求和。
> [!NOTE]
>
> * ① 在学术界,将这种计算规则,称为`位权相加法`。
> * ② `八进制转换为十进制`、`十六进制转换为十进制`和`二进制转换为十进制`的算法相同!!!
* 示例:十进制转十进制
![](./assets/11.svg)
* 示例:二进制转十进制
![](./assets/12.svg)
### 4.2.2 十进制转换二进制
* 规则:将该数不断除以 2 ,直到商为 0 为止,然后将每步得到的余数倒过来,就是对应的二进制。
> [!NOTE]
>
> * ① 在学术界,将这种计算规则,称为`短除法`或`连续除2取余法`。
> * ② 很好理解,只有不断地除以 2 ,就能保证最大的数字不超过 2 ,这不就是二进制(只能有 0 或 1
> * ③ `八进制转换为二进制`、`十六进制转换为二进制`和`十进制转换为二进制`的算法相同!!!
* 示例:十进制转十进制
![](./assets/13.svg)
* 示例:十进制转二进制
![](./assets/14.svg)
### 4.2.3 二进制转八进制
* 规则:从右向左,每 3 位二进制就是一个八进制,不足补 0分组转换法
* 示例011 101 001 -> 351
![](./assets/15.svg)
### 4.2.4 二进制转十六进制
* 规则:从右向左,每 4 位二进制就是一个十六进制,不足补 0分组转换法
* 示例1110 1001 -> 0xE9
![](./assets/16.svg)
# 第五章:原码、反码和补码
## 5.1 概述
* 机器数:一个数在计算机的存储形式是二进制,我们称这些二进制数为机器数。机器数可以是有符号的,用机器数的最高位来存放符号位,`0` 表示正数,`1` 表示负数。
> [!IMPORTANT]
>
> * ① 这里讨论的适用于`有符号位`的整数int 等。
> * ② 这里讨论的不适用于`无符号位`的整数unsinged int 等。
![](./assets/17.svg)
* 真值(数据位):因为机器数带有符号位,所以机器数的形式值不等于其真实表示的值(真值),以机器数 1000 0001 为例,其真正表示的值(首位是符号位)为 -1而形式值却是 129 ,因此将带有符号位的机器数的真正表示的值称为机器数的真值。
> [!IMPORTANT]
>
> * ① 这里讨论的适用于`有符号位`的整数int 等。
> * ② 这里讨论的不适用于`无符号位`的整数unsinged int 等。
![](./assets/18.svg)
## 5.2 原码
* 原码的表示与机器数真值表示的一样,即用第一位表示符号,其余位表示数值。
* 规则:
* 正数的`原码`是它本身对应的二进制数,符号位是 0 。
* 负数的`原码`是它本身绝对值对应的二进制数,但是符号位是 1 。
* `+1` 的原码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 |
| -------- | --------------------- |
| +1 | `0`000 0000 0000 0001 |
* `-1` 的原码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 |
| -------- | --------------------- |
| -1 | `1`000 0000 0000 0001 |
> [!IMPORTANT]
>
> * ① 按照原码的规则,会出现 `+0` 和 `-0` 的情况,即:`0`000 0000 0000 0001+0、`1`000 0000 0000 0001-0显然不符合实际情况。
>* ② 所以,计算机底层虽然存储和计算的都是二进数,但显然不是原码。
## 5.3 反码
* 规则:
* 正数的反码和它的原码相同。
* 负数的反码是在其原码的基础上,符号位不变,其余各位取反。
* `+1` 的反码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 | 反码16位二进制数 |
| -------- | --------------------- | --------------------- |
| +1 | `0`000 0000 0000 0001 | `0`000 0000 0000 0001 |
* `-1` 的反码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 | 反码16位二进制数 |
| -------- | --------------------- | --------------------- |
| -1 | `1`000 0000 0000 0001 | `1`111 1111 1111 1110 |
> [!IMPORTANT]
>
> * ① 按照反码的规则,如果是 `+0`,对应的原码是 `0`000 0000 0000 0000那么其反码还是 `0`000 0000 0000 0000如果是 `-0`,对应的原码是 `1`000 0000 0000 0000其反码是 `1`111 1111 1111 1111显然不符合实际情况。
>* ② 所以,计算机底层虽然存储和计算的都是二进数,但显然不是反码。
## 5.4 补码
* 规则:
* 正数的补码和它的原码相同。
* 负数的补码是在其反码的基础上 + 1 。
* `+1` 的补码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 | 反码16位二进制数 | 补码16位二进制数 |
| -------- | --------------------- | --------------------- | --------------------- |
| +1 | `0`000 0000 0000 0001 | `0`000 0000 0000 0001 | `0`000 0000 0000 0001 |
* `-1` 的补码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 | 反码16位二进制数 | 补码16位二进制数 |
| -------- | --------------------- | --------------------- | --------------------- |
| -1 | `1`000 0000 0000 0001 | `1`111 1111 1111 1110 | `1`111 1111 1111 1111 |
* 如果 `0` ,按照 `+0` 的情况进行处理,如下所示:
![](./assets/19.svg)
* 如果 `0` ,按照 `-0` 的情况进行处理,如下所示:
![](./assets/20.svg)
* `+1``-1``原码``补码`的转换过程,如下所示:
![](./assets/21.svg)
> [!IMPORTANT]
>
> * ① 补码表示法解决了`原码`和`反码`存在的`两种`零(`+0` 和 `-0`)的问题,即:在补码表示法中,只有`一个`零,即 `0000 0000`。
>* ②补码使得`加法运算``减法运算`可以统一处理,通过将减法运算`转换`为加法运算,可以简化硬件设计,提高了运算效率。
> * ③ 计算机底层`存储`和`计算`的都是`二进数的补码`。换言之,当`读取`整数的时候,需要采用`逆向`的转换,即:将补码转换为原码。正数的原码、反码、补码都是一样的,三码合一。负数的补码转换为原码的方法就是先减去 `1` ,得到反码,再按位取反,得到原码(符号位是不能借位的)。
## 5.5 总结
* ① 计算机底层`存储``计算`的都是`二进数的补码`。换言之,当`读取`整数的时候,需要采用`逆向`的转换,即:将补码转换为原码。
* ② 正数的原码、反码和补码都是一样的,三码合一。
* ③ 负数的反码是在其原码的基础上按位取反0 变 1 1 变 0 ),符号位不变;负数的补码是其反码 + 1 。
* ④ 0 的补码是 0 。
* ⑤ 负数的补码转换为原码的方法就是先减去 `1` ,得到反码,再按位取反,得到原码(符号位是不能借位的)。
# 第六章:计算机底层为什么使用补码?
## 6.1 概述
* `加法``减法`是计算机中最基本的运算,计算机时时刻刻都离不开它们,所以它们由硬件直接支持。为了提高加法和减法的运行效率,硬件电路必须设计得尽量简单。
* 对于有符号位的数字来说,内存需要区分符号位和数值位:对于人类来说,很容易识别(最高位是 0 还是 1但是对于计算机来说需要设计专门的电路这无疑增加了硬件的复杂性增加了计算时间。如果能将符号位和数值位等同起来让它们一起参与运算不再加以区分这样硬件电路就可以变得非常简单。
* 此外,加法和减法也可以合并为一种运算,即:加法运算。换言之,减去一个数就相当于加上这个数的相反数,如:`5 - 3` 相当于 `5 +-3``10 --9`相当于 `10 + 9`
* 如果能够实现上述的两个目标,那么只需要设计一种简单的、不用区分符号位和数值位的加法电路,就能同时实现加法运算和减法运算,而且非常高效。其实,这两个目标已经实现了,真正的计算机的硬件电路就是这样设计的。
* 但是,简化硬件电路是有代价的,这个代价就是`有符号数`在存储和读取的时候都要继续转换。这也是对于有符号数的运算来说,计算机底层为什么使用`补码`的原因所在。
## 6.2 补码到底是如何简化硬件电路的?
* 假设 6 和 18 都是 short 类型,现在我们要计算 `6 - 18` 的结果,根据运算规则,它等价于 `6 +-18`。如果按照采用`原码`来计算,那么运算过程是这样的,如下所示:
> [!NOTE]
>
> 直接使用原码表示整数,让符号位也参与运算,那么对于减法来说,结果显然是不正确的。
![](./assets/22.svg)
* 于是,人们开始继续探索,不断试错,终于设计出了`反码`,如下所示:
> [!NOTE]
>
> 直接使用反码表示整数,让符号位也参与运算,对于 6 +-18来说结果貌似正确。
![](./assets/23.svg)
* 如果我们将`被减数``减数`对调一下,即:计算 `18 - 6` 的结果,也就是 `18 +-6`的结果,继续采用`反码`来进行运算,如下所示:
> [!NOTE]
>
> * ① 6 - 186+-18如果采用`反码`计算结果是正确的但是18 - 618 +-6如果采用`反码`计算,结果相差 1 。
> * ② 可以推断:如果按照`反码`来计算,小数 - 大数,结果正确;而大数 - 小数,结果相差 1 。
![](./assets/24.svg)
* 对于这个相差的 `1` 必须进行纠正,但是又不能影响`小数-大数`的结果。于是,人们又绞尽脑汁设计出了`补码`,给`反码`打了一个`“补丁”`,终于把相差的 `1` 给纠正过来了。那么,`6 - 18` 按照`补码`的运算过程,如下所示:
![](./assets/25.svg)
* 那么,`18 - 6` 按照`补码`的运算过程,如下所示:
![](./assets/26.svg)
> [!IMPORTANT]
>
> 总结:采用`补码`的形式正好将相差的 `1`纠正过来,也没有影响到小数减大数,这个“补丁”非常巧妙。
>
> * ① 小数减去大数,结果为负,之前(负数从反码转换为补码需要 +1加上的 1 ,后来(负数从补码转换为反码要 -1还需要减去正好抵消掉所以不会受到影响。
> * ② 大数减去小数,结果为正,之前(负数从反码转换为补码需要 +1加上的 1 ,后来(正数的补码和反码相同,从补码转换为反码不用 -1就没有再减去不能抵消掉这就相当于给计算结果多加了一个 1。
>
> `补码`这种天才般的设计,一举达成了之前加法运算和减法运算提到的两个目标,简化了硬件电路。
## 6.3 问题抛出
* 在 C 语言中,对于`有符号位`的整数,是使用 `0` 作为正数,`1` 作为负数,来表示`符号位`,并使用`数据位`来表示的是数据的`真值`,如下所示:
```c
int a = 10;
int b = -10;
```
![](./assets/27.svg)
* 但是,对于`无符号位`的整数而言,是`没有`符号位和数据位,即:没有原码、反码、补码的概念。无符号位的整数的数值都是直接使用二进制来表示的(也可以理解为,对于无符号位的整数,计算机底层存储的就是其原码),如下所示:
```c
unsigned int a = 10;
unsigned int b = -10;
```
![](./assets/28.svg)
* 这就是导致了一个结果就是:如果我定义一个`有符号`的负数,却让其输出`无符号`,必然造成结果不对,如下所示:
```c
#include <stdio.h>
char *getBinary(int num) {
static char binaryString[33];
int i, j;
for (i = sizeof(num) * 8 - 1, j = 0; i >= 0; i--, j++) {
const int bit = (num >> i) & 1;
binaryString[j] = bit + '0';
}
binaryString[j] = '\0';
return binaryString;
}
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
int num = -10;
printf("b=%s\n", getBinary(num)); // b=11111111111111111111111111110110
printf("b=%d\n", num); // b=-10
printf("b=%u\n", num); // b=4294967286
return 0;
}
```
* 其实C 语言的底层逻辑很简单C 语言压根不关心你定义的是有符号数还是无符号数,它只关心内存(如果定义的是有符号数,那就按照有符号数的规则来存储;如果定义的是无符号数,那就按照无符号数的规则来存储)。换言之,有符号数可以按照无符号数的规则来输出,无符号数也可以按照有符号数的规则来输出,至于输出结果对不对,那是程序员的事情,和 C 语言没有任何关系。
> [!IMPORTANT]
>
> * ① 实际开发中,`printf` 函数中的常量、变量或表达式,需要和格式占位符一一对应;否则,将会出现数据错误的现象。
> * ② 正因为上述的原因很多现代化的编程语言Java 等,直接取消了无符号的概念。但是,很多数据库是使用 C 语言开发的MySQL 等,就提供了创建数据表的字段为无符号类型的功能,即:`UNSIGNED`(正整数) ,不要感觉困惑!!!
> * ③ 对于 `1000 0000 …… 0000 0000` 这个特殊的补码,无法按照上述的方法转换为原码,所以计算机直接规定这个补码对应的值就是 `-2³¹`,至于为什么,下节我们会详细分析。