2024年10月22日 11:01

This commit is contained in:
许大仙 2024-10-22 03:01:00 +00:00
parent 0296bb9f73
commit ad0f2c49ba
63 changed files with 503 additions and 2382 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 819 KiB

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 778 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 794 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 486 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 426 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 495 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 363 KiB

After

Width:  |  Height:  |  Size: 451 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 487 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 74 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 489 KiB

After

Width:  |  Height:  |  Size: 75 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 287 KiB

After

Width:  |  Height:  |  Size: 88 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 287 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 118 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 444 KiB

After

Width:  |  Height:  |  Size: 172 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 156 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 461 KiB

After

Width:  |  Height:  |  Size: 188 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 211 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 253 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 317 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 438 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 253 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 82 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 398 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 507 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 534 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 443 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 400 KiB

View File

@ -1,448 +1,164 @@
# 第一章:数据类型(⭐)
# 第一章:前言
## 1.1 概述 ## 1.1 概述
* 根据`变量`中`存储`的`值`的`不同`,我们可以将`变量`分为两类: * 计算机的底层只有`二进制`,即计算机中`运算`和`存储`的`所有数据`都需要转换为`二进制`,包括:数字、字符、图片、视频等。
* `普通变量`:变量所对应的内存中存储的是`普通值`。
* `指针变量`:变量所对应的内存中存储的是`另一个变量的地址`。
* 如下图所示: ![](./assets/17.jpg)
![](./assets/1.png) ## 1.2 冯·诺依曼体系结构
* 之前,我们也提到现代的计算机(量子计算机除外)几乎都遵循`冯·诺依曼`体系结构,其理论要点如下:
* ① **存储程序**`程序指令`和`数据`都存储在计算机的内存中,这使得程序可以在运行时修改。
* ② **二进制逻辑**:所有数据和指令都以`二进制`形式表示。
* ③ **顺序执行**:指令按照它们在内存中的顺序执行,但可以有条件地改变执行顺序。
* ④ **五大部件**:计算机由`运算器`、`控制器`、`存储器`、`输入设备`和`输出设备`组成。
* ⑤ **指令结构**:指令由操作码和地址码组成,操作码指示要执行的操作,地址码指示操作数的位置。
* ⑥ **中心化控制**计算机的控制单元CPU负责解释和执行指令控制数据流。
* 所以,再次论证了为什么计算机只能识别二进制。
## 3.2 进制
### 3.2.1 常见的进制
* 在生活中,我们最为常用的进制就是`十进制`,其规则是`满 10 进 1` ,即:
![](./assets/18.jpeg)
* 在计算机中,常见的进制有`二进制`、`八进制`和`十六进制`,即:
* 二进制:只能 0 和 1 ,满 2 进 1 。
* 八进制0 ~ 7 ,满 8 进 1 。
* 十六进制0 ~ 9 以及 A ~ F ,满 16 进 1 。
> [!NOTE] > [!NOTE]
> >
> 普通变量和指针变量的相同点: > 在十六进制中,除了 `0``9` 这十个数字之外,还引入了字母,以便表示超过 `9` 的值。其中,字母 `A` 对应十进制的 `10` ,字母 `B` 对应十进制的 `11` ,字母 `C` 对应十进制的 `12`,字母 `D` 对应十进制的 `13`,字母 `E` 对应十进制的 `14`,字母 `F` 对应十进制的 `15`
>
> * 普通变量有内存空间,指针变量也有内存空间。
> * 普通变量有内存地址,指针变量也有内存地址。
> * 普通变量所对应的内存空间中有值,指针变量所对应的内存空间中也有值。
>
> 普通变量和指针变量的不同点:
>
> * 普通变量所对应的内存空间存储的是普通的值,如:整数、小数、字符等;指针变量所对应的内存空间存储的是另外一个变量的地址。
> * 普通变量有普通变量的运算方式,而指针变量有指针变量的运算方式(后续讲解)。
* 那么,在 C 语言中变量的数据类型就可以这么划分,如下所示: * 进制的换算举例,如下所示:
![](./assets/2.png) | 十进制 | 二进制 | 八进制 | 十六进制 |
| ------ | ------ | ------ | -------- |
| 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] > [!NOTE]
> >
> * 根据`普通变量`中`存储`的`值`的类型不同,可以将`普通变量类型`划分为`基本数据类型`(整型、字符类型、浮点类型、布尔类型)和`复合数据类型`(数组类型、结构体类型、共用体类型、枚举类型)。 > 十六进制的范围是0 ~ F 0 ~ 15对应的二进制数的范围是0000 ~ 1111 0 ~ 15
> * 根据`指针变量`所`指向空间`中`存储`的`值`的类型不同,可以将`指针类型`分为`基本数据类型指针`、`复合数据类型指针`、`函数指针`、`数组指针`等,例如:如果指针所指向的空间保存的是 int 类型,那么该指针就是 int 类型的指针。
## 1.2 整数类型 * 每个十六进制数都可以映射到一个唯一的 4 位二进制数,即:
### 1.2.1 概述 | 十六进制 | 二进制 |
| -------- | ------ |
* 整数类型简称整型用于存储整数值12、20、50 等。 | 0 | 0000 |
* 根据所占`内存空间`大小的不同,可以将整数类型划分为: | 1 | 0001 |
* ① 短整型: | 2 | 0010 |
| 3 | 0011 |
| 类型 | 存储空间(内存空间) | 取值范围 | | 4 | 0100 |
| ------------------------------------ | -------------------- | ----------------------------------- | | 5 | 0101 |
| unsigned short (无符号短整型) | 2 字节 | 0 ~ 65,535 (2^16 - 1) | | 6 | 0110 |
| [signed] short有符号短整型默认 | 2 字节 | -32,768 (- 2^15) ~ 32,767 (2^15 -1) | | 7 | 0111 |
| 8 | 1000 |
* ② 整型: | 9 | 1001 |
| A | 1010 |
| 类型 | 存储空间(内存空间) | 取值范围 | | B | 1011 |
| -------------------------------- | -------------------- | ------------------------------------------- | | C | 1100 |
| unsigned int无符号整型 | 4 字节(通常) | 0 ~ 4294967295 (0 ~2^32 -1) | | D | 1101 |
| [signed] int有符号整型默认 | 4 字节(通常) | -2147483648- 2^31 ~ 2147483647 (2^31-1) | | E | 1110 |
| F | 1111 |
* ③ 长整型:
| 类型 | 存储空间(内存空间) | 取值范围 |
| ----------------------------------- | -------------------- | --------------- |
| unsigned long无符号长整型 | 4 字节(通常) | 0 ~2^32 -1 |
| [signed] long有符号长整型默认 | 4 字节(通常) | - 2^31 ~ 2^31-1 |
* ④ 长长整型:
| 类型 | 存储空间(内存空间) | 取值范围 |
| ---------------------------------------- | -------------------- | --------------- |
| unsigned long long无符号长整型 | 8 字节(通常) | 0 ~2^64 -1 |
| [signed] long long有符号长整型默认 | 8 字节(通常) | - 2^63 ~ 2^63-1 |
>[!NOTE] >[!NOTE]
> >
> * ① 数据类型在内存中占用的存储单元字节数就称为该数据类型的长度步长short 占用 2 个字节的内存,就称 short 的长度(步长)是 2。 >由此可见,每个十六进制数字确实由 4 位二进制数表示。
>
> * ② C 语言并没有严格规定各种整数数据类型在内存中所占存储单元的长度,只做了宽泛的限制:
>
> * short 至少占用 2 个字节的存储单元。
> * int 建议为一个机器字长(指计算机的处理器在一次操作中能够处理的二进制数据的位数,机器字长是处理器的“字”长度,它决定了处理器在一个时钟周期内能够处理的数据量,如:早期的计算机的处理器通常是 8 位的机器字长,意味着处理器一次只能处理 8 位(二进制)数据;之后的计算机的处理器有 16 位的机器字长,意味着处理器一次可以处理 16 位的数据;再之后计算机的处理器有 32 位或 64 位的机器字长,意味着处理器一次可以处理 32 位或 64位的数据。即32 位环境下 int 占用 4 个字节的存储单元64 位环境下 int 占用 8 个字节的存储单元。
> * short 的长度(步长)不能大于 intlong 的长度(步长)不能小于 intlong long 不能小于 long。
>
> * ③ 那么,各种整数数据类型在内存中所占存储单元的长度的公式就是 `2 ≤ sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long)`,具体的存储空间由编译系统自行决定。其中,`sizeof` 是测量类型或变量、常量长度的`运算符`。
> [!IMPORTANT] * 二进制和八进制的关系:八进制是以 8 为基数的进制系统8 在二进制中表示为 ( 2^3 );即:一个八进制位可以表示 3 个二进制位。
>
> * ① 之所以这么规定,是为了可以让 C 语言长久使用,因为目前主流的 CPU 都是 64 位,但是在 C 语言刚刚出现的时候CPU 还是以 8 位和 16 位为主。如果当时就将整型定死为 8 位或 16 位,那么现在我们肯定不会再学习 C 语言了。
> * ② 整型分为有符号 signed 和无符号 unsigned 两种,默认是 signed。
> * ③ 在实际开发中,`最常用的整数类型`就是 `int` 类型了,如果取值范围不够,就使用 long 或 long long 。
> * ④ C 语言中的`格式占位符`非常多,只需要大致了解即可;因为,我们在实际开发中,一般都会使用 C++ 或 Rust 以及其它的高级编程语言Java 等,早已经解决了必须通过`格式占位符`来才能将变量进行输入和输出。
### 1.2.2 短整型(了解)
* 语法:
```c
unsigned short x = 10 ; // 无符号短整型
```
```c
short x = -10; // 有符号短整型
```
> [!NOTE] > [!NOTE]
> >
> * ① 有符号表示的是正数、负数和 0 ,即有正负号。无符号表示的是 0 和正数,即正整数,没有符号。 > 八进制的范围是0 ~ 7 对应的二进制数的范围是000 ~ 111。
> * ② 在 `printf` 中`无符号短整型unsigned short`的`格式占位符`是 `%hu` `有符号短整型signed short`的`格式占位符`是 `%hd`
> * ③ 可以通过 `sizeof` 运算符获取`无符号短整型unsigned short` 和 `有符号短整型signed short` 的`存储空间(所占内存空间)`。
> * ③ 可以通过 `#include <limits.h>` 来获取 `无符号短整型unsigned short` 和`有符号短整型signed short`的`取值范围`。
* 每个八进制数位都可以映射到一个唯一的 3 位二进制数,即:
| 八进制 | 二进制 |
* 示例:定义和打印短整型变量 | ------ | ------ |
| 0 | 000 |
```c | 1 | 001 |
#include <stdio.h> | 2 | 010 |
| 3 | 011 |
int main() { | 4 | 100 |
| 5 | 101 |
// 定义有符号 short 类型 | 6 | 110 |
signed short s1 = -100; | 7 | 111 |
printf("s1 = %hd \n", s1); // s1 = -100
// 定义无符号 short 类型
unsigned short s2 = 100;
printf("s2 = %hu \n", s2); // s2 = 100
// 定义 short 类型,默认是有符号
short s3 = -200;
printf("s3 = %hd \n", s3); // s3 = -200
return 0;
}
```
* 示例:获取类型占用的内存大小(存储空间)
```c
#include <stdio.h>
int main() {
size_t s1 = sizeof(unsigned short);
printf("unsigned short 的存储空间是 %zu 字节 \n", s1); // 2
size_t s2 = sizeof(signed short);
printf("signed short 的存储空间是 %zu 字节 \n", s2); // 2
size_t s3 = sizeof(short);
printf("short 的存储空间是 %zu 字节 \n", s3); // 2
return 0;
}
```
* 示例:获取类型的取值范围
```c
#include <limits.h>
#include <stdio.h>
int main() {
printf("unsigned short 类型的范围是[0,%hu]\n", USHRT_MAX); // [0,65535]
printf("short 类型的范围是[%hd,%hd]\n", SHRT_MIN,SHRT_MAX); // [-32768,32767]
return 0;
}
```
### 1.2.3 整型
* 语法:
```c
unsigned int x = 10 ; // 无符号整型
```
```c
int x = -10; // 有符号整型
```
> [!NOTE] > [!NOTE]
> >
> * ① 有符号表示的是正数、负数和 0 ,即有正负号。无符号表示的是 0 和正数,即正整数,没有符号。 > 由此可见,每个八进制数字确实由 3 位二进制数表示。
> * ② 在 `printf` 中`无符号整型unsigned int`的`格式占位符`是 `%u` `有符号整型signed int`的`格式占位符`是 `%d`
> * ③ 可以通过 `sizeof` 运算符获取`无符号整型unsigned int` 和 `有符号整型signed int` 的`存储空间(所占内存空间)`。 ### 3.2.2 C 语言中如何表示不同进制的整数?
> * ③ 可以通过 `#include <limits.h>` 来获取 `无符号整型unsigned int` 和`有符号整型signed int`的`取值范围`。
* 规则如下:
* 在 C 语言中,如果是`二进制`(字面常量),则需要在二进制整数前加上 `0b``0B`
* 在 C 语言中,如果是`八进制`(字面常量),则需要在八进制整数前加上 `0`
* 在 C 语言中,如果是`十进制`(字面常量),正常数字表示即可。
* 在 C 语言中,如果是`十六进制`(字面常量),则需要在十六进制整数前加上 `0x`或`0X` 。
* 示例:定义和打印整型变量 * 示例:
```c ```c
#include <stdio.h> #include <stdio.h>
int main() { int main() {
// 定义有符号 int 类型 int num1 = 0b10100110; // 二进制
signed int i1 = -100; int num2 = 0717563; // 八进制
int num3 = 1000; // 十进制
int num4 = 0xaf72; // 十六进制
printf("i1 = %d \n", i1); // i1 = -100 printf("num1 = %d\n", num1); // num1 = 166
printf("num2 = %d\n", num2); // num2 = 237427
// 定义无符号 int 类型 printf("num3 = %d\n", num3); // num3 = 1000
unsigned int i2 = 100; printf("num4 = %d\n", num4); // num4 = 44914
printf("i2 = %u \n", i2); // i2 = 100
// 定义 int 类型,默认是有符号
short i3 = -200;
printf("i3 = %d \n", i3); // i3 = -200
return 0; return 0;
} }
``` ```
### 3.2.3 输出格式
* 在 C 语言中,可以使用不同的`格式占位符`来`输出`不同`进制`的整数,如下所示:
* `%d`:十进制整数。
* `%o` :八进制整数。
* `%x`:十六进制整数。
* `%#o` :显示前缀 `0` 的八进制整数。
* `%#x` :显示前缀 `0x` 的十六进制整数。
* `%#X` :显示前缀 `0X` 的十六进制整数。
* 示例:获取类型占用的内存大小(存储空间) > [!CAUTION]
```c
#include <stdio.h>
int main() {
size_t i1 = sizeof(unsigned int);
printf("unsigned int 的存储空间是 %zu 字节 \n", i1); // 4
size_t i2 = sizeof(signed int);
printf("signed int 的存储空间是 %zu 字节 \n", i2); // 4
size_t i3 = sizeof(int);
printf("int 的存储空间是 %zu 字节 \n", i3); // 4
return 0;
}
```
* 示例:获取类型的取值范围
```c
#include <limits.h>
#include <stdio.h>
int main() {
printf("unsigned int 类型的范围是[0,%u]\n", UINT_MAX); // [0,4294967295]
printf("int 类型的范围是[%d,%d]\n", INT_MIN,INT_MAX); // [-2147483648,2147483647]
return 0;
}
```
### 1.2.4 长整型(了解)
* 语法:
```c
unsigned long x = 10 ; // 无符号长整型
```
```c
long x = -10; // 有符号长整型
```
> [!NOTE]
> >
> * ① 有符号表示的是正数、负数和 0 ,即有正负号。无符号表示的是 0 和正数,即正整数,没有符号。 > C 语言中没有输出二进制数的格式占位符!!!
> * ② 在 `printf` 中`无符号长整型unsigned long`的`格式占位符`是 `%lu` `有符号长整型signed long`的`格式占位符`是 `%ld`
> * ③ 可以通过 `sizeof` 运算符获取`无符号长整型unsigned long` 和 `有符号长整型signed long` 的`存储空间(所占内存空间)`。
> * ③ 可以通过 `#include <limits.h>` 来获取 `无符号长整型unsigned long` 和`有符号长整型signed long`的`取值范围`。
* 示例:定义和打印长整型变量
```c
#include <stdio.h>
int main() {
// 定义有符号 long 类型
signed long l1 = -100;
printf("l1 = %ld \n", l1); // l1 = -100
// 定义无符号 long 类型
unsigned long l2 = 100;
printf("l2 = %lu \n", l2); // l2 = 100
// 定义 long 类型,默认是有符号
long l3 = -200;
printf("l3 = %ld \n", l3); // l3 = -200
return 0;
}
```
* 示例:获取类型占用的内存大小(存储空间)
```c
#include <stdio.h>
int main() {
size_t l1 = sizeof(unsigned long);
printf("unsigned long 的存储空间是 %zu 字节 \n", l1); // 4
size_t l2 = sizeof(signed long);
printf("signed long 的存储空间是 %zu 字节 \n", l2); // 4
size_t l3 = sizeof(long);
printf("long 的存储空间是 %zu 字节 \n", l3); // 4
return 0;
}
```
* 示例:获取类型的取值范围
```c
#include <limits.h>
#include <stdio.h>
int main() {
printf("unsigned long 类型的范围是[0,%lu]\n", ULONG_MAX); // [0,4294967295]
printf("long 类型的范围是[%ld,%ld]\n", LONG_MIN,LONG_MAX); // [-2147483648,2147483647]
return 0;
}
```
### 1.2.5 长长整型(了解)
* 语法:
```c
unsigned long long x = 10 ; // 无符号长长整型
```
```c
long long x = -10; // 有符号长长整型
```
> [!NOTE]
>
> * ① 有符号表示的是正数、负数和 0 ,即有正负号。无符号表示的是 0 和正数,即正整数,没有符号。
> * ② 在 `printf` 中`无符号长长整型unsigned long long`的`格式占位符`是 `%llu` `有符号长长整型signed long long`的`格式占位符`是 `%lld`
> * ③ 可以通过 `sizeof` 运算符获取`无符号长长整型unsigned long long` 和 `有符号长长整型signed long long` 的`存储空间(所占内存空间)`。
> * ③ 可以通过 `#include <limits.h>` 来获取 `无符号长长整型unsigned long long` 和`有符号长长整型signed long long`的`取值范围`。
* 示例:定义和打印长长整型变量
```c
#include <stdio.h>
int main() {
// 定义有符号 long long 类型
signed long long ll1 = -100;
printf("ll1 = %lld \n", ll1); // ll1 = -100
// 定义无符号 long long 类型
unsigned long long ll2 = 100;
printf("ll2 = %llu \n", ll2); // ll2 = 100
// 定义 long long 类型,默认是有符号
long long ll3 = -200;
printf("ll3 = %lld \n", ll3); // ll3 = -200
return 0;
}
```
* 示例:获取类型占用的内存大小(存储空间)
```c
#include <stdio.h>
int main() {
size_t ll1 = sizeof(unsigned long long);
printf("unsigned long long 的存储空间是 %zu 字节 \n", ll1); // 8
size_t ll2 = sizeof(signed long long);
printf("signed long long 的存储空间是 %zu 字节 \n", ll2); // 8
size_t ll3 = sizeof(long long);
printf("long long 的存储空间是 %zu 字节 \n", ll3); // 8
return 0;
}
```
* 示例:获取类型的取值范围
```c
#include <limits.h>
#include <stdio.h>
int main() {
printf("unsigned long long 类型的范围是[0,%llu]\n", ULLONG_MAX); // [0,18446744073709551615]
printf("long long 类型的范围是[%lld,%lld]\n", LLONG_MIN,LLONG_MAX); // [-9223372036854775808,9223372036854775807]
return 0;
}
```
### 1.2.6 字面量后缀
* `字面量`是`源代码`中一个`固定值`的`表示方法`,用于直接表示数据,即:
```c
int num1 = 100; // 100 就是字面量
```
```c
long num2 = 100L; // 100L 就是字面量
```
```c
long long num3 = 100LL; // 100LL 就是字面量
```
> [!NOTE]
>
> * ① 默认情况下的,整数字面量的类型是 int 类型。
> * ② 如果需要表示 `long` 类型的字面量,需要添加后缀 `l``L` ,建议 `L`
> * ③ 如果需要表示 `long long` 类型的字面量,需要添加后缀 `ll``LL`,建议 `LL`
> * ④ 如果需要表示`无符号`整数类型的字面量,需要添加 `u``U`,建议 `U`
@ -454,1733 +170,335 @@ long long num3 = 100LL; // 100LL 就是字面量
int main() { int main() {
int num = 100; int num = 100;
printf("num = %d\n", num); // num = 100
long num2 = 100L; printf("%d 的十进制整数: %d\n", num, num); // 100 的十进制整数: 100
printf("num2 = %ld\n", num2); // num2 = 100 printf("%d 的八进制整数: %o\n", num, num); // 100 的八进制整数: 144
printf("%d 的十六进制整数: %x\n", num, num); // 100 的十六进制整数: 64
long long num3 = 100LL; printf("%d 的八进制(前缀)整数: %#o\n", num, num); // 100 的八进制(前缀)整数: 0144
printf("num3 = %lld\n", num3); // num3 = 100 printf("%d 的十六进制(前缀)整数: %#x\n", num, num); // 100 的十六进制(前缀)整数: 0x64
printf("%d 的十六进制(前缀)整数: %#X\n", num, num); // 100 的十六进制(前缀)整数: 0X64
unsigned int num4 = 100U;
printf("num4 = %u\n", num4); // num4 = 100
unsigned long num5 = 100LU;
printf("num5 = %lu\n", num5); // num5 = 100
unsigned long long num6 = 100ULL;
printf("num6 = %llu\n", num6); // num6 = 100
return 0; return 0;
} }
``` ```
### 1.2.7 精确宽度类型 ## 3.3 进制的运算规则
* 在前文,我们了解到 C 语言的整数类型short 、int、long、long long在不同计算机上占用的字节宽度可能不一样。但是有的时候我们希望整数类型的存储空间字节宽度是精确的在任意平台计算机上都能一致以提高程序的可移植性。 ### 3.3.1 概述
> [!NOTE] * `十进制`的运算规则,如下所示:
> * 逢`十`进`一`(针对加法而言)。
> * Java 语言中的数据类型的存储空间(字节宽度)是一致的,这也是 Java 语言能够跨平台的原因之一(最主要的原因还是 JVM * 借`一`当`十`(针对减法而言)。
> * 在嵌入式开发中,使用精确宽度类型可以确保代码在各个平台上的一致性。 * `二进制`的运算规则,如下所示:
* 逢`二`进`一`(针对加法而言)。
* 在 C 语言的标准头文件 `<stdint.h>` 中定义了一些新的类型别名,如下所示: * 借`一`当`二`(针对减法而言)。
* `八进制`的运算规则,如下所示:
| 类型名称 | 含义 | * 逢`八`进`一`(针对加法而言)。
| -------- | --------------- | * 借`一`当`八`(针对减法而言)。
| int8_t | 8 位有符号整数 | * `十六进制`的运算规则,如下所示:
| int16_t | 16 位有符号整数 | * 逢`十六`进`一`(针对加法而言)。
| int32_t | 32 位有符号整数 | * 借`一`当`十六`(针对减法而言)。
| int64_t | 64 位有符号整数 |
| uint8_t | 8 位无符号整数 | ### 3.3.2 二进制的运算
| uint16_t | 16 位无符号整数 |
| uint32_t | 32 位无符号整数 | * 二进制的加法:`1 + 0 = 1` 、`1 + 1 = 10`、`11 + 10 = 101`、`111 + 111 = 1110`。
| uint64_t | 64 位无符号整数 |
> [!NOTE]
>
> 上面的这些类型都是类型别名,编译器会指定它们指向的底层类型,如:在某个系统中,如果 int 类型是 32 位,那么 int32_t 就会指向 int ;如果 long 类型是 32 位,那么 int32_t 就会指向 long。
* 示例:
```c
#include <stdio.h>
#include <stdint.h>
int main() {
// 变量 x32 声明为 int32_t 类型,可以保证是 32 位(4个字节)的宽度。
int32_t x32 = 45933945;
printf("x32 = %d \n", x32); // x32 = 45933945
return 0;
}
```
### 1.2.8 sizeof 运算符
* 语法:
```c
sizeof(表达式)
```
> [!NOTE]
>
> * ① sizeof 是运算符,不是内置函数。
>
> * ② 表达式可以是任何类型的数据类型、变量或常量。
> * ③ 用来获取某种数据类型、变量或常量占用的字节数量(内存中的存储单元),并且 `sizeof(...)` 的`返回值类型`是 `size_t` ;并且,如果是变量名称,可以省略 `()`;如果是数据类型,则不能省略 `()`
> * ④ 在 `printf` 中使用占位符 `%zu` 来处理 `size_t` 类型的值。
> * ⑤ 之前也提过C 语言没有一个统一的官方机构来制定或强制执行其标准而是由一个标准委员会负责制定标准。不同的编译器可以选择部分或完全遵循这些标准。因此C 语言的编译器实现可能会有所不同,这就要求程序员在编写跨平台代码时特别注意数据类型的大小和布局。
> * ⑥ 与 C 语言不同Java 和 JavaScript 等语言的标准是强制性的。在 Java 语言中,`int` 类型在所有平台上都是 4 个字节,无论是在 Linux、MacOS 还是 Windows 上。因此,这些语言不需要像 C 语言那样依赖 `sizeof` 来处理不同平台上的数据类型大小差异,因为编译器已经在底层处理了这些差异。换言之,`sizeof` 运算符在 C 语言中的重要性在于它为程序员提供了一个处理不同平台上数据类型大小差异的工具。当然,如果你在 C 语言中,使用精确宽度类型,如:`int8_t`、`int16_t`、`int32_t`、`uint8_t`、 `uint16_t`、`uint32_t` 等,也可以确保代码在各个平台上的一致性。
* 示例:参数是数据类型
```c
#include <stdio.h>
#include <stddef.h>
int main() {
size_t s = sizeof(int);
printf("%zu \n", s); // 4
return 0;
}
```
* 示例:参数是变量
```c
#include <stdio.h>
#include <stddef.h>
int main() {
int num = 10;
size_t s = sizeof(num);
printf("%zu \n", s); // 4
return 0;
}
```
* 示例:参数是常量
```c
#include <stdio.h>
#include <stddef.h>
int main() {
size_t s = sizeof(10);
printf("%zu \n", s); // 4
return 0;
}
```
## 1.3 数值溢出
### 1.3.1 概述
* 在生活中,如果一个容器的容量是固定的,我们不停的向其中注入水,那么当容器中充满水之后,再继续注入,水就会从杯子中溢出来,如下所示:
![](./assets/3.jpg)
* 在程序中也是一样的各种整数类型在内存中占用的存储单元是不同的short 在内存中占用 2 个字节的存储单元int 在内存中占用 4 个字节的存储单元。这也就意味着,各种整数类型只能存储有限的数值,当数值过大或多小的时候,超出的部分就会被直接截掉,那么数值就不能被正确的存储,我们就将这种现象就称为`溢出`overflow
> [!NOTE]
>
> * 如果这个数目前是`最大值`,再进行`加法`计算,数据就会超过该类型能够表示的最大值,叫做`上溢出`(如果最大值 + 1 会“绕回”到最小值)。
> * 如果这个数目前是`最小值`,再进行`减法`计算,数据就会超过该类型能够表示的最小值, 叫做`下溢出`(如果最小值 - 1 会“绕回”到最大值)。
>
> [!IMPORTANT]
>
> * ① 在 C 语言中程序产生数值溢出的时候并不会引发错误而使程序自动停止这是因为计算机底层是采用二进制补码的运算规则进行处理的很多编程语言也是这样处理的Java 等)。
> * ② 但是这可能会导致不可预料的后果1996 年的亚利安 5 号运载火箭爆炸、2004 年的 Comair 航空公司航班停飞事故。
> * ③ 在实际开发中,编程时要特别注意,以避免数值溢出问题,特别是在涉及大数或小数的运算(特指整数)。
### 1.3.2 无符号数的取值范围
* 在 C 语言中,`无符号数`unsigned 类型)的取值范围(最大值和最小值)的计算是很容易的,即:将内存中的所有位,设置为 `0` 就是`最小值`,设置为 `1` 就是`最大值`。
> [!IMPORTANT]
>
> 在 C 语言中,无符号整数,最高位不是符号位,它是数值的一部分。
* 以 `unsigned char` 类型为例,它在内存中占用的存储单元是 1 个字节,即 8 位。如果所有位都设置为 `0` ,它的最小值就是 `0` ;如果所有位设置为 `1` ,它的最大值就是 `2⁸ - 1 = 255` ,如下所示:
![](./assets/4.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/5.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]
>
> `-128` 从原码转换到补码的过程中,符号位被 `1` 覆盖了两次,而负数的符号位本来就是 `1`,被 `1` 覆盖多少次也不会影响到数字的符号。
* 虽然从 `1000 0000` 这个补码推算不出 `-128`,但是从 `-128` 却能推算出 `1000 0000` 这个补码,即:有符号数在存储之前先要转换为补码。
> [!IMPORTANT]
>
> * ① 通过这种方式,`-128` 就成为了补码的最小值 `1000 0000`,而这个值不会与其他任何正数或负数的补码冲突。
> * 如果采用`原码`存储,那么将会出现 `+0``-0` 的情况,即:`0000 0000`、`1000 0000`,这样在取值范围内,就存在两个相同的值,多此一举。
> * 如果采用`原码`存储,最大值不变是 `127` ,但是最小值只能存储到 `-127` ,不能存储到 `-128`,因为 `-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` 就会回到无符号数的最大值。
> [!IMPORTANT]
>
> * ① 对于无符号整数的运算,如:加、减、乘、除、取余等,其最小值是 0 ,最大值是 `2^n - 1` 。如果某个计算结果超出了这个范围,计算机会自动将结果对 `2^N` 取余(模),从而丢失高位,只保留低位。
> * ② 以 `8` 位无符号整数而言,最大值是 `255`1111 1111那么 `255 + 1` 的结果就是 `(2^8 -1 + 1) % 2^8 = 0`,商是 `256`
> * ③ 以 `8` 位无符号整数而言,最小值是 `0`0000 0000那么 `0 - 1` 的结果就是 `(0 - 1) % 2^8 = 255`,商是 `-1`
* 那么,`无符号`的`上溢出`,原理就是这样的:
![](./assets/6.svg)
* 那么,`无符号`的`下溢出`,原理就是这样的:
![](./assets/7.svg)
* 对于`有符号`的数值溢出:
* 当数据到达最大值的时候,再 `+1` 就会回到有符号数的最小值。
* 当数据达到最小值的时候,再 `-1` 就会回到有符号数的最大值。
* 那么,`有符号`的`上溢出`,原理就是这样的:
![](./assets/8.svg)
* 那么,`有符号`的`下溢出`,原理就是这样的:
![](./assets/9.svg)
* 示例:无符号的上溢出和下溢出
```c
#include <limits.h>
#include <stdio.h>
int main() {
unsigned short s1 = USHRT_MAX + 1;
printf("无符号的上溢出 = %hu \n", s1); // 0
unsigned short s2 = 0 - 1;
printf("无符号的下溢出 = %hu \n", s2); // 65535
return 0;
}
```
* 示例:有符号的上溢出和下溢出
```c
#include <limits.h>
#include <stdio.h>
int main() {
short s1 = SHRT_MAX + 1;
printf("有符号的上溢出 = %hd \n", s1); // -32768
short s2 = SHRT_MIN - 1;
printf("有符号的下溢出 = %hd \n", s2); // 32767
return 0;
}
```
## 1.4 浮点类型
### 1.4.1 概述
* 在生活中,我们除了使用`整数`18、25 之外,还会使用到`小数`3.1415926、6.18 等,`小数`在计算机中也被称为`浮点数`(和底层存储有关)。
* `整数`在计算机底层的存储被称为`定点存储`,如下所示:
![](./assets/10.svg)
* `小数`在计算机底层的存储被称为`浮点存储`,如下所示:
![](./assets/11.svg)
> [!NOTE]
>
> * ① 计算机底层就是采取类似科学计数法的形式来存储小数的而科学计数法的表现就是这样的3.12 * 10^-2 其中10 是基数,-2 是指数,而 3.12 是尾数。
> * ② 因为尾数区的内存空间的宽度不同,导致了小数的精度也不相同,所以小数在计算机中也称为浮点数。
* 在 C 语言中,变量的浮点类型,如下所示:
| 类型 | 存储大小 | 值的范围 | 有效小数位数 |
| ----------------------- | -------- | --------------------- | ------------ |
| float单精度 | 4 字节 | 1.2E-38 ~ 3.4E+38 | 6 ~ 9 |
| double双精度 | 8 字节 | 2.3E-308 ~ 1.7E+308 | 15 ~ 18 |
| long double长双精度 | 16 字节 | 3.4E-4932 ~ 1.2E+4932 | 18 或更多 |
> [!NOTE]
>
> * ① 各类型的存储大小和精度受到操作系统、编译器、硬件平台的影响。
> * ② 浮点型数据有两种表现形式:
> * 十进制数形式3.12、512.0f、0.512.512,可以省略 0
> * 科学计数法形式5.12e2e 表示基数 10、5.12E-2E 表示基数 10
> * ③ 在实际开发中,对于浮点类型,建议使用 `double` 类型;如果范围不够,就使用 `long double` 类型。
### 1.4.2 格式占位符
* 对于 `float` 类型的格式占位符,是 `%f` ,默认会保留 `6` 位小数,不足 `6` 位以 `0` 补充;可以指定小数位,如:`%.2f` 表示保留 `2` 位小数。
* 对于 `double` 类型的格式占位符,是 `%lf` ,默认会保留 `6` 位小数,不足 `6` 位以 `0` 补充;可以指定小数位,如:`%.2lf` 表示保留 `2` 位小数。
* 对于 `long double` 类型的格式占位符,是 `%Lf` ,默认会保留 `6` 位小数,不足 `6` 位以 `0` 补充;可以指定小数位,如:`%.2Lf` 表示保留 `2` 位小数。
> [!NOTE]
>
> * ① 如果想输出`科学计数法`形式的 `float` 类型的浮点数,则使用 `%e`
> * ② 如果想输出`科学计数法`形式的 `double` 类型的浮点数,则使用 `%le`
> * ③ 如果想输出`科学计数法`形式的 `long double` 类型的浮点数,则使用 `%Le`
> [!NOTE]
>
> * ① 浮点数还有一种更加智能的输出方式,就是使用 `%g``g` 是 `general format` 的缩写,即:通用格式),`%g` 会根据数值的大小自动判断,选择使用普通的浮点数格式(`%f`)进行输出,还是使用科学计数法(`%e`)进行输出,即:`float` 类型的两种输出形式。
> * ② 同理,`%lg` 会根据数值的大小自动判断,选择使用普通的浮点数格式(`%lf`)进行输出,还是使用科学计数法(`%le`)进行输出,即:`double` 类型的两种输出形式。
> * ③ 同理,`%Lg` 会根据数值的大小自动判断,选择使用普通的浮点数格式(`%Lf`)进行输出,还是使用科学计数法(`%Le`)进行输出,即:`long double` 类型的两种输出形式。
* 示例:
```c
#include <stdio.h>
int main() {
float f1 = 10.0;
printf("f1 = %f \n", f1); // f1 = 10.000000
printf("f1 = %.2f \n", f1); // f1 = 10.00
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
double d1 = 13.14159265354;
printf("d1 = %lf \n", d1); // d1 = 13.141593
printf("d1 = %.2lf \n", d1); // d1 = 13.14
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
long double d1 = 13.14159265354;
printf("d1 = %LF \n", d1); // d1 = 13.141593
printf("d1 = %.2LF \n", d1); // d1 = 13.14
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
float f1 = 3.1415926;
double d2 = 3.14e2;
printf("f1 = %.2f \n", f1); // f1 = 3.14
printf("f1 = %.2e \n", f1); // f1 = 3.14e+00
printf("d2 = %.2lf \n", d2); // d2 = 314.00
printf("d2 = %.2e \n", d2); // d2 = 3.14e+02
return 0;
}
```
### 1.4.3 字面量后缀
* 浮点数字面量默认是 double 类型。
* 如果需要表示 `float` 类型的字面量,需要后面添加后缀 `f``F`,建议 `F`
* 如果需要表示 `long double` 类型的字面量,需要后面添加后缀 `l``L`,建议 `L`
* 示例:
```c
#include <stdio.h>
int main() {
float f1 = 3.1415926f;
double d2 = 3.1415926;
long double d3 = 3.1415926L;
printf("f1 = %.2f \n", f1); // f1 = 3.14
printf("d2 = %.3lf \n", d2); // d2 = 3.142
printf("d3 = %.4Lf \n", d3); // d3 = 3.1416
return 0;
}
```
### 1.4.4 类型占用的内存大小(存储空间)
* 可以通过 `sizeof` 运算符来获取 float、double 以及 long double 类型占用的内存大小(存储空间)。
* 示例:
```c
#include <stdio.h>
int main() {
printf("float 的存储空间是 %zu 字节 \n", sizeof(float)); // 4
printf("double 的存储空间是 %zu 字节 \n", sizeof(double)); // 8
printf("long double 的存储空间是 %zu 字节 \n", sizeof(long double)); // 16
return 0;
}
```
### 1.4.5 类型的取值范围
* 可以通过 `#include <float.h>` 来获取类型的取值范围。
* 示例:
```c
#include <float.h>
#include <stdio.h>
int main() {
printf("float 的取值范围是:[%.38f, %f] \n", FLT_MIN, FLT_MAX);
printf("double 的取值范围是:[%lf, %lf] \n", DBL_MIN, DBL_MAX);
printf("double 的取值范围是:[%Lf, %Lf] \n", LDBL_MIN, LDBL_MAX);
return 0;
}
```
### 1.4.6 整数和浮点数的相互赋值
* 在 C 语言中,整数和浮点数是可以相互赋值的,即:
* 将一个整数赋值给小数类型,只需要在小数点后面加 0 就可以了。
* 将一个浮点数赋值给整数类型,就会将小数部分丢掉,只会取整数部分,会改变数字本身的值。
> [!WARNING]
>
> * ① 在 C 语言中浮点数赋值给整数类型会直接截断小数点后面的数编译器一般只会给出警告让我们注意一下C 语言在检查类型匹配方面不太严格,最好不要养成这样的习惯)。
> * ② 但是,在 Java 等编程语言中,这样的写法是不可以的,会在编译阶段直接报错。
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
float a = 123; // 整数赋值给浮点类型,只需要在小数点,后面加 0 即可
printf("a=%f \n", a); // a=123.000000
int b = 123.00; // 浮点赋值给整数类型,会直接截断小数点后面的数
printf("b=%d \n", b); // b=123
return 0;
}
```
## 1.5 字符类型
### 1.5.1 概述
* 在生活中,我们会经常说:今天天气真 `好`,我的性别是 `女`,我今年 `10` 岁等。像这类数据,在 C 语言中就可以用`字符类型`char来表示。`字符类型`表示`单`个字符,使用单引号(`''`)括起来,如:`'1'`、`'A'`、`'&'`。
* 但是,在生活中,也许会听到:`你是好人,只是现阶段,我想学习`、`好的啊,我们在一起`等。像这类数据,在 C 语言中就可以用`字符串`String来表示。`字符串类型`表示`多`个字符的集合,使用双引号(`""`)括起来,如:`"1"`、`"A"`、`"&"`、`"我们"`。
> [!NOTE]
>
> * ① C 语言的出现在 1972 年,由美国人丹尼斯·里奇设计出来;那个时候,只需要 1 个字节的内存空间就可以完美的表示拉丁体系英文文字a-z、A-Z、0-9 以及一些特殊符号所以C 语言中不支持多个字节的字符,如:中文、日文等。
> * ② 像拉丁体系英文文字a-z、A-Z、0-9 以及一些特殊符号,只需要单个字节的内存存储空间就能存储的,我们就称为窄类型;而像中文、日文等单个字节的内存空间存储不了的,我们就称为宽类型。
> * ③ 在 C 语言中是没有字符串类型是使用字符数组char 数组)来模拟字符串的。字符串中的字符在内存中按照次序、紧挨着排列,整个字符串占用一块连续的内存。
> * ④ 在 C 语言中如果想要输出中文、日文等多字节字符就需要使用字符数组char 数组)。
> * ⑤ 在 C++、Java 等高级编程语言中,已经提供了 String (字符串)类型,原生支持 Unicode可以方便地处理多语言和特殊字符。
* 在 C 语言中,可以使用`转义字符 \`来表示特殊含义的字符。
| **转义字符** | **说明** |
| ------------ | -------- |
| `\b` | 退格 |
| `\n` | 换行符 |
| `\r` | 回车符 |
| `\t` | 制表符 |
| `\"` | 双引号 |
| `\'` | 单引号 |
| `\\` | 反斜杠 |
| ... | |
### 1.5.2 格式占位符
* 在 C 语言中,使用 `%c` 来表示 char 类型。
* 示例:
```c
#include <stdio.h>
int main() {
char c = '&';
printf("c = %c \n", c); // c = &
char c2 = 'a';
printf("c2 = %c \n", c2); // c2 = a
char c3 = 'A';
printf("c3 = %c \n", c3); // c3 = A
return 0;
}
```
### 1.5.3 类型占用的内存大小(存储空间)
* 可以通过 `sizeof` 运算符来获取 char 类型占用的内存大小(存储空间)。
* 示例:
```c
#include <stdio.h>
int main() {
printf("char 的存储空间是 %d 字节\n", sizeof(char)); // 1
printf("unsigned char 的存储空间是 %d 字节\n", sizeof(unsigned char)); // 1
return 0;
}
```
### 1.5.4 类型的取值范围
* 可以通过 `#include <limits.h>` 来获取类型的取值范围。
* 示例:
```c
#include <limits.h>
#include <stdio.h>
int main() {
printf("char 范围是[%d,%d] \n", CHAR_MIN,CHAR_MAX); // [-128,127]
printf("unsigned char 范围是[0,%d]\n", UCHAR_MAX); // [0,255]
return 0;
}
```
### 1.5.5 字符类型的本质
* 在 C 语言中char 本质上就是一个整数,是 ASCII 码中对应的数字,占用的内存大小是 1 个字节(存储空间),所以 char 类型也可以进行数学运算。
![](./assets/12.png)
* char 类型同样分为 signed char无符号和 unsigned char有符号其中 signed char 取值范围 -128 ~ 127unsigned char 取值范围 0 ~ 255默认是否带符号取决于当前运行环境。
* `字符类型的数据`在计算机中`存储`和`读取`的过程,如下所示:
![](./assets/13.png)
* 示例:
```c
#include <limits.h>
#include <stdio.h>
int main() {
// char 类型字面量需要使用单引号包裹
char a1 = 'A';
char a2 = '9';
char a3 = '\t';
printf("c1=%c, c3=%c, c2=%c \n", a1, a3, a2);
// char 类型本质上整数可以进行运算
char b1 = 'b';
char b2 = 101;
printf("%c->%d \n", b1, b1);
printf("%c->%d \n", b2, b2);
printf("%c+%c=%d \n", b1, b2, b1 + b2);
// char 类型取值范围
unsigned char c1 = 200; // 无符号 char 取值范围 0 ~255
signed char c2 = 200; // 有符号 char 取值范围 -128~127c2会超出范围
char c3 = 200; // 当前系统char 默认是 signed char
printf("c1=%d, c2=%d, c3=%d", c1, c2, c3);
return 0;
}
```
### 1.5.6 输出字符方式二(了解)
* 在 C 语言中,除了可以使用 `printf()` 函数输出字符之外,还可以使用 `putchar()`函数输出字符。
> [!NOTE]
>
> * ① `putchar()` 函数每次只能输出一个字符,如果需要输出多个字符需要调用多次;而 `printf()` 函数一次可以输出多个字符,并且 `char` 类型对应的格式占位符是 `%c`
> * ② 在实际开发中,使用 `printf()` 函数居多。
* 示例:
```c
#include <stdio.h>
int main() {
char a = '1';
char b = '2';
char c = '&';
/* 12& */
putchar(a);
putchar(b);
putchar(c);
return 0;
}
```
### 1.5.7 初谈字符串(了解)
* 在 C 语言中没有专门的字符串类型,是使用`字符数组`来模拟字符串的,即:可以使用字符数组来存储字符串。
> [!NOTE]
>
> * ① 在 C 语言中,`数组`和`指针`通常会一起出现,所以当`字符数组`可以保存字符串,也就意味着可以使用`指针`来间接存储字符串。
> * ② 在 C 语言中,可以使用 `puts()` 函数输出字符串,每调用一次 `puts()` 函数,除了输出字符串之外,还会在字符串后面加上换行,即:`\n` 。
> * ③ 在 C 语言中,可以使用 `printf()` 函数输出字符串,并且字符串对应的格式占位符是 `%s`。和 `puts()` 函数不同的是,`printf()` 函数不会在字符串后面加上换行,即:`\n`。
> * ④ 在实际开发中,使用 `printf()` 函数居多。
* 示例:
```c
#include <stdio.h>
int main() {
// 存储字符串
char str[] = "我";
char *str2 = "爱你";
puts(str); // 我
puts(str2); // 爱你
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
// 存储字符串
char str[] = "你";
char *str2 = "是好人";
printf("%s\n", str); // 你
printf("%s\n", str2); // 是好人
return 0;
}
```
## 1.6 布尔类型
### 1.6.1 概述
* 布尔值用于表示 true、false两种状态通常用于逻辑运算和条件判断。
### 1.6.2 早期的布尔类型
* 在 C 语言标准C89并没有为布尔值单独设置一个数据类型所以在判断真、假的时候使用 `0` 表示 `false`(假),`非 0` 表示 `true`(真)。
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
// 使用整型来表示真和假两种状态
int handsome = 0;
printf("帅不帅[0 丑1 帅] ");
scanf("%d", &handsome);
if (handsome) {
printf("你真的很帅!!!");
} else {
printf("你真的很丑!!!");
}
return 0;
}
```
### 1.6.3 宏定义的布尔类型
* 判断真假的时候,以 `0``false`(假)、`1` 为 `true`(真),并不直观;所以,我们可以借助 C 语言的宏定义。
* 示例:
```c
#include <stdio.h>
// 宏定义
#define BOOL int
#define TRUE 1
#define FALSE 0
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
BOOL handsome = 0;
printf("帅不帅[FALSE 丑TRUE 帅] ");
scanf("%d", &handsome);
if (handsome) {
printf("你真的很帅!!!");
} else {
printf("你真的很丑!!!");
}
return 0;
}
```
### 1.6.4 C99 标准中的布尔类型
* 在 C99 中提供了 `_Bool` 关键字,用于表示布尔类型;其实,`_Bool`类型的值是整数类型的别名,和一般整型不同的是,`_Bool`类型的值只能赋值为 `0``1` 0 表示假、1 表示真),其它`非 0` 的值都会被存储为 `1`
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
int temp; // 使用 int 类型的变量临时存储输入
_Bool handsome = 0;
printf("帅不帅[0 丑1 帅] ");
scanf("%d", &temp);
// 将输入值转换为 _Bool 类型
handsome = (temp != 0);
if (handsome) {
printf("你真的很帅!!!");
} else {
printf("你真的很丑!!!");
}
return 0;
}
```
### 1.6.5 C99 标准头文件中的布尔类型(推荐)
* 在 C99 中提供了一个头文件 `<stdbool.h>`,定义了 `bool` 代表 `_Bool``false` 代表 `0` `true` 代表 `1`
> [!IMPORTANT]
>
> * ① 在 C++、Java 等高级编程语言中是有 `boolean` 类型的关键字的。
> * ② 在 C23 标准中,将一些 C11 存在的关键字改为小写并去掉前置下划线,如:`_Bool` 改为 `bool`,以前的写法主要是为了避免与旧的代码发生冲突。
> * ③ 在 C23 标准中,加入了 `true``false` 关键字。
* 示例:
```c
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
char input[10];
bool handsome = false;
printf("帅不帅[false 丑true 帅] ");
scanf("%s", input); // 使用 %s 读取字符串
// 将输入字符串转换为布尔值
if (strcmp(input, "true") == 0) {
handsome = true;
} else if (strcmp(input, "false") == 0) {
handsome = false;
} else {
printf("无效输入!\n");
return 1;
}
if (handsome) {
printf("你真的很帅!!!");
} else {
printf("你真的很丑!!!");
}
return 0;
}
```
## 1.7 数据类型转换
### 1.7.1 概述
* 在 C 语言编程中,经常需要对不同类型的数据进行运算,运算前需要先转换为同一类型,再运算。为了解决数据类型不一致的问题,需要对数据的类型进行转换。
### 1.7.2 自动类型转换(隐式转换)
#### 1.7.2.1 运算过程中的自动类型转换
* 不同类型的数据进行混合运算的时候,会发生数据类型转换,`窄类型会自动转换为宽类型`,这样就不会造成精度损失。
![](./assets/14.png)
* 转换规则:
* ① 不同类型的整数进行运算的时候,窄类型整数会自动转换为宽类型整数。
* ② 不同类型的浮点数进行运算的时候,精度小的类型会自动转换为精度大的类型。
* ③ 整数和浮点数进行运算的时候,整数会自动转换为浮点数。
* 转换方向:
![](./assets/15.png)
> [!WARNING]
>
> 最好避免无符号整数与有符号整数的混合运算,因为这时 C 语言会自动将 signed int 转为 unsigned int ,可能不会得到预期的结果。
* 示例:
```c
#include <stdio.h>
/**
* 不同的整数类型混合运算时,宽度较小的类型会提升为宽度较大的类型。
* 比如 short 转为 int int 转为 long 等。
*/
int main() {
short s1 = 10;
int i = 20;
// s1 是 short 类型i 是 int 类型。
// 当 s1 和 i 运算的时候,会自动转为 int 类型后,然后再计算。
int result = s1 + i;
printf("result = %d \n", result);
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
int n2 = -100;
unsigned int n3 = 20;
// n2 是有符号n3 是无符号。
// 当 n2 和 n3 运算的时候,会自动转为无符号类型后,然后再计算。
int result = n2 + n3;
printf("result = %d \n", result);
return 0;
}
```
* 示例:
```c
#include <stdio.h>
/**
* 不同的浮点数类型混合运算时,宽度较小的类型转为宽度较大的类型。
* 比如 float 转为 double double 转为 long double 。
*/
int main() {
float f1 = 1.25f;
double d2 = 4.58667435;
// f1 是 float 类型d2 是 double 类型。
// 当 f1 和 d2 运算的时候,会自动转为 double 类型后,然后再计算。
double result = f1 + d2;
printf("result = %.8lf \n", result);
return 0;
}
```
* 示例:
```c
#include <stdio.h>
/**
* 整型与浮点型运算,整型转为浮点型
*/
int main() {
int n4 = 10;
double d3 = 1.67;
// n4 是 int 类型d3 是 double 类型。
// 当 n4 和 d3 运算的时候,会自动转为 double 类型后,然后再计算。
double result = n4 + d3;
printf("%.2lf", result);
return 0;
}
```
#### 1.7.2.2 赋值时的自动类型转换
* 在赋值运算中,赋值号两边量的数据类型不同时,等号右边的类型将转换为左边的类型。
* 如果窄类型赋值给宽类型,不会造成精度损失;如果宽类型赋值给窄类型,会造成精度损失。
![](./assets/16.png)
> [!WARNING]
>
> C 语言在检查类型匹配方面不太严格,最好不要养成这样的习惯。
* 示例:
```c
#include <stdio.h>
int main() {
// 赋值:窄类型赋值给宽类型
int a1 = 10;
double a2 = a1;
printf("a2: %.2f\n", a2); // a2: 10.00
// 转换:将宽类型转换为窄类型
double b1 = 10.5;
int b2 = b1;
printf("b2: %d\n", b2); // b2: 10
return 0;
}
```
### 1.7.3 强制类型转换
* 隐式类型转换中的宽类型赋值给窄类型,编译器是会产生警告的,提示程序存在潜在的隐患,如果非常明确地希望转换数据类型,就需要用到强制(或显式)类型转换。
* 语法:
```c
数据类型 变量名 = (类型名)变量、常量或表达式;
```
> [!WARNING]
>
> 强制类型转换可能会导致精度损失!!!
* 示例:
```c
#include <stdio.h>
int main(){
double d1 = 1.934;
double d2 = 4.2;
int num1 = (int)d1 + (int)d2; // d1 转为 1d2 转为 4结果是 5
int num2 = (int)(d1 + d2); // d1+d2 = 6.1346.134 转为 6
int num3 = (int)(3.5 * 10 + 6 * 1.5); // 35.0 + 9.0 = 44.0 -> int = 44
printf("num1=%d \n", num1);
printf("num2=%d \n", num2);
printf("num3=%d \n", num3);
return 0;
}
```
### 1.7.4 数据类型转换只是临时性的
* 无论是自动类型转换还是强制类型转换,都是为了本次运算而进行的临时性转换,其转换的结果只会保存在临时的内存空间,并不会改变数据原先的类型或值,如下所示:
```c {8}
#include <stdio.h>
int main() {
double total = 100.12; // 总价
int count = 2; // 总数
double price = 0.0; // 单价
int totalInt = (int)total; // 强制类型转换
price = total / count; // 计算单价
printf("total = %.2lf\n", total); // total = 100.12
printf("totalInt = %d\n", totalInt); // totalInt = 100
printf("price = %.2lf\n", price); // price = 50.06
return 0;
}
```
* 虽然 `total` 变量,通过强制类型转换变为了 `int` 类型,才可以赋值给 `totalInt`变量;但是,这种转换并没有影响 `total` 变量本身的`类型`和`值`。
> [!NOTE]
>
> * ① 如果 `total` 变量的`值`或`类型`变化了,那么 `total` 的显示结果,就应该是 `100.00` ,而不是 `100.12`
> * ② 那么,`price` 的结果,显而易见就应该是 `50.00` ,而不是 `50.06` 了。
### 1.7.5 自动类型转换 VS 强制类型转换
* 在 C 语言中,有些数据类型即可以自动类型转换,也可以强制类型转换,如:`int --> double`、`double --> int` 等。但是,有些数据类型只能强制类型转换,不能自动类型转换,如:`void* --> int*` 。
* 可以自动类型转换的类型一定可以强制类型转换;但是,可以强制类型转换的类型却不一定能够自动类型转换。
> [!NOTE]
>
> * ① 目前学习到的数据类型,既可以自动类型转换,也可以强制类型转换。
> * ② 后面,如果学到指针,就会发生指针有的时候,只能强制类型转换却不能自动类型转换;需要说明的是,并非所有的指针都可以强制类型转换,是有条件的,后文讲解。
* 可以自动类型转换的类型,在发生类型转换的时候,一般风险较低,不会给程序带来严重的后果,如:`int --> double` 就没什么毛病,而 `double --> int` 无非丢失精度而已。但是 ,只能强制类型转换的类型,在发生类型转换的时候,通常风险较高,如:`char* --> int*` 就非常奇怪,会导致取得的值也很奇怪,进而导致程序崩溃。
> [!IMPORTANT]
>
> * ① 在实际开发中,如果使用 C 语言进行开发,在进行强制类型转换的时候,需要小心谨慎,防止出现一些奇怪的问题,进而导致程序崩溃!!!
> * ② 现代化的高级编程语言Java 等,直接屏蔽了指针。所以,在使用这些编程语言的时候,无需担心进行强制类型转换时,会出现一些奇怪的问题,进而导致程序崩溃!!!
## 1.8 再谈数据类型
* 通过之前的知识我们知道CPU 是直接和内存打交道的CPU 在处理数据的时候会将数据临时存放到内存中。内存那么大CPU 是怎么找到对应的数据的?
* 首先CPU 会将内存按照字节1 Bytes = 8 bit我们也称为存储单元进行划分如下所示
> [!NOTE]
>
> * ① 操作系统其实并不会直接操作实际的内存而是会通过内存管理单元MMU来操作内存并通过虚拟地址映射Virtual Address Mapping将程序使用的虚拟地址转换为物理地址。虚拟地址映射可以实现内存保护、内存共享和虚拟内存等功能使得程序能够使用比实际物理内存更大的内存空间同时确保程序间不会相互干扰。
> * ② 为了方便初学者学习,后文一律会描述 CPU 直接操作内存(这种说法不严谨,但足够简单和方便理解)。
> * ③ 这些存储单元中,存储的都是 0 和 1 这样的数据,因为计算机只能识别二进制数。
![](./assets/17.svg)
* 并且,为了方便管理,每个独立的小单元格,即:存储单元,都有自己唯一的编号(内存地址),如下所示:
> [!NOTE]
>
> 之所以,要给每个存储单元加上内存地址,就是为了`加快`数据的`存取速度`,可以类比生活中的`字典`以及`快递单号`。
![](./assets/18.svg)
* 我们在定义变量的时候,是这么定义的,如下所示:
```c
int num = 10;
```
> [!NOTE]
>
> 上述的代码其实透露了三个重要的信息:
>
> * ① 数据存储在哪里。
> * ② 数据的长度是多少。
> * ③ 数据的处理方式。
* 其实,在编译器对程序进行编译的时候,是这样做的,如下所示:
> [!NOTE]
>
> * ① 编译器在编译的时候,就将变量替换为内存中存储单元的内存地址(知道了你家的门牌号),这样就可以方便的进行存取数据了(解答了上述的问题 ① )。
> * ② 变量中其实存储的是初始化值 10 在内存中存储单元的首地址,我们也知道,数据类型 int 的存储空间是 4 个字节,那么根据首地址 + 4 个字节就可以完整的将数据从内存空间中取出来或存进去(解答了上述的问题 ② )。
> * ③ 我们知道,数据在计算机底层的存储方式是不一样的,如:整数在计算机底层的存储就是计算机补码的方式,浮点数在计算机底层的存储类似于科学计数法;但是,字符类型在计算机底层的存储和整数以及浮点数完全不同,需要查码表,即:在存储的时候,需要先查询码表,转换为二进制进行存储;在读取的时候,也需要先查询码表,将二进制转换为对应的字符(解答了上述的问题 ③ )。
> [!IMPORTANT]
>
> * ① 数据类型只在定义变量的时候声明,而且必须声明;在使用变量的时候,就无需再声明,因为此时的数据类型已经确定的。
> * ② 在实际开发中,我们通常将普通变量等价于内存中某个区域的值(底层到底是怎么转换的,那是编译器帮我们完成的,我们通常无需关心,也没必要关心)。
> * ③ 某些动态的编程语言JavaScript ,在定义变量的时候,是不需要给出数据类型的,编译器会根据赋值情况自动推断出变量的数据类型,貌似很智能;但是,这无疑增加了编译器的工作,降低了程序的性能(动态一时爽,重构火葬场,说的就是动态编程语言,不适合大型项目的开发;所以,之后微软推出了 TypeScript ,就是为了给 JavaScript 增加强类型系统,以提高开发和运行效率)。
* 程序中的变量在内存中的表示,就是这样的,如下所示:
![](./assets/19.svg) ![](./assets/19.svg)
* 二进制的减法:`1 - 0 = 1` 、`10 - 1 = 1`、`101 - 11 = 10`、`1100 - 111 = 101` 。
# 第二章:运算符(⭐)
## 2.1 概述
* 运算符是一种特殊的符号,用于数据的运算、赋值和比较等。
* `表达式`指的是一组运算数、运算符的组合,表达式`一定具有值`,一个变量或一个常量可以是表达式,变量、常量和运算符也可以组成表达式,如:
![](./assets/20.svg) ![](./assets/20.svg)
* `操作数`指的是`参与运算`的`值`或者`对象`,如: ### 3.3.3 八进制的运算
* 八进制的加法:`3 + 4 = 7` 、`5 + 6 = 13`、`75 + 42 = 137`、`2427 + 567 = 3216`。
![](./assets/21.svg) ![](./assets/21.svg)
* 根据`操作数`的`个数`,可以将运算符分为: * 八进制的减法:`6 - 4 = 2` 、`52 - 27 = 33`、`307 - 141 = 146`、`7430 - 1451 = 5757` 。
* 一元运算符(一目运算符)。
* 二元运算符(二目运算符)。
* 三元运算符(三目运算符)。
* 根据`功能`,可以将运算符分为:
* 算术运算符。
* 关系运算符(比较运算符)。
* 逻辑运算符。
* 赋值运算符。
* 逻辑运算符。
* 位运算符。
* 三元运算符。
![](./assets/22.svg)
### 3.3.4 十六进制的运算
* 十六进制的加法:`6 + 7 = D` 、`18 + BA = D2`、`595 + 792 = D27`、`2F87 + F8A = 3F11`。
![](./assets/23.svg)
* 十六进制的减法:`D - 3 = A` 、`52 - 2F = 23`、`E07 - 141 = CC6`、`7CA0 - 1CB1 = 5FEF` 。
![](./assets/24.svg)
## 3.4 进制的转换
### 3.4.1 概述
* 不同进制的转换,如下所示:
![](./assets/25.png)
* 在计算机中,数据是从右往左的方式排列的;其中,最右边的是低位,最左边的是高位,即:
![](./assets/26.svg)
### 3.4.2 二进制和十进制的转换
#### 3.4.2.1 二进制转换为十进制
* 规则:从最低位开始,将每个位上的数提取出来,乘以 2 的 (位数 - 1 )次方,然后求和。
> [!NOTE] > [!NOTE]
> >
> 掌握一个运算符,需要关注以下几个方面: > * ① 在学术界,将这种计算规则,称为`位权相加法`。
> > * ② `八进制转换为十进制`、`十六进制转换为十进制`和`二进制转换为十进制`的算法相同!!!
> * ① 运算符的含义。
> * ② 运算符操作数的个数。
> * ③ 运算符所组成的表达式。
> * ④ 运算符有无副作用,即:运算后是否会修改操作数的值。
> [!CAUTION]
>
> * ① 其实在后端语言C、C++、Java 等,表达式和语法并不会区分的很明显。
> * ② 但是,对于前端 JavaScript 框架中的 React 而言,其在 `JSX` 中要求 `{}`中必须是表达式,而不能是语句,如下所示:
>
> ```js {22,25,28}
> import React, { useState } from 'react';
>
> function Welcome() {
> const [isLoggedIn, setIsLoggedIn] = useState(false);
>
> // 切换登录状态的函数
> const toggleLogin = () => {
> setIsLoggedIn(!isLoggedIn);
> };
>
> // 在 JSX 外部处理逻辑
> let message;
> if (isLoggedIn) {
> message = "Welcome back!";
> } else {
> message = "Please sign in";
> }
>
> return (
> <div>
> {/* 使用 JSX 表达式来渲染内容 */}
> <h1>{message}</h1>
>
> {/* 使用三元运算符 */}
> <p>{isLoggedIn ? "You have new notifications." : "No notifications"}</p>
>
> {/* 使用逻辑运算符 && 渲染内容 */}
> {isLoggedIn && <p>You are logged in as a premium user.</p>}
>
> {/* 切换登录状态按钮 */}
> <button onClick={toggleLogin}>
> {isLoggedIn ? "Log out" : "Log in"}
> </button>
> </div>
> );
> }
>
> export default Welcome;
> ```
## 2.2 算术运算符
* 算术运算符是对数值类型的变量进行运算的,如下所示: * 示例:十进制转十进制
| 运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 | ![](./assets/27.svg)
| ------ | ------------ | ---------- | ------------------------ | ------ |
| `+` | 正号 | 1 | 操作数本身 | ❎ |
| `-` | 负号 | 1 | 操作数符号取反 | ❎ |
| `+` | 加号 | 2 | 两个操作数之和 | ❎ | * 示例:二进制转十进制
| `-` | 减号 | 2 | 两个操作数之差 | ❎ |
| `*` | 乘号 | 2 | 两个操作数之积 | ❎ | ![](./assets/28.svg)
| `/` | 除号 | 2 | 两个操作数之商 | ❎ |
| `%` | 取模(取余) | 2 | 两个操作数相除的余数 | ❎ | #### 3.4.2.2 十进制转换二进制
| `++` | 自增 | 1 | 操作数自增前或自增后的值 | ✅ |
| `--` | 自减 | 1 | 操作数自减前或自减后的值 | ✅ | * 规则:将该数不断除以 2 ,直到商为 0 为止,然后将每步得到的余数倒过来,就是对应的二进制。
> [!NOTE] > [!NOTE]
> >
> 自增和自减: > * ① 在学术界,将这种计算规则,称为`短除法`或`连续除2取余法`。
> * ② 很好理解,只有不断地除以 2 ,就能保证最大的数字不超过 2 ,这不就是二进制(只能有 0 或 1
> * ③ `八进制转换为二进制`、`十六进制转换为二进制`和`十进制转换为二进制`的算法相同!!!
* 示例:十进制转十进制
![](./assets/29.svg)
* 示例:十进制转二进制
![](./assets/30.svg)
### 3.4.3 二进制转八进制
* 规则:从右向左,每 3 位二进制就是一个八进制,不足补 0分组转换法
* 示例011 101 001 -> 351
![](./assets/31.svg)
### 3.4.4 二进制转十六进制
* 规则:从右向左,每 4 位二进制就是一个十六进制,不足补 0分组转换法
* 示例1110 1001 -> 0xE9
![](./assets/32.svg)
## 3.5 原码、反码和补码
### 3.5.1 概述
* 机器数:一个数在计算机的存储形式是二进制,我们称这些二进制数为机器数。机器数可以是有符号的,用机器数的最高位来存放符号位,`0` 表示正数,`1` 表示负数。
> [!IMPORTANT]
> >
> * ① 自增、自减运算符可以写在操作数的前面也可以写在操作数后面,不论前面还是后面,对操作数的副作用是一致的。 > * ① 这里讨论的适用于`有符号位`的整数int 等。
> * ② 自增、自减运算符在前在后,对于表达式的值是不同的。 如果运算符在前,表达式的值是操作数自增、自减之后的值;如果运算符在后,表达式的值是操作数自增、自减之前的值。 > * ② 这里讨论的不适用于`无符号位`的整数unsinged int 等。
> * ③ `变量前++`:变量先自增 1 ,然后再运算;`变量后++`:变量先运算,然后再自增 1 。
> * ④ `变量前--`:变量先自减 1 ,然后再运算;`变量后--`:变量先运算,然后再自减 1 。
> * ⑤ 对于 `i++``i--` 各种编程语言的用法和支持是不同的例如C/C++、Java 等完全支持Python 压根一点都不支持Go 语言虽然支持 `i++``i--` ,却只支持这些操作符作为独立的语句,并且不能嵌入在其它的表达式中。
![](./assets/33.svg)
* 真值(数据位):因为机器数带有符号位,所以机器数的形式值不等于其真实表示的值(真值),以机器数 1000 0001 为例,其真正表示的值(首位是符号位)为 -1而形式值却是 129 ,因此将带有符号位的机器数的真正表示的值称为机器数的真值。
* 示例:正号和负号 > [!IMPORTANT]
>
> * ① 这里讨论的适用于`有符号位`的整数int 等。
> * ② 这里讨论的不适用于`无符号位`的整数unsinged int 等。
```c ![](./assets/34.svg)
#include <stdio.h>
int main() { ### 3.5.2 原码
int x = 12; * 原码的表示与机器数真值表示的一样,即用第一位表示符号,其余位表示数值。
int x1 = -x, x2 = +x; * 规则:
* 正数的`原码`是它本身对应的二进制数,符号位是 0 。
* 负数的`原码`是它本身绝对值对应的二进制数,但是符号位是 1 。
* `+1` 的原码,使用 `16` 位二进数来表示,就是:
int y = -67; | 十进制数 | 原码16位二进制数 |
int y1 = -y, y2 = +y; | -------- | --------------------- |
| +1 | `0`000 0000 0000 0001 |
printf("x1=%d, x2=%d \n", x1, x2); // x1=-12, x2=12 * `-1` 的原码,使用 `16` 位二进数来表示,就是:
printf("y1=%d, y2=%d \n", y1, y2); // y1=67, y2=-67
return 0; | 十进制数 | 原码16位二进制数 |
} | -------- | --------------------- |
``` | -1 | `1`000 0000 0000 0001 |
> [!IMPORTANT]
>
> * ① 按照原码的规则,会出现 `+0``-0` 的情况,即:`0`000 0000 0000 0001+0、`1`000 0000 0000 0001-0显然不符合实际情况。
>* ② 所以,计算机底层虽然存储和计算的都是二进数,但显然不是原码。
### 3.5.3 反码
* 示例:加、减、乘、除(整数之间做除法时,结果只保留整数部分而舍弃小数部分)、取模 * 规则:
```c * 正数的反码和它的原码相同。
#include <stdio.h> * 负数的反码是在其原码的基础上,符号位不变,其余各位取反。
int main() { * `+1` 的反码,使用 `16` 位二进数来表示,就是:
int a = 5; | 十进制数 | 原码16位二进制数 | 反码16位二进制数 |
int b = 2; | -------- | --------------------- | --------------------- |
| +1 | `0`000 0000 0000 0001 | `0`000 0000 0000 0001 |
printf("%d + %d = %d\n", a, b, a + b); // 5 + 2 = 7 * `-1` 的反码,使用 `16` 位二进数来表示,就是:
printf("%d - %d = %d\n", a, b, a - b); // 5 - 2 = 3
printf("%d × %d = %d\n", a, b, a * b); // 5 × 2 = 10
printf("%d / %d = %d\n", a, b, a / b); // 5 / 2 = 2
printf("%d %% %d = %d\n", a, b, a % b); // 5 % 2 = 1
return 0; | 十进制数 | 原码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显然不符合实际情况。
>* ② 所以,计算机底层虽然存储和计算的都是二进数,但显然不是反码。
### 3.5.4 补码
* 示例:取模(运算结果的符号与被模数也就是第一个操作数相同。) * 规则:
```c * 正数的补码和它的原码相同。
#include <stdio.h> * 负数的补码是在其反码的基础上 + 1 。
* `+1` 的补码,使用 `16` 位二进数来表示,就是:
int main() { | 十进制数 | 原码16位二进制数 | 反码16位二进制数 | 补码16位二进制数 |
| -------- | --------------------- | --------------------- | --------------------- |
| +1 | `0`000 0000 0000 0001 | `0`000 0000 0000 0001 | `0`000 0000 0000 0001 |
int res1 = 10 % 3; * `-1` 的补码,使用 `16` 位二进数来表示,就是:
printf("10 %% 3 = %d\n", res1); // 10 % 3 = 1
int res2 = -10 % 3; | 十进制数 | 原码16位二进制数 | 反码16位二进制数 | 补码16位二进制数 |
printf("-10 %% 3 = %d\n", res2); // -10 % 3 = -1 | -------- | --------------------- | --------------------- | --------------------- |
| -1 | `1`000 0000 0000 0001 | `1`111 1111 1111 1110 | `1`111 1111 1111 1111 |
int res3 = 10 % -3; * 如果 `0` ,按照 `+0` 的情况进行处理,如下所示:
printf("10 %% -3 = %d\n", res3); // 10 % -3 = 1
int res4 = -10 % -3; ![](./assets/35.svg)
printf("-10 %% -3 = %d\n", res4); // -10 % -3 = -1
return 0; * 如果 `0` ,按照 `-0` 的情况进行处理,如下所示:
}
```
![](./assets/36.svg)
* `+1``-1` 的`原码`和`补码`的转换过程,如下所示:
* 示例:自增和自减 ![](./assets/37.svg)
```c > [!IMPORTANT]
#include <stdio.h> >
> * ① 补码表示法解决了`原码`和`反码`存在的`两种`零(`+0` 和 `-0`)的问题,即:在补码表示法中,只有`一个`零,即 `0000 0000`
>* ②补码使得`加法运算`和`减法运算`可以统一处理,通过将减法运算`转换`为加法运算,可以简化硬件设计,提高了运算效率。
> * ③ 计算机底层`存储`和`计算`的都是`二进数的补码`。换言之,当`读取`整数的时候,需要采用`逆向`的转换,即:将补码转换为原码。正数的原码、反码、补码都是一样的,三码合一。负数的补码转换为原码的方法就是先减去 `1` ,得到反码,再按位取反,得到原码(符号位是不能借位的)。
int main() { ### 3.5.5 总结
int i1 = 10, i2 = 20; * ① 计算机底层`存储`和`计算`的都是`二进数的补码`。换言之,当`读取`整数的时候,需要采用`逆向`的转换,即:将补码转换为原码。
int i = i1++; * ② 正数的原码、反码和补码都是一样的,三码合一。
printf("i = %d\n", i); // i = 10 * ③ 负数的反码是在其原码的基础上按位取反0 变 1 1 变 0 ),符号位不变;负数的补码是其反码 + 1 。
printf("i1 = %d\n", i1); // i1 = 11 * ④ 0 的补码是 0 。
* ⑤ 负数的补码转换为原码的方法就是先减去 `1` ,得到反码,再按位取反,得到原码(符号位是不能借位的)。
i = ++i1; ## 3.6 计算机底层为什么使用补码?
printf("i = %d\n", i); // i = 12
printf("i1 = %d\n", i1); // i1 = 12
i = i2--; * `加法`和`减法`是计算机中最基本的运算,计算机时时刻刻都离不开它们,所以它们由硬件直接支持。为了提高加法和减法的运行效率,硬件电路必须设计得尽量简单。
printf("i = %d\n", i); // i = 20 * 对于有符号位的数字来说,内存需要区分符号位和数值位:对于人类来说,很容易识别(最高位是 0 还是 1但是对于计算机来说需要设计专门的电路这无疑增加了硬件的复杂性增加了计算时间。如果能将符号位和数值位等同起来让它们一起参与运算不再加以区分这样硬件电路就可以变得非常简单。
printf("i2 = %d\n", i2); // i2 = 19 * 此外,加法和减法也可以合并为一种运算,即:加法运算。换言之,减去一个数就相当于加上这个数的相反数,如:`5 - 3` 相当于 `5 +-3``10 --9`相当于 `10 + 9`
i = --i2; * 如果能够实现上述的两个目标,那么只需要设计一种简单的、不用区分符号位和数值位的加法电路,就能同时实现加法运算和减法运算,而且非常高效。其实,这两个目标已经实现了,真正的计算机的硬件电路就是这样设计的。
printf("i = %d\n", i); // i = 18 * 但是,简化硬件电路是有代价的,这个代价就是`有符号数`在存储和读取的时候都要继续转换。这也是对于有符号数的运算来说,计算机底层为什么使用`补码`的原因所在。
printf("i2 = %d\n", i2); // i2 = 18
return 0; ## 3.7 补码到底是如何简化硬件电路的?
``` * 假设 6 和 18 都是 short 类型,现在我们要计算 `6 - 18` 的结果,根据运算规则,它等价于 `6 +-18`。如果按照采用`原码`来计算,那么运算过程是这样的,如下所示:
* 示例:
```c
#include <stdio.h>
/*
随意给出一个整数,打印显示它的个位数,十位数,百位数的值。
格式如下:
数字xxx的情况如下
个位数:
十位数:
百位数:
例如:
数字153的情况如下
个位数3
十位数5
百位数1
*/
int main() {
int num = 153;
int bai = num / 100;
int shi = num % 100 / 10;
int ge = num % 10;
printf("百位为:%d \n", bai);
printf("十位为:%d \n", shi);
printf("个位为:%d \n", ge);
return 0;
}
```
## 2.3 关系运算符(比较运算符)
* 常见的关系运算符,如下所示:
| 运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 |
| ------ | -------- | ---------- | ---------------- | ------ |
| `==` | 相等 | 2 | 0 或 1 | ❎ |
| `!=` | 不相等 | 2 | 0 或 1 | ❎ |
| `<` | 小于 | 2 | 0 或 1 | ❎ |
| `>` | 大于 | 2 | 0 或 1 | ❎ |
| `<=` | 小于等于 | 2 | 0 或 1 | ❎ |
| `>=` | 大于等于 | 2 | 0 或 1 | ❎ |
> [!NOTE] > [!NOTE]
> >
> * ① C 语言中,没有严格意义上的布尔类型,可以使用 0 或 1表示布尔类型的值。 > 直接使用原码表示整数,让符号位也参与运算,那么对于减法来说,结果显然是不正确的。
> * ② 不要将 `==` 写成 `=``==` 是比较运算符,而 `=` 是赋值运算符。
> * ③ `>=``<=`含义是只需要满足 `大于或等于`、`小于或等于`其中一个条件,结果就返回真。
![](./assets/38.svg)
* 于是,人们开始继续探索,不断试错,终于设计出了`反码`,如下所示:
* 示例:
```c
#include <stdio.h>
int main() {
int a = 8;
int b = 7;
printf("a > b 的结果是:%d \n", a > b); // a > b 的结果是1
printf("a >= b 的结果是:%d \n", a >= b); // a >= b 的结果是1
printf("a < b 的结果是%d \n", a < b); // a < b 的结果是0
printf("a <= b 的结果是:%d \n", a <= b); // a <= b 的结果是0
printf("a == b 的结果是:%d \n", a == b); // a == b 的结果是0
printf("a != b 的结果是:%d \n", a != b); // a != b 的结果是1
return 0;
}
```
## 2.4 逻辑运算符
* 常见的逻辑运算符,如下所示:
| 运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 |
| ------ | ------ | ---------- | ---------------- | ------ |
| `&&` | 逻辑与 | 2 | 0 或 1 | ❎ |
| `\|\|` | 逻辑或 | 2 | 0 或 1 | ❎ |
| `!` | 逻辑非 | 2 | 0 或 1 | ❎ |
* 逻辑运算符提供逻辑判断功能,用于构建更复杂的表达式,如下所示:
| a | b | a && b | a \|\| b | !a |
| ------- | ------- | ------- | -------- | ------- |
| 1 | 1 | 1 | 1 | 0 |
| 1 | 0 | 0 | 1 | 0 |
| 0 | 1 | 0 | 1 | 1 |
| 0 | 0 | 0 | 0 | 1 |
> [!NOTE] > [!NOTE]
> >
> * ① 对于逻辑运算符来说,任何`非零值`都表示`真``零值`表示`假`,如:`5 || 0` 返回 `1` `5 && 0` 返回 `0` > 直接使用反码表示整数,让符号位也参与运算,对于 6 +-18来说结果貌似正确。
> * ② 逻辑运算符的理解:
> * `&&` 的理解就是:`两边条件,同时满足`。
> * `||`的理解就是:`两边条件,二选一`。
> * `!` 的理解就是:`条件取反`。
> * ③ 短路现象:
> * 对于 `a && b` 操作来说,当 a 为假(或 0 )时,因为 `a && b` 结果必定为 0所以不再执行表达式 b。
> * 对于 `a || b` 操作来说,当 a 为真(或非 0 )时,因为 `a || b` 结果必定为 1所以不再执行表达式 b。
![](./assets/39.svg)
* 如果我们将`被减数`和`减数`对调一下,即:计算 `18 - 6` 的结果,也就是 `18 +-6`的结果,继续采用`反码`来进行运算,如下所示:
* 示例:
```c
#include <stdio.h>
int main() {
int a = 0;
int b = 0;
printf("请输入整数a的值");
scanf("%d", &a);
printf("请输入整数b的值");
scanf("%d", &b);
if (a > b) {
printf("%d > %d", a, b);
} else if (a < b) {
printf("%d < %d", a, b);
} else {
printf("%d = %d", a, b);
}
return 0;
}
```
* 示例:
```c
#include <stdio.h>
// 短路现象
int main() {
int i = 0;
int j = 10;
if (i && j++ > 0) {
printf("床前明月光\n"); // 这行代码不会执行
} else {
printf("我叫郭德纲\n");
}
printf("%d \n", j); //10
return 0;
}
```
* 示例:
```c
#include <stdio.h>
// 短路现象
int main() {
int i = 1;
int j = 10;
if (i || j++ > 0) {
printf("床前明月光 \n");
} else {
printf("我叫郭德纲 \n"); // 这行代码不会被执行
}
printf("%d\n", j); //10
return 0;
}
```
## 2.5 赋值运算符
* 常见的赋值运算符,如下所示:
| 运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 |
| ------ | ------------ | ---------- | ---------------- | ------ |
| `==` | 赋值 | 2 | 左边操作数的值 | ✅ |
| `+=` | 相加赋值 | 2 | 左边操作数的值 | ✅ |
| `-=` | 相减赋值 | 2 | 左边操作数的值 | ✅ |
| `*=` | 相乘赋值 | 2 | 左边操作数的值 | ✅ |
| `/=` | 相除赋值 | 2 | 左边操作数的值 | ✅ |
| `%=` | 取余赋值 | 2 | 左边操作数的值 | ✅ |
| `<<=` | 左移赋值 | 2 | 左边操作数的值 | ✅ |
| `>>=` | 右移赋值 | 2 | 左边操作数的值 | ✅ |
| `&=` | 按位与赋值 | 2 | 左边操作数的值 | ✅ |
| `^=` | 按位异或赋值 | 2 | 左边操作数的值 | ✅ |
| `\|=` | 按位或赋值 | 2 | 左边操作数的值 | ✅ |
> [!NOTE] > [!NOTE]
> >
> * ① 赋值运算符的第一个操作数(左值)必须是变量的形式,第二个操作数可以是任何形式的表达式。 > * ① 6 - 186+-18如果采用`反码`计算结果是正确的但是18 - 618 +-6如果采用`反码`计算,结果相差 1 。
> * ② 赋值运算符的副作用针对第一个操作数 > * ② 可以推断:如果按照`反码`来计算,小数 - 大数,结果正确;而大数 - 小数,结果相差 1 。
![](./assets/40.svg)
* 对于这个相差的 `1` 必须进行纠正,但是又不能影响`小数-大数`的结果。于是,人们又绞尽脑汁设计出了`补码`,给`反码`打了一个`“补丁”`,终于把相差的 `1` 给纠正过来了。那么,`6 - 18` 按照`补码`的运算过程,如下所示:
* 示例: ![](./assets/41.svg)
* 那么,`18 - 6` 按照`补码`的运算过程,如下所示:
![](./assets/42.svg)
> [!IMPORTANT]
>
> 总结:采用`补码`的形式正好将相差的 `1`纠正过来,也没有影响到小数减大数,这个“补丁”非常巧妙。
>
> * ① 小数减去大数,结果为负,之前(负数从反码转换为补码需要 +1加上的 1 ,后来(负数从补码转换为反码要 -1还需要减去正好抵消掉所以不会受到影响。
> * ② 大数减去小数,结果为正,之前(负数从反码转换为补码需要 +1加上的 1 ,后来(正数的补码和反码相同,从补码转换为反码不用 -1就没有再减去不能抵消掉这就相当于给计算结果多加了一个 1。
>
> `补码`这种天才般的设计,一举达成了之前加法运算和减法运算提到的两个目标,简化了硬件电路。
## 3.8 问题抛出
* 在 C 语言中,对于`有符号位`的整数,是使用 `0` 作为正数,`1` 作为负数,来表示`符号位`,并使用`数据位`来表示的是数据的`真值`,如下所示:
```c ```c
#include <stdio.h> int a = 10;
int b = -10;
int main() {
int a = 3;
a += 3; // a = a + 3
printf("a = %d\n", a); // a = 6
int b = 3;
b -= 3; // b = b - 3
printf("b = %d\n", b); // b = 0
int c = 3;
c *= 3; // c = c * 3
printf("c = %d\n", c); // c = 9
int d = 3;
d /= 3; // d = d / 3
printf("d = %d\n", d); // d = 1
int e = 3;
e %= 3; // e = e % 3
printf("e = %d\n", e); // e = 0
return 0;
}
``` ```
## 2.6 位运算符(了解) ![](./assets/43.svg)
### 2.6.1 概述 * 但是,对于`无符号位`的整数而言,是`没有`符号位和数据位,即:没有原码、反码、补码的概念。无符号位的整数的数值都是直接使用二进制来表示的(也可以理解为,对于无符号位的整数,计算机底层存储的就是其原码),如下所示:
* C 语言提供了一些位运算符能够让我们操作二进制位bit ```c
* 常见的位运算符,如下所示。 unsigned int a = 10;
unsigned int b = -10;
```
| 运算符 | 描述 | 操作数个数 | 运算规则 | 副作用 | ![](./assets/44.svg)
| ------ | ---------- | ---------- | ------------------------------------------------------------ | ------ |
| `&` | 按位与 | 2 | 两个二进制位都为 1 ,结果为 1 ,否则为 0 。 | ❎ |
| `\|` | 按位或 | 2 | 两个二进制位只要有一个为 1包含两个都为 1 的情况),结果为 1 ,否则为 0 。 | ❎ |
| `^` | 按位异或 | 2 | 两个二进制位一个为 0 ,一个为 1 ,结果为 1否则为 0 。 | ❎ |
| `~` | 按位取反 | 2 | 将每一个二进制位变成相反值,即 0 变成 1 1 变 成 0 。 | ❎ |
| `<<` | 二进制左移 | 2 | 将一个数的各二进制位全部左移指定的位数,左 边的二进制位丢弃,右边补 0。 | ❎ |
| `>>` | 二进制右移 | 2 | 将一个数的各二进制位全部右移指定的位数,正数左补 0负数左补 1右边丢弃。 | ❎ |
> [!NOTE] * 这就是导致了一个结果就是:如果我定义一个`有符号`的负数,却让其输出`无符号`,必然造成结果不对,如下所示:
>
> 操作数在进行位运算的时候,以它的补码形式计算!!!
### 2.6.2 输出二进制位
* 在 C 语言中,`printf` 是没有提供输出二进制位的格式占位符的;但是,我们可以手动实现,以方便后期操作。
* 示例:
```c ```c
#include <stdio.h> #include <stdio.h>
/**
* 获取指定整数的二进制表示
* @param num 整数
* @return 二进制表示的字符串,不包括前导的 '0b' 字符
*/
char *getBinary(int num) { char *getBinary(int num) {
static char binaryString[33]; static char binaryString[33];
int i, j; int i, j;
@ -2196,239 +514,23 @@ char* getBinary(int num) {
int main() { int main() {
int a = 17; // 禁用 stdout 缓冲区
int b = -12; setbuf(stdout, NULL);
printf("整数 %d 的二进制表示:%s \n", a, getBinary(a)); int num = -10;
printf("整数 %d 的二进制表示:%s \n", b, getBinary(b)); printf("b=%s\n", getBinary(num)); // b=11111111111111111111111111110110
printf("b=%d\n", num); // b=-10
printf("b=%u\n", num); // b=4294967286
return 0; return 0;
} }
``` ```
### 2.6.3 按位与 * 其实C 语言的底层逻辑很简单C 语言压根不关心你定义的是有符号数还是无符号数,它只关心内存(如果定义的是有符号数,那就按照有符号数的规则来存储;如果定义的是无符号数,那就按照无符号数的规则来存储)。换言之,有符号数可以按照无符号数的规则来输出,无符号数也可以按照有符号数的规则来输出,至于输出结果对不对,那是程序员的事情,和 C 语言没有任何关系。
* 按位与 `&` 的运算规则是:如果二进制对应的位上都是 1 才是 1 ,否则为 0 ,即: > [!IMPORTANT]
* `1 & 1` 的结果是 `1`
* `1 & 0` 的结果是 `0`
* `0 & 1` 的结果是 `0`
* `0 & 0` 的结果是 `0`
* 示例:`9 & 7 = 1`
![](./assets/22.svg)
* 示例:`-9 & 7 = 7`
![](./assets/23.svg)
### 2.6.4 按位或
* 按位与 `|` 的运算规则是:如果二进制对应的位上只要有 1 就是 1 ,否则为 0 ,即:
* `1 | 1` 的结果是 `1`
* `1 | 0` 的结果是 `1`
* `0 | 1` 的结果是 `1`
* `0 | 0` 的结果是 `0`
* 示例:`9 | 7 = 15`
![](./assets/24.svg)
* 示例:`-9 | 7 = -9`
![](./assets/25.svg)
### 2.6.5 按位异或
* 按位与 `^` 的运算规则是:如果二进制对应的位上一个为 1 一个为 0 就为 1 ,否则为 0 ,即:
* `1 ^ 1` 的结果是 `0`
* `1 ^ 0` 的结果是 `1`
* `0 ^ 1` 的结果是 `1`
* `0 ^ 0` 的结果是 `0`
> [!NOTE]
> >
> 按位异或的场景有: > * ① 实际开发中,`printf` 函数中的常量、变量或表达式,需要和格式占位符一一对应;否则,将会出现数据错误的现象。
> > * ② 正因为上述的原因很多现代化的编程语言Java 等,直接取消了无符号的概念。但是,很多数据库是使用 C 语言开发的MySQL 等,就提供了创建数据表的字段为无符号类型的功能,即:`UNSIGNED`(正整数) ,不要感觉困惑!!!
> * ① 交换两个数值:异或操作可以在不使用临时变量的情况下交换两个变量的值。 > * ③ 对于 `1000 0000 …… 0000 0000` 这个特殊的补码,无法按照上述的方法转换为原码,所以计算机直接规定这个补码对应的值就是 `-2³¹`,至于为什么,下节我们会详细分析。
> * ② 加密或解密:异或操作用于简单的加密和解密算法。
> * ③ 错误检测和校正异或操作可以用于奇偶校验位的计算和检测错误RAID-3 以及以上)。
> * ……
* 示例:`9 ^ 7 = 14`
![](./assets/26.svg)
* 示例:`-9 ^ 7 = -16`
![](./assets/27.svg)
### 2.6.6 按位取反
* 运算规则:如果二进制对应的位上是 1则结果为 0如果是 0 ,则结果为 1 。
* `~0` 的结果是 `1`
* `~1` 的结果是 `0`
* 示例:`~9 = -10`
![](./assets/28.svg)
* 示例:`~-9 = 8`
![](./assets/29.svg)
### 2.6.7 二进制左移
* 在一定范围内,数据每向左移动一位,相当于原数据 × 2。正数、负数都适用
* 示例:`3 << 4 = 48` 3 × 2^4
![](./assets/30.svg)
* 示例:`-3 << 4 = -48` -3 × 2 ^4
![](./assets/31.svg)
### 2.6.8 二进制右移
* 在一定范围内,数据每向右移动一位,相当于原数据 ÷ 2。正数、负数都适用
> [!NOTE]
>
> * ① 如果不能整除,则向下取整。
> * ② 右移运算符最好只用于无符号整数,不要用于负数。因为不同系统对于右移后如何处理负数的符号位,有不同的做法,可能会得到不一样的结果。
* 示例:`69 >> 4 = 4` 69 ÷ 2^4
![](./assets/32.svg)
* 示例:`-69 >> 4 = -5` -69 ÷ 2^4
![](./assets/33.svg)
## 2.7 三元运算符
* 语法:
```c
条件表达式 ? 表达式1 : 表达式2 ;
```
> [!NOTE]
>
> * 如果条件表达式为非 0 (真),则整个表达式的值是表达式 1 。
> * 如果条件表达式为 0 (假),则整个表达式的值是表达式 2 。
* 示例:
```c
#include <stdio.h>
int main() {
int m = 110;
int n = 20;
int result = m > n ? m : n;
printf("result = %d\n", result); // result = 110
return 0;
}
```
## 2.8 运算符的优先级和结合性
* 在数学中,如果一个表达式是 `a + b * c` ,我们知道其运算规则就是:先算乘除再算加减。其实,在 C 语言中也是一样的先算乘法再算加减C 语言中乘除的运算符比加减的运算符的优先级要高。
> [!NOTE]
>
> `优先级`和`结合性`的定义,如下所示:
>
> * ① 所谓的`优先级`:就是当多个运算符出现在同一个表达式中时,先执行哪个运算符。
> * ② 所谓的`结合性`:就是当多个相同优先级的运算符出现在同一个表达式中的时候,是从左到右运算,还是从右到左运算。
> * `左结合性`:具有相同优先级的运算符将`从左到右`(➡️)进行计算。
> * `右结合性`:具有相同优先级的运算符将`从右到左`(⬅️)进行计算。
>
> 总结:先看`优先级`;如果优先级相同,再看`结合性`。
* C 语言中运算符的优先级有几十个,有的运算符优先级不同,有的运算符优先级相同,如下所示:
| 优先级 | 运算符 | 名称或含义 | 结合方向 |
| ------ | -------------- | ------------------------------------------- | ------------- |
| `0` | `()` | 小括号,最高优先级 | ➡️(从左到右) |
| `1` | `++`、`--` | 后缀自增和自减,如:`i++`、`i--` 等 | ➡️(从左到右) |
| | `()` | 小括号,函数调用,如:`sum(1,2)` 等 | |
| | `[]` | 数组下标,如:`arr[0]`、`arr[1]` 等 | |
| | `.` | 结构体或共用体成员访问 | |
| | `->` | 结构体或共用体成员通过指针访问 | |
| `2` | `++`、`--` | 前缀自增和自减,如:`++i`、`--i` 等 | ⬅️(从右到左) |
| | `+` | 一元加运算符,表示操作数的正,如:`+2` 等 | |
| | `-` | 一元减运算符,表示操作数的负,如:`-3` 等 | |
| | `!` | 逻辑非运算符(逻辑运算符) | |
| | `~` | 按位取反运算符(位运算符) | |
| | `typename` | 强制类型转换 | |
| | `*` | 解引用运算符 | |
| | `&` | 取地址运算符 | |
| | `sizeof` | 取大小运算符 | |
| `3` | `/` | 除法运算符(算术运算符) | ➡️(从左到右) |
| | `*` | 乘法运算符(算术运算符) | |
| | `%` | 取模(取余)运算符(算术运算符) | |
| `4` | `+` | 二元加运算符(算术运算符),如:`2 + 3` 等 | ➡️(从左到右) |
| | `-` | 二元减运算符(算术运算符),如:`3 - 2` 等 | |
| `5` | `<<` | 左移位运算符(位运算符) | ➡️(从左到右) |
| | `>>` | 右移位运算符(位运算符) | |
| `6` | `>` | 大于运算符(比较运算符) | ➡️(从左到右) |
| | `>=` | 大于等于运算符(比较运算符) | |
| | `<` | 小于运算符(比较运算符) | |
| | `<=` | 小于等于运算符(比较运算符) | |
| `7` | `==` | 等于运算符(比较运算符) | ➡️(从左到右) |
| | `!=` | 不等于运算符(比较运算符) | |
| `8` | `&` | 按位与运算符(位运算符) | ➡️(从左到右) |
| `9` | `^` | 按位异或运算符(位运算符) | ➡️(从左到右) |
| `10` | `\|` | 按位或运算符(位运算符) | ➡️(从左到右) |
| `11` | `&&` | 逻辑与运算符(逻辑运算符) | ➡️(从左到右) |
| `12` | `\|\|` | 逻辑或运算符(逻辑运算符) | ➡️(从左到右) |
| `13` | `?:` | 三目(三元)运算符 | ⬅️(从右到左) |
| `14` | `=` | 简单赋值运算符(赋值运算符) | ⬅️(从右到左) |
| | `/=` | 除后赋值运算符(赋值运算符) | |
| | `*=` | 乘后赋值运算符(赋值运算符) | |
| | `%=` | 取模后赋值运算符(赋值运算符) | |
| | `+=` | 加后赋值运算符(赋值运算符) | |
| | `-=` | 减后赋值运算符(赋值运算符) | |
| | `<<=` | 左移后赋值运算符(赋值运算符) | |
| | `>>=` | 右移后赋值运算符(赋值运算符) | |
| | `&=` | 按位与后赋值运算符(赋值运算符) | |
| | `^=` | 按位异或后赋值运算符(赋值运算符) | |
| | `\|=` | 按位或后赋值运算符(赋值运算符) | |
| `15` | `,` | 逗号运算符 | ➡️(从左到右) |
> [!WARNING]
>
> * ① 不要过多的依赖运算符的优先级来控制表达式的执行顺序,这样可读性太差,尽量`使用小括号来控制`表达式的执行顺序。
> * ② 不要把一个表达式写得过于复杂,如果一个表达式过于复杂,则把它`分成几步`来完成。
> * ③ 运算符优先级不用刻意地去记忆,总体上:一元运算符 > 算术运算符 > 关系运算符 > 逻辑运算符 > 三元运算符 > 赋值运算符。

View File

@ -1,4 +1,4 @@
# 第一章:相关概念 # 第一章:前言
## 1.1 普通变量和指针变量 ## 1.1 普通变量和指针变量
@ -10,22 +10,22 @@
![](./assets/1.png) ![](./assets/1.png)
1.2 普通变量 ## 1.2 普通变量和指针变量的异同点
> [!NOTE] * 普通变量和指针变量的相同点:
>
> 普通变量和指针变量的相同点:
>
> * 普通变量有内存空间,指针变量也有内存空间。
> * 普通变量有内存地址,指针变量也有内存地址。
> * 普通变量所对应的内存空间中有值,指针变量所对应的内存空间中也有值。
>
> 普通变量和指针变量的不同点:
>
> * 普通变量所对应的内存空间存储的是普通的值,如:整数、小数、字符等;指针变量所对应的内存空间存储的是另外一个变量的地址。
> * 普通变量有普通变量的运算方式,而指针变量有指针变量的运算方式(后续讲解)。
* 那么,在 C 语言中变量的数据类型就可以这么划分,如下所示: * 普通变量有内存空间,指针变量也有内存空间。
* 普通变量有内存地址,指针变量也有内存地址。
* 普通变量所对应的内存空间中有值,指针变量所对应的内存空间中也有值。
普通变量和指针变量的不同点:
* 普通变量所对应的内存空间存储的是普通的值,如:整数、小数、字符等;指针变量所对应的内存空间存储的是另外一个变量的地址。
* 普通变量有普通变量的运算方式,而指针变量有指针变量的运算方式(后续讲解)。
## 1.3 C 语言中变量的分类
* 在 C 语言中变量的数据类型就可以这么划分,如下所示:
![](./assets/2.png) ![](./assets/2.png)
@ -34,9 +34,11 @@
> * 根据`普通变量`中`存储`的`值`的类型不同,可以将`普通变量类型`划分为`基本数据类型`(整型、字符类型、浮点类型、布尔类型)和`复合数据类型`(数组类型、结构体类型、共用体类型、枚举类型)。 > * 根据`普通变量`中`存储`的`值`的类型不同,可以将`普通变量类型`划分为`基本数据类型`(整型、字符类型、浮点类型、布尔类型)和`复合数据类型`(数组类型、结构体类型、共用体类型、枚举类型)。
> * 根据`指针变量`所`指向空间`中`存储`的`值`的类型不同,可以将`指针类型`分为`基本数据类型指针`、`复合数据类型指针`、`函数指针`、`数组指针`等,例如:如果指针所指向的空间保存的是 int 类型,那么该指针就是 int 类型的指针。 > * 根据`指针变量`所`指向空间`中`存储`的`值`的类型不同,可以将`指针类型`分为`基本数据类型指针`、`复合数据类型指针`、`函数指针`、`数组指针`等,例如:如果指针所指向的空间保存的是 int 类型,那么该指针就是 int 类型的指针。
## 1.2 整数类型
### 1.2.1 概述
# 第二章:整数类型(⭐)
## 2.1 概述
* 整数类型简称整型用于存储整数值12、20、50 等。 * 整数类型简称整型用于存储整数值12、20、50 等。
* 根据所占`内存空间`大小的不同,可以将整数类型划分为: * 根据所占`内存空间`大小的不同,可以将整数类型划分为:
@ -87,7 +89,7 @@
> * ③ 在实际开发中,`最常用的整数类型`就是 `int` 类型了,如果取值范围不够,就使用 long 或 long long 。 > * ③ 在实际开发中,`最常用的整数类型`就是 `int` 类型了,如果取值范围不够,就使用 long 或 long long 。
> * ④ C 语言中的`格式占位符`非常多,只需要大致了解即可;因为,我们在实际开发中,一般都会使用 C++ 或 Rust 以及其它的高级编程语言Java 等,早已经解决了必须通过`格式占位符`来才能将变量进行输入和输出。 > * ④ C 语言中的`格式占位符`非常多,只需要大致了解即可;因为,我们在实际开发中,一般都会使用 C++ 或 Rust 以及其它的高级编程语言Java 等,早已经解决了必须通过`格式占位符`来才能将变量进行输入和输出。
### 1.2.2 短整型(了解) ## 2.2 短整型(了解)
* 语法: * 语法:
@ -171,7 +173,7 @@ int main() {
} }
``` ```
### 1.2.3 整型 ## 2.3 整型
* 语法: * 语法:
@ -255,7 +257,7 @@ int main() {
} }
``` ```
### 1.2.4 长整型(了解) ## 2.4 长整型(了解)
* 语法: * 语法:
@ -339,7 +341,7 @@ int main() {
} }
``` ```
### 1.2.5 长长整型(了解) ## 2.5 长长整型(了解)
* 语法: * 语法:
@ -423,7 +425,7 @@ int main() {
} }
``` ```
### 1.2.6 字面量后缀 ## 2.6 字面量后缀
* `字面量`是`源代码`中一个`固定值`的`表示方法`,用于直接表示数据,即: * `字面量`是`源代码`中一个`固定值`的`表示方法`,用于直接表示数据,即:
@ -477,7 +479,7 @@ int main() {
} }
``` ```
### 1.2.7 精确宽度类型 ## 2.7 精确宽度类型
* 在前文,我们了解到 C 语言的整数类型short 、int、long、long long在不同计算机上占用的字节宽度可能不一样。但是有的时候我们希望整数类型的存储空间字节宽度是精确的在任意平台计算机上都能一致以提高程序的可移植性。 * 在前文,我们了解到 C 语言的整数类型short 、int、long、long long在不同计算机上占用的字节宽度可能不一样。但是有的时候我们希望整数类型的存储空间字节宽度是精确的在任意平台计算机上都能一致以提高程序的可移植性。
@ -521,7 +523,7 @@ int main() {
} }
``` ```
### 1.2.8 sizeof 运算符 ## 2.8 sizeof 运算符
* 语法: * 语法:
@ -595,9 +597,11 @@ int main() {
} }
``` ```
## 1.3 数值溢出
### 1.3.1 概述
# 第三章:数值溢出(⭐)
## 3.1 概述
* 在生活中,如果一个容器的容量是固定的,我们不停的向其中注入水,那么当容器中充满水之后,再继续注入,水就会从杯子中溢出来,如下所示: * 在生活中,如果一个容器的容量是固定的,我们不停的向其中注入水,那么当容器中充满水之后,再继续注入,水就会从杯子中溢出来,如下所示:
@ -617,7 +621,7 @@ int main() {
> * ② 但是这可能会导致不可预料的后果1996 年的亚利安 5 号运载火箭爆炸、2004 年的 Comair 航空公司航班停飞事故。 > * ② 但是这可能会导致不可预料的后果1996 年的亚利安 5 号运载火箭爆炸、2004 年的 Comair 航空公司航班停飞事故。
> * ③ 在实际开发中,编程时要特别注意,以避免数值溢出问题,特别是在涉及大数或小数的运算(特指整数)。 > * ③ 在实际开发中,编程时要特别注意,以避免数值溢出问题,特别是在涉及大数或小数的运算(特指整数)。
### 1.3.2 无符号数的取值范围 ## 3.2 无符号数的取值范围
* 在 C 语言中,`无符号数`unsigned 类型)的取值范围(最大值和最小值)的计算是很容易的,即:将内存中的所有位,设置为 `0` 就是`最小值`,设置为 `1` 就是`最大值`。 * 在 C 语言中,`无符号数`unsigned 类型)的取值范围(最大值和最小值)的计算是很容易的,即:将内存中的所有位,设置为 `0` 就是`最小值`,设置为 `1` 就是`最大值`。
@ -666,7 +670,7 @@ $S_n = a_1 \times \frac{1 - r^n}{1 - r}$
> * `unsinged int`4 个字节)的取值范围是:`[0, 2³² - 1]`。 > * `unsinged int`4 个字节)的取值范围是:`[0, 2³² - 1]`。
> * `unsinged long`8 个字节)的取值范围是:`[0, 2⁶⁴ - 1]`。 > * `unsinged long`8 个字节)的取值范围是:`[0, 2⁶⁴ - 1]`。
### 1.3.3 有符号数的取值范围 ## 3.3 有符号数的取值范围
* 在 C 语言中,`有符号数`signed 类型)在计算机底层是以`补码`的形式存储的(计算的时候,也是以补码的形式进行计算的,并且符号位参与计算);但是,在读取的时候,需要采用`逆向`的转换,即:将补码转换为原码。 * 在 C 语言中,`有符号数`signed 类型)在计算机底层是以`补码`的形式存储的(计算的时候,也是以补码的形式进行计算的,并且符号位参与计算);但是,在读取的时候,需要采用`逆向`的转换,即:将补码转换为原码。
@ -724,7 +728,7 @@ $S_n = a_1 \times \frac{1 - r^n}{1 - r}$
> * `int`4 个字节)的取值范围是:`[-2³², 2³² - 1]`。 > * `int`4 个字节)的取值范围是:`[-2³², 2³² - 1]`。
> * `long`8 个字节)的取值范围是:`[-2⁶⁴, 2⁶⁴ - 1]`。 > * `long`8 个字节)的取值范围是:`[-2⁶⁴, 2⁶⁴ - 1]`。
### 1.3.4 数值溢出 ## 3.4 数值溢出
* 对于`无符号`的数值溢出: * 对于`无符号`的数值溢出:
* 当数据到达最大值的时候,再 `+1` 就会回到无符号数的最小值。 * 当数据到达最大值的时候,再 `+1` 就会回到无符号数的最小值。
@ -796,9 +800,11 @@ int main() {
} }
``` ```
## 1.4 浮点类型
### 1.4.1 概述
# 第四章:浮点类型(⭐)
## 4.1 概述
* 在生活中,我们除了使用`整数`18、25 之外,还会使用到`小数`3.1415926、6.18 等,`小数`在计算机中也被称为`浮点数`(和底层存储有关)。 * 在生活中,我们除了使用`整数`18、25 之外,还会使用到`小数`3.1415926、6.18 等,`小数`在计算机中也被称为`浮点数`(和底层存储有关)。
* `整数`在计算机底层的存储被称为`定点存储`,如下所示: * `整数`在计算机底层的存储被称为`定点存储`,如下所示:
@ -830,7 +836,7 @@ int main() {
> * 科学计数法形式5.12e2e 表示基数 10、5.12E-2E 表示基数 10 > * 科学计数法形式5.12e2e 表示基数 10、5.12E-2E 表示基数 10
> * ③ 在实际开发中,对于浮点类型,建议使用 `double` 类型;如果范围不够,就使用 `long double` 类型。 > * ③ 在实际开发中,对于浮点类型,建议使用 `double` 类型;如果范围不够,就使用 `long double` 类型。
### 1.4.2 格式占位符 ## 4.2 格式占位符
* 对于 `float` 类型的格式占位符,是 `%f` ,默认会保留 `6` 位小数,不足 `6` 位以 `0` 补充;可以指定小数位,如:`%.2f` 表示保留 `2` 位小数。 * 对于 `float` 类型的格式占位符,是 `%f` ,默认会保留 `6` 位小数,不足 `6` 位以 `0` 补充;可以指定小数位,如:`%.2f` 表示保留 `2` 位小数。
* 对于 `double` 类型的格式占位符,是 `%lf` ,默认会保留 `6` 位小数,不足 `6` 位以 `0` 补充;可以指定小数位,如:`%.2lf` 表示保留 `2` 位小数。 * 对于 `double` 类型的格式占位符,是 `%lf` ,默认会保留 `6` 位小数,不足 `6` 位以 `0` 补充;可以指定小数位,如:`%.2lf` 表示保留 `2` 位小数。
@ -923,7 +929,7 @@ int main() {
} }
``` ```
### 1.4.3 字面量后缀 ## 4.3 字面量后缀
* 浮点数字面量默认是 double 类型。 * 浮点数字面量默认是 double 类型。
* 如果需要表示 `float` 类型的字面量,需要后面添加后缀 `f``F`,建议 `F` * 如果需要表示 `float` 类型的字面量,需要后面添加后缀 `f``F`,建议 `F`
@ -950,7 +956,7 @@ int main() {
} }
``` ```
### 1.4.4 类型占用的内存大小(存储空间) ## 4.4 类型占用的内存大小(存储空间)
* 可以通过 `sizeof` 运算符来获取 float、double 以及 long double 类型占用的内存大小(存储空间)。 * 可以通过 `sizeof` 运算符来获取 float、double 以及 long double 类型占用的内存大小(存储空间)。
@ -971,7 +977,7 @@ int main() {
} }
``` ```
### 1.4.5 类型的取值范围 ## 4.5 类型的取值范围
* 可以通过 `#include <float.h>` 来获取类型的取值范围。 * 可以通过 `#include <float.h>` 来获取类型的取值范围。
@ -993,7 +999,7 @@ int main() {
} }
``` ```
### 1.4.6 整数和浮点数的相互赋值 ## 4.6 整数和浮点数的相互赋值
* 在 C 语言中,整数和浮点数是可以相互赋值的,即: * 在 C 语言中,整数和浮点数是可以相互赋值的,即:
* 将一个整数赋值给小数类型,只需要在小数点后面加 0 就可以了。 * 将一个整数赋值给小数类型,只需要在小数点后面加 0 就可以了。
@ -1027,9 +1033,9 @@ int main() {
## 1.5 字符类型 # 第五章:字符类型(⭐)
### 1.5.1 概述 ## 5.1 概述
* 在生活中,我们会经常说:今天天气真 `好`,我的性别是 `女`,我今年 `10` 岁等。像这类数据,在 C 语言中就可以用`字符类型`char来表示。`字符类型`表示`单`个字符,使用单引号(`''`)括起来,如:`'1'`、`'A'`、`'&'`。 * 在生活中,我们会经常说:今天天气真 `好`,我的性别是 `女`,我今年 `10` 岁等。像这类数据,在 C 语言中就可以用`字符类型`char来表示。`字符类型`表示`单`个字符,使用单引号(`''`)括起来,如:`'1'`、`'A'`、`'&'`。
* 但是,在生活中,也许会听到:`你是好人,只是现阶段,我想学习`、`好的啊,我们在一起`等。像这类数据,在 C 语言中就可以用`字符串`String来表示。`字符串类型`表示`多`个字符的集合,使用双引号(`""`)括起来,如:`"1"`、`"A"`、`"&"`、`"我们"`。 * 但是,在生活中,也许会听到:`你是好人,只是现阶段,我想学习`、`好的啊,我们在一起`等。像这类数据,在 C 语言中就可以用`字符串`String来表示。`字符串类型`表示`多`个字符的集合,使用双引号(`""`)括起来,如:`"1"`、`"A"`、`"&"`、`"我们"`。
@ -1055,7 +1061,7 @@ int main() {
| `\\` | 反斜杠 | | `\\` | 反斜杠 |
| ... | | | ... | |
### 1.5.2 格式占位符 ## 5.2 格式占位符
* 在 C 语言中,使用 `%c` 来表示 char 类型。 * 在 C 语言中,使用 `%c` 来表示 char 类型。
@ -1082,7 +1088,7 @@ int main() {
} }
``` ```
### 1.5.3 类型占用的内存大小(存储空间) ## 5.3 类型占用的内存大小(存储空间)
* 可以通过 `sizeof` 运算符来获取 char 类型占用的内存大小(存储空间)。 * 可以通过 `sizeof` 运算符来获取 char 类型占用的内存大小(存储空间)。
@ -1102,7 +1108,7 @@ int main() {
} }
``` ```
### 1.5.4 类型的取值范围 ## 5.4 类型的取值范围
* 可以通过 `#include <limits.h>` 来获取类型的取值范围。 * 可以通过 `#include <limits.h>` 来获取类型的取值范围。
@ -1123,7 +1129,7 @@ int main() {
} }
``` ```
### 1.5.5 字符类型的本质 ## 5.5 字符类型的本质
* 在 C 语言中char 本质上就是一个整数,是 ASCII 码中对应的数字,占用的内存大小是 1 个字节(存储空间),所以 char 类型也可以进行数学运算。 * 在 C 语言中char 本质上就是一个整数,是 ASCII 码中对应的数字,占用的内存大小是 1 个字节(存储空间),所以 char 类型也可以进行数学运算。
@ -1166,7 +1172,7 @@ int main() {
} }
``` ```
### 1.5.6 输出字符方式二(了解) ## 5.6 输出字符方式二(了解)
* 在 C 语言中,除了可以使用 `printf()` 函数输出字符之外,还可以使用 `putchar()`函数输出字符。 * 在 C 语言中,除了可以使用 `printf()` 函数输出字符之外,还可以使用 `putchar()`函数输出字符。
@ -1197,7 +1203,7 @@ int main() {
} }
``` ```
### 1.5.7 初谈字符串(了解) ## 5.7 初谈字符串(了解)
* 在 C 语言中没有专门的字符串类型,是使用`字符数组`来模拟字符串的,即:可以使用字符数组来存储字符串。 * 在 C 语言中没有专门的字符串类型,是使用`字符数组`来模拟字符串的,即:可以使用字符数组来存储字符串。
@ -1248,13 +1254,15 @@ int main() {
} }
``` ```
## 1.6 布尔类型
### 1.6.1 概述
# 第六章:布尔类型(⭐)
## 6.1 概述
* 布尔值用于表示 true、false两种状态通常用于逻辑运算和条件判断。 * 布尔值用于表示 true、false两种状态通常用于逻辑运算和条件判断。
### 1.6.2 早期的布尔类型 ## 6.2 早期的布尔类型
* 在 C 语言标准C89并没有为布尔值单独设置一个数据类型所以在判断真、假的时候使用 `0` 表示 `false`(假),`非 0` 表示 `true`(真)。 * 在 C 语言标准C89并没有为布尔值单独设置一个数据类型所以在判断真、假的时候使用 `0` 表示 `false`(假),`非 0` 表示 `true`(真)。
@ -1284,7 +1292,7 @@ int main() {
} }
``` ```
### 1.6.3 宏定义的布尔类型 ## 6.3 宏定义的布尔类型
* 判断真假的时候,以 `0``false`(假)、`1` 为 `true`(真),并不直观;所以,我们可以借助 C 语言的宏定义。 * 判断真假的时候,以 `0``false`(假)、`1` 为 `true`(真),并不直观;所以,我们可以借助 C 语言的宏定义。
@ -1318,7 +1326,7 @@ int main() {
} }
``` ```
### 1.6.4 C99 标准中的布尔类型 ## 6.4 C99 标准中的布尔类型
* 在 C99 中提供了 `_Bool` 关键字,用于表示布尔类型;其实,`_Bool`类型的值是整数类型的别名,和一般整型不同的是,`_Bool`类型的值只能赋值为 `0``1` 0 表示假、1 表示真),其它`非 0` 的值都会被存储为 `1` * 在 C99 中提供了 `_Bool` 关键字,用于表示布尔类型;其实,`_Bool`类型的值是整数类型的别名,和一般整型不同的是,`_Bool`类型的值只能赋值为 `0``1` 0 表示假、1 表示真),其它`非 0` 的值都会被存储为 `1`
@ -1351,7 +1359,7 @@ int main() {
} }
``` ```
### 1.6.5 C99 标准头文件中的布尔类型(推荐) ## 6.5 C99 标准头文件中的布尔类型(推荐)
* 在 C99 中提供了一个头文件 `<stdbool.h>`,定义了 `bool` 代表 `_Bool``false` 代表 `0` `true` 代表 `1` * 在 C99 中提供了一个头文件 `<stdbool.h>`,定义了 `bool` 代表 `_Bool``false` 代表 `0` `true` 代表 `1`
@ -1401,15 +1409,17 @@ int main() {
} }
``` ```
## 1.7 数据类型转换
### 1.7.1 概述
# 第七章:数据类型转换(⭐)
## 7.1 概述
* 在 C 语言编程中,经常需要对不同类型的数据进行运算,运算前需要先转换为同一类型,再运算。为了解决数据类型不一致的问题,需要对数据的类型进行转换。 * 在 C 语言编程中,经常需要对不同类型的数据进行运算,运算前需要先转换为同一类型,再运算。为了解决数据类型不一致的问题,需要对数据的类型进行转换。
### 1.7.2 自动类型转换(隐式转换) ## 7.2 自动类型转换(隐式转换)
#### 1.7.2.1 运算过程中的自动类型转换 ### 7.2.1 运算过程中的自动类型转换
* 不同类型的数据进行混合运算的时候,会发生数据类型转换,`窄类型会自动转换为宽类型`,这样就不会造成精度损失。 * 不同类型的数据进行混合运算的时候,会发生数据类型转换,`窄类型会自动转换为宽类型`,这样就不会造成精度损失。
@ -1528,7 +1538,7 @@ int main() {
} }
``` ```
#### 1.7.2.2 赋值时的自动类型转换 ### 7.2.2 赋值时的自动类型转换
* 在赋值运算中,赋值号两边量的数据类型不同时,等号右边的类型将转换为左边的类型。 * 在赋值运算中,赋值号两边量的数据类型不同时,等号右边的类型将转换为左边的类型。
* 如果窄类型赋值给宽类型,不会造成精度损失;如果宽类型赋值给窄类型,会造成精度损失。 * 如果窄类型赋值给宽类型,不会造成精度损失;如果宽类型赋值给窄类型,会造成精度损失。
@ -1562,7 +1572,7 @@ int main() {
} }
``` ```
### 1.7.3 强制类型转换 ### 7.2.3 强制类型转换
* 隐式类型转换中的宽类型赋值给窄类型,编译器是会产生警告的,提示程序存在潜在的隐患,如果非常明确地希望转换数据类型,就需要用到强制(或显式)类型转换。 * 隐式类型转换中的宽类型赋值给窄类型,编译器是会产生警告的,提示程序存在潜在的隐患,如果非常明确地希望转换数据类型,就需要用到强制(或显式)类型转换。
* 语法: * 语法:
@ -1597,7 +1607,7 @@ int main(){
} }
``` ```
### 1.7.4 数据类型转换只是临时性的 ### 7.2.4 数据类型转换只是临时性的
* 无论是自动类型转换还是强制类型转换,都是为了本次运算而进行的临时性转换,其转换的结果只会保存在临时的内存空间,并不会改变数据原先的类型或值,如下所示: * 无论是自动类型转换还是强制类型转换,都是为了本次运算而进行的临时性转换,其转换的结果只会保存在临时的内存空间,并不会改变数据原先的类型或值,如下所示:
@ -1628,7 +1638,7 @@ int main() {
> * ① 如果 `total` 变量的`值`或`类型`变化了,那么 `total` 的显示结果,就应该是 `100.00` ,而不是 `100.12` > * ① 如果 `total` 变量的`值`或`类型`变化了,那么 `total` 的显示结果,就应该是 `100.00` ,而不是 `100.12`
> * ② 那么,`price` 的结果,显而易见就应该是 `50.00` ,而不是 `50.06` 了。 > * ② 那么,`price` 的结果,显而易见就应该是 `50.00` ,而不是 `50.06` 了。
### 1.7.5 自动类型转换 VS 强制类型转换 ### 7.2.5 自动类型转换 VS 强制类型转换
* 在 C 语言中,有些数据类型即可以自动类型转换,也可以强制类型转换,如:`int --> double`、`double --> int` 等。但是,有些数据类型只能强制类型转换,不能自动类型转换,如:`void* --> int*` 。 * 在 C 语言中,有些数据类型即可以自动类型转换,也可以强制类型转换,如:`int --> double`、`double --> int` 等。但是,有些数据类型只能强制类型转换,不能自动类型转换,如:`void* --> int*` 。
* 可以自动类型转换的类型一定可以强制类型转换;但是,可以强制类型转换的类型却不一定能够自动类型转换。 * 可以自动类型转换的类型一定可以强制类型转换;但是,可以强制类型转换的类型却不一定能够自动类型转换。
@ -1645,7 +1655,11 @@ int main() {
> * ① 在实际开发中,如果使用 C 语言进行开发,在进行强制类型转换的时候,需要小心谨慎,防止出现一些奇怪的问题,进而导致程序崩溃!!! > * ① 在实际开发中,如果使用 C 语言进行开发,在进行强制类型转换的时候,需要小心谨慎,防止出现一些奇怪的问题,进而导致程序崩溃!!!
> * ② 现代化的高级编程语言Java 等,直接屏蔽了指针。所以,在使用这些编程语言的时候,无需担心进行强制类型转换时,会出现一些奇怪的问题,进而导致程序崩溃!!! > * ② 现代化的高级编程语言Java 等,直接屏蔽了指针。所以,在使用这些编程语言的时候,无需担心进行强制类型转换时,会出现一些奇怪的问题,进而导致程序崩溃!!!
## 1.8 再谈数据类型
# 第八章:再谈数据类型(⭐)
## 8.1 内存和内存地址
* 通过之前的知识我们知道CPU 是直接和内存打交道的CPU 在处理数据的时候会将数据临时存放到内存中。内存那么大CPU 是怎么找到对应的数据的? * 通过之前的知识我们知道CPU 是直接和内存打交道的CPU 在处理数据的时候会将数据临时存放到内存中。内存那么大CPU 是怎么找到对应的数据的?
@ -1667,6 +1681,8 @@ int main() {
![](./assets/18.svg) ![](./assets/18.svg)
## 8.2 变量的定义
* 我们在定义变量的时候,是这么定义的,如下所示: * 我们在定义变量的时候,是这么定义的,如下所示:
```c ```c
@ -1699,4 +1715,3 @@ int num = 10;
![](./assets/19.svg) ![](./assets/19.svg)