This commit is contained in:
许大仙 2024-08-23 11:29:18 +08:00
parent b2da313b26
commit 8c9894d099
6 changed files with 163 additions and 49 deletions

View File

@ -1177,13 +1177,13 @@ int main() {
* 规则:
* 正数的`原码`是它本身对应的二进制数,符号位是 0 。
* 负数的`原码`是它本身绝对值对应的二进制数,但是符号位是 1 。
* +1 的原码,使用 `16` 位二进数来表示,就是:
* `+1` 的原码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 |
| -------- | --------------------- |
| +1 | `0`000 0000 0000 0001 |
* -1 的原码,使用 `16` 位二进数来表示,就是:
* `-1` 的原码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 |
| -------- | --------------------- |
@ -1203,13 +1203,13 @@ int main() {
* 正数的反码和它的原码相同。
* 负数的反码是在其原码的基础上,符号位不变,其余各位取反。
* +1 的反码,使用 `16` 位二进数来表示,就是:
* `+1` 的反码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 | 反码16位二进制数 |
| -------- | --------------------- | --------------------- |
| +1 | `0`000 0000 0000 0001 | `0`000 0000 0000 0001 |
* -1 的反码,使用 `16` 位二进数来表示,就是:
* `-1` 的反码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 | 反码16位二进制数 |
| -------- | --------------------- | --------------------- |
@ -1219,7 +1219,7 @@ int main() {
>
> 总结:
>
> * ① 按照反码的规则,如果是 `+0`,对应的原码是 `0`000 0000 0000 0000那么其反码还是 `0`000 0000 ;如果是 `-0`,对应的原码是 `1`000 0000 0000 0000其反码是 `1`111 1111 1111 1111显然不符合实际情况。
> * ① 按照反码的规则,如果是 `+0`,对应的原码是 `0`000 0000 0000 0000那么其反码还是 `0`000 0000 0000 0000;如果是 `-0`,对应的原码是 `1`000 0000 0000 0000其反码是 `1`111 1111 1111 1111显然不符合实际情况。
> * ② 所以,计算机底层虽然存储和计算的都是二进数,但显然不是反码。
### 3.5.4 补码
@ -1228,13 +1228,13 @@ int main() {
* 正数的补码和它的原码相同。
* 负数的补码是在其反码的基础上 + 1 。
* +1 的补码,使用 `16` 位二进数来表示,就是:
* `+1` 的补码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 | 反码16位二进制数 | 补码16位二进制数 |
| -------- | --------------------- | -------------------- | -------------------- |
| +1 | `0`000 0000 0000 0001 | 0000 0000 0000 0001 | 0000 0000 0000 0001 |
| 十进制数 | 原码16位二进制数 | 反码16位二进制数 | 补码16位二进制数 |
| -------- | --------------------- | --------------------- | --------------------- |
| +1 | `0`000 0000 0000 0001 | `0`000 0000 0000 0001 | `0`000 0000 0000 0001 |
* -1 的补码,使用 `16` 位二进数来表示,就是:
* `-1` 的补码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 | 反码16位二进制数 | 补码16位二进制数 |
| -------- | --------------------- | --------------------- | --------------------- |
@ -1258,7 +1258,7 @@ int main() {
>
> * ① 补码表示法解决了`原码`和`反码`存在的`两种`零(`+0` 和 `-0`)的问题,即:在补码表示法中,只有`一个`零,即 `0000 0000`
> * ②补码使得`加法运算`和`减法运算`可以统一处理,通过将减法运算`转换`为加法运算,可以简化硬件设计,提高了运算效率。
> * ③ 计算机底层`存储`和`计算`的都是`二进数的补码`。换言之,当`读取`整数的时候,需要采用`逆向`的转换,即:将补码转换为原码。正数的原码、反码、补码都是一样的,三码合一。负数的补码转换为原码的方法就是先减去 `1` ,得到反码,再按位取反,得到原码。
> * ③ 计算机底层`存储`和`计算`的都是`二进数的补码`。换言之,当`读取`整数的时候,需要采用`逆向`的转换,即:将补码转换为原码。正数的原码、反码、补码都是一样的,三码合一。负数的补码转换为原码的方法就是先减去 `1` ,得到反码,再按位取反,得到原码(符号位是不能借的)
### 3.5.5 总结
@ -1379,5 +1379,5 @@ int main() {
>
> * ① 实际开发中,`printf` 函数中的常量、变量或表达式,需要和格式占位符一一对应;否则,将会出现数据错误的现象。
> * ② 正因为上述的原因很多现代化的编程语言Java 等,直接取消了无符号的概念。但是,很多数据库是使用 C 语言开发的MySQL 等,就提供了创建数据表的字段为无符号类型的功能,即:`UNSIGNED`(正整数) ,不要感觉困惑!!!
> * ③ 对于 `1000 0000 …… 0000 0000` 这个特殊的补码,无法按照上述的方法转换为原码(假设补码是 1000 0000 0000 0000按照补码表示法它的反码就是 0111 1111 1111 1111它的原码就是 1000 0000 0000 0000会发现 1000 0000 0000 0000 的补码和原码是一致的换言之1000 0000 0000 0000 没有对应的常规原码表示,它本身就是补码表示,无法转换为原码),所以计算机直接规定这个补码对应的值就是 `-2³¹`,至于为什么,下节我们会详细分析。
> * ③ 对于 `1000 0000 …… 0000 0000` 这个特殊的补码,无法按照上述的方法转换为原码,所以计算机直接规定这个补码对应的值就是 `-2³¹`,至于为什么,下节我们会详细分析。

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 82 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 220 KiB

View File

@ -593,24 +593,130 @@ int main() {
}
```
### 1.2.9 数值溢出
## 1.3 数值溢出
* 所谓的数值溢出指的是:当超过一个数据类型能够存放的最大范围的时候,数值就会溢出。
* 如果达到了最大值,再进行加法计算,数据就会超过该类型能够表示的最大值,叫做上溢出。
* 如果这个数目前是最小值,再进行减法计算, 数据就会超过该类型的最小值, 叫做下溢出。
### 1.3.1 概述
* 在 C 语言中,`整数`的`数据类型`分为`无符号`和`有符号`的,其在底层表示和存储是不一样的,即:
* `无符号整数不使用最高位作为符号位`,所有的位都用于表示数值,如:对于一个 4 位无符号整数,二进制表示的范围是从 0000 到 1111 ,那么十进制表示的范围是从 0 到 15。
* `有符号整数使用最高位作为符号位`,这意味着它们可以表示正数和负数,通常使用补码来表示有符号整数。在补码表示法中:最高位为 0 表示正数、最高位为 1 表示负数对于一个4位有符号整数二进制表示的范围是从 00000 到 0111 71000 -8到 1111-1
* 在生活中,如果一个容器的容量是固定的,我们不停的向其中注入水,那么当容器中充满水之后,继续注入,就会溢出,如下所示:
![](./assets/0.jpg)
* 在程序中也是一样的各种整数类型在内存中占用的存储单元是不同的short 在内存中占用 2 个字节的存储单元int 在内存中占用 4 个字节的存储单元。这也就意味着,各种整数类型只能存储有限的数值,当数值过大或多小的时候,超出的部分就会被直接截掉,那么数值就不能正确的存储,我们就将这种现象就称为`溢出`overflow
* 如果达到了最大值,再进行加法计算,数据就会超过该类型能够表示的最大值,叫做`上溢出`。
* 如果这个数目前是最小值,再进行减法计算, 数据就会超过该类型的最小值, 叫做`下溢出`。
### 1.3.2 无符号数的取值范围
* 在 C 语言中,`无符号数`unsigned 类型)的取值范围(最大值和最小值)的计算是很容易的,即:将内存中的所有位,设置为 `0` 就是`最小值`,设置为 `1` 就是`最大值`。
> [!IMPORTANT]
>
> 在 C 语言中,无符号整数,最高位不是符号位,它是数值的一部分。
* 以 `unsigned char` 类型为例,它在内存中占用的存储单元是 1 个字节,即 8 位。如果所有位都设置为 `0` ,它的最小值就是 `0` ;如果所有位设置为 `1` ,它的最大值就是 `2⁸ - 1 = 255` ,如下所示:
![](./assets/无符号的取值范围1.drawio.svg)
* 那么,`unsigned char` 的最大值是如何计算出来的?最简单的方法就是这样的,如下所示:
```txt
1 × 2⁰ + 1 × 2¹ + 1 × 2² + 1 × 2³ + 1 × 2⁴ + 1 × 2⁵ + 1 × 2⁶ + 1 × 2⁷
= 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128
= 255
```
* 但是,这种计算方法虽然有效,但是非常麻烦,如果是 8 个字节的 long ,那么计算就非常麻烦了(可能要计算半天)。当然,我们也知道,这就是等比数列(高中知识),等比数列的公式,如下所示:
$S_n = a_1 \times \frac{1 - r^n}{1 - r}$
* 那么,结果就是:$S_8 = 1 \times \frac{1 - 2^8}{1 - 2} = \frac{1 - 256}{-1} = 255$
* 但是,貌似还是很复杂,我们可以换个思路,就是让 `1111 1111``+1` ,然后再 `-1`,这样一增一减正好抵消掉,并且不会影响最终的结果,如下所示:
```txt
1111 1111 + 1 - 1
= 10000 0000 - 1
= 2⁹⁻¹ - 1
= 2⁸ - 1
= 255
```
* 其对应的换算过程,如下所示:
![](./assets/无符号的取值范围2.drawio.svg)
> [!IMPORTANT]
>
> * ① 当内存中所有的位都是 1 的时候,这种“凑整”的技巧非常实用!!!
> * ② 按照上述的技巧,我们可以很容易得计算出:
> * `unsinged char`1 个字节) 的取值范围是:`[0, 2⁸ - 1]`。
> * `unsinged short`2 个字节)的取值范围是:`[0, 2¹⁶ - 1]`。
> * `unsinged int`4 个字节)的取值范围是:`[0, 2³² - 1]`。
> * `unsinged long`8 个字节)的取值范围是:`[0, 2⁶⁴ - 1]`。
### 1.3.3 有符号数的取值范围
* 在 C 语言中,`有符号数`signed 类型)在计算机底层是以`补码`的形式存储的(计算的时候,也是以补码的形式进行计算的,并且符号位参与计算);但是,在读取的时候,需要采用`逆向`的转换,即:将补码转换为原码。
> [!IMPORTANT]
>
> 在 C 语言中,有符号整数,最高位是符号位,用于表示正负数。
* 以 `char` 类型为例,它的取值范围,如下所示:
| 补码 | 反码 | 原码 | 值 |
| ------------- | --------- | --------- | -------- |
| 1111 1111 | 1111 1110 | 1000 0001 | -1 |
| 1111 1110 | 1111 1101 | 1000 0010 | -2 |
| 1111 1101 | 1111 1100 | 1000 0011 | -3 |
| ... | ... | ... | ... |
| 1000 0011 | 1000 0010 | 1111 1101 | -125 |
| 1000 0010 | 1000 0001 | 1111 1110 | -126 |
| 1000 0001 | 1000 0000 | 1111 1111 | -127 |
| **1000 0000** | **---** | **---** | **-128** |
| 0111 1111 | 0111 1111 | 0111 1111 | 127 |
| 0111 1110 | 0111 1110 | 0111 1110 | 126 |
| 0111 1101 | 0111 1101 | 0111 1101 | 125 |
| ... | ... | ... | ... |
| 0000 0010 | 0000 0010 | 0000 0010 | 2 |
| 0000 0001 | 0000 0001 | 0000 0001 | 1 |
| 0000 0000 | 0000 0000 | 0000 0000 | 0 |
* 从上面的列表中,我们可以得知,`char` 类型的取值范围是:`[-2⁸, 2⁸ - 1]`,即:`[-128, 127]`。
* 对于 `-128` 而言,它的补码是 `1000 0000`,是无法按照传统的补码表示法来计算原码的,因为在补码转换到反码的时候需要 `-1` ,而 `1000 0000 - 1`需要向高位借 `1` ,而最高位是符号位是不能借的,这就非常矛盾。
> [!IMPORTANT]
>
> 计算机规定,`1000 0000` 这个特殊的补码就表示 `-128`
* 但是,为什么偏偏是 `-128` ,而不是其它数字?是因为 `-128` 使得 `char` 类型的取值范围保持连贯,中间没有“空隙”。如果我们按照传统的方式来计算 `-128` 的补码,如下所示:
* ① 原码:在原码表示法中,-128 的数据位是 `1000 0000`,但是 char 的数据位只有 `7` 位,那么最高位 `1` 就变为了符号位,剩下的数据位就是 `000 0000`;所以,`-128` 的原码就是 `1000 0000`
* ② 反码:对数据位取反,-128 的反码就是:`1111 1111` 。
* ③ 补码:在反码的基础上 `+1`,得到 `1000 0000`,是因为符号位被覆盖了,补码最终依然是 `1000 0000`
> [!NOTE]
>
> * 在 C 语言中,无符号整数,最高位不是符号位,它是数值的一部分。
> * 在 C 语言中,有符号整数,最高位是符号位,用于表示正负数。
> `-128` 从原码转换到补码的过程中,符号位被 `1` 覆盖了两次,而负数的符号位本来就是 `1`,被 `1` 覆盖多少次也不会影响到数字的符号。
* 虽然从 `1000 0000` 这个补码推算不出 `-128`,但是从 `-128` 却能推算出 `1000 0000` 这个补码,即:有符号数在存储之前先要转换为补码。
> [!IMPORTANT]
>
> * ① 通过这种方式,`-128` 就成为了补码的最小值 `1000 0000`,而这个值不会与其他任何正数或负数的补码冲突。
> * 如果采用原码存储,那么将会出现 `+0``-0` 的情况,即:`0000 0000`、`1000 0000`,这样在取值范围就存在两个相同的值,多此一举。
> * 如果采用原码存储,最大值不变是 `127` ,但是最小值只能存储到 `-127` ,因为 `-128` 的原码是 `1000 0000``-0` 的原码冲突。
> * ② 这就是补码系统的强大之处,它能让整数的范围连贯,并且实现了加法和减法的统一处理。
> * ③ 按照上述的方法,我们可以很容易得计算出:
> * `char`1 个字节) 的取值范围是:`[-2⁸, 2⁸ - 1]`。
> * `short`2 个字节)的取值范围是:`[-2¹⁶, 2¹⁶ - 1]`。
> * `int`4 个字节)的取值范围是:`[-2³², 2³² - 1]`。
> * `long`8 个字节)的取值范围是:`[-2⁶⁴, 2⁶⁴ - 1]`。
### 1.3.4 数值溢出
* 对于无符号的数值溢出:
* 当数据到达最大值的时候,再加 1 就会回到无符号数的最小值。
* 当数据达到最小值的时候,再减 1 就会回到无符号数的最大值。
* 当数据到达最大值的时候,再 `+1` 就会回到无符号数的最小值。
* 当数据达到最小值的时候,再 `-1` 就会回到无符号数的最大值。
* 那么,无符号的上溢出,原理就是这样的:
@ -621,8 +727,8 @@ int main() {
![](./assets/4.png)
* 对于有符号的数值溢出:
* 当数据到达最大值的时候,再 1 就会回到有符号数的最小值。
* 当数据达到最小值的时候,再 1 就会回到有符号数的最大值。
* 当数据到达最大值的时候,再 `+1` 就会回到有符号数的最小值。
* 当数据达到最小值的时候,再 `-1` 就会回到有符号数的最大值。
* 那么,有符号的上溢出,原理就是这样的:
@ -676,9 +782,9 @@ int main() {
}
```
## 1.3 浮点类型
## 1.4 浮点类型
### 1.3.1 概述
### 1.4.1 概述
* 在生活中,我们除了使用`整数`18、25 之外,还会使用到`小数`3.1415926、6.18 等,`小数`在计算机中也被称为`浮点数`(和底层存储有关)。
* `整数`在计算机底层的存储被称为`定点存储`,如下所示:
@ -710,7 +816,7 @@ int main() {
> * 科学计数法形式5.12e2e 表示基数 10、5.12E-2E 表示基数 10
> * ③ 在实际开发中,对于浮点类型,建议使用 double 类型;如果范围不够,就使用 long double 类型。
### 1.3.2 格式占位符
### 1.4.2 格式占位符
* 对于 float 类型的格式占位符,是 `%f` ,默认会保留 `6` 位小数;可以指定小数位,如:`%.2f` 表示保留 `2` 位小数。
* 对于 double 类型的格式占位符,是 `%lf` ,默认会保留 `6` 位小数;可以指定小数位,如:`%.2lf` 表示保留 `2` 位小数。
@ -792,7 +898,7 @@ int main() {
}
```
### 1.3.3 字面量后缀
### 1.4.3 字面量后缀
* 浮点数字面量默认是 double 类型。
* 如果需要表示 float 类型的字面量,需要后面添加后缀 f 或 F。
@ -819,7 +925,7 @@ int main() {
}
```
### 1.3.4 类型占用的内存大小(存储空间)
### 1.4.4 类型占用的内存大小(存储空间)
* 可以通过 sizeof 运算符来获取 float、double 以及 long double 类型占用的内存大小(存储空间)。
@ -840,7 +946,7 @@ int main() {
}
```
### 1.3.5 类型的取值范围
### 1.4.5 类型的取值范围
* 可以通过 `#include <float.h>` 来获取类型的取值范围。
@ -862,9 +968,9 @@ int main() {
}
```
## 1.4 字符类型
## 1.5 字符类型
### 1.4.1 概述
### 1.5.1 概述
* 在生活中,我们会经常说:今天天气真 `好`,我的性别是 `女`,我今年 `10` 岁,像这类数据,在 C 语言中就可以用字符char来表示。
* 在 C 语言中,变量的`字符类型`可以表示`单`个字符,如:`'1'`、`'A'`、`'&'`。
@ -890,7 +996,7 @@ int main() {
| `\\` | 反斜杠 |
| ... | |
### 1.4.2 格式占位符
### 1.5.2 格式占位符
* 在 C 语言中,使用 `%c` 来表示 char 类型。
@ -917,7 +1023,7 @@ int main() {
}
```
### 1.4.3 类型占用的内存大小(存储空间)
### 1.5.3 类型占用的内存大小(存储空间)
* 可以通过 sizeof 运算符来获取 char 类型占用的内存大小(存储空间)。
@ -937,7 +1043,7 @@ int main() {
}
```
### 1.4.4 类型的取值范围
### 1.5.4 类型的取值范围
* 可以通过 `#include <limits.h>` 来获取类型的取值范围。
@ -958,7 +1064,7 @@ int main() {
}
```
### 1.4.5 字符类型的本质
### 1.5.5 字符类型的本质
* 在 C 语言中char 本质上就是一个整数,是 ASCII 码中对应的数字,占用的内存大小是 1 个字节(存储空间),所以 char 类型也可以进行数学运算。
@ -1001,13 +1107,13 @@ int main() {
}
```
## 1.5 布尔类型
## 1.6 布尔类型
### 1.5.1 概述
### 1.6.1 概述
* 布尔值用于表示 true、false两种状态通常用于逻辑运算和条件判断。
### 1.5.2 早期的布尔类型
### 1.6.2 早期的布尔类型
* 在 C 语言标准C89并没有为布尔值单独设置一个数据类型所以在判断真、假的时候使用 `0` 表示 `false`(假),`非 0` 表示 `true`(真)。
@ -1037,7 +1143,7 @@ int main() {
}
```
### 1.5.3 宏定义的布尔类型
### 1.6.3 宏定义的布尔类型
* 判断真假的时候,以 `0``false`(假)、`1` 为 `true`(真),并不直观;所以,我们可以借助 C 语言的宏定义。
@ -1071,7 +1177,7 @@ int main() {
}
```
### 1.5.4 C99 标准中的布尔类型
### 1.6.4 C99 标准中的布尔类型
* 在 C99 中提供了 `_Bool` 关键字,用于表示布尔类型;其实,`_Bool`类型的值是整数类型的别名,和一般整型不同的是,`_Bool`类型的值只能赋值为 `0``1` 0 表示假、1 表示真),其它`非 0` 的值都会被存储为 `1`
@ -1104,7 +1210,7 @@ int main() {
}
```
### 1.5.5 C99 标准头文件中的布尔类型(推荐)
### 1.6.5 C99 标准头文件中的布尔类型(推荐)
* 在 C99 中提供了一个头文件 `<stdbool.h>`,定义了 `bool` 代表 `_Bool``false` 代表 `0` `true` 代表 `1`
@ -1152,15 +1258,15 @@ int main() {
}
```
## 1.6 数据类型转换
## 1.7 数据类型转换
### 1.6.1 概述
### 1.7.1 概述
* 在 C 语言编程中,经常需要对不同类型的数据进行运算,运算前需要先转换为同一类型,再运算。为了解决数据类型不一致的问题,需要对数据的类型进行转换。
### 1.6.2 自动类型转换(隐式转换)
### 1.7.2 自动类型转换(隐式转换)
#### 1.6.2.1 运算过程中的自动类型转换
#### 1.7.2.1 运算过程中的自动类型转换
* 不同类型的数据进行混合运算的时候,会发生数据类型转换,`窄类型会自动转换为宽类型`,这样就不会造成精度损失。
@ -1273,7 +1379,7 @@ int main() {
}
```
#### 1.6.2.2 赋值时的自动类型转换
#### 1.7.2.2 赋值时的自动类型转换
* 在赋值运算中,赋值号两边量的数据类型不同时,等号右边的类型将转换为左边的类型。
* 如果窄类型赋值给宽类型,不会造成精度损失;如果宽类型赋值给窄类型,会造成精度损失。
@ -1342,7 +1448,7 @@ int main(){
}
```
## 1.7 再谈数据类型
## 1.8 再谈数据类型
* 通过之前的知识我们知道CPU 是直接和内存打交道的CPU 在处理数据的时候会将数据临时存放到内存中。内存那么大CPU 是怎么找到对应的数据的?