2024年10月22日 10:47

This commit is contained in:
许大仙 2024-10-22 02:47:49 +00:00
parent c1a66e04eb
commit 5f459e7c63
130 changed files with 3363 additions and 3473 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 209 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 778 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 794 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: 64 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 49 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 240 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 363 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 237 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 487 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 238 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 489 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 287 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 190 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 444 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 193 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 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

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 220 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 195 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 400 KiB

View File

@ -1,374 +1,110 @@
# 第一章:输入输出模型 # 第一章:相关概念
## 1.1 回顾冯·诺依曼体系结构 ## 1.1 运算符、表达式和操作数
* `冯·诺依曼`体系结构的理论要点如下: * 运算符是一种特殊的符号,用于数据的运算、赋值和比较等。
- ① **存储程序**`程序指令`和`数据`都存储在计算机的内存中,这使得程序可以在运行时修改。 * `表达式`指的是一组运算数、运算符的组合,表达式`一定具有值`,一个变量或一个常量可以是表达式,变量、常量和运算符也可以组成表达式,如:
- ② **二进制逻辑**:所有`数据`和`指令`都以`二进制`形式表示。
- ③ **顺序执行**:指令按照它们在内存中的顺序执行,但可以有条件地改变执行顺序。 ![](./assets/20.svg)
- ④ **五大部件**:计算机由`运算器`、`控制器`、`存储器`、`输入设备`和`输出设备`组成。
- ⑤ **指令结构**:指令由操作码和地址码组成,操作码指示要执行的操作,地址码指示操作数的位置。 * `操作数`指的是`参与运算`的`值`或者`对象`,如:
- ⑥ **中心化控制**计算机的控制单元CPU负责解释和执行指令控制数据流。
![](./assets/21.svg)
## 1.2 运算符的分类
* 根据`操作数`的`个数`,可以将运算符分为:
* 一元运算符(一目运算符)。
* 二元运算符(二目运算符)。
* 三元运算符(三目运算符)。
* 根据`功能`,可以将运算符分为:
* 算术运算符。
* 关系运算符(比较运算符)。
* 逻辑运算符。
* 赋值运算符。
* 逻辑运算符。
* 位运算符。
* 三元运算符。
![img](./assets/1.png)
> [!NOTE] > [!NOTE]
> >
> 上述的组件协同工作,构成了一个完整的计算机系统: > 掌握一个运算符,需要关注以下几个方面
> >
> - `运算器`和`控制器`通常被集成在一起组成中央处理器CPU负责数据处理和指令执行。 > * ① 运算符的含义。
> - `存储器`(内存)保存数据和程序,是计算机运作的基础。 > * ② 运算符操作数的个数。
> - `输入设备`和`输出设备`负责与外界的交互,确保用户能够输入信息并接收计算机的处理结果。 > * ③ 运算符所组成的表达式。
> > * ④ 运算符有无副作用,即:运算后是否会修改操作数的值。
> 直到今天,虽然硬件的发展日新月异,但是现代计算机的硬件理论基础还是《冯·诺依曼体系结构》。
## 1.2 冯·诺依曼体系结构的瓶颈
* 计算机是有性能瓶颈的:如果 CPU 有每秒处理 1000 个服务请求的能力,各种总线的负载能力能达到 500 个, 但网卡只能接受 200个请求而硬盘只能负担 150 个的话,那这台服务器得处理能力只能是 150 个请求/秒,有 85% 的处理器计算能力浪费了,在计算机系统当中,`硬盘`的读写速率已经成为影响系统性能进一步提高的瓶颈。
![](./assets/2.jpg)
* 计算机的各个设备部件的延迟从高到低的排列依次是机械硬盘HDD、固态硬盘SSD、内存、CPU 。
![](./assets/3.png)
* 从上图中我们可以知道CPU 是最快的,一个时钟周期是 0.3 ns ,内存访问需要 120 ns ,固态硬盘访问需要 50-150 us传统的硬盘访问需要 1-10 ms而网络访问是最慢需要 40 ms 以上。
> [!NOTE]
>
> 时间的单位换算如下:
>
> * ① 1 秒 = 1000 毫秒,即 1 s = 1000 ms。
> * ② 1 毫秒 = 1000 微妙,即 1 ms = 1000 us 。
> * ③ 1 微妙 = 1000 纳秒,即 1 us = 1000 ns。
* 如果按照上图,将计算机世界的时间和人类世界的时间进行对比,即:
```txt
如果 CPU 的时钟周期按照 1 秒计算,
那么,内存访问就需要 6 分钟;
那么,固态硬盘就需要 2-6 天;
那么,传统硬盘就需要 1-12 个月;
那么,网络访问就需要 4 年以上。
```
> [!NOTE]
>
> * ① 这就中国古典修仙小说中的“天上一天,地上一年”是多么的相似!!!
> * ② 对于 CPU 来说,这个世界真的是太慢了!!!
* 其实,中国古代中的文人,通常以`蜉蝣`来表示时间的短暂(和其他生物的寿命比),也是类似的道理,即:
```txt
鹤寿千岁,以极其游,蜉蝣朝生而暮死,尽其乐,盖其旦暮为期,远不过三日尔。
--- 出自 西汉淮南王刘安《淮南子》
```
```txt
寄蜉蝣于天地,渺沧海之一粟。 哀吾生之须臾,羡长江之无穷。
挟飞仙以遨游,抱明月而长终。 知不可乎骤得,托遗响于悲风。
--- 出自 苏轼《赤壁赋》
```
> [!NOTE]
>
> * ① 从`蜉蝣`的角度来说,从早到晚就是一生;但是,从`人类`角度来说,从早到晚却仅仅只是一天。
> * ② 这和“天上一天,地上一年”是多么的相似,即:如果`蜉蝣`是`人类`的话,那`我们`就是`仙人`了。
* 存储器的层次结构CPU 中也有存储器,即:寄存器、高速缓存 L1、L2 和 L3如下所示
![img](./assets/4.png)
> [!NOTE]
>
> 上图以层次化的方式,展示了价格信息,揭示了一个真理,即:鱼和熊掌不可兼得。
>
> - ① 存储器越往上速度越快,但是价格越来越贵, 越往下速度越慢,但是价格越来越便宜。
> - ② 正是由于计算机各个部件的速度不同,容量不同,价格不同,导致了计算机系统/编程中的各种问题以及相应的解决方案。
* 正是由于 CPU、内存以及 I/O 设备之间的速度差异,从而导致了计算机的性能瓶颈,即所谓的`“冯·诺依曼体系结构的瓶颈”`。
![](./assets/5.svg)
* 因为 CPU 的处理速度远远快于内存和 I/O 设备导致在等待数据处理和传输的时候CPU 大部分处于空闲状态。就是这种显著的速度差异就导致了计算机的性能瓶颈,限制了整个计算机系统的效率。
> [!NOTE]
>
> * 对于硬件的这种显著的速度差异,我们程序员是无法解决的。
> * 但是,为了平衡三者之间的速度鸿沟,我们可以通过引入`缓冲区`技术,来降低系统的 I/O 次数,降低系统的开销。
* 其实,在硬件上也是有`缓冲区`的CPU 内部集成了缓存,将经常使用到的数据从内存中加载到缓存中。
> [!NOTE]
>
> 对于缓存和内存中数据的同步解决方案会有各种各样的算法LRU 等。
![](./assets/6.svg)
## 1.3 缓冲区
### 1.3.1 如果存在缓冲区,键盘输入的数据是怎么到达程序的?
* 当我们在键盘上输入数据并传递给程序时,通常会经历如下的几个步骤:
* ① `键盘生成输入信号`:当我们在键盘上按下某个键的时候,键盘会将这个动作转换为对应的电信号,传递给键盘控制器。
* ② `键盘控制器发送中断信号`:计算机的`键盘控制器`会检测到按键动作,向 CPU 发送中断请求。
* ③ `CPU 执行中断处理程序`CPU 暂停当前任务,进入中断处理状态,操作系统的中断处理程序接收并处理键盘输入。
* ④ `操作系统将输入存入缓冲区`:键盘输入的数据被存入`内存缓冲区`,操作系统会将这些数据暂时存放在缓冲区中,等待程序从缓冲区中读取数据。
* ⑤ `程序读取数据`:程序通过读取函数从缓冲区读取数据进行处理。
* 其对应的图示,如下所示:
![](./assets/7.png)
> [!IMPORTANT]
>
> 其实C 语言中的 `printf` 函数和 `scanf` 函数,其内部就使用了缓冲区。
>
> * ① 当我们使用 `printf` 函数输出数据的时候,数据并不会立即就写出到输出设备(如:屏幕等)。而是先将其放置到 `stdout 缓冲区`中,然后在满足条件的时候,再从缓冲区中刷新到输出设备。
> * ② 当我们使用 `scanf` 函数输入数据的时候,数据并不会立即就从输入设备中读取(如:键盘等)。而是先将其放置到 `stdin 缓冲区`中,然后在满足条件的时候,再从缓冲区中加载数据。
### 1.3.2 如果没有缓冲区,键盘输入的数据是怎么到达程序的?
* 当我们在键盘上输入数据并传递给程序时,通常会经历如下的几个步骤:
* ① `键盘生成输入信号`:当我们在键盘上按下某个键的时候,键盘会将这个动作转换为对应的电信号,传递给键盘控制器。
* ② `键盘控制器发送中断信号`:键盘控制器检测到按键动作,向 CPU 发送`中断请求`,通知操作系统有输入数据。
* ③ `操作系统处理输入`:操作系统接收到`中断信号`后,立即获取键盘数据并处理。由于没有缓冲区,操作系统必须将数据立即传递给程序。
* ④ `程序直接读取数据`:程序必须在键盘每次输入后立即读取数据,并且处理这个输入,不会有任何数据被暂存或积累。
* 其对应的图示,如下所示:
![](./assets/8.png)
> [!NOTE]
>
> 如果没有缓冲区,键盘输入的数据将无法有效地被程序管理和处理,系统的工作效率会显著下降,具体影响体现在以下几个方面:
>
> * ① `程序与设备的频繁交互`:在没有缓冲区的情况下,程序需要直接与键盘设备进行交互。这意味着每次按键输入,操作系统都必须立即将数据传递给程序处理。这样会带来以下问题:
> * **频繁的 I/O 操作**:每一次键盘输入都会触发一个 I/O 操作,将数据直接传输给程序。程序必须每次都立即响应输入设备,执行读操作,导致程序处理器频繁被中断。
> * **实时响应要求**:程序需要时刻等待并响应输入,哪怕是输入非常小的数据(比如一个字符),程序都必须立即读取并处理。这对程序的设计提出了很高的实时性要求,可能会降低程序的运行效率。
> * ② `处理效率低下`:由于没有缓冲区,程序无法积累多个输入数据再进行批量处理。每一次输入必须立即处理,程序执行的效率会受到影响:
> - **I/O 阻塞**:程序可能会因为等待输入设备的响应而阻塞。没有缓冲区的情况下,程序不能继续执行其他任务,必须等待每一次输入完成后才能继续执行其他操作。
> - **浪费系统资源**:程序频繁地切换到处理 I/O 操作,导致处理器资源被大量占用。在处理较大数据量时,这种方式的效率极低,容易造成资源浪费。
> * ③ `用户体验差`:从用户角度来看,程序对键盘输入的响应会显得非常僵硬,无法处理多个输入操作的积累:
> - **输入延迟**:程序必须实时处理每个键盘输入,用户输入数据的速度一旦超过程序的处理能力,可能导致输入延迟或丢失输入。
> - **无法处理复杂输入**:如果用户需要输入多个字符或进行复杂的输入操作(比如连续输入多个命令),程序可能难以一次性正确处理,因为它只能逐一处理每一个输入,而无法一次性获取多个输入进行批量处理。
### 1.3.3 缓冲区的好处
* 使用缓冲区的好处:`减少了 I/O 操作的频率,降低了系统资源的消耗,提高了系统的性能,提升了用户的使用体验`。
### 1.3.4 缓冲区是如何提高 I/O 操作的频率?
* 对于 C 语言中的 `printf` 函数和 `scanf` 函数,其功能如下:
* `printf` 函数:将程序中的数据输出到外部设备(如:显示器)中。
* `scanf` 函数:从外部设备(如:键盘)中读取数据到程序中。
* 这些都是非常典型的 I/O 操作,并且 I/O 过程的效率也是很低的。除了硬件性能本身的差异外I/O 操作的复杂性也是非常重要的因素,每次 I/O 操作都会带来一些固定的开销,如:
* ① 每次 I/O 操作都需要设备初始化和响应等待。
* ② 操作系统管理 I/O 请求,涉及中断处理和上下文切换,这些都消耗了大量时间。
* ③ 应用从用户态切换到内核态的系统调用也会带来额外的时间开销。I/O 操作普遍涉及系统调用)
* ④ ...
* 如果每输入一个字符或每输出一个字符都需要进行一次完整的 I/O 操作,那么这些固定的开销会迅速积累,进而导致系统的性格显著下降。
* 硬件层面的效率低下,我们没有办法通过软件层面的优化去解决。但对于这些大量的固定开销,我们可以通过`缓冲区`来进行效率优化。
> [!IMPORTANT]
>
> * ① 缓冲区的主要目的是暂时存储数据,然后在适当的时机一次性进行大量的 I/O 操作。
> * ② 这样,多个小的 I/O 请求可以被组合成一个大 I/O 的请求,有效地分摊了固定开销,并显著提高了总体性能。
* 对于 `scanf` 函数而言,当用户通过键盘输入字符的时候,这些输入的字符首先被保存在 `stdin` 的缓冲区中,`当满足某个触发条件后`,才传递给程序处理,这样就减少了总的 I/O 次数,提高了效率。
* 对于 `printf` 函数而言,输出的内容首先会保存到 `stdout` 的缓冲区中,`当满足某个触发条件后`,这些内容会一次性输出并显示到屏幕,降低了与显示设备的交互频率。
> [!NOTE]
>
> * ① 如果你还不能理解,就可以将 I/O 操作,看做是搬家。对于搬家而言,需要搬运东西的总量是固定的,搬一趟的时间也是差不多的。我们当然希望:一次性搬的东西尽量多,搬运的次数尽量少,这样总耗时就少。
> * ② 不使用缓冲区,就类似每次搬家只能手提一个东西,需要频繁的往返。而使用缓冲区,就好比我们使用一个小推车,可以一次性的搬运多个东西,极大的提高了效率。
### 1.3.5 缓冲区的分类
* 从上述的内容中,我们可以明确到看到缓冲区有一个显著的特点:`当满足某个触发条件后,程序会开始对缓冲区的数据执行输入或输出操作`。而这种`满足某个条件,就触发数据传输`的行为,就称为`缓冲区的自动刷新`机制。
* 基于这种自动刷新的触发条件的不同,我们可以将缓冲区划分为以下三种类型:
* ① `全缓冲(满缓冲)`:仅当缓冲区达到容量上限时,缓冲区才会自动刷新,并开始处理数据。否则,数据会持续积累在缓冲区中直到缓冲区满触发自动刷新。`文件操作`的输出缓冲区便是这种类型的经典例子。
* ② `行缓冲`:缓冲区一旦遇到换行符,缓冲区就会自动刷新,所有数据都会被传输。`stdout` 缓冲区就是典型的行缓冲区。
* ③ `无缓冲(不缓冲)`:在此模式下,数据不经过中间的缓冲步骤,每次的输入或输出操作都会直接执行。这种方法适用于需要快速、实时响应的场合。`stderr`(标准错误输出)就是这种方式,它经常被用来即时上报错误信息。
* 之前,我们经常会在代码中,会加入以下的代码,其实就是为了让行缓冲变为无缓冲,如下所示:
```c {6}
#include <stdI/O.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
int num = 0;
printf("请输入一个整数:");
scanf("%d", &num);
printf("你输入的整数是:%d\n", num);
return 0;
}
```
* 如果不加入上述的代码,将会这样显示:
![](./assets/9.gif)
* 但是,一旦我们加入了上述的代码,将会这样显示:
![](./assets/10.gif)
> [!NOTE]
>
> * ① setbuf 是 C 语言标准库中的一个函数,用于设置文件流的缓冲区。它允许程序员控制 I/O 操作的缓冲行为,从而影响文件流(如 `stdin`、`stdout` 或文件指针 `FILE *` 类型)的效率和顺序。
> * ② 其定义,如下所示:
>
> ```c
> /**
> * @param stream 缓冲区的文件流
> * @param buf 用户提供的缓冲区,如果为 NULL就是禁用缓冲
> */
> void setbuf(FILE *stream, char *buf);
> ```
> * ③ 不同的编译器和开发环境可能会对输出缓冲进行特殊设置,尤其是在调试模式下,以便提供更好的调试体验,例如:微软的 MSVC 在 debug 模式下即使没有换行符printf 函数的输出通常也会立即显示在控制台上。这种行为是为了帮助程序员更有效地调试程序即时看到他们的输出而不需要等待缓冲区刷新条件。但是遗憾的是GCC 在 debug 模式中,并没有这么做!!!
> [!IMPORTANT]
>
> * ① 无论是哪种类型的缓冲区,当缓冲区满了时,都会触发自动刷新。
>
> * 全缓冲区:唯一的自动刷新条件是缓冲区满。
>
> * 行缓冲区:除了缓冲区满导致的自动刷新,还有遇到换行符的自动刷新机制。
>
> * ② 手动刷新:大多数缓冲区提供了手动刷新的机制,如:使用 `fflush` 函数来刷新 stdout 缓冲区,也可以使用 `setbuf` 函数来禁用缓冲区。
> * ③ `输出缓冲区中的数据需要刷新才能输出到目的地,但输入缓冲区通常不需要刷新,强制刷新输入缓冲区往往会引发未定义行为。`
> * ④ 当程序执行完毕main函数返回缓冲区通常会自动刷新除此之外还有一些独特的机制也可以刷新缓冲区。但这些机制可能因不同的编译器或平台而异不能作为常规手段。`强烈建议依赖手动或者常规自动刷新的机制来完成缓冲区的刷新。`
# 第二章:printf 函数 # 第二章:算术运算符
## 2.1 概述 ## 2.1 概述
* printf 函数的核心作用就是将各种数据类型的数据转换为字符的形式输出到 `stdout` 缓冲区中。 * 算术运算符是对数值类型的变量进行运算的,如下所示:
* 语法:
```c | 运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 |
extern int printf (const char *format, ...); | ------ | ------------ | ---------- | ------------------------ | ------ |
``` | `+` | 正号 | 1 | 操作数本身 | ❎ |
| `-` | 负号 | 1 | 操作数符号取反 | ❎ |
| `+` | 加号 | 2 | 两个操作数之和 | ❎ |
| `-` | 减号 | 2 | 两个操作数之差 | ❎ |
| `*` | 乘号 | 2 | 两个操作数之积 | ❎ |
| `/` | 除号 | 2 | 两个操作数之商 | ❎ |
| `%` | 取模(取余) | 2 | 两个操作数相除的余数 | ❎ |
| `++` | 自增 | 1 | 操作数自增前或自增后的值 | ✅ |
| `--` | 自减 | 1 | 操作数自减前或自减后的值 | ✅ |
> [!NOTE] > [!NOTE]
> >
> * ① format 参数是`格式化字符串`,常见的格式占位符有 `%d`、`%f` 等。 > 自增和自减:
> * ② printf 函数和 scanf 函数只需要大致了解一下用法,不比深究。
> * ③ 在实际开发中,如果我们使用 Qt 开发,或使用 C++ 作为服务器开发,会有更高级的输入输出功能,如:使用 C++ 的标准输出流 `std::cout``std::cin`,或者直接使用日志库(如:`spdlog`、`glog` 等)来处理日志和调试输出。
* printf 函数的语法规则,如下所示:
![](./assets/11.svg)
> [!NOTE]
> >
> * ① 对于 format 参数中的非格式化字符串普通字符printf 函数会将其作为普通字符原封不动的进行显示,如:`我今年 岁`。 > * ① 自增、自减运算符可以写在操作数的前面也可以写在操作数后面,不论前面还是后面,对操作数的副作用是一致的。
> * ② 对于 format 参数中的格式化字符串,即:以 `%`开头的字符,会和后面输出列表中的字符一一匹配,然后将匹配到的字符替换对应的格式化字符,如:`我今年%d岁`中的`%d`会被替换为`18` 。 > * ② 自增、自减运算符在前在后,对于表达式的值是不同的。 如果运算符在前,表达式的值是操作数自增、自减之后的值;如果运算符在后,表达式的值是操作数自增、自减之前的值。
> * ③ `变量前++`:变量先自增 1 ,然后再运算;`变量后++`:变量先运算,然后再自增 1 。
> * ④ `变量前--`:变量先自减 1 ,然后再运算;`变量后--`:变量先运算,然后再自减 1 。
> * ⑤ 对于 `i++``i--` 各种编程语言的用法和支持是不同的例如C/C++、Java 等完全支持Python 压根一点都不支持Go 语言虽然支持 `i++``i--` ,却只支持这些操作符作为独立的语句,并且不能嵌入在其它的表达式中。
## 2.2 应用示例
* 示例:正号和负号
* 示例:
```c ```c
#include <stdio.h> #include <stdio.h>
int main() { int main() {
// 禁用 stdout 缓冲区 int x = 12;
setbuf(stdout, nullptr); int x1 = -x, x2 = +x;
// 声明变量并赋值 int y = -67;
int num = 18; int y1 = -y, y2 = +y;
// 使用输出语句,将变量 num 的值输出,其中 %d 表示输出的是整数 printf("x1=%d, x2=%d \n", x1, x2); // x1=-12, x2=12
printf("我今年%d岁\n", num); printf("y1=%d, y2=%d \n", y1, y2); // y1=67, y2=-67
return 0; return 0;
} }
``` ```
## 2.2 格式占位符的转换说明
* 语法:
```c
%[标志][字段宽度][.精度][长度]说明符
```
> [!IMPORTANT]
>
> * ① `%` 是`格式占位符`的`开头`,是必不可少的,其余部分可以省略。
> * ② `说明符`是`格式占位符`的`结尾`,是必不可少的,其余部分可以省略。
>
> | 格式符 | 说明 |
> | ---------- | ------------------------------------------------------------ |
> | `d``i` | 表示有符号的十进制整数。 |
> | `u` | 表示无符号的十进制整数。 |
> | `o` | 表示无符号的八进制整数。 |
> | `x` | 表示无符号的十六进制整数,使用小写字母(例如:`a-f`)。 |
> | `X` | 表示无符号的十六进制整数,使用大写字母(例如:`A-F`)。 |
> | `f` | 浮点数(普通浮点数表示) |
> | `e` | 强制用科学计数法显示此浮点数使用小写的“e”表示10的幂次。 |
> | `E` | 强制用科学计数法显示此浮点数使用大写的“E”表示10的幂次。 |
> | `g` | 选择最合适的表示方式,浮点数或科学记数法。<br>当选择使用科学计数法显示此浮点数时使用小写的“e”表示10的幂次。 |
> | `G` | 选择最合适的表示方式,浮点数或科学记数法。<br/>当选择使用科学计数法显示此浮点数时使用大写的“E”表示10的幂次。 |
> | `c` | 字符 |
> | `s` | 字符串 |
> | `p` | 指针 |
> [!NOTE]
>
> * ① `[标志]`用于决定一些特殊的格式,如:
> * `-`:左对齐输出。如果没有该标志,默认是右对齐输出。
> * `+`:输出正负号。对于正数,会输出 `+`;对于负数,会输出 `-`
>
> * ② `[字段宽度]`用于指定输出的最小字符宽度,但不会导致截断数据:
> * 如果输出的字符,宽度小于指定的宽度,那么输出的值将会按照指定的**`[标志]`**来进行填充。若标志位没有 0 ,则会填充空格。
> * 如果输出的字符,宽度大于指定的宽度,那么 printf 函数并不会截断,而是完全输出所有字符。
> * ③ `[.精度]`定义打印的精度:
> * 对于整数,表示要输出的最小位数,若位数不足则左侧填充 0 。
> * 对于浮点数,表示要在小数点后面打印的位数。
> * 当有效数字不足时,会自行在后面补 0 。
> * 当有效位数超出时,会截断保留指定的有效位数。这个过程一般会遵守 "四舍五入" 的原则。
> * 但由于浮点数存储的固有精度问题,某些数值可能不能完美表示,导致结果中的数字稍有偏差。
> * 需要注意的是,在不指定`[.精度]`的情况下,浮点数默认显示 6 位小数,多的部分舍弃,不够的话,会在后面补 0 。
> * ④ `[长度]`主要描述参数的数据类型或大小。常见的长度修饰符有:
>
> | 长度修饰符 | 说明 |
> | ------------------ | ------------------------------------------------------------ |
> | `h` | 与整数说明符一起使用,表示 short 类型。 |
> | `l (小写的 L)` | 通常与整数或浮点数说明符一起使用,表示 long对于整数或 double对于浮点数。 |
> | `ll (两个小写的L)` | 与整数说明符一起使用,表示 long long 类型的整数。 |
> | `L (大写的L)` | 与浮点数说明符一起使用,表示 long double 。 |
* 示例:加、减、乘、除(整数之间做除法时,结果只保留整数部分而舍弃小数部分)、取模
* 示例:
```c ```c
#include <stdio.h> #include <stdio.h>
int main() { int main() {
// 禁用 stdout 缓冲区 int a = 5;
setbuf(stdout, nullptr); int b = 2;
printf("|%4f|\n", 3.14159f); printf("%d + %d = %d\n", a, b, a + b); // 5 + 2 = 7
printf("|%10f|\n", 3.14159f); printf("%d - %d = %d\n", a, b, a - b); // 5 - 2 = 3
printf("|%.4f|\n", 3.14159f); printf("%d × %d = %d\n", a, b, a * b); // 5 × 2 = 10
printf("|%4.1f|\n", 3.14159f); printf("%d / %d = %d\n", a, b, a / b); // 5 / 2 = 2
printf("|%04.1f|\n", 3.14159f); printf("%d %% %d = %d\n", a, b, a % b); // 5 % 2 = 1
printf("|% 4.1f|\n", 3.14159f);
printf("|%-4.1f|\n", 3.14159f);
printf("|%+4.1f|\n", 3.14159f);
return 0; return 0;
} }
@ -376,184 +112,89 @@ int main() {
* 示例:取模(运算结果的符号与被模数也就是第一个操作数相同。)
```c
#include <stdio.h>
int main() {
int res1 = 10 % 3;
printf("10 %% 3 = %d\n", res1); // 10 % 3 = 1
int res2 = -10 % 3;
printf("-10 %% 3 = %d\n", res2); // -10 % 3 = -1
int res3 = 10 % -3;
printf("10 %% -3 = %d\n", res3); // 10 % -3 = 1
int res4 = -10 % -3;
printf("-10 %% -3 = %d\n", res4); // -10 % -3 = -1
return 0;
}
```
* 示例:自增和自减
```c
#include <stdio.h>
int main() {
int i1 = 10, i2 = 20;
int i = i1++;
printf("i = %d\n", i); // i = 10
printf("i1 = %d\n", i1); // i1 = 11
i = ++i1;
printf("i = %d\n", i); // i = 12
printf("i1 = %d\n", i1); // i1 = 12
i = i2--;
printf("i = %d\n", i); // i = 20
printf("i2 = %d\n", i2); // i2 = 19
i = --i2;
printf("i = %d\n", i); // i = 18
printf("i2 = %d\n", i2); // i2 = 18
return 0;
```
* 示例: * 示例:
```c ```c
#include <stdio.h> #include <stdio.h>
/*
随意给出一个整数,打印显示它的个位数,十位数,百位数的值。
格式如下:
数字xxx的情况如下
个位数:
十位数:
百位数:
例如:
数字153的情况如下
个位数3
十位数5
百位数1
*/
int main() { int main() {
// 禁用 stdout 缓冲区 int num = 153;
setbuf(stdout, nullptr);
int i = 40; int bai = num / 100;
float x = 839.21f; int shi = num % 100 / 10;
int ge = num % 10;
printf("|%d|%5d|%-5d|%5.3d|\n", i, i, i, i); printf("百位为:%d \n", bai);
printf("|%f|%10f|%10.2f|%-10.2f|\n", x, x, x, x); printf("十位为:%d \n", shi);
printf("个位为:%d \n", ge);
return 0;
}
```
## 2.3 格式占位符中的特殊符号 %
* 在格式占位符中 `%`用于表示转换的开头。如果我们也希望打印一个 `%`,就可以使用 `%%` 来表示一个 `%`
* 示例:
```c {11}
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
int progress = 50;
// 下载进度: 50%
printf("下载进度: %d%%\n", progress);
return 0;
}
```
## 2.4 格式占位符中的特殊符号 *
* 如果我们希望变量在程序运行期间能够打印小数点后的位置以及打印结果的总宽度,就可以在格式占位符中通过 * 来代替。
* 示例:
```c {11}
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
int width = 5;
int point = 2;
printf("|%*.*f|", width, point, 3.1415); // | 3.14|
return 0;
}
```
## 2.5 格式占位符中的 %f 和 %lf
* 格式占位符 `%f` 是用来输出 `float` 类型的数据的,而 格式占位符 `%lf` 是用来输出 `double` 类型的数据的。
> [!IMPORTANT]
>
> * ① `%f``%lf` 是完全等价的。
> * ② 在 C99 之后的标准中,当使用 printf 函数打印浮点数的时候,不管是 float 还是 double 都会自动提升到 double 来进行处理。
> * ③ 仅限于 printf 函数scanf 函数没有这样的特点scanf 函数中的 %f 和 %lf 是不一样的。
* 示例:
```c {10-11}
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
double num = 123.456;
printf("使用%%f打印的结果是: %f\n", num);
printf("使用%%lf打印的结果是: %lf\n", num);
return 0;
}
```
## 2.6 printf 函数中的返回值
* 对于 printf 函数其实是有返回值的,如下所示:
```c
extern int printf (const char *format, ...);
```
> [!NOTE]
>
> * ① 如果输出成功,将返回函数实际输出的字符总数。并且当输出成功时,返回值是一个非负数。
> * ② 如果输出失败,返回值就是一个负数。
> * ③ 在实际开发中printf 函数的返回值比较少被接受处理。
* 示例:
```c {8,11}
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
int ret = printf("hello\n");
printf("ret = %d\n", ret); // 正常输出了6个字符所以返回值是6
int ret2 = printf("");
printf("ret2 = %d\n", ret2); // 正常输出了0个字符所以返回值是0
return 0;
}
```
## 2.7 行缓冲注意事项
* printf 函数将数据输出到 stdout 的行缓冲区,但要将这些数据真正展示到外部设备(如屏幕),则需依靠 stdout 的自动刷新机制。
> [!NOTE]
>
> 为了增加输出的实时性和可预测性,有如下的常见策略:
>
> * ① 输出字符串的末尾添加换行符 `"\n"` ,这样可以立即触发缓冲区的刷新。
> * ② 使用 setbuf 函数禁用 stdout 的行缓冲区。
> * ③ 使用 fflush 函数手动刷新 stdout 的行缓冲区。
> * ④ ...
>
> 本人选择的是第 ② 种方案;但是,如果你选择第 ① 种方案,那么应该在不影响程序逻辑的前提下。
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
int chinese, math, english;
float average;
printf("请输入语文成绩:");
scanf("%d", &chinese);
printf("请输入数学成绩:");
scanf("%d", &math);
printf("请输入英语成绩:");
scanf("%d", &english);
average = (chinese + math + english) / 3.0;
printf("平均成绩为:%.2f\n", average);
return 0; return 0;
} }
@ -561,22 +202,26 @@ int main() {
# 第三章:scanf 函数 # 第三章:关系运算符(比较运算符)
## 3.1 概述 ## 3.1 概述
* scanf 函数的核心作用就是从 `stdin 缓冲区`读取字符形式的数据,并将其转换为特定类型的数据。 * 常见的关系运算符,如下所示:
* 语法:
```c | 运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 |
extern int scanf (const char *__restrict __format, ...) | ------ | -------- | ---------- | ---------------- | ------ |
``` | `==` | 相等 | 2 | 0 或 1 | ❎ |
| `!=` | 不相等 | 2 | 0 或 1 | ❎ |
| `<` | 小于 | 2 | 0 或 1 | ❎ |
| `>` | 大于 | 2 | 0 或 1 | ❎ |
| `<=` | 小于等于 | 2 | 0 或 1 | ❎ |
| `>=` | 大于等于 | 2 | 0 或 1 | ❎ |
> [!NOTE] > [!NOTE]
> >
> * ① scanf 函数和 printf 函数最大的不同就是,在参数列表中中的参数是变量的地址,即:将读取到的值存放在哪个地址。 > * ① C 语言中,没有严格意义上的布尔类型,可以使用 0 或 1表示布尔类型的值
> * ② 也可以认为scanf 函数的格式是:`scanf(格式化字符串, &变量1, &变量2, ...);`,但是变量前面的 `&` 在某些情况下是可以省略的。 > * ② 不要将 `==` 写成 `=``==` 是比较运算符,而 `=` 是赋值运算符
> * ③ 对于 scanf 函数中的格式化字符串,除了格式占位符之外,通常不需要普通字符 > * ③ `>=``<=`含义是只需要满足 `大于或等于`、`小于或等于`其中一个条件,结果就返回真
@ -587,106 +232,53 @@ extern int scanf (const char *__restrict __format, ...)
int main() { int main() {
// 禁用 stdout 缓冲区 int a = 8;
setbuf(stdout, nullptr); int b = 7;
int chinese, math, english; printf("a > b 的结果是:%d \n", a > b); // a > b 的结果是1
float average; printf("a >= b 的结果是:%d \n", a >= b); // a >= b 的结果是1
printf("a < b 的结果是%d \n", a < b); // a < b 的结果是0
printf("请输入语文成绩:"); printf("a <= b 的结果是:%d \n", a <= b); // a <= b 的结果是0
scanf("%d", &chinese); printf("a == b 的结果是:%d \n", a == b); // a == b 的结果是0
printf("a != b 的结果是:%d \n", a != b); // a != b 的结果是1
printf("请输入数学成绩:");
scanf("%d", &math);
printf("请输入英语成绩:");
scanf("%d", &english);
average = (chinese + math + english) / 3.0;
printf("平均成绩为:%.2f\n", average);
return 0; return 0;
} }
``` ```
## 3.2 格式占位符的转换说明 # 第四章:逻辑运算符
* 语法: ## 4.1 概述
```c * 常见的逻辑运算符,如下所示:
%[*][字段宽度][长度]说明符
```
> [!IMPORTANT] | 运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 |
> | ------ | ------ | ---------- | ---------------- | ------ |
> * ① `%` 是`格式占位符`的`开头`,是必不可少的,其余部分可以省略。 | `&&` | 逻辑与 | 2 | 0 或 1 | ❎ |
> * ② `说明符`是`格式占位符`的`结尾`,是必不可少的,其余部分可以省略。 | `\|\|` | 逻辑或 | 2 | 0 或 1 | ❎ |
> | `!` | 逻辑非 | 2 | 0 或 1 | ❎ |
> | 格式符 | 说明 |
> | ------------ | ------------------------------------------------------------ |
> | `d` | 表示有符号的十进制整数。 |
> | `i` | `scanf` 的 i 会自动判断输入的整数的进制,支持八进制、十进制和十六进制。<br>`scanf` 中的 i 和 printf 中的 i 不一样。 |
> | `u` | 表示无符号的十进制整数。 |
> | `o` | 表示无符号的八进制整数。 |
> | `x` | 表示无符号的十六进制整数,使用小写字母(例如:`a-f`)。 |
> | `X` | 表示无符号的十六进制整数,使用大写字母(例如:`A-F`)。 |
> | `f` | 浮点数(普通浮点数表示) |
> | `e` | 强制用科学计数法显示此浮点数使用小写的“e”表示10的幂次。 |
> | `E` | 强制用科学计数法显示此浮点数使用大写的“E”表示10的幂次。 |
> | `g` | 选择最合适的表示方式,浮点数或科学记数法。<br>当选择使用科学计数法显示此浮点数时使用小写的“e”表示10的幂次。 |
> | `G` | 选择最合适的表示方式,浮点数或科学记数法。<br/>当选择使用科学计数法显示此浮点数时使用大写的“E”表示10的幂次。 |
> | `c` | 字符 |
> | `s` | 字符串 |
> | `p` | 指针 |
> | `%[字符集]` | 告诉`scanf`只接受和存储来自指定字符集的字符。<br>例如:`%[abc]`将只读取 'a'、'b' 或 'c'字符,其他的字符将导致读取停止。 |
> | `%[^字符集]` | 这是扫描集的否定形式,告诉`scanf`接受和存储除了指定字符集之外的所有字符。<br>例如:`%[^abc]`将读取除了'a'、'b', 和 'c'之外的所有字符,直到遇到这三个字符中的任何一个为止。 |
* 逻辑运算符提供逻辑判断功能,用于构建更复杂的表达式,如下所示:
| a | b | a && b | a \|\| b | !a |
* 示例: | ------- | ------- | ------- | -------- | ------- |
| 1 | 1 | 1 | 1 | 0 |
```c | 1 | 0 | 0 | 1 | 0 |
#include <stdio.h> | 0 | 1 | 0 | 1 | 1 |
| 0 | 0 | 0 | 0 | 1 |
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
int chinese, math, english;
float average;
printf("请输入语文成绩:");
scanf("%d", &chinese);
printf("请输入数学成绩:");
scanf("%d", &math);
printf("请输入英语成绩:");
scanf("%d", &english);
average = (chinese + math + english) / 3.0;
printf("平均成绩为:%.2f\n", average);
return 0;
}
```
## 3.3 scanf 函数的工作原理
* scanf 函数本质上是一个`模式匹配`函数,试图将 `stdin` 缓冲区中的字符和格式字符串进行匹配。其会从左到右依次匹配格式字符串中的每一项:
* 如果匹配成功,那么 scanf 函数会继续处理格式字符串的剩余部分。
* 如果匹配失败,那么 scanf 函数将不再处理格式字符串的剩余部分,会立即返回。
* 除此之外scanf 函数的转换说明符大都默认忽略前置的空白字符,这样的设计让输入对用户更好友好,比如:
* `%d` 忽略前置的`空白字符` (包括空格符、水平和垂直制表符、换页符和换行符),然后匹配十进制的有符号整数。
* `%f` 忽略前置的`空白字符`(包括空格符、水平和垂直制表符、换页符和换行符),,然后匹配浮点数。
* ...
> [!NOTE] > [!NOTE]
> >
> 在实际开发中scanf 函数最常用的格式字符串是 `%d,%d` 或者 `%d %d` > * ① 对于逻辑运算符来说,任何`非零值`都表示`真``零值`表示`假`,如:`5 || 0` 返回 `1` `5 && 0` 返回 `0`
> * ② 逻辑运算符的理解:
> * `&&` 的理解就是:`两边条件,同时满足`。
> * `||`的理解就是:`两边条件,二选一`。
> * `!` 的理解就是:`条件取反`。
> * ③ 短路现象:
> * 对于 `a && b` 操作来说,当 a 为假(或 0 )时,因为 `a && b` 结果必定为 0所以不再执行表达式 b。
> * 对于 `a || b` 操作来说,当 a 为真(或非 0 )时,因为 `a || b` 结果必定为 1所以不再执行表达式 b。
## 4.2 应用示例
* 示例: * 示例:
@ -695,73 +287,332 @@ int main() {
int main() { int main() {
int num; int a = 0;
int b = 0;
printf("请输入一个整数:"); printf("请输入整数a的值");
scanf("%d", &num); scanf("%d", &a);
printf("请输入整数b的值");
scanf("%d", &b);
int absNum; if (a > b) {
printf("%d > %d", a, b);
if (num < 0) { } else if (a < b) {
absNum = -num; printf("%d < %d", a, b);
} else { } else {
absNum = num; printf("%d = %d", a, b);
} }
printf("%d的绝对值是%d", num, absNum);
return 0; return 0;
} }
``` ```
## 3.4 录入字符数据的特殊性
* scanf 函数用 `%c` 格式占位符来读取单个字符时,并不会跳过空白字符,%c 会读取输入的下一个字符,无论它是什么,包括空白字符。
> [!IMPORTANT]
>
> 在录入字符时,尤其是一行录入多个数据且包含输入字符时,一定要在转换说明前面留出一个空格,以匹配可能的空格。
* 示例: * 示例:
```c {12} ```c
#include <stdio.h> #include <stdio.h>
// 短路现象
int main() { int main() {
// 禁用 stdout 缓冲区 int i = 0;
setbuf(stdout, nullptr); int j = 10;
if (i && j++ > 0) {
char ch; printf("床前明月光\n"); // 这行代码不会执行
int num; } else {
printf("我叫郭德纲\n");
printf("请输入一个数字以及一个字符: "); }
scanf("%d %c", &num, &ch); // 注意 %c 前的空格 printf("%d \n", j); //10
printf("你输入的数字是: %d\n", num);
printf("你输入的字符是: %c\n", ch);
return 0; return 0;
} }
``` ```
## 3.5 scanf 函数的返回值
* 对于 scanf 函数其实是有返回值的,如下所示:
* 示例:
```c ```c
extern int scanf (const char *__restrict __format, ...) #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;
}
```
# 第五章:赋值运算符
## 5.1 概述
* 常见的赋值运算符,如下所示:
| 运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 |
| ------ | ------------ | ---------- | ---------------- | ------ |
| `==` | 赋值 | 2 | 左边操作数的值 | ✅ |
| `+=` | 相加赋值 | 2 | 左边操作数的值 | ✅ |
| `-=` | 相减赋值 | 2 | 左边操作数的值 | ✅ |
| `*=` | 相乘赋值 | 2 | 左边操作数的值 | ✅ |
| `/=` | 相除赋值 | 2 | 左边操作数的值 | ✅ |
| `%=` | 取余赋值 | 2 | 左边操作数的值 | ✅ |
| `<<=` | 左移赋值 | 2 | 左边操作数的值 | ✅ |
| `>>=` | 右移赋值 | 2 | 左边操作数的值 | ✅ |
| `&=` | 按位与赋值 | 2 | 左边操作数的值 | ✅ |
| `^=` | 按位异或赋值 | 2 | 左边操作数的值 | ✅ |
| `\|=` | 按位或赋值 | 2 | 左边操作数的值 | ✅ |
> [!NOTE]
>
> * ① 赋值运算符的第一个操作数(左值)必须是变量的形式,第二个操作数可以是任何形式的表达式。
> * ② 赋值运算符的副作用针对第一个操作数。
## 5.2 应用示例
* 示例:
```c
#include <stdio.h>
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;
}
```
# 第六章:位运算符(了解)
## 6.1 概述
* C 语言提供了一些位运算符能够让我们操作二进制位bit
* 常见的位运算符,如下所示。
| 运算符 | 描述 | 操作数个数 | 运算规则 | 副作用 |
| ------ | ---------- | ---------- | ------------------------------------------------------------ | ------ |
| `&` | 按位与 | 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]
>
> 操作数在进行位运算的时候,以它的补码形式计算!!!
## 6.2 输出二进制位
* 在 C 语言中,`printf` 是没有提供输出二进制位的格式占位符的;但是,我们可以手动实现,以方便后期操作。
* 示例:
```c
#include <stdio.h>
/**
* 获取指定整数的二进制表示
* @param num 整数
* @return 二进制表示的字符串,不包括前导的 '0b' 字符
*/
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() {
int a = 17;
int b = -12;
printf("整数 %d 的二进制表示:%s \n", a, getBinary(a));
printf("整数 %d 的二进制表示:%s \n", b, getBinary(b));
return 0;
}
```
## 6.3 按位与
* 按位与 `&` 的运算规则是:如果二进制对应的位上都是 1 才是 1 ,否则为 0 ,即:
* `1 & 1` 的结果是 `1`
* `1 & 0` 的结果是 `0`
* `0 & 1` 的结果是 `0`
* `0 & 0` 的结果是 `0`
* 示例:`9 & 7 = 1`
![](./assets/22.svg)
* 示例:`-9 & 7 = 7`
![](./assets/23.svg)
## 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)
## 6.5 按位异或
* 按位与 `^` 的运算规则是:如果二进制对应的位上一个为 1 一个为 0 就为 1 ,否则为 0 ,即:
* `1 ^ 1` 的结果是 `0`
* `1 ^ 0` 的结果是 `1`
* `0 ^ 1` 的结果是 `1`
* `0 ^ 0` 的结果是 `0`
> [!NOTE]
>
> 按位异或的场景有:
>
> * ① 交换两个数值:异或操作可以在不使用临时变量的情况下交换两个变量的值。
> * ② 加密或解密:异或操作用于简单的加密和解密算法。
> * ③ 错误检测和校正异或操作可以用于奇偶校验位的计算和检测错误RAID-3 以及以上)。
> * ……
* 示例:`9 ^ 7 = 14`
![](./assets/26.svg)
* 示例:`-9 ^ 7 = -16`
![](./assets/27.svg)
## 6.6 按位取反
* 运算规则:如果二进制对应的位上是 1则结果为 0如果是 0 ,则结果为 1 。
* `~0` 的结果是 `1`
* `~1` 的结果是 `0`
* 示例:`~9 = -10`
![](./assets/28.svg)
* 示例:`~-9 = 8`
![](./assets/29.svg)
## 6.7 二进制左移
* 在一定范围内,数据每向左移动一位,相当于原数据 × 2。正数、负数都适用
* 示例:`3 << 4 = 48` 3 × 2^4
![](./assets/30.svg)
* 示例:`-3 << 4 = -48` -3 × 2 ^4
![](./assets/31.svg)
## 6.8 二进制右移
* 在一定范围内,数据每向右移动一位,相当于原数据 ÷ 2。正数、负数都适用
> [!NOTE]
>
> * ① 如果不能整除,则向下取整。
> * ② 右移运算符最好只用于无符号整数,不要用于负数。因为不同系统对于右移后如何处理负数的符号位,有不同的做法,可能会得到不一样的结果。
* 示例:`69 >> 4 = 4` 69 ÷ 2^4
![](./assets/32.svg)
* 示例:`-69 >> 4 = -5` -69 ÷ 2^4
![](./assets/33.svg)
# 第七章:三元运算符
## 7.1 概述
* 语法:
```c
条件表达式 ? 表达式1 : 表达式2 ;
``` ```
> [!NOTE] > [!NOTE]
> >
> * ① 只要成功匹配并读取了一个数据输入项,那么函数的返回值就会是一个`正数`。注意,函数返回正数不意味着所有输入都能匹配成功,只要匹配成功一个输入项,返回值就是一个正数。 > * 如果条件表达式为非 0 (真),则整个表达式的值是表达式 1 。
> * ② 如果返回值是`0`,那说明 scanf 没有成功匹配任何数据输入项,这通常是因为数据输入项`完全不匹配`。 > * 如果条件表达式为 0 (假),则整个表达式的值是表达式 2 。
> * ③ 如果函数返回值是`负数`,说明 scanf 读到了 EOF流末尾或者发生了错误。在 Windows 系统终端里,键入"Ctrl + Z" 表示输入 EOF在类Unix平台中这个按键则是"Ctrl + D",可以了解一下。
## 7.2 应用示例
* 示例: * 示例:
@ -770,31 +621,86 @@ extern int scanf (const char *__restrict __format, ...)
int main() { int main() {
// 禁用 stdout 缓冲区 int m = 110;
setbuf(stdout, nullptr); int n = 20;
int result = m > n ? m : n;
int num1, num2; printf("result = %d\n", result); // result = 110
char ch;
int ret = scanf("%d %d %c", &num1, &num2, &ch);
/*
若键入的数据是 100 200 A则正常匹配录入 3 个数据ret 等于 3
若键入的数据是 100 A 200则正常匹配录入 1 个数据ret 等于 1
若键入的数据是 A 100 200则正常匹配录入 0 个数据ret 等于 0
*/
printf("ret = %d\n", ret);
return 0; return 0;
} }
``` ```
# 第八章:运算符的优先级和结合性
## 7.1 概述
* 在数学中,如果一个表达式是 `a + b * c` ,我们知道其运算规则就是:先算乘除再算加减。其实,在 C 语言中也是一样的先算乘法再算加减C 语言中乘除的运算符比加减的运算符的优先级要高。
## 7.2 优先级和结合性
* `优先级`和`结合性`的定义,如下所示:
* ① 所谓的`优先级`:就是当多个运算符出现在同一个表达式中时,先执行哪个运算符。
* ② 所谓的`结合性`:就是当多个相同优先级的运算符出现在同一个表达式中的时候,是从左到右运算,还是从右到左运算。
* `左结合性`:具有相同优先级的运算符将`从左到右`(➡️)进行计算。
* `右结合性`:具有相同优先级的运算符将`从右到左`(⬅️)进行计算。
> [!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]
>
> * ① 不要过多的依赖运算符的优先级来控制表达式的执行顺序,这样可读性太差,尽量`使用小括号来控制`表达式的执行顺序。
> * ② 不要把一个表达式写得过于复杂,如果一个表达式过于复杂,则把它`分成几步`来完成。
> * ③ 运算符优先级不用刻意地去记忆,总体上:一元运算符 > 算术运算符 > 关系运算符 > 逻辑运算符 > 三元运算符 > 赋值运算符。

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

View File

@ -0,0 +1,449 @@
# 第一章: 字符集和字符集编码
## 1.1 概述
* 字符集和字符集编码(简称编码)计算机系统中处理文本数据的两个基本概念,它们密切相关但又有区别。
* 字符集Character Set是一组字符的集合其中每个字符都被分配了一个`唯一的编号`(通常是数字)。字符可以是字母、数字、符号、控制代码(如换行符)等。`字符集定义了可以表示的字符的范围`,但它并不直接定义如何将这些字符存储在计算机中。
> [!NOTE]
>
> ASCII美国信息交换标准代码是最早期和最简单的字符集之一它只包括了英文字母、数字和一些特殊字符共 128 个字符。每个字符都分配给了一个从 0 到 127 的数字。
* 字符集编码Character Encoding简称编码是一种方案或方法`它定义了如何将字符集中的字符转换为计算机存储和传输的数据(通常是一串二进制数字)`。简而言之,编码是字符到二进制数据之间的映射规则。
> [!NOTE]
>
> ASCII 编码方案定义了如何将 ASCII 字符集中的每个字符表示为 7 位的二进制数字。例如:大写字母`'A'`在 ASCII 编码中表示为二进制的`1000001`,十进制的 `65`
* `字符集`和`字符集编码`之间的关系如下:
![](./assets/1.png)
* Linux 中安装帮助手册:
![](./assets/2.gif)
## 1.2 ASCII 编码
* 从`冯·诺依曼`体系结构中,我们知道,计算机中所有的`数据`和`指令`都是以`二进制`的形式表示的;所以,计算机中对于文本数据的数据也是以二进制来存储的,那么对应的流程如下:
![](./assets/3.png)
* 我们知道,计算机是上个世纪 60 年代在美国研制成功的,为了实现字符和二进制的转换,美国就制定了一套字符编码,即英语字符和二进制位之间的关系,即 ASCII American Standard Code for Information Interchange编码
- ASCII 编码只包括了英文字符、数字和一些特殊字符,一共 128 个字符,并且每个字符都分配了唯一的数字,范围是 0 - 127。
- ASCII 编码中的每个字符都使用 7 位的二进制数字表示;但是,计算机中的存储的最小单位是 1 B = 8 位,那么最高位统一规定为 0 。
> [!NOTE]
>
> - ① 其实,早期是没有字符集的概念的,只是后来为了解决乱码问题,而产生了字符集的概念。
> - ② 对于英文体系来说,`a-zA-Z0-9`以及一些`特殊字符`一共 `128` 就可以满足实际存储需求;所以,在也是为什么 ASCII 码使用 7 位二进制2^7 = 128 )来存储的。
* 在操作系统中就内置了对应的编码表Linux 也不例外;可以使用如下的命令查看:
```shell
man ascii
```
![](./assets/4.gif)
* 其对应的 ASCII 编码表,如下所示:
![](./assets/5.gif)
* 但是,随着计算机的发展,计算机开始了东征之路,由美国传播到东方:
![](./assets/6.png)
- 先是传播到了欧洲,欧洲在兼容 ASCII 编码的基础上,推出了 ISO8859-1 编码,即:
- ISO8859-1 编码包括基本的拉丁字母表、数字、标点符号,以及西欧语言中特有的一些字符,如:法语中的 `è`、德语中的 `ü` 等。
- ISO 8859-1 为每个字符分配一个单字节8 位)编码,意味着它可以表示最多 256 2^8个不同的字符编号从 0 到 255
- ISO 8859-1 的前 128 个字符与 ASCII 编码完全一致,这使得 ASCII 编码的文本可以无缝转换为 ISO 8859-1 编码。
![](./assets/7.gif)
![](./assets/8.gif)
- 计算机继续传播到了亚洲,亚洲(双字节)各个国家分别给出了自己国家对应的字符集编码,如:
- 日本推出了 Shift-JIS 编码:
- 单字节 ASCII 范围0 - 127。
- 双字节范围:
- 第一个字节129 - 159 和 224 - 239 。
- 第二个字节64 - 126 和 128 - 252 。
- 韩国推出了 EUC-KR 编码:
- 单字节 ASCII 范围0 - 127。
- 双字节范围:从 41281 - 65278。
- 中国推出了 GBK 编码:
- 单字节 ASCII 范围0 - 127。
- 双字节范围33088 - 65278 。
> [!NOTE]
>
> - ① 通过上面日本、韩国、中国的编码十进制范围,我们可以看到,虽然这些编码系统在技术上的编码范围存在重叠(特别是在高位字节区域),但因为它们各自支持的字符集完全不同,所以实际上它们并不直接冲突。
> - ② 但是,如果一个中国人通过 GBK 编码写的文章,通过邮件发送给韩国人,因为韩国和中国在字符集编码上的高位字节有重叠部分,必然会造成歧义。
## 1.3 Unicode 编码
- 在 Unicode 之前世界上存在着数百种不同的编码系统每一种编码系统都是为了支持特定语言或一组语言的字符集。这些编码系统包括ASCII、ISO 8859 系列、GBK、Shift-JIS、EUC-KR 等,它们各自有不同的字符范围和编码方式。这种多样性虽然在局部范围内解决了字符表示的问题,但也带来了以下几个方面的挑战:
- `编码冲突`:由于不同的编码系统可以为相同的字节值分配不同的字符,因此在不同编码之间转换文本时,如果没有正确处理编码信息,就很容易产生乱码。这种编码冲突在尝试处理多种语言的文本时尤为突出。
- `编码的复杂性`:随着全球化的发展,软件和系统需要支持越来越多的语言,这就要求开发者和系统同时处理多种不同的编码系统。这不仅增加了开发和维护的复杂性,而且也增加了出错的风险。
- `资源限制`:在早期计算机技术中,内存和存储资源相对有限。不同的编码标准要求系统存储多套字符集数据,这无疑增加了对有限资源的消耗。
- ……
- 针对上述的种种问题为了推行全球化Unicode 应运而生Unicode 的核心规则和设计原则是建立一个全球统一的字符集,使得世界上所有的文字和符号都能被唯一地识别和使用,无论使用者位于何地或使用何种语言。这套规则包括了字符的编码、表示、处理和转换机制,旨在确保不同系统和软件间能够无缝交换和处理文本数据。
- `通用字符集 (UCS)`Unicode 为每一个字符分配一个唯一的编号(称为`“码点”`)。这些码点被组织在一个统一的字符集中,官方称之为 “通用字符集”Universal Character SetUCS。码点通常表示为 `U+` 后跟一个十六进制数,例如:`U+0041` 代表大写的英文字母 `“A”`
- `编码平面和区段`Unicode 码点被划分为多个 “平面Planes每个平面包含 6553616^4个码点。目前Unicode定义了 17 个平面(从 0 到16每个平面被分配了一个编号从 “基本多文种平面BMP” 的 0 开始,到 16 号平面结束。这意味着 Unicode 理论上可以支持超过 110万17*65536个码点。
- Unicode 仅仅只是字符集,给每个字符设置了唯一的数字编号而已,却没有给出这些数字编号实际如何存储,可以通过如下命令查看:
![](./assets/9.gif)
- 为了在计算机系统中表示 Unicode 字符,定义了几种编码方案,这些方案包括 UTF-8、UTF-16 和 UTF-32 等。
- **UTF-8**:使用 1 - 4 个字节表示每个 Unicode 字符,兼容 ASCII是网络上最常用的编码。
- **UTF-16**:使用 2 - 4 个字节表示每个 Unicode 字符,适合于需要经常处理基本多文种平面之外字符的应用。
- **UTF-32**:使用固定的 4 个字节表示每个 Unicode 字符,简化了字符处理,但增加了存储空间的需求。
> [!NOTE]
>
> * ① 只有 UTF-8 兼容 ASCIIUTF-32 和 UTF-16 都不兼容 ASCII因为它们没有单字节编码。
> * UTF-8 使用尽量少的字节来存储一个字符不但能够节省存储空间而且在网络传输时也能节省流量所以很多纯文本类型的文件各种编程语言的源文件、各种日志文件和配置文件等以及绝大多数的网页百度、新浪、163 等都采用 UTF-8 编码。但是UTF-8 的缺点是效率低,不但在存储和读取时都要经过转换,而且在处理字符串时也非常麻烦。例如:要在一个 UTF-8 编码的字符串中找到第 10 个字符,就得从头开始一个一个地检索字符,这是一个很耗时的过程,因为 UTF-8 编码的字符串中每个字符占用的字节数不一样,如果不从头遍历每个字符,就不知道第 10 个字符位于第几个字节处就无法定位。不过随着算法的逐年精进UTF-8 字符串的定位效率也越来越高了,往往不再是槽点了。
> * UTF-32 是“以空间换效率”,正好弥补了 UTF-8 的缺点UTF-32 的优势就是效率高UTF-32 在存储和读取字符时不需要任何转换,在处理字符串时也能最快速地定位字符。例如:在一个 UTF-32 编码的字符串中查找第 10 个字符,很容易计算出它位于第 37 个字节处直接获取就行不用再逐个遍历字符了没有比这更快的定位字符的方法了。但是UTF-32 的缺点也很明显,就是太占用存储空间了,在网络传输时也会消耗很多流量。我们平常使用的字符编码值一般都比较小,用一两个字节存储足以,用四个字节简直是暴殄天物,甚至说是不能容忍的,所以 UTF-32 在应用上不如 UTF-8 和 UTF-16 广泛。
> * UTF-16 可以看做是 UTF-8 和 UTF-32 的折中方案,它平衡了存储空间和处理效率的矛盾。对于常用的字符,用两个字节存储足以,这个时候 UTF-16 是不需要转换的,直接存储字符的编码值即可。
> * ② 总而言之,**UTF-8** 编码兼容性强,适合大多数应用,特别是英文文本处理。**UTF-16** 编码适合处理大量亚洲字符,但在处理英文或其他拉丁字符时相对浪费空间。**UTF-32**编码简单直接,但非常浪费空间,适合需要固定字符宽度的特殊场景。
> * ③ 在实际应用中UTF-8 通常是最常用的编码方式,因为它在兼容性和空间效率之间提供了良好的平衡。
> [!IMPORTANT]
>
> * ① Windows 内核、.NET Framework、Java String 内部采用的都是 `UTF-16` 编码,主要原因是为了在兼顾字符处理效率的同时,能够有效处理多种语言的字符集,即:历史遗留问题、兼容性要求和多语言支持的需要。
> * ② 不过UNIX 家族的操作系统Linux、Mac OS、iOS 等)内核都采用 `UTF-8` 编码,主要是为了兼容性和灵活性,因为 UTF-8 编码可以无缝处理 ASCII 字符,同时也能够支持多字节的 Unicode 字符,即:为了最大限度地兼容 ASCII同时保持系统的简单性、灵活性和效率。
- `Unicode 字符集`和对应的`UTF-8 字符编码`之间的关系,如下所示:
![](./assets/10.png)
>[!NOTE]
>
>`宽字符`和`窄字符`是编程和计算机系统中对字符类型的一种分类,主要用于描述字符在内存中的表示形式及其与编码方式的关系。
>
>* ① `窄字符`通常指使用单个字节8 位)来表示的字符。在许多传统的编码系统中,窄字符通常代表 ASCII 字符或其它单字节字符集中的字符。换言之,`窄字符`适合处理简单的单字节字符集ASCII适用于处理西方语言的应用。
>* ② `宽字符`指使用多个字节(通常是两个或更多)来表示的字符。这些字符通常用于表示比 ASCII 范围更广的字符集,如 Unicode 字符。换言之,`宽字符`适合处理多字节字符集UTF-32、UTF-16 等,适用于需要处理多种语言和符号的国际化应用。
>
>在现代编程中,`窄字符`通常与 `UTF-8` 编码关联,特别是在处理文本输入、输出和网络传输时。尽管 `UTF-8` 是变长编码,由于其高效的空间利用和对 `ASCII` 的优化,通常与`窄字符`概念关联。而`宽字符`通常与 `UTF-16` 编码或 `UTF-32`编码关联,这些编码使用更大的固定或半固定长度来表示字符,适合处理更大的字符集。
# 第二章: WSL2 中设置默认编码为中文
## 2.1 概述
* 查看 WSL2 的 Linux 发行版的默认编码:
```shell
echo $LANG
```
![](./assets/11.gif)
> [!NOTE]
>
> `C.UTF-8` 是一种字符编码设置,结合了 `C` 区域设定和 `UTF-8` 字符编码。
>
> * ① **C 区域设定**:这是一个标准的、最小化的区域设置,通常用于系统默认的语言环境。`C` 区域设定下,所有字符都被认为是 ASCII 字符集的一部分,这意味着仅支持基本的英文字符和符号。在 `C` 区域设定中,字符串的排序和比较是基于简单的二进制值比较,这与本地化的语言设置相比相对简单。
> * ② **UTF-8 编码**UTF-8 是一种变长的字符编码方式,可以编码所有的 Unicode 字符。它是一种广泛使用的字符编码,能够支持多种语言和符号。每个 UTF-8 字符可以由1到4个字节表示这使得它兼容 ASCII对于标准 ASCII 字符UTF-8 只使用一个字节)。
>
> 因此,`C.UTF-8` 结合了 `C` 区域设定和 UTF-8 字符编码的优势。使用 `C.UTF-8` 时,系统默认语言环境保持简单和高效,同时支持更广泛的字符集,特别是多语言和非英语字符。这样可以在需要兼容性的同时,提供对全球化字符的支持。
## 2.2 AlmaLinux9 设置默认编码
* ① 搜索中文语言包:
```shell
dnf search locale zh
```
![](./assets/12.gif)
* ② 安装中文语言包:
```shell
dnf -y install glibc-langpack-zh
```
![](./assets/13.gif)
* ③ 切换语言环境为中文:
```shell
localectl set-locale LANG=zh_CN.UTF-8
```
![](./assets/14.gif)
* ④ 手动加载配置文件,使其生效:
```shell
source /etc/locale.conf
```
![](./assets/15.gif)
## 2.3 Ubuntu 22.04 设置默认编码
* ① 安装中文语言包:
```shell
apt update -y && apt install language-pack-zh-hans -y
```
![](./assets/16.gif)
* ② 切换环境为中文:
```shell
update-locale LANG=zh_CN.UTF-8 LANGUAGE=zh_CN:zh
```
![](./assets/17.gif)
* ③ 手动加载配置文件,使其生效:
```shell
source /etc/default/locale
```
![](./assets/18.gif)
# 第三章:在 C 语言中使用中文字符
## 3.1 概述
* 大部分 C 语言文章或教材对中文字符的处理讳莫如深,甚至只字不提,导致很多初学者认为 C 语言只能处理英文,而不支持中文。
* 其实这是不对的。C 语言作为一门系统级别的编程语言,理应支持世界上任何一个国家的文字,如:中文、日文、韩文等。
> [!NOTE]
>
> 如果 C 语言不支持中文,那么简体中文 Windows 操作系统将无从谈起,我们只能被迫使用英文 Windows 操作系统,这对计算机的传播而言将会是一种巨大的阻碍。
## 3.2 中文字符的存储
* 要想正确的存储中文字符,需要解决如下的两个问题:
* ① 足够长的数据类型char 的长度是 1 个字节,只能存储拉丁体系的问题,并不能存储中文字符,所以至少需要 2 个字节的内存空间。
* ② 包含中文的字符集C 语言规定,对于中文、日文、韩文等非 ASCII 编码之外的单个字符,需要有专门的字符类型,也就是需要使用宽字符的编码方式。而常见的宽字符的编码有 UTF-16 和 UTF-32它们都是基于 Unicode 字符集的,都能够支持全球的文字。
> [!NOTE]
>
> 上文提及过,在现代编程中,`窄字符`通常与 `UTF-8` 编码关联,特别是在处理文本输入、输出和网络传输时。尽管 `UTF-8` 是变长编码,由于其高效的空间利用和对 `ASCII` 的优化,通常与`窄字符`概念关联。而`宽字符`通常与 `UTF-16` 编码或 `UTF-32`编码关联,这些编码使用更大的固定或半固定长度来表示字符,适合处理更大的字符集。
* 在真正实现的时候,微软的 MSVC 编译器采用 UTF-16 编码,即:使用 2 个字节来存储一个字符,使用 unsigned short 类型就可以容纳。而 GCC、LLVM/Clang 采用 UTF-32 编码,使用 4 个字节存储字符,用 unsigned int 类型就可以容纳。
> [!NOTE]
>
> 不同的编译器可以使用不同的整数类型,来存储宽字符,这对于跨平台开发来说,非常不友好。
* 为了解决上述的问题C 语言推出了一种全新的类型 `wchar_t` 类型,用来存储宽字符类型。
* 在微软的 MSVC 编译器中,它的长度是 2 个字节。
* 在 GCC、LLVM/Clang 中,它的长度是 4 个字节。
> [!NOTE]
>
> * ① `wchar_t` 中的 `w`是 wide 的首字母,`t` 是 type 的首字母,所以 `wchar_t` 就是宽字符类型,足够见名知意。
> * ② `wchar_t` 是用 typedef 关键字定义的一个别名,后文讲解,`wchar_t` 在不同的编译器下长度不一样。
> * ③ `wchar_t` 类型位于 `<wchar.h>` 头文件中,它使得代码在具有良好移植性的同时,也节省了不少内存,以后我们就用它来存储宽字符。
* 对于普通的拉丁体系的字符,我们使用 `''` 括起来,来表示字符,如:`'A'`、`'&'` 等。但是,如果要想表示宽字符,就需要加上 `L` 前缀了,如:`L'A'`、`L'中'`。
> [!NOTE]
>
> 宽字符字面量中的 `L``Long` 的缩写意思是比普通的字符char要长。
* 示例:
```c
#include <stddef.h>
int main() {
/* 存储宽字符,如:中文 */
wchar_t a = L'中';
wchar_t b = L'中';
wchar_t c = L'中';
wchar_t d = L'中';
wchar_t e = L'中';
return 0;
}
```
## 3.3 中文字符的输出
* 对于宽字符,就不能使用 `putchar` 函数和 `printf` 函数来进行输出了,需要使用 `putwchar` 函数和 `wprintf` 函数。
> [!NOTE]
>
> * ① `putchar` 函数和 `printf` 函数,只能输出窄字符,即:`char` 类型表示的字符。
> * ② `putwchar` 函数可以用来输出宽字符,用法和 `putchar` 函数类似。
> * ③ `wprintf`函数可以用来输出宽字符,用法和 `printf` 函数类型,只不过格式占位符是 `%lc`
> * ④ 在输出宽字符之前,还需要使用 `setlocale` 函数进行本地化设置,告诉程序如何才能正确地处理各个国家的语言文化。
* 示例:
```c
#include <locale.h>
#include <stddef.h>
#include <wchar.h>
int main() {
/* 存储宽字符,如:中文 */
wchar_t a = L'中';
wchar_t b = L'国';
wchar_t c = L'人';
wchar_t d = L'你';
wchar_t e = L'好';
// 将本地环境设置为简体中文
setlocale(LC_ALL, "zh_CN.UTF-8");
// 使用专门的 putwchar 输出宽字符
putwchar(a);
putwchar(b);
putwchar(c);
putwchar(d);
putwchar(e);
putwchar(L'\n'); // 只能使用宽字符
// 使用通用的 wprintf 输出宽字符
wprintf(L"%lc %lc %lc %lc %lc\n", a, b, c, d, e);
return 0;
}
```
## 3.4 宽字符串
* 如果给字符串加上 `L` 前缀,就变成了宽字符串,即:它包含的每个字符都是宽字符,一律采用 UTF-16 或者 UTF-32 编码。
> [!NOTE]
>
> * ① 输出宽字符串可以使用 <wchar.h> 头文件中的 wprintf 函数,对应的格式控制符是`%ls`。
> * ② 不加`L`前缀的窄字符串也可以处理中文,我们之前就在 `printf` 函数中,使用格式占位符 `%s` 输出含有中文的字符串,至于为什么,看下文讲解。
* 示例:
```c
#include <locale.h>
#include <stddef.h>
#include <wchar.h>
int main() {
/* 存储宽字符,如:中文 */
wchar_t a[] = L"中国人";
wchar_t *b = L"你好";
// 将本地环境设置为简体中文
setlocale(LC_ALL, "zh_CN.UTF-8");
// 使用通用的 wprintf 输出字符串
wprintf(L"%ls %ls\n", a, b);
return 0;
}
```
# 第四章C 语言到底使用什么编码?
## 4.1 概述
* 在 C 语言中,只有 `char` 类型的`窄字符`才会使用 ASCII 编码。而 `char` 类型的`窄字符串`、`wchar_t` 类型的`宽字符`和`宽字符串`都不使用 ASCII 编码。
* `wchar_t` 类型的`宽字符`和`宽字符串`使用 UTF-16 或者 UTF-32 编码,这个在上文已经讲解了,现在只剩下 `char` 类型的`窄字符串`没有讲解了,这也是下文的重点。
> [!NOTE]
>
> * ① 其实,对于`char` 类型的窄字符串C 语言并没有规定使用哪一种特定的编码,只要选用的编码能够适应当前的环境即可。换言之,`char` 类型的窄字符串的编码与操作系统以及编译器有关。
> * ② 但是,`char` 类型的窄字符串一定不是 ASCII 编码,因为 ASCII 编码只能显示拉丁体系的文字,而不能输出中文、日文、韩文等。
> * ③ 讨论窄字符串的编码要从以下两个方面下手。
## 4.2 源文件使用什么编码?
* 源文件用来保存我们编写的代码,它最终会被存储到本地硬盘,或者远程服务器,这个时候就要尽量压缩文件体积,以节省硬盘空间或者网络流量,而代码中大部分的字符都是 ASCII 编码中的字符,用一个字节足以容纳,所以 UTF-8 编码是一个不错的选择。
* UTF-8 兼容 ASCII代码中的大部分字符可以用一个字节保存。另外UTF-8 基于 Unicode支持全世界的字符我们编写的代码可以给全球的程序员使用真正做到技术无国界。
* 常见的 IDE 或者编辑器Sublime Text、Vim 等,在创建源文件的时候一般默认就是 UTF-8 编码。就算不是,我们也会推荐设置为 UTF-8 编码,如下所示:
![](./assets/19.png)
* 对于 C 语言编译器来说,它往往支持多种编码格式的源文件。微软的 MSVC 、GCC 和 LLVM/Clang 都支持 UTF-8 和本地编码的源文件。
## 4.3 窄字符串使用什么编码?
* 前文提到,可以使用 `puts` 函数或 `printf` 函数来输出窄字符串,如下所示:
```c
#include <stdio.h>
int main() {
// 存储字符串
char str[] = "我";
char *str2 = "爱你";
puts(str); // 我
puts(str2); // 爱你
// 存储字符串
char str3[] = "你";
char *str4 = "是好人";
printf("%s\n", str3); // 你
printf("%s\n", str4); // 是好人
return 0;
}
```
* 像 `"我"`、`"爱你"`、`"你"`、`"是好人"`就是需要被处理的窄字符串,当程序运行的时候,它们会被加载进内存。并且,这些字符串中是包含中文的,所以一定不会使用 ASCII 编码。
> [!NOTE]
>
> 其实,对于代码中需要被处理的窄字符串,不同的编译器差别还是挺大的:
>
> * 微软的 MSVC 编译器使用本地编码来保存这些字符。对于简体中文版的 Windows使用的是 GBK 编码。
> * GCC、LLVM/Clang 编译器使用和源文件相同的编码来保存这些字符:如果源文件使用的是 UTF-8 编码,那么这些字符也使用 UTF-8 编码;如果源文件使用的是 GBK 编码,那么这些字符也使用 GBK 编码。
## 4.4 总结
* ① 对于 `char` 类型的窄字符,在 C 语言中,使用的是 `ASCII` 编码。
* ② 对于 `wchar_t` 类型的`宽字符`和`宽字符串`,在 C 语言中,使用的 `UTF-16` 编码或者 `UTF-32` 编码,它们都是基于 Unicode 字符集的。
* ③ 对于 `char` 类型的`窄字符串`,微软的 MSVC 编译器使用本地编码GCC、LLVM/Clang 使用和源文件编码相同的编码。
* ④ 处理窄字符和处理宽字符使用的函数也不一样,如下所示:
* `<stdio.h>` 头文件中的 `putchar`、`puts`、`printf` 函数只能用来处理窄字符。
* `<wchar.h>` 头文件中的 `putwchar`、`wprintf` 函数只能用来处理宽字符。
> [!IMPORTANT]
>
> * ① C 语言作为一门较为底层和古老的语言,对于字符的处理,之所以有这么多种方式,是因为历史遗留的原因和早期计算机资源有限的背景密切相关。
> * ② 现代化的编程语言C++ 、Java、Python 等都对字符串处理进行了改进和抽象C++ 中的 `std::string` 和 Java 中的 `String`。并且现代编程语言通常会自动管理内存这样开发者就不需要手动处理字符串的内存分配和释放从而减少了内存泄漏和缓冲区溢出等问题。当然现代编程语言通常内置了对各种字符编码的支持能够方便地处理不同语言的字符Java 的 `String` 类和 Python 的 `str` 类型都默认支持 Unicode可以轻松处理中文等多字节字符。
## 4.5 编码字符集和运行字符集
* 源文件使用的字符集,通常称为`编码字符集`,即:写代码的时候所使用的字符集。
> [!NOTE]
>
> 源文件需要保存到硬盘,或者在网络上传输,使用的编码要尽量节省存储空间,同时要方便跨国交流,所以一般使用 UTF-8这就是选择编码字符集的标准。
* 程序中的字符或者字符串使用的字符集,通常称为`运行字符集`,即:程序运行时所使用的字符集。
> [!NOTE]
>
> 程序中的字符或者字符串在程序运行后必须被载入到内存才能进行后续的处理对于这些字符来说要尽量选用能够提高处理速度的编码UTF-16 和 UTF-32 编码就能够快速定位(查找)字符。
* `编码字符集`是站在`存储`和`传输`的角度,而`运行字符集`是站在`处理`或者`操作`的角度,所以它们并不一定相同。

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

View File

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 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: 51 KiB

View File

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Do not edit this file with editors other than draw.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="641px" height="61px" viewBox="-0.5 -0.5 641 61" content="&lt;mxfile host=&quot;Electron&quot; agent=&quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/24.7.17 Chrome/128.0.6613.36 Electron/32.0.1 Safari/537.36&quot; version=&quot;24.7.17&quot; scale=&quot;1&quot; border=&quot;0&quot;&gt;&#10; &lt;diagram name=&quot;第 1 页&quot; id=&quot;HEK4woBdrfgql7qMwJSw&quot;&gt;&#10; &lt;mxGraphModel dx=&quot;1416&quot; dy=&quot;818&quot; grid=&quot;1&quot; gridSize=&quot;10&quot; guides=&quot;1&quot; tooltips=&quot;1&quot; connect=&quot;1&quot; arrows=&quot;1&quot; fold=&quot;1&quot; page=&quot;1&quot; pageScale=&quot;1&quot; pageWidth=&quot;827&quot; pageHeight=&quot;1169&quot; math=&quot;0&quot; shadow=&quot;0&quot;&gt;&#10; &lt;root&gt;&#10; &lt;mxCell id=&quot;0&quot; /&gt;&#10; &lt;mxCell id=&quot;1&quot; parent=&quot;0&quot; /&gt;&#10; &lt;mxCell id=&quot;9DnvKgpYVnukXR7bQpn0-1&quot; value=&quot;CPU&quot; style=&quot;rounded=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&#10; &lt;mxGeometry x=&quot;80&quot; y=&quot;420&quot; width=&quot;120&quot; height=&quot;60&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;9DnvKgpYVnukXR7bQpn0-5&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;flowAnimation=1;shadow=1;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;9DnvKgpYVnukXR7bQpn0-2&quot; target=&quot;9DnvKgpYVnukXR7bQpn0-1&quot;&gt;&#10; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;9DnvKgpYVnukXR7bQpn0-2&quot; value=&quot;内存&quot; style=&quot;rounded=0;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&#10; &lt;mxGeometry x=&quot;330&quot; y=&quot;420&quot; width=&quot;120&quot; height=&quot;60&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;9DnvKgpYVnukXR7bQpn0-4&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;flowAnimation=1;shadow=1;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;9DnvKgpYVnukXR7bQpn0-3&quot; target=&quot;9DnvKgpYVnukXR7bQpn0-2&quot;&gt;&#10; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;mxCell id=&quot;9DnvKgpYVnukXR7bQpn0-3&quot; value=&quot;IO 设备&quot; style=&quot;rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&#10; &lt;mxGeometry x=&quot;600&quot; y=&quot;420&quot; width=&quot;120&quot; height=&quot;60&quot; as=&quot;geometry&quot; /&gt;&#10; &lt;/mxCell&gt;&#10; &lt;/root&gt;&#10; &lt;/mxGraphModel&gt;&#10; &lt;/diagram&gt;&#10;&lt;/mxfile&gt;&#10;" style="background-color: rgb(255, 255, 255);"><defs><style>@keyframes ge-flow-animation-_-1MwRo2DOFU8_5qbC4R {&#xa; to {&#xa; stroke-dashoffset: 0;&#xa; }&#xa;}</style></defs><rect fill="#ffffff" width="100%" height="100%" x="0" y="0"/><g><g data-cell-id="0"><g data-cell-id="1"><g data-cell-id="9DnvKgpYVnukXR7bQpn0-1"><g id="cell-9DnvKgpYVnukXR7bQpn0-1"><g><rect x="0" y="0" width="120" height="60" fill="#f5f5f5" stroke="#666666" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 30px; margin-left: 1px;"><div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">CPU</div></div></div></foreignObject><image x="1" y="23.5" width="118" height="17" xlink:href="data:image/png;base64,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"/></switch></g></g></g></g><g data-cell-id="9DnvKgpYVnukXR7bQpn0-5"><g id="cell-9DnvKgpYVnukXR7bQpn0-5"><g style="filter: drop-shadow(rgba(0, 0, 0, 0.25) 2px 3px 2px);"><path d="M 250 30 L 126.37 30" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke" stroke-dasharray="8" style="animation: 500ms linear 0s infinite normal none running ge-flow-animation-_-1MwRo2DOFU8_5qbC4R; stroke-dashoffset: 16;"/><path d="M 121.12 30 L 128.12 26.5 L 126.37 30 L 128.12 33.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/></g></g></g><g data-cell-id="9DnvKgpYVnukXR7bQpn0-2"><g id="cell-9DnvKgpYVnukXR7bQpn0-2"><g><rect x="250" y="0" width="120" height="60" fill="#f8cecc" stroke="#b85450" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 30px; margin-left: 251px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">内存</div></div></div></foreignObject><image x="251" y="23.5" width="118" height="17" xlink:href="data:image/png;base64,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"/></switch></g></g></g></g><g data-cell-id="9DnvKgpYVnukXR7bQpn0-4"><g id="cell-9DnvKgpYVnukXR7bQpn0-4"><g style="filter: drop-shadow(rgba(0, 0, 0, 0.25) 2px 3px 2px);"><path d="M 520 30 L 376.37 30" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke" stroke-dasharray="8" style="animation: 500ms linear 0s infinite normal none running ge-flow-animation-_-1MwRo2DOFU8_5qbC4R; stroke-dashoffset: 16;"/><path d="M 371.12 30 L 378.12 26.5 L 376.37 30 L 378.12 33.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/></g></g></g><g data-cell-id="9DnvKgpYVnukXR7bQpn0-3"><g id="cell-9DnvKgpYVnukXR7bQpn0-3"><g><rect x="520" y="0" width="120" height="60" fill="#fff2cc" stroke="#d6b656" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 30px; margin-left: 521px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">IO 设备</div></div></div></foreignObject><image x="521" y="23.5" width="118" height="17" xlink:href="data:image/png;base64,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"/></switch></g></g></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,1382 +1,788 @@
# 第一章:概述 # 第一章:输入输出模型
* `流程控制结构`是用来控制程序中`各语句执行顺序`的语句,并且可以将语句组合成能`完成一定功能`的`小逻辑模块`。 ## 1.1 回顾冯·诺依曼体系结构
* 在程序设计中规定了`三种`流程结构,如下所示:
* `顺序结构`:程序从上到下逐行执行,中间没有任何判断和跳转。
* `分支结构`:根据条件,有选择的执行某段代码。在 C 语言中,有 `if...else``switch...case` 两种分支语句。
* `循环结构`:根据循环条件,重复性的执行某段代码。在 C 语言中,有 `for`、`while`、`do...while` 三种循环结构。
* 在生活中的`洗衣工厂`,就包含了上述的三种流程结构,如下所示: * `冯·诺依曼`体系结构的理论要点如下:
- ① **存储程序**`程序指令`和`数据`都存储在计算机的内存中,这使得程序可以在运行时修改。
- ② **二进制逻辑**:所有`数据`和`指令`都以`二进制`形式表示。
- ③ **顺序执行**:指令按照它们在内存中的顺序执行,但可以有条件地改变执行顺序。
- ④ **五大部件**:计算机由`运算器`、`控制器`、`存储器`、`输入设备`和`输出设备`组成。
- ⑤ **指令结构**:指令由操作码和地址码组成,操作码指示要执行的操作,地址码指示操作数的位置。
- ⑥ **中心化控制**计算机的控制单元CPU负责解释和执行指令控制数据流。
![](./assets/1.jpg) ![img](./assets/1.png)
# 第二章:顺序结构
## 2.1 概述
* 程序从上到下逐行地执行,表达式语句都是顺序执行的,并且上一行对某个变量的修改对下一行会产生影响。
![](./assets/2.png)
## 2.2 应用示例
* 示例:
```c
#include <stdio.h>
int main() {
int x = 1;
int y = 2;
printf("x = %d \n", x); // x = 1
printf("y = %d \n", y); // y = 2
// 对 x 和 y 的值进行修改
x++;
y = 2 * x + y;
x = x * 10;
printf("x = %d \n", x); // x = 20
printf("y = %d \n", y); // y = 6
return 0;
}
```
# 第三章:分支结构(⭐)
## 3.1 概述
* 根据特定条件执行不同的代码块,从而实现灵活的程序控制和更复杂的逻辑。
## 3.2 单分支结构
### 3.2.1 概述
* 语法:
```c
if(条件表达式){
语句;
}
```
> [!NOTE] > [!NOTE]
> >
> * ① 在 C 语言中,严格意义上是没有 boolean 类型的,使用`非0` 表示`真true``0` 表示`假false`。 > 上述的组件协同工作,构成了一个完整的计算机系统:
> * ② 当条件表达式为真(`非0` ),就会执行代码块中的语句;否则,就不会执行代码块中的语句。 >
> - `运算器`和`控制器`通常被集成在一起组成中央处理器CPU负责数据处理和指令执行。
> - `存储器`(内存)保存数据和程序,是计算机运作的基础。
> - `输入设备`和`输出设备`负责与外界的交互,确保用户能够输入信息并接收计算机的处理结果。
>
> 直到今天,虽然硬件的发展日新月异,但是现代计算机的硬件理论基础还是《冯·诺依曼体系结构》。
* 流程图,如下所示: ## 1.2 冯·诺依曼体系结构的瓶颈
* 计算机是有性能瓶颈的:如果 CPU 有每秒处理 1000 个服务请求的能力,各种总线的负载能力能达到 500 个, 但网卡只能接受 200个请求而硬盘只能负担 150 个的话,那这台服务器得处理能力只能是 150 个请求/秒,有 85% 的处理器计算能力浪费了,在计算机系统当中,`硬盘`的读写速率已经成为影响系统性能进一步提高的瓶颈。
![](./assets/2.jpg)
* 计算机的各个设备部件的延迟从高到低的排列依次是机械硬盘HDD、固态硬盘SSD、内存、CPU 。
![](./assets/3.png) ![](./assets/3.png)
### 3.2.2 应用示例 * 从上图中我们可以知道CPU 是最快的,一个时钟周期是 0.3 ns ,内存访问需要 120 ns ,固态硬盘访问需要 50-150 us传统的硬盘访问需要 1-10 ms而网络访问是最慢需要 40 ms 以上。
* 需求:成年人心率的正常范围是每分钟 60~100 次。体检时,如果心率不在此范围内,则提示需要做进一步的检查。 > [!NOTE]
>
> 时间的单位换算如下:
>
> * ① 1 秒 = 1000 毫秒,即 1 s = 1000 ms。
> * ② 1 毫秒 = 1000 微妙,即 1 ms = 1000 us 。
> * ③ 1 微妙 = 1000 纳秒,即 1 us = 1000 ns。
* 如果按照上图,将计算机世界的时间和人类世界的时间进行对比,即:
```txt
* 示例: 如果 CPU 的时钟周期按照 1 秒计算,
那么,内存访问就需要 6 分钟;
```c 那么,固态硬盘就需要 2-6 天;
#include <stdio.h> 那么,传统硬盘就需要 1-12 个月;
那么,网络访问就需要 4 年以上。
int main() {
int heartBeats = 0;
printf("请输入您的心率:");
scanf("%d", &heartBeats);
if (heartBeats < 60 || heartBeats > 100) {
printf("您的心率不在正常范围内,请做进一步的检查。\n");
}
printf("体检结束!!!");
return 0;
}
```
### 3.2.3 应用示例
* 需求:根据年龄判断,如果是未成年人,则提示 "未成年人请在家长陪同下访问!" 。
* 示例:
```c
#include <stdio.h>
int main() {
int age = 0;
printf("请输入你的年龄:");
scanf("%d", &age);
if (age < 18) {
printf("未成年人请在家长陪同下访问!\n");
}
printf("欢迎继续访问!");
return 0;
}
```
## 3.3 双分支结构
### 3.3.1 概述
* 语法:
```c
if(条件表达式) {
语句块1;
}else {
语句块2;
}
``` ```
> [!NOTE] > [!NOTE]
> >
> * ① 在 C 语言中,严格意义上是没有 boolean 类型的,使用`非0` 表示`真true``0` 表示`假false`。 > * ① 这就中国古典修仙小说中的“天上一天,地上一年”是多么的相似!!!
> * ② 当条件表达式为真(`非0` ),就会执行代码块 1 中的语句;否则,执行代码块 2 中的语句。 > * ② 对于 CPU 来说,这个世界真的是太慢了!!!
* 流程图,如下所示 * 其实,中国古代中的文人,通常以`蜉蝣`来表示时间的短暂(和其他生物的寿命比),也是类似的道理,即:
![](./assets/4.png) ```txt
鹤寿千岁,以极其游,蜉蝣朝生而暮死,尽其乐,盖其旦暮为期,远不过三日尔。
### 3.3.2 应用示例 --- 出自 西汉淮南王刘安《淮南子》
* 需求:判断一个整数,是奇数还是偶数。
* 示例:
```c
#include <stdio.h>
int main() {
int num = 0;
printf("请输入一个整数:");
scanf("%d", &num);
if (num % 2 == 0) {
printf("%d 是偶数\n", num);
} else {
printf("%d 是奇数\n", num);
}
return 0;
}
``` ```
### 3.3.2 应用示例 ```txt
寄蜉蝣于天地,渺沧海之一粟。 哀吾生之须臾,羡长江之无穷。
* 需求输入年龄如果大于18岁则输出 "你年龄大于18要对自己的行为负责!";否则,输出 "你的年龄不大这次放过你了。" 挟飞仙以遨游,抱明月而长终。 知不可乎骤得,托遗响于悲风。
--- 出自 苏轼《赤壁赋》
* 示例:
```c
#include <stdio.h>
int main() {
int age = 0;
printf("请输入年龄:");
scanf("%d", &age);
if (age > 18) {
printf("你年龄大于18要对自己的行为负责!\n");
} else {
printf("你的年龄不大,这次放过你了!\n");
}
return 0;
}
```
### 3.3.3 应用示例
* 需求:判定某个年份是否为闰年?
>[!NOTE]
>
>* ① year 是 400 的整倍数: year%400==0
>* ② 能被 4 整除,但不能被 100 整除year % 4 == 0 && year % 100 != 0
* 示例:
```c
#include <stdio.h>
int main() {
int year = 0;
printf("请输入年份:");
scanf("%d", &year);
if (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)) {
printf("%d 是闰年\n", year);
} else {
printf("%d 不是闰年\n", year);
}
return 0;
}
```
## 3.4 多重分支结构
### 3.4.1 概述
* 语法:
```c
if (条件表达式1) {
语句块1;
} else if (条件表达式2) {
语句块2;
}
...
} else if (条件表达式n) {
语句块n;
} else {
语句块n+1;
}
``` ```
> [!NOTE] > [!NOTE]
> >
> * ① 在 C 语言中,严格意义上是没有 boolean 类型的,使用`非0` 表示`真true``0` 表示`假false`。 > * ① 从`蜉蝣`的角度来说,从早到晚就是一生;但是,从`人类`角度来说,从早到晚却仅仅只是一天。
> * ② 首先判断关系表达式 1 的结果是真(值为 `非0`)还是假(值为 `0` > * ② 这和“天上一天,地上一年”是多么的相似,即:如果`蜉蝣`是`人类`的话,那`我们`就是`仙人`了。
> * 如果为真,就执行语句块 1然后结束当前多分支。
> * 如果是假,就继续判断条件表达式 2看其结果是真还是假。
> * 如果是真,就执行语句块 2然后结束当前多分支。
> * 如果是假,就继续判断条件表达式…看其结果是真还是假。
> * ...
> * 如果没有任何关系表达式为真,就执行语句块 n+1然后结束当前多分支。
> * ③ 当条件表达式之间是`互斥`(彼此之间没有交集)关系时,条件判断语句及执行语句间顺序无所谓。
> * ④ 当条件表达式之间是`包含`关系时,必须`小上大下 / 子上父下`,否则范围小的条件表达式将不可能被执行。
> * ⑤ 当 if-else 结构是多选一的时候,最后的 else 是可选的,可以根据需要省略。
> * ⑥ 如果语句块中只有一条执行语句的时候,`{}`是可以省略的;但是,强烈建议保留!!!
* 流程图,如下所示: * 存储器的层次结构CPU 中也有存储器,即:寄存器、高速缓存 L1、L2 和 L3如下所示
![image-20240722075241253](./assets/5.png) ![img](./assets/4.png)
### 3.4.1 应用示例
* 需求:张三参加考试,他和父亲达成协议,如果成绩不到 60 分没有任何奖励;如果成绩 60分到 80 分,奖励一个肉夹馍;如果成绩 80 分(含)到 90 分,奖励一个 ipad如果成绩 90 分及以上,奖励一部华为 mate60 pro 。
* 示例:
```c
#include <stdio.h>
int main() {
int score = 0;
printf("请输入分数:");
scanf("%d", &score);
// 容错:分数不可能小于 0 或大于 100
if (score < 0 || score > 100) {
printf("输入的分数有误!\n");
return 0;
}
if (score >= 90) {
printf("奖励你一部华为 mate60 pro\n");
} else if (score >= 80) {
printf("奖励你一个 ipad\n");
} else if (score >= 60) {
printf("奖励你一个肉夹馍\n");
} else {
printf("你的成绩不及格,没有任何奖励!");
}
return 0;
}
```
### 3.4.2 应用示例
* 需求:判断水的温度,如果大于 95℃则打印 "开水";如果大于 70℃ 且小于等于 95℃则打印 "热水";如果大于 40℃ 且小于等于 70℃则打印 "温水";如果小于等于 40℃则打印 "凉水"。
* 示例:
```c
#include <stdio.h>
int main() {
int temperature = 0;
printf("请输入水的温度:");
scanf("%d", &temperature);
if (temperature > 95) {
printf("开水 \n");
} else if (temperature > 70 && temperature <= 95) {
printf("热水 \n");
} else if (temperature > 40 && temperature <= 70) {
printf("温水 \n");
} else {
printf("凉水 \n");
}
return 0;
}
```
## 3.5 多重分支结构 switch
### 3.5.1 概述
* 语法:
```c
switch(表达式){
case 常量值1:
语句块1;
//break;
case 常量值2:
语句块2;
//break;
...
case 常量值n:
语句块n;
//break;
[default:
语句块n+1;
]
}
```
> [!NOTE] > [!NOTE]
> >
> * ① switch 后面表达式的值必须是一个整型char、short、int、long 等)或枚举类型 > 上图以层次化的方式,展示了价格信息,揭示了一个真理,即:鱼和熊掌不可兼得。
> * ② case 后面的值必须是常量,不能是变量。 >
> * ③ default 是可选的,当没有匹配的 case 的时候,就执行 default > - ① 存储器越往上速度越快,但是价格越来越贵, 越往下速度越慢,但是价格越来越便宜。
> * ④ break 语句可以使程序跳出 switch 语句块,如果没有 break会执行下一个 case 语句块,直到遇到 break 或者执行到 switch 结尾,这个现象称为穿透 > - ② 正是由于计算机各个部件的速度不同,容量不同,价格不同,导致了计算机系统/编程中的各种问题以及相应的解决方案。
* 流程图,如下所示: * 正是由于 CPU、内存以及 I/O 设备之间的速度差异,从而导致了计算机的性能瓶颈,即所谓的`“冯·诺依曼体系结构的瓶颈”`。
![](./assets/6.png) ![](./assets/5.svg)
### 3.5.2 应用示例 * 因为 CPU 的处理速度远远快于内存和 I/O 设备导致在等待数据处理和传输的时候CPU 大部分处于空闲状态。就是这种显著的速度差异就导致了计算机的性能瓶颈,限制了整个计算机系统的效率。
* 需求编写一个程序该程序可以接收一个字符比如a、b、c、d其中 a 表示星期一b 表示星期二…,根据用户的输入显示相应的信息,要求使用 switch 语句。
* 示例:
```c
#include <stdio.h>
int main() {
char chs;
printf("请输入一个字符a、b、c、d");
scanf("%c", &chs);
switch (chs) {
case 'a':
printf("今天是星期一 \n");
printf("窗前明月光 \n");
break;
case 'b':
printf("今天是星期二 \n");
printf("疑是地上霜 \n");
break;
case 'c':
printf("今天是星期三 \n");
printf("举头望明月 \n");
break;
case 'd':
printf("今天是星期四 \n");
printf("低头思故乡 \n");
break;
default:
printf("输入错误!");
break;
}
return 0;
}
```
### 3.5.3 应用示例
* 需求编写程序输入月份输出该月份有多少天。说明1 月、3 月、5 月、7月、8 月、10 月、12 月有 31 天4 月、6 月、9 月、11 月有 30 天2 月有 28 天或 29 天。
* 示例:
```c
#include <stdio.h>
int main() {
int month;
printf("请输入月份 (1-12)");
scanf("%d", &month);
switch (month) {
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
printf("%d 月有 31 天\n", month);
break;
case 4:
case 6:
case 9:
case 11:
printf("%d 月有 30 天\n", month);
break;
case 2:
printf("%d 月有 28 天或 29 天\n", month);
break;
default:
printf("输入错误!");
break;
}
return 0;
}
```
### 3.5.4 switch 和 if else if 的比较
* ① 如果判断条件是判等,而且符合整型、枚举类型,虽然两个语句都可以使用,建议使用 swtich 语句。
* ② 如果判断条件是区间判断,大小判断等,使用 if...else...if。
## 3.6 嵌套分支
### 3.6.1 概述
* 嵌套分支是指,在一个分支结构中又嵌套了另一个分支结构,里面的分支的结构称为内层分支,外面的分支结构称为外层分支。
> [!NOTE] > [!NOTE]
> >
> 嵌套分支层数不宜过多,建议最多不要超过 3 层。 > * 对于硬件的这种显著的速度差异,我们程序员是无法解决的。
> * 但是,为了平衡三者之间的速度鸿沟,我们可以通过引入`缓冲区`技术,来降低系统的 I/O 次数,降低系统的开销。
### 3.6.2 应用示例 * 其实,在硬件上也是有`缓冲区`的CPU 内部集成了缓存,将经常使用到的数据从内存中加载到缓存中。
* 需求:根据淡旺季的月份和年龄,打印票价。
> [!NOTE] > [!NOTE]
> >
> * ① 4 -10 是旺季: > 对于缓存和内存中数据的同步解决方案会有各种各样的算法LRU 等。
> * 成人18-6060 。
> * 儿童(<18半价 ![](./assets/6.svg)
> * 老人(>601/3 。
> * ② 其余是淡季: ## 1.3 缓冲区
> * 成人40。
> * 其他20。 ### 1.3.1 如果存在缓冲区,键盘输入的数据是怎么到达程序的?
* 当我们在键盘上输入数据并传递给程序时,通常会经历如下的几个步骤:
* ① `键盘生成输入信号`:当我们在键盘上按下某个键的时候,键盘会将这个动作转换为对应的电信号,传递给键盘控制器。
* ② `键盘控制器发送中断信号`:计算机的`键盘控制器`会检测到按键动作,向 CPU 发送中断请求。
* ③ `CPU 执行中断处理程序`CPU 暂停当前任务,进入中断处理状态,操作系统的中断处理程序接收并处理键盘输入。
* ④ `操作系统将输入存入缓冲区`:键盘输入的数据被存入`内存缓冲区`,操作系统会将这些数据暂时存放在缓冲区中,等待程序从缓冲区中读取数据。
* ⑤ `程序读取数据`:程序通过读取函数从缓冲区读取数据进行处理。
* 其对应的图示,如下所示:
* 示例:
```c
#include <stdio.h>
int main() {
int month;
int age;
double price = 60;
printf("请输入月份 (1-12)");
scanf("%d", &month);
printf("请输入年龄:");
scanf("%d", &age);
// 旺季
if (month >= 4 && month <= 10) {
if (age < 18) {
price /= 2;
} else if (age > 60) {
price /= 3;
}
} else {
if (age >= 18) {
price = 40;
} else {
price = 20;
}
}
printf("票价: %.2lf\n", price);
return 0;
}
```
# 第四章:随机数
## 4.1 概述
* 所谓的随机数就是没有规则,并且不能预测的一些数字,也称为真随机数。
* 程序中也是可以产生随机数的,但是是通过一些固定规则产生的,称为伪随机数。
* 常见的伪随机数线性同余方程LCG的公式如下所示
$X_{n+1} = (a \cdot X_n + b) \mod m$
* 其中X 是伪随机序列a 是乘数(通常选择一个大于 0 的常数,典型值有 1664525b 是增量(选择一个大于 0 的常数,典型值有 1013904223 m 是模数( 通常选择一个大的常数,常见值有 ( 2^{32} ) ,即 4294967296
> [!NOTE]
>
> 假设 a = 31 b = 13 m = 100 ;那么,伪随机数的公式就是 `X_{n+1} = (31 × X_n + 13) % 100 `
>
> * 如果 `X_{n}` = 1 ,那么 `X_{n+1}` = 44 。
> * 如果 `X_{n}` = 44 ,那么 `X_{n+1}` = 77 。
> * 如果 `X_{n}` = 77 ,那么 `X_{n+1}` = 0 。
> * ...
>
> 最后,将得到 44、77、0、13、16、9 、92、65、28 ... ,其中 1 也称为初始种子(随机数种子)。
* 工作原理:
* ① 设置初始种子X_0
* 种子值是算法生成随机数序列的起点。
* 不同的种子值会产生不同的随机数序列。
* ② 递归生成随机数:
* 从初始种子开始,通过公式不断生成新的随机数。
* 每次迭代都使用前一次生成的随机数作为输入。
> [!NOTE]
>
> 如果种子的值相同,那么每次生成的随机数将相同,解决方案就是将种子的值设置为当前的时间戳。
## 4.2 C 语言中随机数的产生
* ① 设置随机数种子:
```c
srand(10); // seed 种⼦ rand random 随机
```
> [!NOTE]
>
> 随机数函数在 `#include <stdlib.h>` 中声明。
* ② 根据随机数种⼦计算出⼀个伪随机数:
```c
// 根据种⼦值产⽣⼀个 0-32767 范围的随机数
int result = rand();
```
* ③ 产生一个指定范围内的随机数:
```c
int random_in_range(int min, int max) {
return rand() % (max - min + 1) + min;
}
```
* 示例:
```c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// 生成指定范围的随机数的函数
int randomInRange(int min, int max) {
return rand() % (max - min + 1) + min;
}
int main() {
// 使用当前时间作为种子
srand(time(0));
// 定义范围
int min = 1;
int max = 100;
// 生成并打印随机数
for (int i = 0; i < 10; ++i) {
int random = randomInRange(min, max);
printf("%d \n", random);
}
return 0;
}
```
# 第五章:循环结构(⭐)
## 5.1 概述
* 循环结构:在某些条件满足的情况下,反复执行特定代码的功能。
## 5.2 for 循环
### 5.2.1 概述
* 语法:
```c
for(初始化条件①;循环条件表达式②;迭代语句④){
循环体语句③
}
```
> [!NOTE]
>
> * ① 初始化条件,用于初始化循环变量,只会执行一次,且循环开始前就执行(可以声明多个变量,但是必须是同一类型,用逗号 `,` 隔开)。
> * ② 循环条件表达式每次循环都执行,同 while 循环一样,每次先判断后执行循环体语句。
> * ③ 迭代语句每次循环都执行,在大括号中循环体语句之后执行(如果有多个变量更新,用逗号 `,` 隔开)。
* 流程图,如下所示:
![](./assets/7.png) ![](./assets/7.png)
> [!NOTE] > [!IMPORTANT]
> >
> 执行过程是:① --> ② --> ③ --> ④ --> ② --> ③ --> ④ --> ... --> ② 。 > 其实C 语言中的 `printf` 函数和 `scanf` 函数,其内部就使用了缓冲区。
### 5.2.2 应用示例
* 需求:输出 5 行 `Hello World!`
* 示例:
```c
#include <stdio.h>
int main() {
for (int i = 1; i <= 5; ++i) {
printf("Hello World!\n");
}
return 0;
}
```
### 5.2.3 应用示例
* 需求:求 1 ~ 100 之内所有偶数的和,以及偶数的个数。
* 示例:
```c
#include <stdio.h>
int main() {
int sum = 0;
int count = 0;
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
sum += i;
count++;
}
}
printf("1 ~ 100 中的所有偶数的和为: %d \n", sum);
printf("1 ~ 100 中的所有偶数的个数为: %d \n", count);
return 0;
}
```
### 5.2.4 应用示例
* 需求:输出所有的水仙花数,所谓水仙花数是指一个 3 位数,其各个位上数字立方和等于其本身,例如:`153 = 1×1×1 + 3×3×3 + 5×5×5`。
* 示例:
```c
#include <stdio.h>
int main() {
int count = 0;
for (int i = 100; i <= 999; i++) {
// 获取三位数
int ge = i % 10;
int shi = i / 10 % 10;
int bai = i / 100;
// 判定是否为水仙花数
if (ge * ge * ge + shi * shi * shi + bai * bai * bai == i) {
printf("水仙花数:%d\n", i);
count++;
}
}
printf("水仙花数总个数:%d\n", count);
return 0;
}
```
### 5.2.5 应用示例
* 需求:将 1 ~ 10 倒序输出10 、9 、8 ...
* 示例:
```c
#include <stdio.h>
int main() {
for (int i = 10; i >= 0; i--) {
printf("%d ", i);
}
return 0;
}
```
### 5.2.6 应用示例
* 需求:输入两个正整数 m 和 n 求其最大公约数和最小公倍数例如12 和 20 的最大公约数是 4 ,最小公倍数是 60 。
> [!NOTE]
> >
> * 如果数 a 能被数 b 整除,且结果是整数,那么 a 就叫做 b 的倍数b 就叫做 a 的约数(因数)。 > * ① 当我们使用 `printf` 函数输出数据的时候,数据并不会立即就写出到输出设备(如:屏幕等)。而是先将其放置到 `stdout 缓冲区`中,然后在满足条件的时候,再从缓冲区中刷新到输出设备。
> * 如果一个整数同时是几个整数的约数,则称该整数为这些整数的公约数;其中,数值最大的称为最大公约数。 > * ② 当我们使用 `scanf` 函数输入数据的时候,数据并不会立即就从输入设备中读取(如:键盘等)。而是先将其放置到 `stdin 缓冲区`中,然后在满足条件的时候,再从缓冲区中加载数据。
> * 如果一个整数同时为两个或多个整数的倍数的数,则称该整数为这些整数的公倍数;其中,数值最小的称为最小公倍数。
### 1.3.2 如果没有缓冲区,键盘输入的数据是怎么到达程序的?
* 当我们在键盘上输入数据并传递给程序时,通常会经历如下的几个步骤:
* ① `键盘生成输入信号`:当我们在键盘上按下某个键的时候,键盘会将这个动作转换为对应的电信号,传递给键盘控制器。
* ② `键盘控制器发送中断信号`:键盘控制器检测到按键动作,向 CPU 发送`中断请求`,通知操作系统有输入数据。
* ③ `操作系统处理输入`:操作系统接收到`中断信号`后,立即获取键盘数据并处理。由于没有缓冲区,操作系统必须将数据立即传递给程序。
* ④ `程序直接读取数据`:程序必须在键盘每次输入后立即读取数据,并且处理这个输入,不会有任何数据被暂存或积累。
* 示例: * 其对应的图示,如下所示:
```c
#include <stdio.h>
int main() {
int m = 12, n = 20;
// 取出两个数中的较小值
int min = (m < n) ? m : n;
for (int i = min; i >= 1; i--) {
if (m % i == 0 && n % i == 0) {
printf("最大公约数是:%d\n", i); // 公约数
break; //跳出当前循环结构
}
}
// 取出两个数中的较大值
int max = (m > n) ? m : n;
for (int i = max; i <= m * n; i++) {
if (i % m == 0 && i % n == 0) {
printf("最小公倍数是:%d\n", i); // 公倍数
break;
}
}
return 0;
}
```
## 5.3 while 循环
### 5.3.1 概述
* 语法:
```c
初始化条件①;
while (循环条件语句②) {
循环体语句③;
迭代语句④;
}
```
> [!NOTE]
>
> * ① `while(循环条件部分)` 中循环条件为`非0`值,表示 `true`、`真`;为`0`值,表示 `false`、`假`。
> * ② 当循环条件表达式成立,就执行循环体语句,直到条件不成立停止循环。
> * ③ 为避免死循环,循环条件表达式不能永远成立,且随着循环次数增加,应该越来越趋向于不成立。
> * ④ for 循环和 while 循环`可以相互转换`,二者没有性能上的差别。
> * ⑤ for 循环与 while 循环的区别:`初始化条件部分的作用域不同`。
* 流程图,如下所示:
![](./assets/8.png) ![](./assets/8.png)
> [!NOTE] > [!NOTE]
> >
> 执行过程是:① --> ② --> ③ --> ④ --> ② --> ③ --> ④ --> ... --> ② 。 > 如果没有缓冲区,键盘输入的数据将无法有效地被程序管理和处理,系统的工作效率会显著下降,具体影响体现在以下几个方面:
>
> * ① `程序与设备的频繁交互`:在没有缓冲区的情况下,程序需要直接与键盘设备进行交互。这意味着每次按键输入,操作系统都必须立即将数据传递给程序处理。这样会带来以下问题:
> * **频繁的 I/O 操作**:每一次键盘输入都会触发一个 I/O 操作,将数据直接传输给程序。程序必须每次都立即响应输入设备,执行读操作,导致程序处理器频繁被中断。
> * **实时响应要求**:程序需要时刻等待并响应输入,哪怕是输入非常小的数据(比如一个字符),程序都必须立即读取并处理。这对程序的设计提出了很高的实时性要求,可能会降低程序的运行效率。
> * ② `处理效率低下`:由于没有缓冲区,程序无法积累多个输入数据再进行批量处理。每一次输入必须立即处理,程序执行的效率会受到影响:
> - **I/O 阻塞**:程序可能会因为等待输入设备的响应而阻塞。没有缓冲区的情况下,程序不能继续执行其他任务,必须等待每一次输入完成后才能继续执行其他操作。
> - **浪费系统资源**:程序频繁地切换到处理 I/O 操作,导致处理器资源被大量占用。在处理较大数据量时,这种方式的效率极低,容易造成资源浪费。
> * ③ `用户体验差`:从用户角度来看,程序对键盘输入的响应会显得非常僵硬,无法处理多个输入操作的积累:
> - **输入延迟**:程序必须实时处理每个键盘输入,用户输入数据的速度一旦超过程序的处理能力,可能导致输入延迟或丢失输入。
> - **无法处理复杂输入**:如果用户需要输入多个字符或进行复杂的输入操作(比如连续输入多个命令),程序可能难以一次性正确处理,因为它只能逐一处理每一个输入,而无法一次性获取多个输入进行批量处理。
### 5.3.2 应用示例 ### 1.3.3 缓冲区的好处
* 需求:输出 5 行 `Hello World!` * 使用缓冲区的好处:`减少了 I/O 操作的频率,降低了系统资源的消耗,提高了系统的性能,提升了用户的使用体验`。
### 1.3.4 缓冲区是如何提高 I/O 操作的频率?
* 对于 C 语言中的 `printf` 函数和 `scanf` 函数,其功能如下:
* `printf` 函数:将程序中的数据输出到外部设备(如:显示器)中。
* `scanf` 函数:从外部设备(如:键盘)中读取数据到程序中。
* 这些都是非常典型的 I/O 操作,并且 I/O 过程的效率也是很低的。除了硬件性能本身的差异外I/O 操作的复杂性也是非常重要的因素,每次 I/O 操作都会带来一些固定的开销,如:
* ① 每次 I/O 操作都需要设备初始化和响应等待。
* ② 操作系统管理 I/O 请求,涉及中断处理和上下文切换,这些都消耗了大量时间。
* ③ 应用从用户态切换到内核态的系统调用也会带来额外的时间开销。I/O 操作普遍涉及系统调用)
* ④ ...
* 如果每输入一个字符或每输出一个字符都需要进行一次完整的 I/O 操作,那么这些固定的开销会迅速积累,进而导致系统的性格显著下降。
* 硬件层面的效率低下,我们没有办法通过软件层面的优化去解决。但对于这些大量的固定开销,我们可以通过`缓冲区`来进行效率优化。
* 示例: > [!IMPORTANT]
>
> * ① 缓冲区的主要目的是暂时存储数据,然后在适当的时机一次性进行大量的 I/O 操作。
> * ② 这样,多个小的 I/O 请求可以被组合成一个大 I/O 的请求,有效地分摊了固定开销,并显著提高了总体性能。
```c * 对于 `scanf` 函数而言,当用户通过键盘输入字符的时候,这些输入的字符首先被保存在 `stdin` 的缓冲区中,`当满足某个触发条件后`,才传递给程序处理,这样就减少了总的 I/O 次数,提高了效率。
#include <stdio.h> * 对于 `printf` 函数而言,输出的内容首先会保存到 `stdout` 的缓冲区中,`当满足某个触发条件后`,这些内容会一次性输出并显示到屏幕,降低了与显示设备的交互频率。
int main() {
int i = 1;
while (i <= 5) {
printf("Hello World!\n");
i++;
}
return 0;
}
```
### 5.3.3 应用示例
* 需求:求 1 ~ 100 之内所有偶数的和,以及偶数的个数。
* 示例:
```c
#include <stdio.h>
int main() {
int sum = 0;
int count = 0;
int i = 1;
while (i <= 100) {
if (i % 2 == 0) {
sum += i;
count++;
}
i++;
}
printf("1 ~ 100 中的所有偶数的和为: %d \n", sum);
printf("1 ~ 100 中的所有偶数的个数为: %d \n", count);
return 0;
}
```
### 5.3.4 应用示例
* 需求:世界最高山峰是珠穆朗玛峰,它的高度是 8848.86 米,假如我有一张足够大的纸,它的厚度是 0.1 毫米。请问,我折叠多少次,可以折成珠穆朗玛峰的高度?
* 示例:
```c
#include <stdio.h>
int main() {
// 折叠的次数
int count = 0;
// 珠峰的高度
int zfHeight = 8848860;
// 每次折叠的高度
double paperHeight = 0.1;
while (paperHeight <= zfHeight) {
count++;
paperHeight *= 2;
}
printf("需要折叠 %d 次,才能得到珠峰的高度。\n", count);
printf("折纸的高度为 %.2f 米,超过了珠峰的高度", paperHeight / 1000);
return 0;
}
```
### 5.3.5 应用示例
* 需求:给出一个整数 n ,判断该整数是否是 2 的幂次方。如果是,就输出 yes ;否则,输出 no 。
> [!NOTE] > [!NOTE]
> >
> 思路: > * ① 如果你还不能理解,就可以将 I/O 操作,看做是搬家。对于搬家而言,需要搬运东西的总量是固定的,搬一趟的时间也是差不多的。我们当然希望:一次性搬的东西尽量多,搬运的次数尽量少,这样总耗时就少。
> > * ② 不使用缓冲区,就类似每次搬家只能手提一个东西,需要频繁的往返。而使用缓冲区,就好比我们使用一个小推车,可以一次性的搬运多个东西,极大的提高了效率。
> * ① 2^ 0 = 1 2^1 = 2 2^2 = 42^3 = 82^4 = 162^5 = 32 ...,规律:每一个数字都是前一个数字的 2 倍(任意一个数字,不断的除以 2 ,最终看结果是否是数字 1 )。
> * ② 循环终止条件:
> * 结果是 1 的时候,就可以结束,输出 yes 。
> * 如果在除以 2 的时候,无法被 2 整数,也可以结束,输出 no ,如: 100 / 2 = 5050 / 2 = 25 。
### 1.3.5 缓冲区的分类
* 从上述的内容中,我们可以明确到看到缓冲区有一个显著的特点:`当满足某个触发条件后,程序会开始对缓冲区的数据执行输入或输出操作`。而这种`满足某个条件,就触发数据传输`的行为,就称为`缓冲区的自动刷新`机制。
* 基于这种自动刷新的触发条件的不同,我们可以将缓冲区划分为以下三种类型:
* ① `全缓冲(满缓冲)`:仅当缓冲区达到容量上限时,缓冲区才会自动刷新,并开始处理数据。否则,数据会持续积累在缓冲区中直到缓冲区满触发自动刷新。`文件操作`的输出缓冲区便是这种类型的经典例子。
* ② `行缓冲`:缓冲区一旦遇到换行符,缓冲区就会自动刷新,所有数据都会被传输。`stdout` 缓冲区就是典型的行缓冲区。
* ③ `无缓冲(不缓冲)`:在此模式下,数据不经过中间的缓冲步骤,每次的输入或输出操作都会直接执行。这种方法适用于需要快速、实时响应的场合。`stderr`(标准错误输出)就是这种方式,它经常被用来即时上报错误信息。
* 示例: * 之前,我们经常会在代码中,会加入以下的代码,其实就是为了让行缓冲变为无缓冲,如下所示:
```c ```c {6}
#include <stdio.h> #include <stdI/O.h>
int main() { int main() {
// 禁用 stdout 缓冲区 // 禁用 stdout 缓冲区
setbuf(stdout, NULL); setbuf(stdout, nullptr);
int n = 0;
printf("请输入一个整数:");
scanf("%d", &n);
while (n > 1 && n % 2 == 0) {
n /= 2;
}
if (n == 1) {
printf("yes");
} else {
printf("no");
}
return 0;
}
```
### 5.3.6 应用示例
* 需求整数反转123 --> 321 。
> [!NOTE]
>
> 思路:从右边开始,依次获取每一位数字,再拼接起来。
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
int num = 0; int num = 0;
int original = 0;
int rev = 0;
printf("请输入一个整数:"); printf("请输入一个整数:");
scanf("%d", &num); scanf("%d", &num);
original = num; printf("你输入的整数是:%d\n", num);
// 从右边开始,依次获取每个数字,然后拼接到 rev 中 return 0;
/** }
* 第 1 次123 % 10 = 3rev = 0 * 10 + 3 = 3 ```
* 第 2 次12 % 10 = 2rev = 3 * 10 + 2 = 32
* 第 3 次1 % 10 = 1rev = 32 * 10 + 1 = 321 * 如果不加入上述的代码,将会这样显示:
![](./assets/9.gif)
* 但是,一旦我们加入了上述的代码,将会这样显示:
![](./assets/10.gif)
> [!NOTE]
>
> * ① setbuf 是 C 语言标准库中的一个函数,用于设置文件流的缓冲区。它允许程序员控制 I/O 操作的缓冲行为,从而影响文件流(如 `stdin`、`stdout` 或文件指针 `FILE *` 类型)的效率和顺序。
> * ② 其定义,如下所示:
>
> ```c
> /**
> * @param stream 缓冲区的文件流
> * @param buf 用户提供的缓冲区,如果为 NULL就是禁用缓冲
> */
> void setbuf(FILE *stream, char *buf);
> ```
> * ③ 不同的编译器和开发环境可能会对输出缓冲进行特殊设置,尤其是在调试模式下,以便提供更好的调试体验,例如:微软的 MSVC 在 debug 模式下即使没有换行符printf 函数的输出通常也会立即显示在控制台上。这种行为是为了帮助程序员更有效地调试程序即时看到他们的输出而不需要等待缓冲区刷新条件。但是遗憾的是GCC 在 debug 模式中,并没有这么做!!!
> [!IMPORTANT]
>
> * ① 无论是哪种类型的缓冲区,当缓冲区满了时,都会触发自动刷新。
>
> * 全缓冲区:唯一的自动刷新条件是缓冲区满。
>
> * 行缓冲区:除了缓冲区满导致的自动刷新,还有遇到换行符的自动刷新机制。
>
> * ② 手动刷新:大多数缓冲区提供了手动刷新的机制,如:使用 `fflush` 函数来刷新 stdout 缓冲区,也可以使用 `setbuf` 函数来禁用缓冲区。
> * ③ `输出缓冲区中的数据需要刷新才能输出到目的地,但输入缓冲区通常不需要刷新,强制刷新输入缓冲区往往会引发未定义行为。`
> * ④ 当程序执行完毕main函数返回缓冲区通常会自动刷新除此之外还有一些独特的机制也可以刷新缓冲区。但这些机制可能因不同的编译器或平台而异不能作为常规手段。`强烈建议依赖手动或者常规自动刷新的机制来完成缓冲区的刷新。`
# 第二章printf 函数
## 2.1 概述
* printf 函数的核心作用就是将各种数据类型的数据转换为字符的形式输出到 `stdout` 缓冲区中。
* 语法:
```c
extern int printf (const char *format, ...);
```
> [!NOTE]
>
> * ① format 参数是`格式化字符串`,常见的格式占位符有 `%d`、`%f` 等。
> * ② printf 函数和 scanf 函数只需要大致了解一下用法,不比深究。
> * ③ 在实际开发中,如果我们使用 Qt 开发,或使用 C++ 作为服务器开发,会有更高级的输入输出功能,如:使用 C++ 的标准输出流 `std::cout``std::cin`,或者直接使用日志库(如:`spdlog`、`glog` 等)来处理日志和调试输出。
* printf 函数的语法规则,如下所示:
![](./assets/11.svg)
> [!NOTE]
>
> * ① 对于 format 参数中的非格式化字符串普通字符printf 函数会将其作为普通字符原封不动的进行显示,如:`我今年 岁`。
> * ② 对于 format 参数中的格式化字符串,即:以 `%`开头的字符,会和后面输出列表中的字符一一匹配,然后将匹配到的字符替换对应的格式化字符,如:`我今年%d岁`中的`%d`会被替换为`18` 。
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
// 声明变量并赋值
int num = 18;
// 使用输出语句,将变量 num 的值输出,其中 %d 表示输出的是整数
printf("我今年%d岁\n", num);
return 0;
}
```
## 2.2 格式占位符的转换说明
* 语法:
```c
%[标志][字段宽度][.精度][长度]说明符
```
> [!IMPORTANT]
>
> * ① `%` 是`格式占位符`的`开头`,是必不可少的,其余部分可以省略。
> * ② `说明符`是`格式占位符`的`结尾`,是必不可少的,其余部分可以省略。
>
> | 格式符 | 说明 |
> | ---------- | ------------------------------------------------------------ |
> | `d``i` | 表示有符号的十进制整数。 |
> | `u` | 表示无符号的十进制整数。 |
> | `o` | 表示无符号的八进制整数。 |
> | `x` | 表示无符号的十六进制整数,使用小写字母(例如:`a-f`)。 |
> | `X` | 表示无符号的十六进制整数,使用大写字母(例如:`A-F`)。 |
> | `f` | 浮点数(普通浮点数表示) |
> | `e` | 强制用科学计数法显示此浮点数使用小写的“e”表示10的幂次。 |
> | `E` | 强制用科学计数法显示此浮点数使用大写的“E”表示10的幂次。 |
> | `g` | 选择最合适的表示方式,浮点数或科学记数法。<br>当选择使用科学计数法显示此浮点数时使用小写的“e”表示10的幂次。 |
> | `G` | 选择最合适的表示方式,浮点数或科学记数法。<br/>当选择使用科学计数法显示此浮点数时使用大写的“E”表示10的幂次。 |
> | `c` | 字符 |
> | `s` | 字符串 |
> | `p` | 指针 |
> [!NOTE]
>
> * ① `[标志]`用于决定一些特殊的格式,如:
> * `-`:左对齐输出。如果没有该标志,默认是右对齐输出。
> * `+`:输出正负号。对于正数,会输出 `+`;对于负数,会输出 `-`
>
> * ② `[字段宽度]`用于指定输出的最小字符宽度,但不会导致截断数据:
> * 如果输出的字符,宽度小于指定的宽度,那么输出的值将会按照指定的**`[标志]`**来进行填充。若标志位没有 0 ,则会填充空格。
> * 如果输出的字符,宽度大于指定的宽度,那么 printf 函数并不会截断,而是完全输出所有字符。
> * ③ `[.精度]`定义打印的精度:
> * 对于整数,表示要输出的最小位数,若位数不足则左侧填充 0 。
> * 对于浮点数,表示要在小数点后面打印的位数。
> * 当有效数字不足时,会自行在后面补 0 。
> * 当有效位数超出时,会截断保留指定的有效位数。这个过程一般会遵守 "四舍五入" 的原则。
> * 但由于浮点数存储的固有精度问题,某些数值可能不能完美表示,导致结果中的数字稍有偏差。
> * 需要注意的是,在不指定`[.精度]`的情况下,浮点数默认显示 6 位小数,多的部分舍弃,不够的话,会在后面补 0 。
> * ④ `[长度]`主要描述参数的数据类型或大小。常见的长度修饰符有:
>
> | 长度修饰符 | 说明 |
> | ------------------ | ------------------------------------------------------------ |
> | `h` | 与整数说明符一起使用,表示 short 类型。 |
> | `l (小写的 L)` | 通常与整数或浮点数说明符一起使用,表示 long对于整数或 double对于浮点数。 |
> | `ll (两个小写的L)` | 与整数说明符一起使用,表示 long long 类型的整数。 |
> | `L (大写的L)` | 与浮点数说明符一起使用,表示 long double 。 |
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
printf("|%4f|\n", 3.14159f);
printf("|%10f|\n", 3.14159f);
printf("|%.4f|\n", 3.14159f);
printf("|%4.1f|\n", 3.14159f);
printf("|%04.1f|\n", 3.14159f);
printf("|% 4.1f|\n", 3.14159f);
printf("|%-4.1f|\n", 3.14159f);
printf("|%+4.1f|\n", 3.14159f);
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
int i = 40;
float x = 839.21f;
printf("|%d|%5d|%-5d|%5.3d|\n", i, i, i, i);
printf("|%f|%10f|%10.2f|%-10.2f|\n", x, x, x, x);
return 0;
}
```
## 2.3 格式占位符中的特殊符号 %
* 在格式占位符中 `%`用于表示转换的开头。如果我们也希望打印一个 `%`,就可以使用 `%%` 来表示一个 `%`
* 示例:
```c {11}
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
int progress = 50;
// 下载进度: 50%
printf("下载进度: %d%%\n", progress);
return 0;
}
```
## 2.4 格式占位符中的特殊符号 *
* 如果我们希望变量在程序运行期间能够打印小数点后的位置以及打印结果的总宽度,就可以在格式占位符中通过 * 来代替。
* 示例:
```c {11}
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
int width = 5;
int point = 2;
printf("|%*.*f|", width, point, 3.1415); // | 3.14|
return 0;
}
```
## 2.5 格式占位符中的 %f 和 %lf
* 格式占位符 `%f` 是用来输出 `float` 类型的数据的,而 格式占位符 `%lf` 是用来输出 `double` 类型的数据的。
> [!IMPORTANT]
>
> * ① `%f``%lf` 是完全等价的。
> * ② 在 C99 之后的标准中,当使用 printf 函数打印浮点数的时候,不管是 float 还是 double 都会自动提升到 double 来进行处理。
> * ③ 仅限于 printf 函数scanf 函数没有这样的特点scanf 函数中的 %f 和 %lf 是不一样的。
* 示例:
```c {10-11}
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
double num = 123.456;
printf("使用%%f打印的结果是: %f\n", num);
printf("使用%%lf打印的结果是: %lf\n", num);
return 0;
}
```
## 2.6 printf 函数中的返回值
* 对于 printf 函数其实是有返回值的,如下所示:
```c
extern int printf (const char *format, ...);
```
> [!NOTE]
>
> * ① 如果输出成功,将返回函数实际输出的字符总数。并且当输出成功时,返回值是一个非负数。
> * ② 如果输出失败,返回值就是一个负数。
> * ③ 在实际开发中printf 函数的返回值比较少被接受处理。
* 示例:
```c {8,11}
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
int ret = printf("hello\n");
printf("ret = %d\n", ret); // 正常输出了6个字符所以返回值是6
int ret2 = printf("");
printf("ret2 = %d\n", ret2); // 正常输出了0个字符所以返回值是0
return 0;
}
```
## 2.7 行缓冲注意事项
* printf 函数将数据输出到 stdout 的行缓冲区,但要将这些数据真正展示到外部设备(如屏幕),则需依靠 stdout 的自动刷新机制。
> [!NOTE]
>
> 为了增加输出的实时性和可预测性,有如下的常见策略:
>
> * ① 输出字符串的末尾添加换行符 `"\n"` ,这样可以立即触发缓冲区的刷新。
> * ② 使用 setbuf 函数禁用 stdout 的行缓冲区。
> * ③ 使用 fflush 函数手动刷新 stdout 的行缓冲区。
> * ④ ...
>
> 本人选择的是第 ② 种方案;但是,如果你选择第 ① 种方案,那么应该在不影响程序逻辑的前提下。
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
int chinese, math, english;
float average;
printf("请输入语文成绩:");
scanf("%d", &chinese);
printf("请输入数学成绩:");
scanf("%d", &math);
printf("请输入英语成绩:");
scanf("%d", &english);
average = (chinese + math + english) / 3.0;
printf("平均成绩为:%.2f\n", average);
return 0;
}
```
# 第三章scanf 函数
## 3.1 概述
* scanf 函数的核心作用就是从 `stdin 缓冲区`读取字符形式的数据,并将其转换为特定类型的数据。
* 语法:
```c
extern int scanf (const char *__restrict __format, ...)
```
> [!NOTE]
>
> * ① scanf 函数和 printf 函数最大的不同就是,在参数列表中中的参数是变量的地址,即:将读取到的值存放在哪个地址。
> * ② 也可以认为scanf 函数的格式是:`scanf(格式化字符串, &变量1, &变量2, ...);`,但是变量前面的 `&` 在某些情况下是可以省略的。
> * ③ 对于 scanf 函数中的格式化字符串,除了格式占位符之外,通常不需要普通字符。
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
int chinese, math, english;
float average;
printf("请输入语文成绩:");
scanf("%d", &chinese);
printf("请输入数学成绩:");
scanf("%d", &math);
printf("请输入英语成绩:");
scanf("%d", &english);
average = (chinese + math + english) / 3.0;
printf("平均成绩为:%.2f\n", average);
return 0;
}
```
## 3.2 格式占位符的转换说明
* 语法:
```c
%[*][字段宽度][长度]说明符
```
> [!IMPORTANT]
>
> * ① `%` 是`格式占位符`的`开头`,是必不可少的,其余部分可以省略。
> * ② `说明符`是`格式占位符`的`结尾`,是必不可少的,其余部分可以省略。
>
> | 格式符 | 说明 |
> | ------------ | ------------------------------------------------------------ |
> | `d` | 表示有符号的十进制整数。 |
> | `i` | `scanf` 的 i 会自动判断输入的整数的进制,支持八进制、十进制和十六进制。<br>`scanf` 中的 i 和 printf 中的 i 不一样。 |
> | `u` | 表示无符号的十进制整数。 |
> | `o` | 表示无符号的八进制整数。 |
> | `x` | 表示无符号的十六进制整数,使用小写字母(例如:`a-f`)。 |
> | `X` | 表示无符号的十六进制整数,使用大写字母(例如:`A-F`)。 |
> | `f` | 浮点数(普通浮点数表示) |
> | `e` | 强制用科学计数法显示此浮点数使用小写的“e”表示10的幂次。 |
> | `E` | 强制用科学计数法显示此浮点数使用大写的“E”表示10的幂次。 |
> | `g` | 选择最合适的表示方式,浮点数或科学记数法。<br>当选择使用科学计数法显示此浮点数时使用小写的“e”表示10的幂次。 |
> | `G` | 选择最合适的表示方式,浮点数或科学记数法。<br/>当选择使用科学计数法显示此浮点数时使用大写的“E”表示10的幂次。 |
> | `c` | 字符 |
> | `s` | 字符串 |
> | `p` | 指针 |
> | `%[字符集]` | 告诉`scanf`只接受和存储来自指定字符集的字符。<br>例如:`%[abc]`将只读取 'a'、'b' 或 'c'字符,其他的字符将导致读取停止。 |
> | `%[^字符集]` | 这是扫描集的否定形式,告诉`scanf`接受和存储除了指定字符集之外的所有字符。<br>例如:`%[^abc]`将读取除了'a'、'b', 和 'c'之外的所有字符,直到遇到这三个字符中的任何一个为止。 |
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
int chinese, math, english;
float average;
printf("请输入语文成绩:");
scanf("%d", &chinese);
printf("请输入数学成绩:");
scanf("%d", &math);
printf("请输入英语成绩:");
scanf("%d", &english);
average = (chinese + math + english) / 3.0;
printf("平均成绩为:%.2f\n", average);
return 0;
}
```
## 3.3 scanf 函数的工作原理
* scanf 函数本质上是一个`模式匹配`函数,试图将 `stdin` 缓冲区中的字符和格式字符串进行匹配。其会从左到右依次匹配格式字符串中的每一项:
* 如果匹配成功,那么 scanf 函数会继续处理格式字符串的剩余部分。
* 如果匹配失败,那么 scanf 函数将不再处理格式字符串的剩余部分,会立即返回。
* 除此之外scanf 函数的转换说明符大都默认忽略前置的空白字符,这样的设计让输入对用户更好友好,比如:
* `%d` 忽略前置的`空白字符` (包括空格符、水平和垂直制表符、换页符和换行符),然后匹配十进制的有符号整数。
* `%f` 忽略前置的`空白字符`(包括空格符、水平和垂直制表符、换页符和换行符),,然后匹配浮点数。
* ...
> [!NOTE]
>
> 在实际开发中scanf 函数最常用的格式字符串是 `%d,%d` 或者 `%d %d`
* 示例:
```c
#include <stdio.h>
int main() {
int num;
printf("请输入一个整数:");
scanf("%d", &num);
int absNum;
if (num < 0) {
absNum = -num;
} else {
absNum = num;
}
printf("%d的绝对值是%d", num, absNum);
return 0;
}
```
## 3.4 录入字符数据的特殊性
* scanf 函数用 `%c` 格式占位符来读取单个字符时,并不会跳过空白字符,%c 会读取输入的下一个字符,无论它是什么,包括空白字符。
> [!IMPORTANT]
>
> 在录入字符时,尤其是一行录入多个数据且包含输入字符时,一定要在转换说明前面留出一个空格,以匹配可能的空格。
* 示例:
```c {12}
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
char ch;
int num;
printf("请输入一个数字以及一个字符: ");
scanf("%d %c", &num, &ch); // 注意 %c 前的空格
printf("你输入的数字是: %d\n", num);
printf("你输入的字符是: %c\n", ch);
return 0;
}
```
## 3.5 scanf 函数的返回值
* 对于 scanf 函数其实是有返回值的,如下所示:
```c
extern int scanf (const char *__restrict __format, ...)
```
> [!NOTE]
>
> * ① 只要成功匹配并读取了一个数据输入项,那么函数的返回值就会是一个`正数`。注意,函数返回正数不意味着所有输入都能匹配成功,只要匹配成功一个输入项,返回值就是一个正数。
> * ② 如果返回值是`0`,那说明 scanf 没有成功匹配任何数据输入项,这通常是因为数据输入项`完全不匹配`。
> * ③ 如果函数返回值是`负数`,说明 scanf 读到了 EOF流末尾或者发生了错误。在 Windows 系统终端里,键入"Ctrl + Z" 表示输入 EOF在类Unix平台中这个按键则是"Ctrl + D",可以了解一下。
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
int num1, num2;
char ch;
int ret = scanf("%d %d %c", &num1, &num2, &ch);
/*
若键入的数据是 100 200 A则正常匹配录入 3 个数据ret 等于 3
若键入的数据是 100 A 200则正常匹配录入 1 个数据ret 等于 1
若键入的数据是 A 100 200则正常匹配录入 0 个数据ret 等于 0
*/ */
// 循环结束的条件是 num == 0 printf("ret = %d\n", ret);
while (num != 0) {
// 获取 num 右边的第一位数字
int temp = num % 10;
// 去掉最后一位数字
num /= 10;
// 将 temp 拼接到 rev 的后面
rev = rev * 10 + temp;
}
printf("%d 的反转是 %d\n", original, rev);
return 0;
}
```
## 5.4 do-while 循环
### 5.4.1 概述
* 语法:
```c
①初始化部分;
do{
③循环体部分
④迭代部分
}while(②循环条件部分);
```
> [!NOTE]
>
> * ① `do{} while();`最后有一个分号。
> * ② do-while 结构的循环体语句是至少会执行一次,这个和 for 、while 是不一样的。
> * ③ 循环的三个结构 for、while、do-while 三者是可以相互转换的。
* 流程图,如下所示:
![](./assets/9.png)
> [!NOTE]
>
> 执行过程是:① --> ③ --> ④ --> ② --> ③ --> ④ --> ② --> ... --> ② 。
### 5.4.2 应用示例
* 需求:求 1 ~ 100 之内所有偶数的和,以及偶数的个数。
```c
#include <stdio.h>
int main() {
int sum = 0;
int count = 0;
int i = 1;
do {
if (i % 2 == 0) {
sum += i;
count++;
}
i++;
} while (i <= 100);
printf("1 ~ 100 中的所有偶数的和为: %d \n", sum);
printf("1 ~ 100 中的所有偶数的个数为: %d \n", count);
return 0;
}
```
### 5.4.3 应用示例
* 需求:实现 ATM 取款机功能。
* 示例:
```c
#include <stdio.h>
int main() {
// 账户余额
double balance = 0.0;
// 客户选择
int selection;
// 存款金额
double addMoney;
// 取款金额
double minusMoney;
// 退出标识
bool exitFlag = false;
do {
printf("=========ATM========\n");
printf("\t1、存款\n");
printf("\t2、取款\n");
printf("\t3、显示余额\n");
printf("\t4、退出\n");
printf("请选择(1-4)");
scanf("%d", &selection);
switch (selection) {
case 1:
printf("您当前的余额是: %.2f\n", balance);
printf("请输入存款金额:");
scanf("%lf", &addMoney);
balance += addMoney;
printf("存款成功,您当前的余额是:%.2f\n", balance);
break;
case 2:
printf("您当前的余额是: %.2f\n", balance);
printf("请输入取款金额:");
scanf("%lf", &minusMoney);
if (minusMoney > balance) {
printf("余额不足,取款失败。\n");
} else {
balance -= minusMoney;
printf("取款成功,您的余额为:%.2f\n", balance);
}
break;
case 3:
printf("您的账户余额为:%.2f\n", balance);
break;
case 4:
exitFlag = true;
printf("欢迎下次再来。\n");
break;
default:
printf("输入有误,请重新输入。\n");
break;
}
} while (!exitFlag);
return 0;
}
```
## 5.5 嵌套循环
### 5.5.1 概述
* 所谓的嵌套循环,是指一个循环结构 A 的循环体是另一个循环结构 B 。例如for 循环里面还有一个for 循环,就是嵌套循环。
* 语法:
```c
for(初始化语句①; 循环条件语句②; 迭代语句⑦) {
for(初始化语句③; 循环条件语句④; 迭代语句⑥) {
循环体语句⑤;
}
}
```
* 其中for 、while 、do-while 均可以作为外层循环或内层循环。
- 外层循环:循环结构 A
- 内层循环:循环结构 B
![](./assets/10.png)
> [!NOTE]
>
> * ① 实际上,嵌套循环就是将内层循环当成外层循环的循环体。当只有内层循环的循环条件为 false ,才会完全跳出内层循环,才可结束外层的当次循环,开始下一次循环。
> * ② 假设外层循环次数为 m 次,内层循环次数为 n 次,则内层循环体实际上需要执行 m × n 次。
> * ③ 从二维图形的角度看,外层循环控制`行数`,内层循环控制`列数`。
> * ④ 实际开发中,我们最多见到的嵌套循环是两层,一般不会出现超过三层的嵌套循环。如果将要出现,一定要停下来重新梳理业务逻辑,重新思考算法的实现,控制在三层以内;否则,可读性会很差。
### 5.5.2 应用示例
* 需求:打印 5 行 `*` ,要求每行 6 个 `*`
* 示例:
```c
#include <stdio.h>
int main() {
for (int i = 1; i <= 5; ++i) {
for (int j = 1; j < 6; ++j) {
printf("* ");
}
printf("\n");
}
return 0;
}
```
### 5.5.3 应用示例
* 需求:打印 5 行直角三角形。
* 示例:
```c
#include <stdio.h>
int main() {
for (int i = 1; i <= 5; ++i) {
for (int j = 1; j <= i; ++j) {
printf("* ");
}
printf("\n");
}
return 0;
}
```
### 5.5.4 应用示例
* 需求:打印 5 行倒直角三角形。
* 示例:
```c
#include <stdio.h>
int main() {
for (int i = 1; i <= 5; ++i) {
for (int j = 1; j <= 6 - i; ++j) {
printf("* ");
}
printf("\n");
}
return 0;
}
```
### 5.5.5 应用示例
* 需求:打印 9 `×` 9 乘法表。
* 示例:
```c
#include <stdio.h>
int main() {
for (int i = 1; i <= 9; ++i) {
for (int j = 1; j <= i; ++j) {
printf("%d × %d = %d ", i, j, i * j);
}
printf("\n");
}
return 0;
}
```
## 5.6 无限循环
* 语法:
```c
while(1){
...
}
```
```c
for(;;){
...
}
```
> [!NOTE]
>
> * ① 在开发中有的时候并不确定需要循环多少次就需要根据循环体内部的某些条件来控制循环的结束break
> * ② 如果上述的循环结构不能终止,就会构成死循环;所以,在实际开发中,要避免出现死循环!!!
* 示例:从键盘读入个数不确定的整数,并判断读入的正数和负数的个数,输入为 0 时结束程序
```c
#include <stdio.h>
int main() {
// 记录输入的整数
int num = 0;
// 记录正数个数
int positiveCount = 0;
// 记录负数个数
int negativeCount = 0;
while (true) {
printf("请输入一个整数:");
scanf("%d", &num);
if (num > 0) {
positiveCount++;
} else if (num < 0) {
negativeCount++;
} else {
printf("程序结束!\n");
break;
}
}
printf("正数的个数:%d\n", positiveCount);
printf("负数的个数:%d\n", negativeCount);
return 0;
}
```
## 5.7 跳转控制语句
### 5.7.1 break
* break 的使用场景break 语句用于终止某个语句块的执行用在switch语句或者循环语句中。
> [!NOTE]
>
> break 一旦执行,就结束(或跳出)当前循环结构;并且,此关键字的后面,不能声明其它语句。
* 流程图,如下所示:
![](./assets/11.png)
* 示例:打印 0 ~ 10 ,如果遇到 `3` ,就停止打印
```c
#include <stdio.h>
int main() {
for (int i = 0; i < 10; ++i) {
if (i == 3) {
break;
}
printf("%d \n", i);
}
printf("程序结束!\n");
return 0; return 0;
} }
@ -1384,133 +790,11 @@ int main() {
* 示例:编写程序,要求输入一个数字,判断该数字是否是质数
```c
#include <stdio.h>
int main() {
bool isFlag = false;
int num = 0;
do {
printf("请输入一个整数(必须大于 1 ");
scanf("%d", &num);
if (num <= 1) {
printf("输入的数字不是合法,请重新输入!!!\n");
isFlag = true;
} else {
isFlag = false;
}
} while (isFlag);
bool isPrime = true;
for (int i = 2; i < num; i++) {
if (num % i == 0) {
isPrime = false;
break;
}
}
if (isPrime) {
printf("%d 是一个质数\n", num);
} else {
printf("%d 不是一个质数\n", num);
}
printf("程序结束!\n");
return 0;
}
```
### 5.7.2 continue
* continue 的使用场景continue 语句用于结束本次循环,继续执行下一次循环。
> [!NOTE]
>
> continue 一旦执行,就结束(或跳出)当次循环结构;并且,此关键字的后面,不能声明其它语句。
* 流程图,如下所示:
![](./assets/12.png)
* 示例:打印 0 ~ 10 ,如果遇到 `3` ,就继续下一次打印
```c
#include <stdio.h>
int main() {
for (int i = 0; i < 10; ++i) {
if (i == 3) {
continue;
}
printf("%d \n", i);
}
printf("程序结束!\n");
return 0;
}
```
* 示例:输出 100 以内(包括 100的数字跳过那些 7 的倍数或包含 7 的数字
```c
#include <stdio.h>
int main() {
for (int i = 1; i <= 100; i++) {
if (i % 7 == 0 || i % 10 == 7 || i / 10 == 7) {
continue;
}
printf("%d ", i);
}
printf("程序结束!\n");
return 0;
}
```
### 5.7.3 return
* return :并非专门用于结束循环的,它的功能是结束一个方法。当一个方法执行到一个 return 语句的时候,这个方法将被结束。
> [!NOTE]
>
> 和 break 和 continue 不同的是return 直接结束整个方法,不管这个 return 处于多少层循环之内。
* 示例:
```c
#include <stdio.h>
int main() {
for (int i = 1; i <= 100; i++) {
if (i % 7 == 0 || i % 10 == 7 || i / 10 == 7) {
return 0; // 结束整个函数或方法
}
printf("%d ", i);
}
printf("程序结束!\n");
return 0;
}
```

View File

@ -1,510 +0,0 @@
# 第一章内存泄漏Memory Leak
## 1.1 概述
* 有没有过这样的日子,总感觉我们的电脑,不是一个尖端的设备,而像一只疲惫的蜗牛。它在缓慢的爬行,并试图背着重重的楼房去跑马拉松,如下所示:
![](./assets/1.jpeg)
> [!NOTE]
>
> 儿歌《蜗牛与黄鹂鸟》的歌词是这样的,如下所示:
>
> * 阿门阿前一棵葡萄树。
> * 阿嫩阿嫩绿地刚发芽。
> * 蜗牛背着那重重的壳呀。
> * 一步一步地往上爬。
> * 阿树阿上两只黄鹂鸟。
> * 阿嘻阿嘻哈哈在笑它。
> * 葡萄成熟还早得很哪。
> * 现在上来干什么。
> * 阿黄阿黄鹂儿不要笑。
> * 等我爬上它就成熟了。
>
> 虽然歌曲的主旨是想通过蜗牛与黄鹂鸟的对话,表达了努力和坚持的重要性,即使速度慢,只要坚定地往前走,总会达到目标。但是,也从侧面说明了蜗牛的速度真的很慢。
* 亦或者,我们的电脑就像一个蹒跚学步的孩子在发脾气,我们多么希望她们耐心点,并配合我们。可是,她们总是拒绝和我们合作,如下所示:
![](./assets/2.jpg)
* 如果这些场景,你都感觉很熟悉,那么你很有可能就是`内存泄漏`的受害者。
> [!NOTE]
>
> * ① `内存泄漏`虽然不可见,但是它会悄悄的蚕食计算机的性能,让曾经快速的系统变成一台陈旧的机器。
> * ② 最为糟糕的时,和留下明显迹象的`漏水`不同,`内存泄漏`是不可见的,这使得它们难以识别,甚至难以修复。也正是因为这个特点,让开发人员和计算机用户都感觉头疼。
## 1.2 什么是内存泄漏?
* 我们可以将我们的计算机想象成一个繁华的城市,城市的`道路`就代表着计算器的`内存`(计算机的内存是有限的,普遍的家用个人台式机电脑最多只支持 `4` 根内存条。如果是 `DDR4` 的话,最多也就支持 `128` GB。就算是服务器也不是无穷无尽的在其上运行的`程序`就像`车辆`一样,每辆车都执行各自的任务,如下所示:
> [!NOTE]
>
> * ① 操作系统或计算机允许程序自己分配内存,并自由使用。并且,当程序执行完自己的任务之后,还可以释放掉内存,将内存还给操作系统或计算机。
> * ② 需要说明的是,并不是程序结束运行,才会释放掉内存:在 C/C++ 等语言中,是可以在程序执行完任务之后,由程序员手动释放之前申请的内存,即:调用释放内存的函数。而 Java 等 GC 的编程语言,会由 GC 帮助程序员释放内存,当然从理论上讲会有稍许停顿。但是,像 Java 语言中的 ZGC 现在已经可以控制在 10ms 了,人几乎感觉不到!!!
> * ③ 所谓的`分配内存`,就是程序向计算机或操作系统,申请一块内存空间,然后自己使用。
> * ④ 所谓的`释放内存`,就是程序告诉计算机或操作系统,不再需要使用之前申请的内存空间,那么就可以将之前申请的内存空间,归还给操作系统或计算机,让其它的程序使用。
> * ⑤ 上面例子中的`程序`就像`车辆`一样,每辆车都执行各自的任务,类似于程序在执行的时候,向操作系统申请自己的内存空间,并完成自己的任务。
![](./assets/3.jpg)
* 但是,如果有些车辆在完成自己的任务之后,就决定无限期的停在路上,而不是离开。那么,可以想象到的是,随着时间的推移,这些停放的汽车就会开始阻塞城市的道路,减慢交通速度,如下所示:
![](./assets/4.jpg)
> [!NOTE]
>
> * ① 需要说明的是,道路或网络的利用率并非越高越好。
> * ② 如果使用 D0 表示道路或网络空闲时的时延(数据包(或车辆)几乎没有排队,时延 D0 只是基本的传输或行驶时间),而 D 表示道路或网络当前的时延(数据包(或车辆)可能需要排队,这导致了额外的时延,时延 D 是包含了排队时间的总时延),那么在理想的条件下,可以使用如下的表达式来表示 D、D0 以及道路或网络利用率 U 之间的关系,即:$U = \frac{D - D_0}{D}$,经过换算一下,其结果就是:$D = \frac{D_0}{1 - U}$。
> * ③ 显而易见,道路或网络利用率并不是越大越好,过高的道路或网络利用率会产生非常大的时延。
* 由此可见,在极端情况下,这座城市甚至可能陷入停顿。
> [!NOTE]
>
> 这实际上就是`内存泄漏`对计算机的影响,即:
>
> * ① 程序可能会变慢,甚至崩溃,特别是在长时间运行的程序中。
> * ② `内存泄漏`会逐渐耗尽系统内存,造成资源浪费,并导致系统性能下降。
* 再或者,在生活中,我们必然需要用水,如果规定每个人一个月的用水量不能超过 `10t`,那么三口之间每个月的用水量就不能超过 `30t`。假设,由于水管老化或小动物(老鼠)的影响,而导致家中的水管产生轻微的破损,产生漏水的现象,如下所示:
![](./assets/5.jpg)
* 那么,家中隐藏的漏水问题在很长一段时间内是不会被注意到的。亦或者,假设每个人的用水量都没有限制,那么如果要用到 `30t` ,必然会比之前没有漏水的时候,产生的水费也要多很多。
> [!IMPORTANT]
>
> 官方定义:`内存泄漏`是指计算机程序无意中消耗的一种特定类型的内存,其中程序无法释放不再需要或使用的内存(这种内存虽然不再被程序使用,但仍然占据着系统资源),进而导致这些内存无法被系统或其他程序再次使用,随着时间的推荐,会逐渐耗尽系统内存,并最终导致系统性能下降。
## 1.3 什么会触发内存泄漏?
* 导致`内存泄漏`的原因很多,具体取决于编程语言、平台和特定的应用程序场景。以下是一些最常见的原因:
* ① **未关闭的资源**:未能关闭文件、数据库连接或网络套接字等资源可能会导致`内存泄漏`。如果这些资源保持打开状态,可能会随着时间的推移而累积并消耗大量内存。
* ② **未释放的对象引用**:保留不再需要的对象引用可以防止垃圾回收器(在具有它们的语言中)回收内存。
* ③ **循环引用**:在某些语言中,两个相互引用的对象可能会导致两个对象都无法被垃圾回收的情况,即使程序的其他部分没有引用它们。
* ④ **静态集合**:使用随时间增长而从未清除的静态数据结构可能会导致`内存泄漏`。例如:将元素添加到静态列表而不删除它们可能会导致列表无限增长。
* ⑤ **事件侦听器**:不分离事件侦听器或回调可能会导致`内存泄漏`,尤其是在 Web 浏览器等环境中。如果对象已附加到事件但不再使用,则不会对其进行垃圾回收,因为该事件仍包含对它的引用。
* ⑥ **中间件和第三方库**:有时,`内存泄漏`的原因可能不在于应用程序代码,而在于它使用的中间件或第三方库。这些组件中的错误或低效代码可能会导致`内存泄漏`。
* ⑦ **内存管理不当**:在开发人员手动管理内存的语言,如: C、C++ 中,使用后未能释放内存或使用 “悬空指针” 可能会导致泄漏。
* ⑧ **内存碎片**:虽然不是传统意义上的泄漏,但碎片会导致内存使用效率低下。随着时间的推移,内存分配之间的小间隙会累积,从而难以分配更大的内存块。
* ⑨ **孤立线程**:生成但未正确终止的线程可能会消耗内存资源。这些孤立线程会随着时间的推移而累积,尤其是在长时间运行的应用程序中。
* ⑩ **缓存过度使用**:在没有适当驱逐策略的情况下实施缓存机制可能会导致内存无限消耗,尤其是在缓存无限增长的情况下。
* 在 C 语言中,可以使用 `while` 循环并结合 `malloc` 函数来实现一个内存泄漏的例子,即:
```c
#include <stdbool.h>
#include <stdlib.h>
int main() {
while (true) { // 死循环
malloc(1024); // 分配1024个字节的内存
}
return 0;
}
```
* 如果我们在 Windows 上运行该程序,就可以打开 Windows 的任务管理器(快捷键是`Ctrl + Shift + ESC`),将会发现内存的使用率在飙升。当然,稍等片刻后程序会被终止,是因为 Windows 的内存管理机制,发现我们的程序占用内存太多,会让它崩溃,防止系统卡死(其它的操作系统也有相应的措施)。
![](./assets/6.gif)
## 1.4 内存泄漏会导致什么后果?
* ① **内存使用量增加**:随着泄漏和释放的内存越来越多,整体系统内存使用量会增加。这会减少可用于其他进程和应用程序的内存,从而降低系统速度。
* ② **增加分页**:随着`内存泄漏`的累积,系统可能会开始将内存内容交换到磁盘以释放 RAM从而导致更多的磁盘 I/O。这会导致性能降低因为磁盘操作比内存操作慢得多。
* ③ **内存不足错误**:如果`内存泄漏`足够多,系统最终可能会完全耗尽可用内存。这可能会导致崩溃、内存分配失败和程序终止。
* ④ **资源争用**较高的内存使用率还会导致对缓存和资源CPU 时间等)的更多争用,因为系统尝试管理有限的资源。这会进一步降低性能。
* ⑤ **应用程序不稳定**:随着内存使用量随着时间的推移而增长,存在`内存泄漏`的应用程序可能会遇到崩溃、意外行为和间歇性故障。这会导致不稳定和可靠性问题。
* ⑥ **安全风险**`内存泄漏`会使数据在内存中的延迟时间超过预期。此数据可能包含密码、密钥或其他敏感信息,如果恶意软件或攻击者访问这些信息,则会带来安全风险。
## 1.5 检测内存泄漏的工具或技术
* ① **分析工具**
* ① Valgrind用于构建动态分析工具的检测框架最有名的 Memcheck 的套件,可以检测 C 和 C++ 程序中的内存泄漏。
* ② Java VisualVM适用于 Java 应用程序的监控、故障排除和分析工具。
* ③ .NET Memory Profiler用于查找内存泄漏并优化 .NET 应用程序中的内存使用的工具。
* ④ Golang pprof该工具可让您收集 Go 程序的 CPU 配置文件、跟踪和堆配置文件。
* ② **浏览器开发工具**Chrome、Firefox 和 Edge 等现代 Web 浏览器附带内置的开发人员工具,可帮助识别 Web 应用程序中的内存泄漏,尤其是 JavaScript 中的内存泄漏。
* ③ **静态分析**Lint、SonarQube 或 Clang Static Analyzer 等工具可以扫描代码以识别可能导致内存泄漏的模式。
* ④ **自动化测试**将内存泄漏检测整合到自动化测试中有助于在开发周期的早期捕获泄漏JUnit适用于 Java或 pytest适用于 Python等工具可以与内存分析工具集成以自动执行此过程。
* ⑤ **堆分析**检查应用程序的堆转储可以深入了解正在消耗内存的对象Eclipse MAT内存分析器工具或 Java 堆分析工具 jhat等工具可以协助进行此分析。
* ⑥ **指标**实施指标来监控一段时间内的内存使用情况有助于识别导致内存消耗增加的模式或特定操作Prometheus 和 Grafana 等。
* ⑦ **第三方库和中间件**:一些第三方解决方案提供内置的内存泄漏检测功能。如果我们怀疑这些组件可能是泄漏源,则必须查看与这些组件相关的文档或论坛。
* ⑧ **手动代码审查**:有时,识别内存泄漏的最佳方法是对代码进行彻底的手动审查,尤其是在分配和释放内存的区域中。
* ⑨ **压力测试**:在高负载或长时间运行应用程序,有助于暴露在正常情况下可能不明显的内存泄漏。
## 1.6 如何避免内存泄漏?
* ① **及时释放内存**:在程序中,确保在不再需要使用内存时及时释放它。
* ② **智能指针**:使用智能指针来帮助在 C++ 等编程语言中进行自动内存管理。
* ③ **将编程语言与垃圾回收器一起使用**:内存分配和释放由 Python 和 Java 等编程语言自动处理,这些语言包含内置的垃圾收集系统。
* ④ **利用内存管理策略:** 有效的内存管理可以防止内存泄漏。这包括始终监控我们的软件使用了多少内存,并了解何时分配和取消分配内存,即:检测内存泄漏的工具或技术。
## 1.7 总结
* **内存泄漏**是由于未释放不再使用的内存,导致内存资源逐渐减少,但不会立即导致程序崩溃,而是`长时间`运行后可能出现性能问题或最终崩溃。
# 第二章内存溢出Out Of MemoryOOM
## 2.1 概述
* 首先,说明一点,在国内的很多文章中,都将 `Out Of MemoryOOM`翻译为 `内存溢出`,但是本人认为翻译为`内存不足`更为贴切。
* 在生活中,我们在使用计算机的时候,可能会遇到打开视频网站的时候,视频网站崩溃了,并且在浏览器上显示报错信息`Error Code Out Of Memory`,如下所示:
![](./assets/7.png)
* 当然我们在使用微软办公套件Outlook 的时候,可能也会遇到系统提示 `Out Of Memory`,如下所示:
![](./assets/8.jpg)
* 亦或者,我们在打游戏的时候,会遇到系统提示 `Out Of Memory`,如下所示:
![](./assets/9.png)
* 上述的种种情景都表明了内存溢出内存不足OOM是`立即显现`的问题,尤其是当系统无法分配足够内存时,会直接导致程序崩溃或异常。
> [!NOTE]
>
> * ① 内存泄漏是一种`逐渐积累`的问题会耗尽系统内存可能最终导致内存不足理解站着茅坑不拉稀最终可能导致可用的茅坑越来越少后面的人就只能等着o(╥﹏╥)o
> * ② 内存溢出(不足)是一种`立即显现`的问题,当系统无法分配足够内存时,会`直接`导致程序崩溃或异常(理解:大象塞进冰箱,冰箱不是无限大,最终可能导致大象身体的一部分露出来,这不就`溢出`吗?换言之,就是冰箱(内存)的容量有限啊,`不`能满`足`实际需要)。
> [!IMPORTANT]
>
> 官方定义:当计算机没有足够的内存来执行操作或运行应用程序时,会发生内存不足 OOM 错误。此内存可以是`物理 RAM`(随机存取内存) 或`虚拟内存`,它使用磁盘空间扩展物理内存。当系统耗尽可用内存时,它无法再满足`内存分配`请求,从而导致 OOM 错误。此错误表示除非释放或添加内存,否则系统无法处理进一步的需求。
## 2.2 什么会触发内存溢出?
* 导致`内存溢出`的原因很多,具体取决于编程语言、平台和特定的应用程序场景。以下是一些最常见的原因:
* ① **无限循环或递归**:如果程序中的循环或递归没有正确终止条件,可能会一直运行,消耗掉所有可用内存。
* ② **内存泄漏**:程序不断分配内存而不释放,最终导致可用内存耗尽。这通常是因为程序在使用完某些数据后,没有正确地释放相关的内存。
* ③ **处理大数据集**:如果程序试图一次性加载或处理一个超大的数据集,而该数据集的大小超过了系统的可用内存,这可能会导致内存溢出。
* ④ **资源过度分配**:一些程序在运行时,可能会为某些资源(如缓存、临时数据)分配过多的内存,导致整体系统内存不足。
* ⑤ **错误的内存管理**在手动管理内存的编程语言中C 或 C++),如果程序错误地管理内存(如:重复释放、未释放或非法访问内存),也可能引发内存泄漏,进而导致内存溢出。
* ⑥ **并发操作**:如果多个进程或线程并发地进行大量内存分配操作,且这些操作没有得到有效控制,也可能导致系统内存被耗尽。
* ⑦ **外部库或工具的 Bug**:使用的第三方库或工具中存在内存管理相关的 bug也可能导致内存溢出。
## 2.3 如何避免内存溢出?
* ① **优化数据处理**
* 分块处理大数据集:如果需要处理大数据集,可以将数据分块处理,而不是一次性加载整个数据集到内存中。例如:处理大型文件时,可以逐行读取或分批读取。
* 使用流式处理对于需要处理大量数据的操作可以采用流式处理streaming这样只保留当前处理的部分数据在内存中而非全部数据。
* ② **管理对象生命周期**
* 及时释放不再使用的对象在使用动态分配内存的编程语言C++、C#、Java 等确保在对象不再需要时及时释放内存。即使在使用垃圾回收机制的语言Java、Python也要尽量避免保留对不必要对象的引用以便垃圾回收器可以及时清理它们。
* 使用智能指针或自动内存管理在手动管理内存的编程语言中使用智能指针C++中的`std::unique_ptr`或`std::shared_ptr`)来自动管理内存,减少内存泄漏的风险。
* ③ **优化算法**
* 选择更高效的算法对于需要大量计算或数据处理的任务选择内存占用更少的算法。例如尽量使用原地in-place算法它们不需要额外的内存空间。
* 减少冗余数据:避免在内存中存储冗余数据,尽可能在计算过程中利用已有的数据结构,避免重复分配相同的数据。
* ④ **监控和调试**
* 使用内存分析工具在开发过程中使用内存分析工具Valgrind、VisualVM、Py-Spy等来监控程序的内存使用情况查找和修复内存泄漏或不必要的内存分配。
* 设置内存使用限制:在某些环境中,可以设置程序的最大内存使用量,这样当程序达到内存限制时,可以捕捉并处理内存溢出的情况。
* ⑤ **避免无限循环和递归**
- 设置循环或递归的终止条件:确保所有循环和递归都有明确的终止条件,避免因逻辑错误导致无限执行,从而耗尽内存。
- 使用尾递归优化:在支持尾递归优化的语言中,尽量使用尾递归,以减少递归调用带来的内存消耗。
* ⑥ **并发编程中的内存管理**
* 控制并发操作的内存分配:在并发编程中,尽量避免多个线程或进程同时大量分配内存。可以通过任务分配、锁机制等方式合理控制并发操作的内存使用。
* 避免死锁:确保在并发编程中避免死锁情况,因为死锁可能会导致内存资源无法被释放,从而引发内存溢出。
* ⑦ **使用适当的数据结构**
* 选择合适的数据结构:根据需要选择内存效率更高的数据结构。例如,使用数组而不是链表来存储连续的数据,使用哈希表来提高查找效率等。
* 避免不必要的缓存:在程序中使用缓存时,确保缓存的大小是合理的,并且有清理机制,防止缓存占用过多内存。
> [!NOTE]
>
> 避免内存溢出通常需要良好的内存管理实践,如:优化数据处理算法、合理控制资源分配、以及定期检查和释放不再使用的内存。
## 2.4 总结
* `内存溢出`则是由于内存资源耗尽,程序试图分配新内存时失败,通常会导致程序的`立即`崩溃或异常终止。
# 第三章:内存泄漏 VS 内存溢出
## 3.1 概述
* `内存泄漏`是由于未释放不再使用的内存导致内存资源逐渐减少,但不会立即导致程序崩溃,而是长时间运行后可能出现性能问题或最终崩溃。
* `内存溢出`则是由于内存资源耗尽,程序试图分配新内存时失败,通常会导致程序的立即崩溃或异常终止。
> [!NOTE]
>
> * ① `内存泄漏`和`内存溢出`都与内存管理不当有关,但它们发生的机制和直接影响是不同的。
> * ② 避免`内存泄漏`和`内存溢出`都是编写高效、可靠软件的重要方面。
## 3.2 内存泄漏和内存溢出的联系和区别
> [!IMPORTANT]
>
> `内存泄漏`和`内存溢出`之间并不是必然的因果关系,而是两者可能会相互影响。
* ① `内存泄漏`导致`内存溢出`的可能性:
* 如果一个程序长期运行并且持续发生`内存泄漏`,未被释放的内存会慢慢积累,最终占用系统的大部分内存资源。如果`内存泄漏`严重到占用了所有可用内存,那么程序就会因为无法再分配新的内存,而出现`内存溢出`Out of Memory的情况。
* 因此,`内存泄漏`可以**间接**地导致`内存溢出`,特别是在长时间运行的程序或系统中。
* ② `内存泄漏`和`内存溢出`的区别:
* `内存泄漏`是指程序持续占用内存却不释放,导致可用内存逐渐减少。这种情况可能会在`长时间`内不显现问题,特别是如果程序只泄漏了少量内存。
* `内存溢出`则是一个更`急剧`的问题,它通常在程序尝试分配超过系统可用内存的大块内存时`立刻`发生,导致程序崩溃或异常终止。
* ③ 不必然性:
* 一个程序可能会发生`内存泄漏`,但因为泄漏的内存量很小,系统资源丰富,所以在短时间内不会出现`内存溢出`。
* `内存溢出`也可以在没有`内存泄漏`的情况下发生,如:一个程序需要处理非常大的数据集,直接导致内存不足。
> [!IMPORTANT]
>
> * ① `内存泄漏`有可能会在长时间积累后导致`内存溢出`,但这并不是必然的。
> * ② `内存溢出`可以在多种情况下发生,而`内存泄漏`只是其中可能的一个诱因。
> * ③ 因此,虽然`内存泄漏`可能最终引发`内存溢出`,但两者之间并非每次都是直接关联的。
# 第四章:内存泄漏检测和性能分析(⭐)
## 4.1 内存泄漏检测
### 4.1.1 概述
* C 语言中的指针是否使用是个颇具争议的话题现代化的高级编程语言通过各种策略和机制在编译期就能解决指针危险的问题。但是遗憾的是C 语言的指针很大程度上,在运行期才会暴露问题。
* 幸运的是,我们可以使用 `Valgrind` 项目来进行`内存泄漏检测`和`性能分析`,而 `Valgrind` 只支持 Linux 。
### 4.1.2 安装
* 在 WSL2 上安装 Valgrind
```shell
dnf -y upgrade && dnf -y install valgrind # AlmaLinux
```
```shell
apt -y update && apt -y upgrade && apt -y install valgrind # Ubuntu
```
![](./assets/10.gif)
* 查看 valgrind 可执行文件的安装位置:
```shell
which valgrind
```
![](./assets/11.gif)
### 4.1.3 整合
* CLion 中将工具链设置为 WSL2
![](./assets/12.gif)
* CLion 中配置 valgrind 的路径:
![](./assets/13.png)
* 查看 WSL2 中 cmake 的版本:
```shell
cmake --version
```
![](./assets/14.png)
* 修改项目中 CMakeLists.txt 中 cmake 的版本:
```{1} txt
cmake_minimum_required(VERSION 3.26.5) # 3.26.5
# 项目名称和版本号
project(c-study VERSION 1.0 LANGUAGES C)
# 设置 C 标准
set(CMAKE_C_STANDARD 23)
set(CMAKE_C_STANDARD_REQUIRED True)
# 辅助函数,用于递归查找所有源文件
function(collect_sources result dir)
file(GLOB_RECURSE new_sources "${dir}/*.c")
set(${result} ${${result}} ${new_sources} PARENT_SCOPE)
endfunction()
# 查找顶层 include 目录(如果存在)
if (EXISTS "${CMAKE_SOURCE_DIR}/include")
include_directories(${CMAKE_SOURCE_DIR}/include)
endif ()
# 查找所有源文件
set(SOURCES)
collect_sources(SOURCES ${CMAKE_SOURCE_DIR})
# 用于存储已经处理过的可执行文件名,防止重复
set(EXECUTABLE_NAMES)
# 创建可执行文件
foreach (SOURCE ${SOURCES})
# 获取文件的相对路径
file(RELATIVE_PATH REL_PATH ${CMAKE_SOURCE_DIR} ${SOURCE})
# 将路径中的斜杠替换为下划线,生成唯一的可执行文件名
string(REPLACE "/" "_" EXECUTABLE_NAME ${REL_PATH})
string(REPLACE "\\" "_" EXECUTABLE_NAME ${EXECUTABLE_NAME})
string(REPLACE "." "_" EXECUTABLE_NAME ${EXECUTABLE_NAME})
# 处理与 CMakeLists.txt 文件同名的问题
if (${EXECUTABLE_NAME} STREQUAL "CMakeLists_txt")
set(EXECUTABLE_NAME "${EXECUTABLE_NAME}_exec")
endif ()
# 检查是否已经创建过同名的可执行文件
if (NOT EXECUTABLE_NAME IN_LIST EXECUTABLE_NAMES)
list(APPEND EXECUTABLE_NAMES ${EXECUTABLE_NAME})
# 链接 math 库
LINK_LIBRARIES(m)
# 创建可执行文件
add_executable(${EXECUTABLE_NAME} ${SOURCE})
# 查找源文件所在的目录,并添加为包含目录(头文件可能在同一目录下)
get_filename_component(DIR ${SOURCE} DIRECTORY)
target_include_directories(${EXECUTABLE_NAME} PRIVATE ${DIR})
# 检查并添加子目录中的 include 目录(如果存在)
if (EXISTS "${DIR}/include")
target_include_directories(${EXECUTABLE_NAME} PRIVATE ${DIR}/include)
endif ()
# 检查并添加 module 目录中的所有 C 文件(如果存在)
if (EXISTS "${DIR}/module")
file(GLOB_RECURSE MODULE_SOURCES "${DIR}/module/*.c")
target_sources(${EXECUTABLE_NAME} PRIVATE ${MODULE_SOURCES})
endif ()
endif ()
endforeach ()
```
* 在 CLion 中正常运行代码:
![](./assets/15.gif)
* 在 CLion 中通过 valgrind 运行代码:
![](./assets/16.gif)
## 4.2 性能分析
### 4.2.1 概述
* `perf` 是一个 Linux 下的性能分析工具,主要用于监控和分析系统性能。它可以帮助开发者和系统管理员了解系统中哪些部分在消耗资源、识别性能瓶颈以及分析程序的运行效率。
### 4.2.2 安装
#### 4.2.2.1 AlmaLinux9
* 在 WSL2 中的 AlmaLinux 安装 perf
```shell
dnf -y install perf
```
![](./assets/17.gif)
#### 4.2.2.2 Ubuntu 22.04
* 在 WSL2 中的 Ubuntu 安装 perf
```shell
apt -y update \
&& apt -y install linux-tools-common \
linux-tools-generic linux-tools-$(uname -r)
```
![](./assets/18.gif)
> [!NOTE]
>
> 之所以报错的原因,在于 WSL2 中的 Ubuntu 的内核是定制化的(微软自己维护的),并非 Ubuntu 的母公司 Canonical 发布的标准内核,所以需要我们手动编译安装。
* 查看内核版本:
```shell
uname -sr
```
![](./assets/19.gif)
* 设置环境变量,方便后续引用:
```shell
export KERNEL_VERSION=$(uname -r | cut -d'-' -f1)
```
![](./assets/20.gif)
* 安装依赖库:
```shell
apt -y update && \
apt -y install binutils-dev debuginfod default-jdk \
default-jre libaio-dev libbabeltrace-dev libcap-dev \
libdw-dev libdwarf-dev libelf-dev libiberty-dev \
liblzma-dev libnuma-dev libperl-dev libpfm4-dev \
libslang2-dev libssl-dev libtraceevent-dev libunwind-dev \
libzstd-dev libzstd1 python3-setuptools python3 \
python3-dev systemtap-sdt-dev zlib1g-dev bc dwarves \
bison flex libnewt-dev libdwarf++0 \
libelf++0 libbfb0-dev python-dev-is-python3
```
![](./assets/21.gif)
* 下载源码:
```shell
git clone \
--depth 1 \
--single-branch --branch=linux-msft-wsl-${KERNEL_VERSION} \
https://github.com/microsoft/WSL2-Linux-Kernel.git
```
![](./assets/22.gif)
* 编译内核代码:
```shell
cd WSL2-Linux-Kernel
```
```shell
make -j $(nproc) KCONFIG_CONFIG=Microsoft/config-wsl
```
![](./assets/23.gif)
* 编译 perf 工具:
```shell
cd tools/perf
```
```shell
make clean && make
```
![](./assets/24.gif)
* 复制到 PATH 变量所指向的路径中:
```shell
cp perf /usr/bin/
```
![](./assets/25.gif)
### 4.2.3 整合
* CLion 中配置 perf 的路径:
![](./assets/26.png)
* 在 CLion 中通过 perf 运行代码:
![](./assets/27.gif)

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 628 KiB

View File

Before

Width:  |  Height:  |  Size: 586 KiB

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,104 +1,83 @@
# 第一章:数组的概念 # 第一章:概述
## 1.1 为什么需要数组? * `流程控制结构`是用来控制程序中`各语句执行顺序`的语句,并且可以将语句组合成能`完成一定功能`的`小逻辑模块`。
* 在程序设计中规定了`三种`流程结构,如下所示:
* `顺序结构`:程序从上到下逐行执行,中间没有任何判断和跳转。
* `分支结构`:根据条件,有选择的执行某段代码。在 C 语言中,有 `if...else``switch...case` 两种分支语句。
* `循环结构`:根据循环条件,重复性的执行某段代码。在 C 语言中,有 `for`、`while`、`do...while` 三种循环结构。
### 1.1.1 需求分析 1 * 在生活中的`洗衣工厂`,就包含了上述的三种流程结构,如下所示:
* 需要统计某公司 50 个员工的工资情况,例如:计算平均工资、最高工资等。如果使用之前的知识,我们需要声明 50 个变量来分别记录每位员工的工资,即: ![](./assets/1.jpg)
```c
#include <stdio.h>
int main(){
double num1 = 0; # 第二章:顺序结构
double num2 = 0;
double num3 = 0;
...
printf("请输入第 1 个员工的工资:");
scanf("%lf",&num1);
printf("请输入第 2 个员工的工资:");
scanf("%lf",&num2);
printf("请输入第 3 个员工的工资:");
scanf("%lf",&num3);
...
return 0;
}
```
* 这样会感觉特别机械和麻烦全是复制Ctrl + c和粘贴Ctrl + vCV 大法);此时,我们就可以将所有的`数据`全部存储到一个`容器(数组)`中进行统一管理,并进行其它的操作,如:求最值、求平均值等,如下所示: ## 2.1 概述
```c * 程序从上到下逐行地执行,表达式语句都是顺序执行的,并且上一行对某个变量的修改对下一行会产生影响。
#include <stdio.h>
int main(){
// 声明数组
double nums[50];
// 数组的长度
int length = sizeof(nums) / sizeof(double);
// 使用 for 循环向数组中添加值
for(int i = 0;i < length;i++){
printf("请输入第 &d 个员工的工资:",i);
scanf("%lf",&num[i]);
}
// 其它操作,如:求最值,求平均值等
...
return 0;
}
```
### 1.1.2 需求分析 2
* 在现实生活中,我们会使用很多 APP 或微信小程序等,即:
![](./assets/1.png)
* 同样的道理,如果我们使用变量来存储每个商品信息,那么就需要非常多的变量;但是,如果我们将这些`商品信息`都存储到一个`容器(数组)`中,进行统一管理;那么,之后的数据处理将会非常方便。
### 1.1.3 容器的概念
* `生活中的容器`:水杯(装水、饮料的容器)、衣柜(装衣服等物品的容器)、集装箱(装货物等物品的容器)。
* `程序中的容器`:将多个数据存储到一起,并且每个数据称为该容器中的元素。
## 1.2 什么是数组?
* 数组Array是将多个`相同数据类型`的`数据`按照一定的顺序排序的`集合`,并使用一个`标识符`命名,以及通过`编号(索引,亦称为下标)`的方式对这些数据进行统一管理。
![](./assets/2.png) ![](./assets/2.png)
## 1.3 数组的相关概念 ## 2.2 应用示例
* `数组名`:本质上是一个标识符常量,命名需要符合标识符规则和规范。 * 示例:
* `元素`:同一个数组中的元素必须是相同的数据类型。
* `索引(下标)`:从 0 开始的连续数字。
* `数组的长度`:就是元素的个数。
## 1.4 数组的特点 ```c
#include <stdio.h>
* ① 创建数组的时候,会在内存中开辟一整块`连续的空间`,占据空间的大小,取决于数组的长度和数组中元素的类型。 int main() {
* ② 数组中的元素在内存中是依次紧密排列且有序的。
* ③ 数组一旦初始化完成,且长度就确定的,并且`数组的长度一旦确定,就不能更改`。 int x = 1;
* ④ 我们可以直接通过索引(下标)来获取指定位置的元素,速度很快。 int y = 2;
* ⑤ 数组名中引用的是这块连续空间的首地址。 printf("x = %d \n", x); // x = 1
printf("y = %d \n", y); // y = 2
// 对 x 和 y 的值进行修改
x++;
y = 2 * x + y;
x = x * 10;
printf("x = %d \n", x); // x = 20
printf("y = %d \n", y); // y = 6
return 0;
}
```
# 第二章:数组的操作(⭐)
## 2.1 数组的定义 # 第三章:分支结构(⭐)
### 2.1.1 动态初始化 ## 3.1 概述
* 根据特定条件执行不同的代码块,从而实现灵活的程序控制和更复杂的逻辑。
## 3.2 单分支结构
### 3.2.1 概述
* 语法: * 语法:
```c ```c
数据类型 数组名[元素个数|长度]; if(条件表达式){
语句;
}
``` ```
> [!NOTE] > [!NOTE]
> >
> * ① 数据类型:表示的是数组中每一个元素的数据类型。 > * ① 在 C 语言中,严格意义上是没有 boolean 类型的,使用`非0` 表示`真true``0` 表示`假false`。
> * ② 数组名:必须符合标识符规则和规范。 > * ② 当条件表达式为真(`非0` ),就会执行代码块中的语句;否则,就不会执行代码块中的语句。
> * ③ 元素个数或长度:表示的是数组中最多可以容纳多少个元素(不能是负数、也不能是 0 )。
* 流程图,如下所示:
![](./assets/3.png)
### 3.2.2 应用示例
* 需求:成年人心率的正常范围是每分钟 60~100 次。体检时,如果心率不在此范围内,则提示需要做进一步的检查。
@ -109,87 +88,198 @@ int main(){
int main() { int main() {
// 先指定元素的个数和类型,再进行初始化 int heartBeats = 0;
printf("请输入您的心率:");
scanf("%d", &heartBeats);
// 定义数组 if (heartBeats < 60 || heartBeats > 100) {
int arr[3]; printf("您的心率不在正常范围内,请做进一步的检查。\n");
}
// 给数组元素赋值 printf("体检结束!!!");
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
return 0; return 0;
} }
``` ```
### 2.1.2 静态初始化 1 ### 3.2.3 应用示例
* 需求:根据年龄判断,如果是未成年人,则提示 "未成年人请在家长陪同下访问!" 。
* 示例:
```c
#include <stdio.h>
int main() {
int age = 0;
printf("请输入你的年龄:");
scanf("%d", &age);
if (age < 18) {
printf("未成年人请在家长陪同下访问!\n");
}
printf("欢迎继续访问!");
return 0;
}
```
## 3.3 双分支结构
### 3.3.1 概述
* 语法: * 语法:
```c ```c
数据类型 数组名[元素个数|长度] = {元素1,元素2,...} if(条件表达式) {
语句块1;
}else {
语句块2;
}
``` ```
> [!NOTE] > [!NOTE]
> >
> * ① 静态部分初始化:如果数组初始化的元素个数`小于`数组声明的长度,那么就会从数组开始位置依次赋值,不够的就补 0 。 > * ① 在 C 语言中,严格意义上是没有 boolean 类型的,使用`非0` 表示`真true``0` 表示`假false`
> * ② 静态全部初始化:数组初始化的元素个数`等于`数组的长度。 > * ② 当条件表达式为真(`非0` ),就会执行代码块 1 中的语句;否则,执行代码块 2 中的语句
> [!TIP] * 流程图,如下所示:
>
> 在 CLion 中开启`嵌入提示(形参名称-->显示数组索引的提示)`功能,即: ![](./assets/4.png)
>
> ![](./assets/3.png) ### 3.3.2 应用示例
>
> 这样,在 CLion 中,将会显示数组初始化时每个元素对应的索引,即: * 需求:判断一个整数,是奇数还是偶数。
>
> ![](./assets/4.png)
* 示例:静态部分初始化 * 示例:
```c ```c
#include <stdio.h> #include <stdio.h>
int main() { int main() {
// 定义数组和部分初始化: int num = 0;
// 会将给定的值从数组的开始位置一个个的赋值,没有赋值的地方,用 0 填充 printf("请输入一个整数:");
int arr[5] = {1, 2}; scanf("%d", &num);
if (num % 2 == 0) {
printf("%d 是偶数\n", num);
} else {
printf("%d 是奇数\n", num);
}
return 0; return 0;
} }
``` ```
### 3.3.2 应用示例
* 需求输入年龄如果大于18岁则输出 "你年龄大于18要对自己的行为负责!";否则,输出 "你的年龄不大这次放过你了。"
* 示例:静态全部初始化
* 示例:
```c ```c
#include <stdio.h> #include <stdio.h>
int main() { int main() {
// 定义数组和全部初始化:数组初始化的元素个数等于数组的长度。 int age = 0;
int arr[5] = {1, 2, 3, 4, 5}; printf("请输入年龄:");
scanf("%d", &age);
if (age > 18) {
printf("你年龄大于18要对自己的行为负责!\n");
} else {
printf("你的年龄不大,这次放过你了!\n");
}
return 0; return 0;
} }
``` ```
### 2.1.3 静态初始化 2 ### 3.3.3 应用示例
* 需求:判定某个年份是否为闰年?
>[!NOTE]
>
>* ① year 是 400 的整倍数: year%400==0
>* ② 能被 4 整除,但不能被 100 整除year % 4 == 0 && year % 100 != 0
* 示例:
```c
#include <stdio.h>
int main() {
int year = 0;
printf("请输入年份:");
scanf("%d", &year);
if (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)) {
printf("%d 是闰年\n", year);
} else {
printf("%d 不是闰年\n", year);
}
return 0;
}
```
## 3.4 多重分支结构
### 3.4.1 概述
* 语法: * 语法:
```c ```c
数据类型 数组名[] = {元素1,元素2,...} if (条件表达式1) {
语句块1;
} else if (条件表达式2) {
语句块2;
}
...
} else if (条件表达式n) {
语句块n;
} else {
语句块n+1;
}
``` ```
> [!NOTE] > [!NOTE]
> >
> 没有给出数组中元素的个数,将由系统根据初始化的元素,自动推断出数组中元素的个数。 > * ① 在 C 语言中,严格意义上是没有 boolean 类型的,使用`非0` 表示`真true``0` 表示`假false`。
> * ② 首先判断关系表达式 1 的结果是真(值为 `非0`)还是假(值为 `0`
> * 如果为真,就执行语句块 1然后结束当前多分支。
> * 如果是假,就继续判断条件表达式 2看其结果是真还是假。
> * 如果是真,就执行语句块 2然后结束当前多分支。
> * 如果是假,就继续判断条件表达式…看其结果是真还是假。
> * ...
> * 如果没有任何关系表达式为真,就执行语句块 n+1然后结束当前多分支。
> * ③ 当条件表达式之间是`互斥`(彼此之间没有交集)关系时,条件判断语句及执行语句间顺序无所谓。
> * ④ 当条件表达式之间是`包含`关系时,必须`小上大下 / 子上父下`,否则范围小的条件表达式将不可能被执行。
> * ⑤ 当 if-else 结构是多选一的时候,最后的 else 是可选的,可以根据需要省略。
> * ⑥ 如果语句块中只有一条执行语句的时候,`{}`是可以省略的;但是,强烈建议保留!!!
* 流程图,如下所示:
![image-20240722075241253](./assets/5.png)
### 3.4.1 应用示例
* 需求:张三参加考试,他和父亲达成协议,如果成绩不到 60 分没有任何奖励;如果成绩 60分到 80 分,奖励一个肉夹馍;如果成绩 80 分(含)到 90 分,奖励一个 ipad如果成绩 90 分及以上,奖励一部华为 mate60 pro 。
@ -200,28 +290,33 @@ int main() {
int main() { int main() {
// 指定元素的类型,不指定元素个数,同时进行初始化 int score = 0;
int arr[] = {1, 2, 3, 4, 5}; printf("请输入分数:");
scanf("%d", &score);
// 容错:分数不可能小于 0 或大于 100
if (score < 0 || score > 100) {
printf("输入的分数有误!\n");
return 0;
}
if (score >= 90) {
printf("奖励你一部华为 mate60 pro\n");
} else if (score >= 80) {
printf("奖励你一个 ipad\n");
} else if (score >= 60) {
printf("奖励你一个肉夹馍\n");
} else {
printf("你的成绩不及格,没有任何奖励!");
}
return 0; return 0;
} }
``` ```
### 2.1.4 静态初始化 3 ### 3.4.2 应用示例
* 在 C 语言中,也可以只给部分元素赋值。当 {} 中的值少于元素的个数的时候,只会给前面的部分元素赋值,至于剩下的元素就会自动初始化为 0 。 * 需求:判断水的温度,如果大于 95℃则打印 "开水";如果大于 70℃ 且小于等于 95℃则打印 "热水";如果大于 40℃ 且小于等于 70℃则打印 "温水";如果小于等于 40℃则打印 "凉水"。
```c
int arr[10] = {1,2,3,4,5};
```
> [!NOTE]
>
> * ① 数组 `arr` 在内存中开辟了 `10` 个连续的内存空间,但是只会给前 `5` 个内存空间赋值初始化值,即:`arr[0] ~ arr[4]` 分别是 `1`、`2`、`3`、`4`、`5`,而 `arr[5] ~ arr[9]` 就会被自动初始化为 `0`
> * ② 当赋值的元素少于数组总体元素的时候,剩余的元素自动初始化为 `0`,其规则如下:
> * 对于 `short`、`int`、`long`,就是整数 `0`
> * 对于 `char`,就是字符 `'\0'`。需要注意的是,`'\0'` 的十进制数就是 `0`
> * 对于 `float`、`double`,就是小数 `0.0`
@ -229,186 +324,66 @@ int arr[10] = {1,2,3,4,5};
```c ```c
#include <stdio.h> #include <stdio.h>
int main() {
int arr[10] = {1, 2, 3, 4, 5};
printf("arr[0] = %d \n", arr[0]); // arr[0] = 1 int main() {
printf("arr[1] = %d \n", arr[1]); // arr[1] = 2
printf("arr[2] = %d \n", arr[2]); // arr[2] = 3 int temperature = 0;
printf("arr[3] = %d \n", arr[3]); // arr[3] = 4 printf("请输入水的温度:");
printf("arr[4] = %d \n", arr[4]); // arr[4] = 5 scanf("%d", &temperature);
printf("arr[5] = %d \n", arr[5]); // arr[5] = 0
printf("arr[6] = %d \n", arr[6]); // arr[6] = 0 if (temperature > 95) {
printf("arr[7] = %d \n", arr[7]); // arr[7] = 0 printf("开水 \n");
printf("arr[8] = %d \n", arr[8]); // arr[8] = 0 } else if (temperature > 70 && temperature <= 95) {
printf("arr[9] = %d \n", arr[9]); // arr[9] = 0 printf("热水 \n");
} else if (temperature > 40 && temperature <= 70) {
printf("温水 \n");
} else {
printf("凉水 \n");
}
return 0; return 0;
} }
``` ```
## 3.5 多重分支结构 switch
### 3.5.1 概述
## 2.2 访问数组元素
* 语法: * 语法:
```c ```c
数组名[索引|下标]; switch(表达式){
case 常量值1:
语句块1;
//break;
case 常量值2:
语句块2;
//break;
...
case 常量值n:
语句块n;
//break;
[default:
语句块n+1;
]
}
``` ```
> [!NOTE] > [!NOTE]
> >
> 假设数组 `arr` 有 n 个元素,如果使用的数组的下标 `< 0``> n-1` ,那么将会产生数组越界访问,即超出了数组合法空间的访问;那么,数组的索引范围是 `[0,arr.length - 1]` > * ① switch 后面表达式的值必须是一个整型char、short、int、long 等)或枚举类型。
> * ② case 后面的值必须是常量,不能是变量。
> * ③ default 是可选的,当没有匹配的 case 的时候,就执行 default 。
> * ④ break 语句可以使程序跳出 switch 语句块,如果没有 break会执行下一个 case 语句块,直到遇到 break 或者执行到 switch 结尾,这个现象称为穿透。
* 流程图,如下所示:
* 示例:
```c
#include <stdio.h>
int main() {
// 先指定元素的个数和类型,再进行初始化
// 定义数组
int arr[3];
// 给数组元素赋值
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
// 访问数组元素
printf("arr[0] = %d\n", arr[0]); // arr[0] = 10
printf("arr[1] = %d\n", arr[1]); // arr[1] = 20
printf("arr[2] = %d\n", arr[2]); // arr[2] = 30
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
// 定义数组和部分初始化:
// 会将给定的值从数组的开始位置一个个的赋值,没有赋值的地方,用 0 填充
int arr[5] = {1, 2};
// 访问数组元素
printf("arr[0] = %d\n", arr[0]); // arr[0] = 1
printf("arr[1] = %d\n", arr[1]); // arr[1] = 2
printf("arr[2] = %d\n", arr[2]); // arr[2] = 0
printf("arr[3] = %d\n", arr[3]); // arr[3] = 0
printf("arr[4] = %d\n", arr[4]); // arr[4] = 0
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
// 指定元素的类型,不指定元素个数,同时进行初始化
int arr[] = {1, 2, 3, 4, 5};
// 访问数组元素
printf("arr[0] = %d\n", arr[0]); // arr[0] = 1
printf("arr[1] = %d\n", arr[1]); // arr[1] = 2
printf("arr[2] = %d\n", arr[2]); // arr[2] = 3
printf("arr[3] = %d\n", arr[3]); // arr[3] = 4
printf("arr[4] = %d\n", arr[4]); // arr[4] = 5
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
// 定义数组和全部初始化:数组初始化的元素个数等于数组的长度。
int arr[5] = {1, 2, 3, 4, 5};
// 访问数组元素
printf("arr[0] = %d\n", arr[0]); // arr[0] = 1
printf("arr[1] = %d\n", arr[1]); // arr[1] = 2
printf("arr[2] = %d\n", arr[2]); // arr[2] = 3
printf("arr[3] = %d\n", arr[3]); // arr[3] = 4
printf("arr[4] = %d\n", arr[4]); // arr[4] = 5
return 0;
}
```
## 2.3 数组越界
* 数组下标必须在指定范围内使用,超出范围视为越界。
![](./assets/5.png)
> [!NOTE]
>
> * ① C 语言是不会做数组下标越界的检查,并且编译器也不会报错;但是,编译器不报错,并不意味着程序就是正确!
> * ② 在其它高级编程语言Java、JavaScript、Rust 等中,如果数组越界访问,编译器是会直接报错的!!!
* 示例:
```c
#include <stdio.h>
int main() {
// 定义数组和全部初始化:数组初始化的元素个数等于数组的长度。
int arr[] = {1, 2, 3, 4, 5};
// 访问数组元素
printf("arr[0] = %d\n", arr[0]); // arr[0] = 1
printf("arr[1] = %d\n", arr[1]); // arr[1] = 2
printf("arr[2] = %d\n", arr[2]); // arr[2] = 3
printf("arr[3] = %d\n", arr[3]); // arr[3] = 4
printf("arr[4] = %d\n", arr[4]); // arr[4] = 5
printf("arr[-1] = %d\n", arr[-1]); // 得到的是不确定的结果
printf("arr[5] = %d\n", arr[5]); // 得到的是不确定的结果
return 0;
}
```
## 2.4 计算数组的长度
* 数组长度(元素个数)是在数组定义的时候明确指定且固定的,我们不能在运行的时候直接获取数组长度;但是,我们可以通过 sizeof 运算符间接计算出数组的长度。
* 计算步骤,如下所示:
* ① 使用 sizeof 运算符计算出整个数组的字节长度。
* ② 由于数组成员是同一数据类型;那么,每个元素的字节长度一定相等,那么`数组的长度 = 整个数组的字节长度 ÷ 单个元素的字节长度 `。
![](./assets/6.png) ![](./assets/6.png)
> [!NOTE] ### 3.5.2 应用示例
>
> * ① 在很多编程语言中都内置了获取数组的长度的属性或方法Java 中的 arr.length 或 Rust 的 arr.len()。 * 需求编写一个程序该程序可以接收一个字符比如a、b、c、d其中 a 表示星期一b 表示星期二…,根据用户的输入显示相应的信息,要求使用 switch 语句。
> * ② 但是C 语言没有内置的获取数组长度的属性或方法,只能通过 sizeof 运算符间接来计算得到。
> * ③ 数组一旦`声明`或`定义`,其`长度`就`固定`了,`不能动态变化`。
@ -419,183 +394,286 @@ int main() {
int main() { int main() {
// 定义数组和全部初始化:数组初始化的元素个数等于数组的长度。 char chs;
int arr[] = {1, 2, 3, 4, 5}; printf("请输入一个字符a、b、c、d");
scanf("%c", &chs);
// 计算数组的长度 switch (chs) {
size_t length = sizeof(arr) / sizeof(arr[0]); case 'a':
printf("今天是星期一 \n");
// 遍历数组 printf("窗前明月光 \n");
for (int i = 0; i < length; i++) { break;
printf("%d \n", arr[i]); case 'b':
printf("今天是星期二 \n");
printf("疑是地上霜 \n");
break;
case 'c':
printf("今天是星期三 \n");
printf("举头望明月 \n");
break;
case 'd':
printf("今天是星期四 \n");
printf("低头思故乡 \n");
break;
default:
printf("输入错误!");
break;
} }
return 0; return 0;
} }
``` ```
## 2.5 遍历数组 ### 3.5.3 应用示例
* 遍历数组是指按顺序访问数组中的每个元素,以便读取或修改它们,编程中一般使用循环结构对数组进行遍历。 * 需求编写程序输入月份输出该月份有多少天。说明1 月、3 月、5 月、7月、8 月、10 月、12 月有 31 天4 月、6 月、9 月、11 月有 30 天2 月有 28 天或 29 天
* 示例:声明一个存储有 12、2、31、24、15、36、67、108、29、51 的数组,并遍历数组所有元素 * 示例:
```c ```c
#include <stdio.h> #include <stdio.h>
int main() { int main() {
// 定义数组并初始化 int month;
int arr[] = {12, 2, 31, 24, 15, 36, 67, 108, 29, 51}; printf("请输入月份 (1-12)");
scanf("%d", &month);
// 计算数组的长度 switch (month) {
size_t length = sizeof(arr) / sizeof(int); case 1:
case 3:
// 遍历数组 case 5:
for (int i = 0; i < length; i++) { case 7:
printf("%d\n", arr[i]); case 8:
case 10:
case 12:
printf("%d 月有 31 天\n", month);
break;
case 4:
case 6:
case 9:
case 11:
printf("%d 月有 30 天\n", month);
break;
case 2:
printf("%d 月有 28 天或 29 天\n", month);
break;
default:
printf("输入错误!");
break;
} }
return 0; return 0;
} }
``` ```
### 3.5.4 switch 和 if else if 的比较
* ① 如果判断条件是判等,而且符合整型、枚举类型,虽然两个语句都可以使用,建议使用 swtich 语句。
* ② 如果判断条件是区间判断,大小判断等,使用 if...else...if。
## 3.6 嵌套分支
### 3.6.1 概述
* 嵌套分支是指,在一个分支结构中又嵌套了另一个分支结构,里面的分支的结构称为内层分支,外面的分支结构称为外层分支。
> [!NOTE]
>
> 嵌套分支层数不宜过多,建议最多不要超过 3 层。
### 3.6.2 应用示例
* 需求:根据淡旺季的月份和年龄,打印票价。
> [!NOTE]
>
> * ① 4 -10 是旺季:
> * 成人18-6060 。
> * 儿童(<18半价
> * 老人(>601/3 。
> * ② 其余是淡季:
> * 成人40。
> * 其他20。
* 示例:声明长度为 10 的 int 类型数组,给数组元素依次赋值为 0 ~ 9 ,并遍历数组所有元素
* 示例:
```c ```c
#include <stdio.h> #include <stdio.h>
int main() { int main() {
// 定义数组 int month;
int arr[10]; int age;
double price = 60;
// 计算数组的长度 printf("请输入月份 (1-12)");
size_t length = sizeof(arr) / sizeof(int); scanf("%d", &month);
// 给数组的每个元素赋值 printf("请输入年龄:");
for (int i = 0; i < length; i++) { scanf("%d", &age);
arr[i] = i;
// 旺季
if (month >= 4 && month <= 10) {
if (age < 18) {
price /= 2;
} else if (age > 60) {
price /= 3;
}
} else {
if (age >= 18) {
price = 40;
} else {
price = 20;
}
} }
// 遍历数组 printf("票价: %.2lf\n", price);
for (int i = 0; i < length; i++) {
printf("%d\n", arr[i]); return 0;
}
```
# 第四章:随机数
## 4.1 概述
* 所谓的随机数就是没有规则,并且不能预测的一些数字,也称为真随机数。
* 程序中也是可以产生随机数的,但是是通过一些固定规则产生的,称为伪随机数。
* 常见的伪随机数线性同余方程LCG的公式如下所示
$X_{n+1} = (a \cdot X_n + b) \mod m$
* 其中X 是伪随机序列a 是乘数(通常选择一个大于 0 的常数,典型值有 1664525b 是增量(选择一个大于 0 的常数,典型值有 1013904223 m 是模数( 通常选择一个大的常数,常见值有 ( 2^{32} ) ,即 4294967296
> [!NOTE]
>
> 假设 a = 31 b = 13 m = 100 ;那么,伪随机数的公式就是 `X_{n+1} = (31 × X_n + 13) % 100 `
>
> * 如果 `X_{n}` = 1 ,那么 `X_{n+1}` = 44 。
> * 如果 `X_{n}` = 44 ,那么 `X_{n+1}` = 77 。
> * 如果 `X_{n}` = 77 ,那么 `X_{n+1}` = 0 。
> * ...
>
> 最后,将得到 44、77、0、13、16、9 、92、65、28 ... ,其中 1 也称为初始种子(随机数种子)。
* 工作原理:
* ① 设置初始种子X_0
* 种子值是算法生成随机数序列的起点。
* 不同的种子值会产生不同的随机数序列。
* ② 递归生成随机数:
* 从初始种子开始,通过公式不断生成新的随机数。
* 每次迭代都使用前一次生成的随机数作为输入。
> [!NOTE]
>
> 如果种子的值相同,那么每次生成的随机数将相同,解决方案就是将种子的值设置为当前的时间戳。
## 4.2 C 语言中随机数的产生
* ① 设置随机数种子:
```c
srand(10); // seed 种⼦ rand random 随机
```
> [!NOTE]
>
> 随机数函数在 `#include <stdlib.h>` 中声明。
* ② 根据随机数种⼦计算出⼀个伪随机数:
```c
// 根据种⼦值产⽣⼀个 0-32767 范围的随机数
int result = rand();
```
* ③ 产生一个指定范围内的随机数:
```c
int random_in_range(int min, int max) {
return rand() % (max - min + 1) + min;
}
```
* 示例:
```c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// 生成指定范围的随机数的函数
int randomInRange(int min, int max) {
return rand() % (max - min + 1) + min;
}
int main() {
// 使用当前时间作为种子
srand(time(0));
// 定义范围
int min = 1;
int max = 100;
// 生成并打印随机数
for (int i = 0; i < 10; ++i) {
int random = randomInRange(min, max);
printf("%d \n", random);
} }
return 0; return 0;
} }
``` ```
## 2.6 一维数组的内存分析
### 2.6.1 数组内存图
* 假设数组是如下的定义: # 第五章:循环结构(⭐)
## 5.1 概述
* 循环结构:在某些条件满足的情况下,反复执行特定代码的功能。
## 5.2 for 循环
### 5.2.1 概述
* 语法:
```c ```c
int arr[] = {1,2,3,4,5}; for(初始化条件①;循环条件表达式②;迭代语句④){
循环体语句③
}
``` ```
* 那么,对应的内存结构,如下所示: > [!NOTE]
>
> * ① 初始化条件,用于初始化循环变量,只会执行一次,且循环开始前就执行(可以声明多个变量,但是必须是同一类型,用逗号 `,` 隔开)。
> * ② 循环条件表达式每次循环都执行,同 while 循环一样,每次先判断后执行循环体语句。
> * ③ 迭代语句每次循环都执行,在大括号中循环体语句之后执行(如果有多个变量更新,用逗号 `,` 隔开)。
* 流程图,如下所示:
![](./assets/7.png) ![](./assets/7.png)
> [!NOTE] > [!NOTE]
> >
> * ① 数组名 `arr` 就是记录该数组的首地址,即 `arr[0]` 的地址。 > 执行过程是:① --> ② --> ③ --> ④ --> ② --> ③ --> ④ --> ... --> ② 。
> * ② 数组中的各个元素是连续分布的,假设 `arr[0]` 的地址是 `0xdea7bff880`,则 `arr[1] 的地址 = arr[0] 的地址 + int 字节数4 = 0xdea7bff880 + 4 = 0xdea7bff884` ,依次类推...
* 在 C 语言中,我们可以通过 `&arr``&arr[0]` 等形式获取数组或数组元素的地址,即:
```c
#include <stdio.h>
int main() {
// 定义数组
int arr[10];
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(int);
// 给数组的每个元素赋值
for (int i = 0; i < length; i++) {
arr[i] = i;
}
printf("数组的地址是 = %p\n", arr);
// 遍历数组
for (int i = 0; i < length; i++) {
printf("数组元素 %d 的地址是 = %p\n", arr[i], &arr[i]);
}
return 0;
}
```
### 2.6.2 数组的注意事项
* `C 语言规定,数组一旦声明,数组名指向的地址将不可更改`。因为在声明数组的时候,编译器会自动会数组分配内存地址,这个地址和数组名是绑定的,不可更改。
> [!WARNING]
>
> 如果之后试图更改数组名对应的地址,编译器就会报错。
* 示例:错误演示 ### 5.2.2 应用示例
```c * 需求:输出 5 行 `Hello World!`
int num[5]; // 声明数组
// 使用大括号重新赋值是不允许的,必须在数组声明的时候赋值,否则编译将会报错
num = {1,2,3,4,5} ; // [!code error]
```
* 示例:错误演示
```c
int num[] = {1,2,3,4,5};
// 使用大括号重新赋值是不允许的,必须在数组声明的时候赋值,否则编译将会报错
num = {2,3,4,5,6}; // [!code error]
```
* 示例:错误演示
```c
int num[5];
// 报错,需要和 Java 区别一下,在 C 中不可以
num = NULL; // [!code error]
```
* 示例:错误演示
```c
int a[] = {1,2,3,4,5}
// 报错,需要和 Java 区别一下,在 C 中不可以
int b[5] = a ; // [!code error]
```
## 2.7 数组应用案例
### 2.7.1 应用示例
* 需求:计算数组中所有元素的和以及平均数。
@ -606,40 +684,278 @@ int b[5] = a ; // [!code error]
int main() { int main() {
// 定义数组并初始化 for (int i = 1; i <= 5; ++i) {
int arr[] = {12, 2, 31, 24, 15, 36, 67, 108, 29, 51}; printf("Hello World!\n");
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(int);
// 变量保存总和
int sum = 0;
// 遍历数组
for (int i = 0; i < length; i++) {
sum += arr[i];
} }
double avg = (double)sum / length;
printf("数组的和为:%d\n", sum); // 数组的和为375
printf("数组的平均值为:%.2lf\n", avg); //数组的平均值为37.50
return 0; return 0;
} }
``` ```
### 2.7.2 应用示例 ### 5.2.3 应用示例
* 需求:计算数组的最值(最大值和最小值)。 * 需求:求 1 ~ 100 之内所有偶数的和,以及偶数的个数。
* 示例:
```c
#include <stdio.h>
int main() {
int sum = 0;
int count = 0;
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
sum += i;
count++;
}
}
printf("1 ~ 100 中的所有偶数的和为: %d \n", sum);
printf("1 ~ 100 中的所有偶数的个数为: %d \n", count);
return 0;
}
```
### 5.2.4 应用示例
* 需求:输出所有的水仙花数,所谓水仙花数是指一个 3 位数,其各个位上数字立方和等于其本身,例如:`153 = 1×1×1 + 3×3×3 + 5×5×5`。
* 示例:
```c
#include <stdio.h>
int main() {
int count = 0;
for (int i = 100; i <= 999; i++) {
// 获取三位数
int ge = i % 10;
int shi = i / 10 % 10;
int bai = i / 100;
// 判定是否为水仙花数
if (ge * ge * ge + shi * shi * shi + bai * bai * bai == i) {
printf("水仙花数:%d\n", i);
count++;
}
}
printf("水仙花数总个数:%d\n", count);
return 0;
}
```
### 5.2.5 应用示例
* 需求:将 1 ~ 10 倒序输出10 、9 、8 ...
* 示例:
```c
#include <stdio.h>
int main() {
for (int i = 10; i >= 0; i--) {
printf("%d ", i);
}
return 0;
}
```
### 5.2.6 应用示例
* 需求:输入两个正整数 m 和 n 求其最大公约数和最小公倍数例如12 和 20 的最大公约数是 4 ,最小公倍数是 60 。
> [!NOTE]
>
> * 如果数 a 能被数 b 整除,且结果是整数,那么 a 就叫做 b 的倍数b 就叫做 a 的约数(因数)。
> * 如果一个整数同时是几个整数的约数,则称该整数为这些整数的公约数;其中,数值最大的称为最大公约数。
> * 如果一个整数同时为两个或多个整数的倍数的数,则称该整数为这些整数的公倍数;其中,数值最小的称为最小公倍数。
* 示例:
```c
#include <stdio.h>
int main() {
int m = 12, n = 20;
// 取出两个数中的较小值
int min = (m < n) ? m : n;
for (int i = min; i >= 1; i--) {
if (m % i == 0 && n % i == 0) {
printf("最大公约数是:%d\n", i); // 公约数
break; //跳出当前循环结构
}
}
// 取出两个数中的较大值
int max = (m > n) ? m : n;
for (int i = max; i <= m * n; i++) {
if (i % m == 0 && i % n == 0) {
printf("最小公倍数是:%d\n", i); // 公倍数
break;
}
}
return 0;
}
```
## 5.3 while 循环
### 5.3.1 概述
* 语法:
```c
初始化条件①;
while (循环条件语句②) {
循环体语句③;
迭代语句④;
}
```
> [!NOTE]
>
> * ① `while(循环条件部分)` 中循环条件为`非0`值,表示 `true`、`真`;为`0`值,表示 `false`、`假`。
> * ② 当循环条件表达式成立,就执行循环体语句,直到条件不成立停止循环。
> * ③ 为避免死循环,循环条件表达式不能永远成立,且随着循环次数增加,应该越来越趋向于不成立。
> * ④ for 循环和 while 循环`可以相互转换`,二者没有性能上的差别。
> * ⑤ for 循环与 while 循环的区别:`初始化条件部分的作用域不同`。
* 流程图,如下所示:
![](./assets/8.png)
> [!NOTE]
>
> 执行过程是:① --> ② --> ③ --> ④ --> ② --> ③ --> ④ --> ... --> ② 。
### 5.3.2 应用示例
* 需求:输出 5 行 `Hello World!`
* 示例:
```c
#include <stdio.h>
int main() {
int i = 1;
while (i <= 5) {
printf("Hello World!\n");
i++;
}
return 0;
}
```
### 5.3.3 应用示例
* 需求:求 1 ~ 100 之内所有偶数的和,以及偶数的个数。
* 示例:
```c
#include <stdio.h>
int main() {
int sum = 0;
int count = 0;
int i = 1;
while (i <= 100) {
if (i % 2 == 0) {
sum += i;
count++;
}
i++;
}
printf("1 ~ 100 中的所有偶数的和为: %d \n", sum);
printf("1 ~ 100 中的所有偶数的个数为: %d \n", count);
return 0;
}
```
### 5.3.4 应用示例
* 需求:世界最高山峰是珠穆朗玛峰,它的高度是 8848.86 米,假如我有一张足够大的纸,它的厚度是 0.1 毫米。请问,我折叠多少次,可以折成珠穆朗玛峰的高度?
* 示例:
```c
#include <stdio.h>
int main() {
// 折叠的次数
int count = 0;
// 珠峰的高度
int zfHeight = 8848860;
// 每次折叠的高度
double paperHeight = 0.1;
while (paperHeight <= zfHeight) {
count++;
paperHeight *= 2;
}
printf("需要折叠 %d 次,才能得到珠峰的高度。\n", count);
printf("折纸的高度为 %.2f 米,超过了珠峰的高度", paperHeight / 1000);
return 0;
}
```
### 5.3.5 应用示例
* 需求:给出一个整数 n ,判断该整数是否是 2 的幂次方。如果是,就输出 yes ;否则,输出 no 。
> [!NOTE] > [!NOTE]
> >
> 思路: > 思路:
> >
> * ① 假设数组中的第一个元素是最大值或最小值,并使用变量 max 或 min 保存。 > * ① 2^ 0 = 1 2^1 = 2 2^2 = 42^3 = 82^4 = 162^5 = 32 ...,规律:每一个数字都是前一个数字的 2 倍(任意一个数字,不断的除以 2 ,最终看结果是否是数字 1
> * ② 遍历数组中的每个元素: > * ② 循环终止条件
> * 如果有元素比最大值还要大,就让变量 max 保存最大值。 > * 结果是 1 的时候,就可以结束,输出 yes
> * 如果有元素比最小值还要小,就让变量 min 保存最小值。 > * 如果在除以 2 的时候,无法被 2 整数,也可以结束,输出 no ,如: 100 / 2 = 5050 / 2 = 25
@ -650,37 +966,34 @@ int main() {
int main() { int main() {
// 定义数组并初始化 // 禁用 stdout 缓冲区
int arr[] = {12, 2, 31, 24, 15, -36, 67, 108, 29, 51}; setbuf(stdout, NULL);
// 计算数组的长度 int n = 0;
size_t length = sizeof(arr) / sizeof(int); printf("请输入一个整数:");
scanf("%d", &n);
// 定义最大值 while (n > 1 && n % 2 == 0) {
int max = arr[0]; n /= 2;
// 定义最小值
int min = arr[0];
// 遍历数组
for (int i = 0; i < length; i++) {
if (arr[i] >= max) {
max = arr[i];
}
if (arr[i] <= min) {
min = arr[i];
}
} }
printf("数组的最大值为:%d\n", max); // 数组的最大值为108 if (n == 1) {
printf("数组的最小值为:%d\n", min); // 数组的最小值为:-36 printf("yes");
} else {
printf("no");
}
return 0; return 0;
} }
``` ```
### 2.7.3 应用示例 ### 5.3.6 应用示例
* 需求:统计数组中某个元素出现的次数,要求:使用无限循环,如果输入的数字是 0 ,就退出。 * 需求整数反转123 --> 321 。
> [!NOTE]
>
> 思路:从右边开始,依次获取每一位数字,再拼接起来。
@ -691,95 +1004,196 @@ int main() {
int main() { int main() {
// 定义数组并初始化 // 禁用 stdout 缓冲区
int arr[] = {12, 2, 31, 24, 2, -36, 67, 108, 29, 51}; setbuf(stdout, NULL);
// 计算数组的长度 int num = 0;
size_t length = sizeof(arr) / sizeof(int); int original = 0;
int rev = 0;
printf("请输入一个整数:");
scanf("%d", &num);
original = num;
// 遍历数组 // 从右边开始,依次获取每个数字,然后拼接到 rev 中
printf("当前数组中的元素是:"); /**
for (int i = 0; i < length; i++) { * 第 1 次123 % 10 = 3rev = 0 * 10 + 3 = 3
printf("%d ", arr[i]); * 第 2 次12 % 10 = 2rev = 3 * 10 + 2 = 32
* 第 3 次1 % 10 = 1rev = 32 * 10 + 1 = 321
*/
// 循环结束的条件是 num == 0
while (num != 0) {
// 获取 num 右边的第一位数字
int temp = num % 10;
// 去掉最后一位数字
num /= 10;
// 将 temp 拼接到 rev 的后面
rev = rev * 10 + temp;
} }
printf("\n"); printf("%d 的反转是 %d\n", original, rev);
// 无限循环 return 0;
while (true) { }
// 统计的数字 ```
int num;
// 统计数字出现的次数 ## 5.4 do-while 循环
### 5.4.1 概述
* 语法:
```c
①初始化部分;
do{
③循环体部分
④迭代部分
}while(②循环条件部分);
```
> [!NOTE]
>
> * ① `do{} while();`最后有一个分号。
> * ② do-while 结构的循环体语句是至少会执行一次,这个和 for 、while 是不一样的。
> * ③ 循环的三个结构 for、while、do-while 三者是可以相互转换的。
* 流程图,如下所示:
![](./assets/9.png)
> [!NOTE]
>
> 执行过程是:① --> ③ --> ④ --> ② --> ③ --> ④ --> ② --> ... --> ② 。
### 5.4.2 应用示例
* 需求:求 1 ~ 100 之内所有偶数的和,以及偶数的个数。
```c
#include <stdio.h>
int main() {
int sum = 0;
int count = 0; int count = 0;
// 输入数字
printf("请输入要统计的数字:");
scanf("%d", &num);
// 0 作为结束条件 int i = 1;
if (num == 0) { do {
if (i % 2 == 0) {
sum += i;
count++;
}
i++;
} while (i <= 100);
printf("1 ~ 100 中的所有偶数的和为: %d \n", sum);
printf("1 ~ 100 中的所有偶数的个数为: %d \n", count);
return 0;
}
```
### 5.4.3 应用示例
* 需求:实现 ATM 取款机功能。
* 示例:
```c
#include <stdio.h>
int main() {
// 账户余额
double balance = 0.0;
// 客户选择
int selection;
// 存款金额
double addMoney;
// 取款金额
double minusMoney;
// 退出标识
bool exitFlag = false;
do {
printf("=========ATM========\n");
printf("\t1、存款\n");
printf("\t2、取款\n");
printf("\t3、显示余额\n");
printf("\t4、退出\n");
printf("请选择(1-4)");
scanf("%d", &selection);
switch (selection) {
case 1:
printf("您当前的余额是: %.2f\n", balance);
printf("请输入存款金额:");
scanf("%lf", &addMoney);
balance += addMoney;
printf("存款成功,您当前的余额是:%.2f\n", balance);
break;
case 2:
printf("您当前的余额是: %.2f\n", balance);
printf("请输入取款金额:");
scanf("%lf", &minusMoney);
if (minusMoney > balance) {
printf("余额不足,取款失败。\n");
} else {
balance -= minusMoney;
printf("取款成功,您的余额为:%.2f\n", balance);
}
break;
case 3:
printf("您的账户余额为:%.2f\n", balance);
break;
case 4:
exitFlag = true;
printf("欢迎下次再来。\n");
break;
default:
printf("输入有误,请重新输入。\n");
break; break;
} }
// 遍历数组,并计数 } while (!exitFlag);
for (int i = 0; i < length; i++) {
if (arr[i] == num) {
count++;
}
}
printf("您输入的数字 %d 在数组中出现了 %d 次\n", num, count);
}
return 0; return 0;
} }
``` ```
### 2.7.4 应用示例 ## 5.5 嵌套循环
* 需求:将数组 a 中的全部元素复制到数组 b 中。 ### 5.5.1 概述
* 所谓的嵌套循环,是指一个循环结构 A 的循环体是另一个循环结构 B 。例如for 循环里面还有一个for 循环,就是嵌套循环。
* 语法:
* 示例:
```c ```c
#include <stdio.h> for(初始化语句①; 循环条件语句②; 迭代语句⑦) {
for(初始化语句③; 循环条件语句④; 迭代语句⑥) {
#define SIZE 10 循环体语句⑤;
int main() {
// 定义数组并初始化
int a[] = {12, 2, 31, 24, 15, -36, 67, 108, 29, 51};
int b[SIZE];
// 复制数组
for (int i = 0; i < SIZE; i++) {
b[i] = a[i];
} }
// 打印数组 b 中的全部元素
for (int i = 0; i < SIZE; i++) {
printf("%d ", b[i]);
}
return 0;
} }
``` ```
### 2.7.5 应用示例 * 其中for 、while 、do-while 均可以作为外层循环或内层循环。
- 外层循环:循环结构 A
- 内层循环:循环结构 B
* 需求:数组对称位置的元素互换。 ![](./assets/10.png)
> [!NOTE] > [!NOTE]
> >
> 思路:假设数组一共有 10 个元素,那么: > * ① 实际上,嵌套循环就是将内层循环当成外层循环的循环体。当只有内层循环的循环条件为 false ,才会完全跳出内层循环,才可结束外层的当次循环,开始下一次循环。
> > * ② 假设外层循环次数为 m 次,内层循环次数为 n 次,则内层循环体实际上需要执行 m × n 次。
> * a[0] 和 a[9] 互换。 > * ③ 从二维图形的角度看,外层循环控制`行数`,内层循环控制`列数`。
> * a[1] 和 a[8] 互换。 > * ④ 实际开发中,我们最多见到的嵌套循环是两层,一般不会出现超过三层的嵌套循环。如果将要出现,一定要停下来重新梳理业务逻辑,重新思考算法的实现,控制在三层以内;否则,可读性会很差。
> * ...
>
> 规律就是 `a[i] <--互换--> arr[arr.length -1 -i]`
### 5.5.2 应用示例
* 需求:打印 5 行 `*` ,要求每行 6 个 `*`
@ -790,37 +1204,22 @@ int main() {
int main() { int main() {
// 原始数组 for (int i = 1; i <= 5; ++i) {
int arr[] = {12, 2, 31, 24, 15, -36, 67, 108, 29, 51}; for (int j = 1; j < 6; ++j) {
printf("* ");
// 计算数组的长度
size_t SIZE = sizeof(arr) / sizeof(arr[0]);
// 打印原始数组中的全部元素
printf("原始数组:");
for (int i = 0; i < SIZE; i++) {
printf("%d ", arr[i]);
} }
printf("\n"); printf("\n");
// 交换数组
for (int i = 0; i < SIZE / 2; i++) {
int temp = arr[i];
arr[i] = arr[SIZE - 1 - i];
arr[SIZE - 1 - i] = temp;
} }
// 打印交换后的数组
printf("交换后数组:");
for (int i = 0; i < SIZE; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0; return 0;
} }
``` ```
### 5.5.3 应用示例
* 需求:打印 5 行直角三角形。
* 示例: * 示例:
@ -830,44 +1229,20 @@ int main() {
int main() { int main() {
// 原始数组 for (int i = 1; i <= 5; ++i) {
int arr[] = {12, 2, 31, 24, 15, -36, 67, 108, 29, 51}; for (int j = 1; j <= i; ++j) {
printf("* ");
// 计算数组的长度
size_t SIZE = sizeof(arr) / sizeof(arr[0]);
// 打印原始数组中的全部元素
printf("原始数组:");
for (int i = 0; i < SIZE; i++) {
printf("%d ", arr[i]);
} }
printf("\n"); printf("\n");
// 交换数组
for (int i = 0, j = SIZE - 1 - i; i < SIZE / 2; i++, j--) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
} }
// 打印交换后的数组
printf("交换后数组:");
for (int i = 0; i < SIZE; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0; return 0;
} }
``` ```
### 2.7.6 应用示例 ### 5.5.4 应用示例
* 需求:将数组中的最大值移动到数组的最末尾。 * 需求:打印 5 行倒直角三角形。
> [!NOTE]
>
> 思路:从数组的下标 `0` 开始依次遍历到 `length - 1` ,如果 `i` 下标当前的值比 `i+1` 下标的值大,则交换;否则,就不交换。
@ -878,47 +1253,20 @@ int main() {
int main() { int main() {
// 原始数组 for (int i = 1; i <= 5; ++i) {
int arr[] = {12, 2, 31, -24, 15, -36, 67, 891, 29, 51}; for (int j = 1; j <= 6 - i; ++j) {
printf("* ");
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(arr[0]);
// 打印原始数组中的全部元素
printf("原始数组:");
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
} }
printf("\n"); printf("\n");
// 移动最大值到数组的最后一个位置
for (int i = 0; i < length - 1; i++) {
if (arr[i] > arr[i + 1]) {
int temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
} }
}
// 打印移动之后的数组
printf("移动之后的数组:");
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0; return 0;
} }
``` ```
### 2.7.7 应用示例 ### 5.5.5 应用示例
* 需求:实现冒泡排序,即将数组的元素从小到大排列。
> [!NOTE]
>
> 思路:一层循环,能实现最大值移动到数组的最后;那么,二层循环(控制内部循环数组的长度)就能实现将数组的元素从小到大排序。
* 需求:打印 9 `×` 9 乘法表。
@ -929,202 +1277,220 @@ int main() {
int main() { int main() {
// 原始数组 for (int i = 1; i <= 9; ++i) {
int arr[] = {12, 2, 31, -24, 15, -36, 67, 891, 29, 51}; for (int j = 1; j <= i; ++j) {
printf("%d × %d = %d ", i, j, i * j);
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(arr[0]);
// 打印原始数组中的全部元素
printf("原始数组:");
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
} }
printf("\n"); printf("\n");
for (int j = 0; j < length - 1; j++) {
for (int i = 0; i < length - 1 - j; i++) {
if (arr[i] > arr[i + 1]) {
int temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
} }
}
}
// 打印移动之后的数组
printf("移动之后的数组:");
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0; return 0;
} }
``` ```
### 2.7.8 应用示例 ## 5.6 无限循环
* 需求:数组中的元素是从小到大排列的,现在要求根据指定的元素获取其在数组中的位置。
> [!NOTE]
>
> 二分查找(折半查找)的前提条件是:数组中的元素必须是`有序`的(从小到大或从大到小)。其基本步骤,如下所示:
>
> * ① 确定初始范围:定义数组的起始索引 `min = 0` 和结束索引 `max = len - 1`
> * ② 计算中间索引:在每次迭代中,计算中间位置 `mid = (min + right) / 2`
> * ③ 比较中间值:
> * 如果`目标值`比 `arr[mid]` 小,则继续在`左`半部分查找,那么 `min` 不变,而`max = mid - 1` 。
> * 如果`目标值`比 `arr[mid]` 大,则继续在`右`半部分查找,那么 `max` 不变,而`min = mid + 1` 。
> * 如果`目标值`和 `arr[mid]` 相等,则找到了目标,返回该索引。
> * ④ 结束条件:当 `min > max` 的时候,表示查找范围为空,即:元素不存在,返回 `-1`
* 示例:
```c
#include <stdio.h>
/**
* 二分查找
*
* @param arr 数组
* @param len 数组长度
* @param num 要查找的数据
* @return 返回数据的下标,没有找到返回-1
*/
int search(int arr[], int len, int num) {
int min = 0;
int max = len - 1;
while (min <= max) {
int mid = (min + max) / 2;
if (num < arr[mid]) { // 说明要查找的数据在左半边
max = mid - 1;
} else if (num > arr[mid]) { // 说明要查找的数据在右半边
min = mid + 1;
} else { // 说明找到了
return mid;
}
}
return -1;
}
int main() {
int arr[] = {1, 2, 3, 4, 5, 6};
int len = sizeof(arr) / sizeof(arr[0]);
int index = search(arr, len, -1);
printf("index = %d\n", index);
return 0;
}
```
# 第三章:多维数组(⭐)
## 3.1 概述
### 3.1.1 引入
* 我们在数学、物理和计算机科学等学科中学习过`一维坐标`、`二维坐标`以及`三维坐标`。
* 其中,`一维坐标`通常用于描述在线段或直线上的点的位置,主要应用有:
* **数轴**:一维坐标可以用来表示数轴上的数值位置,这在基础数学和初等代数中非常常见。
![](./assets/8.png)
* **时间轴**:时间可以看作是一维的,它可以用一维坐标表示,例如:秒、分钟、小时等。
![](./assets/9.png)
* **统计数据**:一维坐标常用于表示单变量的数据集,如:测量身高、体重、温度等。
![](./assets/10.jpg)
* 其中,`二维坐标`用于描述平面上的点的位置。主要应用包括:
* **几何学**:在几何学中,二维坐标用于表示平面图形的顶点、边和面积等。
![](./assets/11.png)
* **地图和导航**:地理坐标系统(经纬度)使用二维坐标来表示地球表面的任意位置。
![image-20240724112326592](./assets/12.png)
* **图形设计和计算机图形学**:二维坐标在绘制图形、设计图案和用户界面中非常重要。
![](./assets/13.png)
* **物理学**:二维运动和场,例如:在描述物体在平面上的运动轨迹时使用二维坐标。
![](./assets/14.jpg)
* 其中,三维坐标用于描述空间中点的位置。主要应用包括:
* **几何学**:三维坐标在空间几何中用于表示立体图形的顶点、边、面和体积。
![](./assets/15.png)
* **计算机图形学**:三维建模和动画需要使用三维坐标来创建和操控虚拟对象。
![](./assets/16.png)
* **工程和建筑设计**:在设计建筑物、机械部件和其他工程项目时,使用三维坐标来精确定位和规划。
![](./assets/17.png)
* **物理学**:三维空间中的力、运动和场,例如:描述物体在空间中的位置和运动轨迹。
![](./assets/18.png)
* 总而言之,一维、二维和三维坐标系统在不同的领域中各有其重要的应用,从基础数学到高级科学和工程技术,它们帮助我们更好地理解和描述世界的结构和行为。
### 3.1.2 多维数组
* 在 C 语言中,多维数组就是数组嵌套,即:在数组中包含数组,数组中的每一个元素还是一个数组类型,如下所示:
![](./assets/19.png)
> [!NOTE]
>
> * ① 如果数组中嵌套的每一个元素是一个常量值,那么该数组就是一维数组。
> * ② 如果数组中嵌套的每一个元素是一个一维数组,那么该数组就是二维数组。
> * ③ 如果数组中嵌套的每一个元素是一个二维数组,那么该数组就是三维数组.
> * ④ 依次类推...
* 一维数组和多维数组的理解:
* 从内存角度看:一维数组或多维数组都是占用的一整块连续的内存空间。
* 从数据操作角度看:
* 一维数组可以直接通过`下标`访问到数组中的某个元素0、1、...
* 二维数组要想访问某个元素,先要获取某个一维数组,然后在一维数组中获取对应的数据。
> [!NOTE]
>
> * ① C 语言中的一维数组或多维数组都是占用的一整块连续的内存空间其它编程语言可不是这样的Java 等。
> * ② 在实际开发中,最为常用的就是二维数组或三维数组了,以二维数组居多!!!
## 3.2 二维数组的定义
### 3.2.1 动态初始化
* 语法: * 语法:
```c ```c
数据类型 数组名[几个⼀维数组元素][每个⼀维数组中有几个具体的数据元素]; while(1){
...
}
```
```c
for(;;){
...
}
``` ```
> [!NOTE] > [!NOTE]
> >
> * ① 二维数组在实际开发中,最为常见的应用场景就是表格或矩阵了。 > * ① 在开发中有的时候并不确定需要循环多少次就需要根据循环体内部的某些条件来控制循环的结束break
> * ② 几个一维数组元素 = 行数。 > * ② 如果上述的循环结构不能终止,就会构成死循环;所以,在实际开发中,要避免出现死循环!!!
> * ③ 每个⼀维数组中有几个具体的数据元素 = 列数。
* 示例:从键盘读入个数不确定的整数,并判断读入的正数和负数的个数,输入为 0 时结束程序
```c
#include <stdio.h>
int main() {
// 记录输入的整数
int num = 0;
// 记录正数个数
int positiveCount = 0;
// 记录负数个数
int negativeCount = 0;
while (true) {
printf("请输入一个整数:");
scanf("%d", &num);
if (num > 0) {
positiveCount++;
} else if (num < 0) {
negativeCount++;
} else {
printf("程序结束!\n");
break;
}
}
printf("正数的个数:%d\n", positiveCount);
printf("负数的个数:%d\n", negativeCount);
return 0;
}
```
## 5.7 跳转控制语句
### 5.7.1 break
* break 的使用场景break 语句用于终止某个语句块的执行用在switch语句或者循环语句中。
> [!NOTE]
>
> break 一旦执行,就结束(或跳出)当前循环结构;并且,此关键字的后面,不能声明其它语句。
* 流程图,如下所示:
![](./assets/11.png)
* 示例:打印 0 ~ 10 ,如果遇到 `3` ,就停止打印
```c
#include <stdio.h>
int main() {
for (int i = 0; i < 10; ++i) {
if (i == 3) {
break;
}
printf("%d \n", i);
}
printf("程序结束!\n");
return 0;
}
```
* 示例:编写程序,要求输入一个数字,判断该数字是否是质数
```c
#include <stdio.h>
int main() {
bool isFlag = false;
int num = 0;
do {
printf("请输入一个整数(必须大于 1 ");
scanf("%d", &num);
if (num <= 1) {
printf("输入的数字不是合法,请重新输入!!!\n");
isFlag = true;
} else {
isFlag = false;
}
} while (isFlag);
bool isPrime = true;
for (int i = 2; i < num; i++) {
if (num % i == 0) {
isPrime = false;
break;
}
}
if (isPrime) {
printf("%d 是一个质数\n", num);
} else {
printf("%d 不是一个质数\n", num);
}
printf("程序结束!\n");
return 0;
}
```
### 5.7.2 continue
* continue 的使用场景continue 语句用于结束本次循环,继续执行下一次循环。
> [!NOTE]
>
> continue 一旦执行,就结束(或跳出)当次循环结构;并且,此关键字的后面,不能声明其它语句。
* 流程图,如下所示:
![](./assets/12.png)
* 示例:打印 0 ~ 10 ,如果遇到 `3` ,就继续下一次打印
```c
#include <stdio.h>
int main() {
for (int i = 0; i < 10; ++i) {
if (i == 3) {
continue;
}
printf("%d \n", i);
}
printf("程序结束!\n");
return 0;
}
```
* 示例:输出 100 以内(包括 100的数字跳过那些 7 的倍数或包含 7 的数字
```c
#include <stdio.h>
int main() {
for (int i = 1; i <= 100; i++) {
if (i % 7 == 0 || i % 10 == 7 || i / 10 == 7) {
continue;
}
printf("%d ", i);
}
printf("程序结束!\n");
return 0;
}
```
### 5.7.3 return
* return :并非专门用于结束循环的,它的功能是结束一个方法。当一个方法执行到一个 return 语句的时候,这个方法将被结束。
> [!NOTE]
>
> 和 break 和 continue 不同的是return 直接结束整个方法,不管这个 return 处于多少层循环之内。
@ -1135,223 +1501,16 @@ int main() {
int main() { int main() {
// 定义二维数组并初始化 for (int i = 1; i <= 100; i++) {
int arr[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}}; if (i % 7 == 0 || i % 10 == 7 || i / 10 == 7) {
return 0; // 结束整个函数或方法
}
printf("%d ", i);
}
// 输出二维数组中的元素 printf("程序结束!\n");
printf("%d ", arr[0][0]);
printf("%d ", arr[0][1]);
printf("%d ", arr[0][2]);
printf("%d \n", arr[0][3]);
printf("%d ", arr[1][0]);
printf("%d ", arr[1][1]);
printf("%d ", arr[1][2]);
printf("%d \n", arr[1][3]);
printf("%d ", arr[2][0]);
printf("%d ", arr[2][1]);
printf("%d ", arr[2][2]);
printf("%d ", arr[2][3]);
return 0; return 0;
} }
``` ```
### 3.2.2 静态初始化 1
* 语法:
```c
数据类型 数组名[行数][列数] = {{元素1,元素2,...},{元素3,...},...}
```
> [!NOTE]
>
> * ① 行数 = 几个一维数组元素。
> * ② 列数 = 每个⼀维数组中有几个具体的数据元素。
* 示例:
```c
#include <stdio.h>
int main() {
// 定义二维数组并初始化
int arr[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
// 输出二维数组中的元素
printf("%d ", arr[0][0]);
printf("%d ", arr[0][1]);
printf("%d ", arr[0][2]);
printf("%d \n", arr[0][3]);
printf("%d ", arr[1][0]);
printf("%d ", arr[1][1]);
printf("%d ", arr[1][2]);
printf("%d \n", arr[1][3]);
printf("%d ", arr[2][0]);
printf("%d ", arr[2][1]);
printf("%d ", arr[2][2]);
printf("%d ", arr[2][3]);
return 0;
}
```
### 3.2.3 静态初始化 2
* 语法:
```c
数据类型 数组名[][列数] = {{元素1,元素2,...},{元素3,...},...}
```
> [!NOTE]
>
> * ① 列数 = 每个⼀维数组中有几个具体的数据元素。
> * ② 可以`不`指定`行数``必须`指定`列`数,编译器会根据元素的个数和列的个数,自动推断出行数!!!
* 示例:
```c
#include <stdio.h>
int main() {
// 定义二维数组
int arr[][4] = {{1, 2, 3, 4}, {5, 6}, {9, 10, 11, 12}};
// 输出二维数组中的元素
printf("%d ", arr[0][0]);
printf("%d ", arr[0][1]);
printf("%d ", arr[0][2]);
printf("%d \n", arr[0][3]);
printf("%d ", arr[1][0]);
printf("%d \n", arr[1][1]);
printf("%d ", arr[2][0]);
printf("%d ", arr[2][1]);
printf("%d ", arr[2][2]);
printf("%d ", arr[2][3]);
return 0;
}
```
## 3.3 二维数组的理解
* 如果二维数组是这么定义的,即:
```c
int arr[3][4];
```
* 那么,这个二维数组 `arr` 可以看做是 `3` 个一维数组组成,它们分别是 `arr[0]`、`arr[1]`、`arr[2]`。这 `3` 个一维数组都各有 4 个元素,如:一维数组 `arr[0]` 中的元素是 `arr[0][0]`、`arr[0][1]`、`arr[0][2]`、`arr[0][3]`,即:
![](./assets/20.png)
## 3.4 二维数组的遍历
* 访问二维数组的元素,需要使用两个下标(索引),一个用于访问行(第一维),另一个用于访问列(第二维),我们通常称为行下标(行索引)或列下标(列索引)。
* 所以,遍历二维数组,需要使用双层循环结构。
> [!NOTE]
>
> 如果一个二维数组是这么定义的,即:`int arr[3][4]`,那么:
>
> * `行的长度 = sizeof(arr) / sizeof(arr[0])` ,因为 `arr` 是二维数组的`总`的内存空间;而 `arr[0]` 、`arr[1]`、`arr[2]` 是二维数组中一维数组的内存空间 。
> * `列的长度 = sizeof(arr[0]) / sizeof(arr[0][0])`,因为`arr[0]` 、`arr[1]`、`arr[2]` 是二维数组中一维数组的内存空间 ,而 `arr[0][0]`、`arr[0][1]`、... 是一维数组中元素的内存空间。
* 示例:
```c
#include <stdio.h>
int main() {
// 定义二维数组
int arr[][4] = {{1, 2, 3, 4}, {5, 6}, {9, 10, 11, 12}};
// 获取行列数
int row = sizeof(arr) / sizeof(arr[0]);
int col = sizeof(arr[0]) / sizeof(arr[0][0]);
// 打印二维数组元素
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
```
## 3.5 二维数组的内存分析
* 用`矩阵形式`3 行 4 列形式)表示二维数组,是`逻辑`上的概念,能形象地表示出行列关系。而在`内存`中,各元素是连续存放的,不是二维的,是`线性`的。
* C 语言中,二维数组中元素排列的顺序是`按行存放`的。即:先顺序存放第一行的元素,再存放第二行的元素。例如:数组`a[3][4] `在内存中的存放,如下所示:
![](./assets/21.png)
> [!NOTE]
>
> * ① 这就是 `C` 语言的二维数组在进行静态初始化的时候,`可以`忽略`行数`的原因所在(底层的`内存结构`是`线性`的),因为可以根据 `元素的总数 ÷ 每列元素的个数 = 行数`的公式计算出`行数`。
> * ② 如果你学过 `Java` 语言可能会感觉困惑Java 语言中的二维数组在进行静态初始化,是`不能`忽略`行数`的,是因为 Java 编译器会根据`行数`去堆内存空间先开辟出一维数组,然后再继续...,所以当然`不能`忽略`行数`。
## 3.6 二维数组的应用案例
* 需求:现在有三个班,每个班五名同学,用二维数组保存他们的成绩,并求出每个班级平均分、以及所有班级平均分,数据要求从控制台输入。
* 示例:
```c
#include <stdio.h>
int main() {
// 定义二维数组,用于保存成绩
double arr[3][5];
// 获取二维数组的行数和列数
int row = sizeof(arr) / sizeof(arr[0]);
int col = sizeof(arr[0]) / sizeof(arr[0][0]);
// 从控制台输入成绩
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
printf("请输入第%d个班级的第%d个学生的成绩", i + 1, j + 1);
scanf("%lf", &arr[i][j]);
}
}
// 总分
double totalSum = 0;
// 遍历数组,求总分和各个班级的平均分
for (int i = 0; i < row; i++) {
double sum = 0;
for (int j = 0; j < col; j++) {
totalSum += arr[i][j];
sum += arr[i][j];
}
printf("第%d个班级的总分为%.2lf\n", i + 1, sum);
printf("第%d个班级的平均分为%.2lf\n", i + 1, sum / col);
}
printf("所有班级的总分为:%.2lf\n", totalSum);
printf("所有班级的平均分为:%.2lf\n", totalSum / (row * col));
return 0;
}
```

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 150 KiB

View File

Before

Width:  |  Height:  |  Size: 440 KiB

After

Width:  |  Height:  |  Size: 440 KiB

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 232 KiB

View File

Before

Width:  |  Height:  |  Size: 536 KiB

After

Width:  |  Height:  |  Size: 536 KiB

View File

Before

Width:  |  Height:  |  Size: 926 KiB

After

Width:  |  Height:  |  Size: 926 KiB

View File

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 134 KiB

View File

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 179 KiB

View File

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 145 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 MiB

After

Width:  |  Height:  |  Size: 7.2 MiB

View File

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

Before

Width:  |  Height:  |  Size: 718 KiB

After

Width:  |  Height:  |  Size: 718 KiB

View File

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

Before

Width:  |  Height:  |  Size: 926 KiB

After

Width:  |  Height:  |  Size: 926 KiB

View File

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 155 KiB

View File

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 151 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

Before

Width:  |  Height:  |  Size: 471 KiB

After

Width:  |  Height:  |  Size: 471 KiB

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 297 KiB

View File

@ -0,0 +1,510 @@
# 第一章内存泄漏Memory Leak
## 1.1 概述
* 有没有过这样的日子,总感觉我们的电脑,不是一个尖端的设备,而像一只疲惫的蜗牛。它在缓慢的爬行,并试图背着重重的楼房去跑马拉松,如下所示:
![](./assets/1.jpeg)
> [!NOTE]
>
> 儿歌《蜗牛与黄鹂鸟》的歌词是这样的,如下所示:
>
> * 阿门阿前一棵葡萄树。
> * 阿嫩阿嫩绿地刚发芽。
> * 蜗牛背着那重重的壳呀。
> * 一步一步地往上爬。
> * 阿树阿上两只黄鹂鸟。
> * 阿嘻阿嘻哈哈在笑它。
> * 葡萄成熟还早得很哪。
> * 现在上来干什么。
> * 阿黄阿黄鹂儿不要笑。
> * 等我爬上它就成熟了。
>
> 虽然歌曲的主旨是想通过蜗牛与黄鹂鸟的对话,表达了努力和坚持的重要性,即使速度慢,只要坚定地往前走,总会达到目标。但是,也从侧面说明了蜗牛的速度真的很慢。
* 亦或者,我们的电脑就像一个蹒跚学步的孩子在发脾气,我们多么希望她们耐心点,并配合我们。可是,她们总是拒绝和我们合作,如下所示:
![](./assets/2.jpg)
* 如果这些场景,你都感觉很熟悉,那么你很有可能就是`内存泄漏`的受害者。
> [!NOTE]
>
> * ① `内存泄漏`虽然不可见,但是它会悄悄的蚕食计算机的性能,让曾经快速的系统变成一台陈旧的机器。
> * ② 最为糟糕的时,和留下明显迹象的`漏水`不同,`内存泄漏`是不可见的,这使得它们难以识别,甚至难以修复。也正是因为这个特点,让开发人员和计算机用户都感觉头疼。
## 1.2 什么是内存泄漏?
* 我们可以将我们的计算机想象成一个繁华的城市,城市的`道路`就代表着计算器的`内存`(计算机的内存是有限的,普遍的家用个人台式机电脑最多只支持 `4` 根内存条。如果是 `DDR4` 的话,最多也就支持 `128` GB。就算是服务器也不是无穷无尽的在其上运行的`程序`就像`车辆`一样,每辆车都执行各自的任务,如下所示:
> [!NOTE]
>
> * ① 操作系统或计算机允许程序自己分配内存,并自由使用。并且,当程序执行完自己的任务之后,还可以释放掉内存,将内存还给操作系统或计算机。
> * ② 需要说明的是,并不是程序结束运行,才会释放掉内存:在 C/C++ 等语言中,是可以在程序执行完任务之后,由程序员手动释放之前申请的内存,即:调用释放内存的函数。而 Java 等 GC 的编程语言,会由 GC 帮助程序员释放内存,当然从理论上讲会有稍许停顿。但是,像 Java 语言中的 ZGC 现在已经可以控制在 10ms 了,人几乎感觉不到!!!
> * ③ 所谓的`分配内存`,就是程序向计算机或操作系统,申请一块内存空间,然后自己使用。
> * ④ 所谓的`释放内存`,就是程序告诉计算机或操作系统,不再需要使用之前申请的内存空间,那么就可以将之前申请的内存空间,归还给操作系统或计算机,让其它的程序使用。
> * ⑤ 上面例子中的`程序`就像`车辆`一样,每辆车都执行各自的任务,类似于程序在执行的时候,向操作系统申请自己的内存空间,并完成自己的任务。
![](./assets/3.jpg)
* 但是,如果有些车辆在完成自己的任务之后,就决定无限期的停在路上,而不是离开。那么,可以想象到的是,随着时间的推移,这些停放的汽车就会开始阻塞城市的道路,减慢交通速度,如下所示:
![](./assets/4.jpg)
> [!NOTE]
>
> * ① 需要说明的是,道路或网络的利用率并非越高越好。
> * ② 如果使用 D0 表示道路或网络空闲时的时延(数据包(或车辆)几乎没有排队,时延 D0 只是基本的传输或行驶时间),而 D 表示道路或网络当前的时延(数据包(或车辆)可能需要排队,这导致了额外的时延,时延 D 是包含了排队时间的总时延),那么在理想的条件下,可以使用如下的表达式来表示 D、D0 以及道路或网络利用率 U 之间的关系,即:$U = \frac{D - D_0}{D}$,经过换算一下,其结果就是:$D = \frac{D_0}{1 - U}$。
> * ③ 显而易见,道路或网络利用率并不是越大越好,过高的道路或网络利用率会产生非常大的时延。
* 由此可见,在极端情况下,这座城市甚至可能陷入停顿。
> [!NOTE]
>
> 这实际上就是`内存泄漏`对计算机的影响,即:
>
> * ① 程序可能会变慢,甚至崩溃,特别是在长时间运行的程序中。
> * ② `内存泄漏`会逐渐耗尽系统内存,造成资源浪费,并导致系统性能下降。
* 再或者,在生活中,我们必然需要用水,如果规定每个人一个月的用水量不能超过 `10t`,那么三口之间每个月的用水量就不能超过 `30t`。假设,由于水管老化或小动物(老鼠)的影响,而导致家中的水管产生轻微的破损,产生漏水的现象,如下所示:
![](./assets/5.jpg)
* 那么,家中隐藏的漏水问题在很长一段时间内是不会被注意到的。亦或者,假设每个人的用水量都没有限制,那么如果要用到 `30t` ,必然会比之前没有漏水的时候,产生的水费也要多很多。
> [!IMPORTANT]
>
> 官方定义:`内存泄漏`是指计算机程序无意中消耗的一种特定类型的内存,其中程序无法释放不再需要或使用的内存(这种内存虽然不再被程序使用,但仍然占据着系统资源),进而导致这些内存无法被系统或其他程序再次使用,随着时间的推荐,会逐渐耗尽系统内存,并最终导致系统性能下降。
## 1.3 什么会触发内存泄漏?
* 导致`内存泄漏`的原因很多,具体取决于编程语言、平台和特定的应用程序场景。以下是一些最常见的原因:
* ① **未关闭的资源**:未能关闭文件、数据库连接或网络套接字等资源可能会导致`内存泄漏`。如果这些资源保持打开状态,可能会随着时间的推移而累积并消耗大量内存。
* ② **未释放的对象引用**:保留不再需要的对象引用可以防止垃圾回收器(在具有它们的语言中)回收内存。
* ③ **循环引用**:在某些语言中,两个相互引用的对象可能会导致两个对象都无法被垃圾回收的情况,即使程序的其他部分没有引用它们。
* ④ **静态集合**:使用随时间增长而从未清除的静态数据结构可能会导致`内存泄漏`。例如:将元素添加到静态列表而不删除它们可能会导致列表无限增长。
* ⑤ **事件侦听器**:不分离事件侦听器或回调可能会导致`内存泄漏`,尤其是在 Web 浏览器等环境中。如果对象已附加到事件但不再使用,则不会对其进行垃圾回收,因为该事件仍包含对它的引用。
* ⑥ **中间件和第三方库**:有时,`内存泄漏`的原因可能不在于应用程序代码,而在于它使用的中间件或第三方库。这些组件中的错误或低效代码可能会导致`内存泄漏`。
* ⑦ **内存管理不当**:在开发人员手动管理内存的语言,如: C、C++ 中,使用后未能释放内存或使用 “悬空指针” 可能会导致泄漏。
* ⑧ **内存碎片**:虽然不是传统意义上的泄漏,但碎片会导致内存使用效率低下。随着时间的推移,内存分配之间的小间隙会累积,从而难以分配更大的内存块。
* ⑨ **孤立线程**:生成但未正确终止的线程可能会消耗内存资源。这些孤立线程会随着时间的推移而累积,尤其是在长时间运行的应用程序中。
* ⑩ **缓存过度使用**:在没有适当驱逐策略的情况下实施缓存机制可能会导致内存无限消耗,尤其是在缓存无限增长的情况下。
* 在 C 语言中,可以使用 `while` 循环并结合 `malloc` 函数来实现一个内存泄漏的例子,即:
```c
#include <stdbool.h>
#include <stdlib.h>
int main() {
while (true) { // 死循环
malloc(1024); // 分配1024个字节的内存
}
return 0;
}
```
* 如果我们在 Windows 上运行该程序,就可以打开 Windows 的任务管理器(快捷键是`Ctrl + Shift + ESC`),将会发现内存的使用率在飙升。当然,稍等片刻后程序会被终止,是因为 Windows 的内存管理机制,发现我们的程序占用内存太多,会让它崩溃,防止系统卡死(其它的操作系统也有相应的措施)。
![](./assets/6.gif)
## 1.4 内存泄漏会导致什么后果?
* ① **内存使用量增加**:随着泄漏和释放的内存越来越多,整体系统内存使用量会增加。这会减少可用于其他进程和应用程序的内存,从而降低系统速度。
* ② **增加分页**:随着`内存泄漏`的累积,系统可能会开始将内存内容交换到磁盘以释放 RAM从而导致更多的磁盘 I/O。这会导致性能降低因为磁盘操作比内存操作慢得多。
* ③ **内存不足错误**:如果`内存泄漏`足够多,系统最终可能会完全耗尽可用内存。这可能会导致崩溃、内存分配失败和程序终止。
* ④ **资源争用**较高的内存使用率还会导致对缓存和资源CPU 时间等)的更多争用,因为系统尝试管理有限的资源。这会进一步降低性能。
* ⑤ **应用程序不稳定**:随着内存使用量随着时间的推移而增长,存在`内存泄漏`的应用程序可能会遇到崩溃、意外行为和间歇性故障。这会导致不稳定和可靠性问题。
* ⑥ **安全风险**`内存泄漏`会使数据在内存中的延迟时间超过预期。此数据可能包含密码、密钥或其他敏感信息,如果恶意软件或攻击者访问这些信息,则会带来安全风险。
## 1.5 检测内存泄漏的工具或技术
* ① **分析工具**
* ① Valgrind用于构建动态分析工具的检测框架最有名的 Memcheck 的套件,可以检测 C 和 C++ 程序中的内存泄漏。
* ② Java VisualVM适用于 Java 应用程序的监控、故障排除和分析工具。
* ③ .NET Memory Profiler用于查找内存泄漏并优化 .NET 应用程序中的内存使用的工具。
* ④ Golang pprof该工具可让您收集 Go 程序的 CPU 配置文件、跟踪和堆配置文件。
* ② **浏览器开发工具**Chrome、Firefox 和 Edge 等现代 Web 浏览器附带内置的开发人员工具,可帮助识别 Web 应用程序中的内存泄漏,尤其是 JavaScript 中的内存泄漏。
* ③ **静态分析**Lint、SonarQube 或 Clang Static Analyzer 等工具可以扫描代码以识别可能导致内存泄漏的模式。
* ④ **自动化测试**将内存泄漏检测整合到自动化测试中有助于在开发周期的早期捕获泄漏JUnit适用于 Java或 pytest适用于 Python等工具可以与内存分析工具集成以自动执行此过程。
* ⑤ **堆分析**检查应用程序的堆转储可以深入了解正在消耗内存的对象Eclipse MAT内存分析器工具或 Java 堆分析工具 jhat等工具可以协助进行此分析。
* ⑥ **指标**实施指标来监控一段时间内的内存使用情况有助于识别导致内存消耗增加的模式或特定操作Prometheus 和 Grafana 等。
* ⑦ **第三方库和中间件**:一些第三方解决方案提供内置的内存泄漏检测功能。如果我们怀疑这些组件可能是泄漏源,则必须查看与这些组件相关的文档或论坛。
* ⑧ **手动代码审查**:有时,识别内存泄漏的最佳方法是对代码进行彻底的手动审查,尤其是在分配和释放内存的区域中。
* ⑨ **压力测试**:在高负载或长时间运行应用程序,有助于暴露在正常情况下可能不明显的内存泄漏。
## 1.6 如何避免内存泄漏?
* ① **及时释放内存**:在程序中,确保在不再需要使用内存时及时释放它。
* ② **智能指针**:使用智能指针来帮助在 C++ 等编程语言中进行自动内存管理。
* ③ **将编程语言与垃圾回收器一起使用**:内存分配和释放由 Python 和 Java 等编程语言自动处理,这些语言包含内置的垃圾收集系统。
* ④ **利用内存管理策略:** 有效的内存管理可以防止内存泄漏。这包括始终监控我们的软件使用了多少内存,并了解何时分配和取消分配内存,即:检测内存泄漏的工具或技术。
## 1.7 总结
* **内存泄漏**是由于未释放不再使用的内存,导致内存资源逐渐减少,但不会立即导致程序崩溃,而是`长时间`运行后可能出现性能问题或最终崩溃。
# 第二章内存溢出Out Of MemoryOOM
## 2.1 概述
* 首先,说明一点,在国内的很多文章中,都将 `Out Of MemoryOOM`翻译为 `内存溢出`,但是本人认为翻译为`内存不足`更为贴切。
* 在生活中,我们在使用计算机的时候,可能会遇到打开视频网站的时候,视频网站崩溃了,并且在浏览器上显示报错信息`Error Code Out Of Memory`,如下所示:
![](./assets/7.png)
* 当然我们在使用微软办公套件Outlook 的时候,可能也会遇到系统提示 `Out Of Memory`,如下所示:
![](./assets/8.jpg)
* 亦或者,我们在打游戏的时候,会遇到系统提示 `Out Of Memory`,如下所示:
![](./assets/9.png)
* 上述的种种情景都表明了内存溢出内存不足OOM是`立即显现`的问题,尤其是当系统无法分配足够内存时,会直接导致程序崩溃或异常。
> [!NOTE]
>
> * ① 内存泄漏是一种`逐渐积累`的问题会耗尽系统内存可能最终导致内存不足理解站着茅坑不拉稀最终可能导致可用的茅坑越来越少后面的人就只能等着o(╥﹏╥)o
> * ② 内存溢出(不足)是一种`立即显现`的问题,当系统无法分配足够内存时,会`直接`导致程序崩溃或异常(理解:大象塞进冰箱,冰箱不是无限大,最终可能导致大象身体的一部分露出来,这不就`溢出`吗?换言之,就是冰箱(内存)的容量有限啊,`不`能满`足`实际需要)。
> [!IMPORTANT]
>
> 官方定义:当计算机没有足够的内存来执行操作或运行应用程序时,会发生内存不足 OOM 错误。此内存可以是`物理 RAM`(随机存取内存) 或`虚拟内存`,它使用磁盘空间扩展物理内存。当系统耗尽可用内存时,它无法再满足`内存分配`请求,从而导致 OOM 错误。此错误表示除非释放或添加内存,否则系统无法处理进一步的需求。
## 2.2 什么会触发内存溢出?
* 导致`内存溢出`的原因很多,具体取决于编程语言、平台和特定的应用程序场景。以下是一些最常见的原因:
* ① **无限循环或递归**:如果程序中的循环或递归没有正确终止条件,可能会一直运行,消耗掉所有可用内存。
* ② **内存泄漏**:程序不断分配内存而不释放,最终导致可用内存耗尽。这通常是因为程序在使用完某些数据后,没有正确地释放相关的内存。
* ③ **处理大数据集**:如果程序试图一次性加载或处理一个超大的数据集,而该数据集的大小超过了系统的可用内存,这可能会导致内存溢出。
* ④ **资源过度分配**:一些程序在运行时,可能会为某些资源(如缓存、临时数据)分配过多的内存,导致整体系统内存不足。
* ⑤ **错误的内存管理**在手动管理内存的编程语言中C 或 C++),如果程序错误地管理内存(如:重复释放、未释放或非法访问内存),也可能引发内存泄漏,进而导致内存溢出。
* ⑥ **并发操作**:如果多个进程或线程并发地进行大量内存分配操作,且这些操作没有得到有效控制,也可能导致系统内存被耗尽。
* ⑦ **外部库或工具的 Bug**:使用的第三方库或工具中存在内存管理相关的 bug也可能导致内存溢出。
## 2.3 如何避免内存溢出?
* ① **优化数据处理**
* 分块处理大数据集:如果需要处理大数据集,可以将数据分块处理,而不是一次性加载整个数据集到内存中。例如:处理大型文件时,可以逐行读取或分批读取。
* 使用流式处理对于需要处理大量数据的操作可以采用流式处理streaming这样只保留当前处理的部分数据在内存中而非全部数据。
* ② **管理对象生命周期**
* 及时释放不再使用的对象在使用动态分配内存的编程语言C++、C#、Java 等确保在对象不再需要时及时释放内存。即使在使用垃圾回收机制的语言Java、Python也要尽量避免保留对不必要对象的引用以便垃圾回收器可以及时清理它们。
* 使用智能指针或自动内存管理在手动管理内存的编程语言中使用智能指针C++中的`std::unique_ptr`或`std::shared_ptr`)来自动管理内存,减少内存泄漏的风险。
* ③ **优化算法**
* 选择更高效的算法对于需要大量计算或数据处理的任务选择内存占用更少的算法。例如尽量使用原地in-place算法它们不需要额外的内存空间。
* 减少冗余数据:避免在内存中存储冗余数据,尽可能在计算过程中利用已有的数据结构,避免重复分配相同的数据。
* ④ **监控和调试**
* 使用内存分析工具在开发过程中使用内存分析工具Valgrind、VisualVM、Py-Spy等来监控程序的内存使用情况查找和修复内存泄漏或不必要的内存分配。
* 设置内存使用限制:在某些环境中,可以设置程序的最大内存使用量,这样当程序达到内存限制时,可以捕捉并处理内存溢出的情况。
* ⑤ **避免无限循环和递归**
- 设置循环或递归的终止条件:确保所有循环和递归都有明确的终止条件,避免因逻辑错误导致无限执行,从而耗尽内存。
- 使用尾递归优化:在支持尾递归优化的语言中,尽量使用尾递归,以减少递归调用带来的内存消耗。
* ⑥ **并发编程中的内存管理**
* 控制并发操作的内存分配:在并发编程中,尽量避免多个线程或进程同时大量分配内存。可以通过任务分配、锁机制等方式合理控制并发操作的内存使用。
* 避免死锁:确保在并发编程中避免死锁情况,因为死锁可能会导致内存资源无法被释放,从而引发内存溢出。
* ⑦ **使用适当的数据结构**
* 选择合适的数据结构:根据需要选择内存效率更高的数据结构。例如,使用数组而不是链表来存储连续的数据,使用哈希表来提高查找效率等。
* 避免不必要的缓存:在程序中使用缓存时,确保缓存的大小是合理的,并且有清理机制,防止缓存占用过多内存。
> [!NOTE]
>
> 避免内存溢出通常需要良好的内存管理实践,如:优化数据处理算法、合理控制资源分配、以及定期检查和释放不再使用的内存。
## 2.4 总结
* `内存溢出`则是由于内存资源耗尽,程序试图分配新内存时失败,通常会导致程序的`立即`崩溃或异常终止。
# 第三章:内存泄漏 VS 内存溢出
## 3.1 概述
* `内存泄漏`是由于未释放不再使用的内存导致内存资源逐渐减少,但不会立即导致程序崩溃,而是长时间运行后可能出现性能问题或最终崩溃。
* `内存溢出`则是由于内存资源耗尽,程序试图分配新内存时失败,通常会导致程序的立即崩溃或异常终止。
> [!NOTE]
>
> * ① `内存泄漏`和`内存溢出`都与内存管理不当有关,但它们发生的机制和直接影响是不同的。
> * ② 避免`内存泄漏`和`内存溢出`都是编写高效、可靠软件的重要方面。
## 3.2 内存泄漏和内存溢出的联系和区别
> [!IMPORTANT]
>
> `内存泄漏`和`内存溢出`之间并不是必然的因果关系,而是两者可能会相互影响。
* ① `内存泄漏`导致`内存溢出`的可能性:
* 如果一个程序长期运行并且持续发生`内存泄漏`,未被释放的内存会慢慢积累,最终占用系统的大部分内存资源。如果`内存泄漏`严重到占用了所有可用内存,那么程序就会因为无法再分配新的内存,而出现`内存溢出`Out of Memory的情况。
* 因此,`内存泄漏`可以**间接**地导致`内存溢出`,特别是在长时间运行的程序或系统中。
* ② `内存泄漏`和`内存溢出`的区别:
* `内存泄漏`是指程序持续占用内存却不释放,导致可用内存逐渐减少。这种情况可能会在`长时间`内不显现问题,特别是如果程序只泄漏了少量内存。
* `内存溢出`则是一个更`急剧`的问题,它通常在程序尝试分配超过系统可用内存的大块内存时`立刻`发生,导致程序崩溃或异常终止。
* ③ 不必然性:
* 一个程序可能会发生`内存泄漏`,但因为泄漏的内存量很小,系统资源丰富,所以在短时间内不会出现`内存溢出`。
* `内存溢出`也可以在没有`内存泄漏`的情况下发生,如:一个程序需要处理非常大的数据集,直接导致内存不足。
> [!IMPORTANT]
>
> * ① `内存泄漏`有可能会在长时间积累后导致`内存溢出`,但这并不是必然的。
> * ② `内存溢出`可以在多种情况下发生,而`内存泄漏`只是其中可能的一个诱因。
> * ③ 因此,虽然`内存泄漏`可能最终引发`内存溢出`,但两者之间并非每次都是直接关联的。
# 第四章:内存泄漏检测和性能分析(⭐)
## 4.1 内存泄漏检测
### 4.1.1 概述
* C 语言中的指针是否使用是个颇具争议的话题现代化的高级编程语言通过各种策略和机制在编译期就能解决指针危险的问题。但是遗憾的是C 语言的指针很大程度上,在运行期才会暴露问题。
* 幸运的是,我们可以使用 `Valgrind` 项目来进行`内存泄漏检测`和`性能分析`,而 `Valgrind` 只支持 Linux 。
### 4.1.2 安装
* 在 WSL2 上安装 Valgrind
```shell
dnf -y upgrade && dnf -y install valgrind # AlmaLinux
```
```shell
apt -y update && apt -y upgrade && apt -y install valgrind # Ubuntu
```
![](./assets/10.gif)
* 查看 valgrind 可执行文件的安装位置:
```shell
which valgrind
```
![](./assets/11.gif)
### 4.1.3 整合
* CLion 中将工具链设置为 WSL2
![](./assets/12.gif)
* CLion 中配置 valgrind 的路径:
![](./assets/13.png)
* 查看 WSL2 中 cmake 的版本:
```shell
cmake --version
```
![](./assets/14.png)
* 修改项目中 CMakeLists.txt 中 cmake 的版本:
```{1} txt
cmake_minimum_required(VERSION 3.26.5) # 3.26.5
# 项目名称和版本号
project(c-study VERSION 1.0 LANGUAGES C)
# 设置 C 标准
set(CMAKE_C_STANDARD 23)
set(CMAKE_C_STANDARD_REQUIRED True)
# 辅助函数,用于递归查找所有源文件
function(collect_sources result dir)
file(GLOB_RECURSE new_sources "${dir}/*.c")
set(${result} ${${result}} ${new_sources} PARENT_SCOPE)
endfunction()
# 查找顶层 include 目录(如果存在)
if (EXISTS "${CMAKE_SOURCE_DIR}/include")
include_directories(${CMAKE_SOURCE_DIR}/include)
endif ()
# 查找所有源文件
set(SOURCES)
collect_sources(SOURCES ${CMAKE_SOURCE_DIR})
# 用于存储已经处理过的可执行文件名,防止重复
set(EXECUTABLE_NAMES)
# 创建可执行文件
foreach (SOURCE ${SOURCES})
# 获取文件的相对路径
file(RELATIVE_PATH REL_PATH ${CMAKE_SOURCE_DIR} ${SOURCE})
# 将路径中的斜杠替换为下划线,生成唯一的可执行文件名
string(REPLACE "/" "_" EXECUTABLE_NAME ${REL_PATH})
string(REPLACE "\\" "_" EXECUTABLE_NAME ${EXECUTABLE_NAME})
string(REPLACE "." "_" EXECUTABLE_NAME ${EXECUTABLE_NAME})
# 处理与 CMakeLists.txt 文件同名的问题
if (${EXECUTABLE_NAME} STREQUAL "CMakeLists_txt")
set(EXECUTABLE_NAME "${EXECUTABLE_NAME}_exec")
endif ()
# 检查是否已经创建过同名的可执行文件
if (NOT EXECUTABLE_NAME IN_LIST EXECUTABLE_NAMES)
list(APPEND EXECUTABLE_NAMES ${EXECUTABLE_NAME})
# 链接 math 库
LINK_LIBRARIES(m)
# 创建可执行文件
add_executable(${EXECUTABLE_NAME} ${SOURCE})
# 查找源文件所在的目录,并添加为包含目录(头文件可能在同一目录下)
get_filename_component(DIR ${SOURCE} DIRECTORY)
target_include_directories(${EXECUTABLE_NAME} PRIVATE ${DIR})
# 检查并添加子目录中的 include 目录(如果存在)
if (EXISTS "${DIR}/include")
target_include_directories(${EXECUTABLE_NAME} PRIVATE ${DIR}/include)
endif ()
# 检查并添加 module 目录中的所有 C 文件(如果存在)
if (EXISTS "${DIR}/module")
file(GLOB_RECURSE MODULE_SOURCES "${DIR}/module/*.c")
target_sources(${EXECUTABLE_NAME} PRIVATE ${MODULE_SOURCES})
endif ()
endif ()
endforeach ()
```
* 在 CLion 中正常运行代码:
![](./assets/15.gif)
* 在 CLion 中通过 valgrind 运行代码:
![](./assets/16.gif)
## 4.2 性能分析
### 4.2.1 概述
* `perf` 是一个 Linux 下的性能分析工具,主要用于监控和分析系统性能。它可以帮助开发者和系统管理员了解系统中哪些部分在消耗资源、识别性能瓶颈以及分析程序的运行效率。
### 4.2.2 安装
#### 4.2.2.1 AlmaLinux9
* 在 WSL2 中的 AlmaLinux 安装 perf
```shell
dnf -y install perf
```
![](./assets/17.gif)
#### 4.2.2.2 Ubuntu 22.04
* 在 WSL2 中的 Ubuntu 安装 perf
```shell
apt -y update \
&& apt -y install linux-tools-common \
linux-tools-generic linux-tools-$(uname -r)
```
![](./assets/18.gif)
> [!NOTE]
>
> 之所以报错的原因,在于 WSL2 中的 Ubuntu 的内核是定制化的(微软自己维护的),并非 Ubuntu 的母公司 Canonical 发布的标准内核,所以需要我们手动编译安装。
* 查看内核版本:
```shell
uname -sr
```
![](./assets/19.gif)
* 设置环境变量,方便后续引用:
```shell
export KERNEL_VERSION=$(uname -r | cut -d'-' -f1)
```
![](./assets/20.gif)
* 安装依赖库:
```shell
apt -y update && \
apt -y install binutils-dev debuginfod default-jdk \
default-jre libaio-dev libbabeltrace-dev libcap-dev \
libdw-dev libdwarf-dev libelf-dev libiberty-dev \
liblzma-dev libnuma-dev libperl-dev libpfm4-dev \
libslang2-dev libssl-dev libtraceevent-dev libunwind-dev \
libzstd-dev libzstd1 python3-setuptools python3 \
python3-dev systemtap-sdt-dev zlib1g-dev bc dwarves \
bison flex libnewt-dev libdwarf++0 \
libelf++0 libbfb0-dev python-dev-is-python3
```
![](./assets/21.gif)
* 下载源码:
```shell
git clone \
--depth 1 \
--single-branch --branch=linux-msft-wsl-${KERNEL_VERSION} \
https://github.com/microsoft/WSL2-Linux-Kernel.git
```
![](./assets/22.gif)
* 编译内核代码:
```shell
cd WSL2-Linux-Kernel
```
```shell
make -j $(nproc) KCONFIG_CONFIG=Microsoft/config-wsl
```
![](./assets/23.gif)
* 编译 perf 工具:
```shell
cd tools/perf
```
```shell
make clean && make
```
![](./assets/24.gif)
* 复制到 PATH 变量所指向的路径中:
```shell
cp perf /usr/bin/
```
![](./assets/25.gif)
### 4.2.3 整合
* CLion 中配置 perf 的路径:
![](./assets/26.png)
* 在 CLion 中通过 perf 运行代码:
![](./assets/27.gif)