2024年10月17日 16:56

This commit is contained in:
许大仙 2024-10-17 08:56:34 +00:00
parent a7820a23ad
commit b044e72377
462 changed files with 10045 additions and 9085 deletions

View File

@ -6,15 +6,16 @@ export const sidebar: DefaultTheme.Sidebar = {
text: 'C 语言基础',
collapsed: true,
items: [
{ text: '编程基础', link: `/notes/01_c-basic/00_${commonDirectoryName}/` },
{ text: 'C 语言入门一', link: `/notes/01_c-basic/01_${commonDirectoryName}/` },
{ text: 'C 语言入门二', link: `/notes/01_c-basic/02_${commonDirectoryName}/` },
{ text: '变量和进制', link: `/notes/01_c-basic/03_${commonDirectoryName}/` },
{ text: '数据类型和运算符', link: `/notes/01_c-basic/04_${commonDirectoryName}/` },
{ text: '流程控制', link: `/notes/01_c-basic/05_${commonDirectoryName}/` },
{ text: '内存泄漏和内存溢出', link: `/notes/01_c-basic/06_${commonDirectoryName}/` },
{ text: '数组一', link: `/notes/01_c-basic/07_${commonDirectoryName}/` },
{ text: '数组二', link: `/notes/01_c-basic/08_${commonDirectoryName}/` },
{ text: '编程基础一', link: `/notes/01_c-basic/00_${commonDirectoryName}/` },
{ text: '编程基础二', link: `/notes/01_c-basic/01_${commonDirectoryName}/` },
{ text: 'C 语言入门一', link: `/notes/01_c-basic/02_${commonDirectoryName}/` },
{ text: 'C 语言入门二', link: `/notes/01_c-basic/03_${commonDirectoryName}/` },
{ text: '变量和进制', link: `/notes/01_c-basic/04_${commonDirectoryName}/` },
{ text: '数据类型和运算符', link: `/notes/01_c-basic/05_${commonDirectoryName}/` },
{ text: '流程控制', link: `/notes/01_c-basic/06_${commonDirectoryName}/` },
{ text: '内存泄漏和内存溢出', link: `/notes/01_c-basic/07_${commonDirectoryName}/` },
{ text: '数组一', link: `/notes/01_c-basic/08_${commonDirectoryName}/` },
{ text: '数组二', link: `/notes/01_c-basic/09_${commonDirectoryName}/` },
]
},
{

View File

@ -921,12 +921,12 @@ int main() { // 定义主函数
> [!NOTE]
>
> * ① C 语言的语法相对**非常宽松**和灵活,给予了程序员很大的自由度,但出错的概率也大大增加了。
> * ① C 语言的语法相对`非常宽松`和`灵活`,给予了程序员很大的自由度,但出错的概率也大大增加了。
> * ② C 语言给程序员很大的自主性和控制权限,但即便是熟练的 C 程序员也无法保证能够完美的控制程序,如:内存管理,手动的内存管理不仅增加了编程的复杂性,而且非常容易出错,非常容易造成内存泄漏和使用未初始化的内存导致未定义行为。
> * ③ C 语言追求效率所以没有专门的异常机制来指示程序出现的问题。C 程序甚至不会去检查数组下标越界的问题。
> * ...
* ② C 语言缺少一些必要的高级特性使得C程序的可维护性很差。
* ② C 语言缺少一些必要的高级特性,使得 C 程序的可维护性很差。
> [!NOTE]
>
@ -945,7 +945,7 @@ int main() { // 定义主函数
> [!NOTE]
>
> * ① 由于 C 语言提供了接近操作系统层面的编程能力,当涉及到与操作系统直接交互的特定功能,如:系统调用或使用特定平台的库函数时,就会出现不同平台间的实现差异。
> * ② 这些差异并不是由 C 语言本身的语法差异引起的,而是由不同操作系统平台的不同特点所引起的。
> * ② `这些差异并不是由 C 语言本身的语法差异引起的,而是由不同操作系统平台的不同特点所引起的`
> * ③ 这种差异性一方面影响了 C 语言的跨平台性,也增加了 C 语言的学习成本,例如:尽管 Windows 和 Linux 平台上的 C 语言核心编程逻辑相似,但在实现平台特定功能时所需的 API 调用和编程细节可能大不相同。
> * ④ 需要注意的是,尽管存在平台间的差异,但这并不意味着 Windows 和 Linux 下的 C 语言是两种截然不同的语言。适应不同的平台环境和编程接口对程序员而言,不算什么特别困难的事情。

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 785 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 401 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 122 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 803 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 786 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -1,843 +1,300 @@
# 第一章:开发环境的安装和配置(⭐)
## 1.1 什么是编译器?
* 要开发 C/C++ 程序,需要安装 C/C++ 编译器,目前有两种主流实现,即:
* GCCGNU Compiler Collection全平台实现即支持 Windows、MacOS、Linux 等。
* MSVCMicrosoft Visual C++):只支持 Windows 系统。
* GCC 在 Windows 上的版本有很多,如:[MinGW-w64](https://www.mingw-w64.org/)、[Gygwin](https://cygwin.com/)、[MSYS2](https://www.msys2.org/)。它们之间的区别,如下所示:
| 特性 | MinGW-w64 | Cygwin | MSYS2 |
| ------------ | ----------------------------- | ------------------------------------- | ----------------------------------------------------- |
| **简介** | Minimalist GNU for Windows | POSIX 兼容环境和工具集 | 结合了 MinGW 和 Cygwin 的工具集 |
| **编译器** | 提供 GCC 编译器 | 提供 POSIX 兼容环境,包含大量工具 | 提供 MinGW-w64 工具链和 Cygwin 环境 |
| **生成文件** | Windows 原生可执行文件 | POSIX 兼容的可执行文件 | 可以生成 Windows 原生可执行文件或 POSIX 文件 |
| **依赖** | 无需额外依赖 | 依赖 Cygwin DLL | 根据使用工具链决定MinGW-w64 无依赖Cygwin 有依赖) |
| **工具和库** | 基本的编译工具 | 丰富的 Unix 工具和库 | 丰富的工具和库,强大的包管理系统 |
| **性能** | 性能较好 | 可能较低,因为通过兼容层调用系统 | 取决于使用的工具链MinGW-w64 性能较好) |
| **复杂度** | 简单易用 | 设置和使用相对复杂 | 较为灵活,复杂度介于 MinGW-w64 和 Cygwin 之间 |
| **适用场景** | 开发 Windows 原生应用 | 运行和开发 Unix 程序 | 混合使用 Unix 工具和开发 Windows 原生应用 |
| **优点** | 轻量级,直接生成 Windows 应用 | 完整的 POSIX 兼容环境,丰富的工具 | 灵活的环境,强大的包管理系统 |
| **缺点** | 工具和库较少 | 生成文件依赖 Cygwin DLL性能可能较低 | 环境较大,占用更多空间,复杂性比 MinGW-w64 高 |
> [!IMPORTANT]
>
> * ① C 语言是一门`面向过程`的`编译型`的`编程语言`,其最大特点在于 `运行速度极快`,仅次于`汇编语言`,这使得 C 语言在需要高性能的场景中得到广泛应用,如:操作系统、硬件驱动程序和嵌入式系统。然而,`开发效率较低`也是 C 语言的一大特点,程序员需要手动管理内存和处理低级别的操作,这对编程能力要求较高。
> * ② C 语言不仅仅是一门`编程`语言,更是计算机行业的`核心`语言。它是计算机专业的基础课程,无论是操作系统的构建、硬件驱动的开发,还是数据库系统的实现,都离不开 C 语言的支持。`学习 C 语言` 不仅是掌握编程技能的必经之路,也是深入理解计算机底层原理的关键。如果忽视了 C 语言的学习,将难以深入理解计算机系统的工作原理,也很难在计算机行业中取得长远的发展。
> [!NOTE]
>
> * ① MinGW-w64 、Cygwin 以及 MSYS2 任选其一安装即可。
> * ② 目前的 Win10 和 Win11 版本支持 WSL2 Windows Sub Linux 2 ,即 Windows 的子系统 Linux可以实现在 Windows 系统上安装一个 Linux ,然后再运行 Linux 中的 GCC 工具链。
> * ③ 本人的操作系统是 Win11 ,安装和配置都将以该系统为基础作为演示,后续不再赘述!!!
> 总结就是一句话C 语言重要(赞),很重要(大赞),非常重要(非常赞),绝对重要(无敌赞),史无前例的重要(一赞到底)!!!
## 1.2 编译器的安装和配置
### 1.2.2 MinGW-w64 的安装和配置
#### 1.2.2.1 安装
# 第一章:计算机组成原理
* 下载到本地:略。
## 1.1 计算机系统
> [!NOTE]
>
> 下载地址在[这里](https://github.com/niXman/mingw-builds-binaries/releases/download/13.2.0-rt_v11-rev1/x86_64-13.2.0-release-posix-seh-ucrt-rt_v11-rev1.7z)。
* 计算机Computer俗称`"电脑"`,是一种能够接收和存储信息,并按照存储在其内部的程序对海量的数据进行自动、高速的处理,然后将处理结果输出的现代化智能电子设备。
* 计算机有很多形式,如:台式电脑、笔记本电脑、智能手机、平板电脑等,还有生产环境中提供重要业务支撑的各种服务器。
![](./assets/1.png)
![](./assets/1.jpg)
* 解压到指定的目录,即:
> [!NOTE]
>
> 本人的解压目录是:`D:\develop\mingw64` 。
* 一个完整的`计算机系统`由`硬件Hardware系统`和`软件Software系统`两大部分组成,即:
![](./assets/2.png)
#### 1.2.2.2 配置 path 环境变量
## 1.2 冯·诺依曼体系结构
* 配置环境变量,以便任意目录都可以执行 gcc 命令,即:
* `冯·诺依曼`是一位多才多艺的科学家,他在数学、物理学、计算机科学、经济学等领域都有杰出的贡献。
![](./assets/3.jpg)
* `冯·诺依曼`的主要成就:
- 在计算机科学领域的最著名贡献是提出了`冯·诺依曼`体系结构1946 年),这是`现代计算机设计的基础`。
- 促进了计算机的可编程性和通用性,使得计算机能够执行各种复杂的任务。
- 对核武器设计、自动化控制系统、人工智能等领域的发展产生了重要影响。
- ……
> [!IMPORTANT]
>
> `冯·诺依曼体系结构`是现代计算机(量子计算机除外)设计的`基础`。
* `冯·诺依曼`体系结构的理论要点如下:
- ① **存储程序**`程序指令`和`数据`都存储在计算机的内存中,这使得程序可以在运行时修改。
- ② **二进制逻辑**:所有`数据`和`指令`都以`二进制`形式表示。
- ③ **顺序执行**:指令按照它们在内存中的顺序执行,但可以有条件地改变执行顺序。
- ④ **五大部件**:计算机由`运算器`、`控制器`、`存储器`、`输入设备`和`输出设备`组成。
- ⑤ **指令结构**:指令由操作码和地址码组成,操作码指示要执行的操作,地址码指示操作数的位置。
- ⑥ **中心化控制**计算机的控制单元CPU负责解释和执行指令控制数据流。
![](./assets/3.png)
![](./assets/4.png)
![](./assets/5.png)
![](./assets/6.png)
> [!NOTE]
>
> 因为,本人安装 MinGW-w64 的目录是 `D:\develop\mingw64`,所以本人需要配置的 path 环境变量就是`D:\develop\mingw64\bin`。
> 上述的组件协同工作,构成了一个完整的计算机系统:
>
> * `运算器`和`控制器`通常被集成在一起组成中央处理器CPU负责数据处理和指令执行。
> * `存储器`(内存)保存数据和程序,是计算机运作的基础。
> * `输入设备`和`输出设备`负责与外界的交互,确保用户能够输入信息并接收计算机的处理结果。
![](./assets/7.png)
## 1.3 各种硬件处理速度和性能优化
* 计算机的性能短板:如果 CPU 有每秒处理 1000 个服务请求的能力,各种总线的负载能力能达到 500 个, 但网卡只能接受 200个请求而硬盘只能负担 150 个的话,那这台服务器得处理能力只能是 150 个请求/秒,有 85% 的处理器计算能力浪费了,在计算机系统当中,`硬盘`的读写速率已经成为影响系统性能进一步提高的瓶颈。
![img](./assets/5.jpg)
* 计算机的各个设备部件的延迟从高到低的排列依次是机械硬盘HDD、固态硬盘SSD、内存、CPU 。
![img](./assets/6.png)
* 从上图中我们可以知道CPU 是最快的,一个时钟周期是 0.3 ns ,内存访问需要 120 ns ,固态硬盘访问需要 50-150 us传统的硬盘访问需要 1-10 ms而网络访问是最慢需要 40 ms 以上。
* 时间的单位换算如下:
```txt
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 年以上。
```
* 所以,对于 CPU 来说,这个世界真的是太慢了!!!
* 其实,中国古代中的文人,通常以`蜉蝣`来表示时间的短暂(和其他生物的寿命比),也是类似的道理,即:
```txt
鹤寿千岁,以极其游,蜉蝣朝生而暮死,尽其乐,盖其旦暮为期,远不过三日尔。
--- 出自 西汉淮南王刘安《淮南子》
```
```txt
寄蜉蝣于天地,渺沧海之一粟。 哀吾生之须臾,羡长江之无穷。
挟飞仙以遨游,抱明月而长终。 知不可乎骤得,托遗响于悲风。
--- 出自 苏轼《赤壁赋》
```
>[!NOTE]
>
>对于`蜉蝣`来说,从早到晚就是一生;而对于我们`人类`而言,却仅仅只是一天。
* 存储器的层次结构CPU 中也有存储器,即:寄存器、高速缓存 L1、L2 和 L3如下所示
![img](./assets/7.png)
>[!NOTE]
>
>上图以层次化的方式,展示了价格信息,揭示了一个真理,即:鱼和熊掌不可兼得。
>
>- ① 存储器越往上速度越快,但是价格越来越贵, 越往下速度越慢,但是价格越来越便宜。
>- ② 正是由于计算机各个部件的速度不同,容量不同,价格不同,导致了计算机系统/编程中的各种问题以及相应的解决方案。
>
> [!IMPORTANT]
>
> * ① CPU 都是直接和内存打交道的CPU 会直接从内存中读取数据,待数据处理完毕之后,会将结果再次写入到内存中。
> * ② 如果需要将数据持久化(永久)保存(内存是易失性存储器,内存中的数据是以电荷形式存储在存储单元中的。
> * ③ 当计算机关闭或断电时,这些电荷很快消散,导致存储在内存中的数据丢失),那么就需要将内存中的数据刷新到磁盘或硬盘上,即:落盘。
## 1.4 计算机软件
### 1.4.1 操作系统的来源
* 在上古时期,硬件资源不够丰富,计算机设计的也非常简陋。那个时候,很多应用程序都是直接跑在硬件上的,即:一个计算机只能跑一个应用程序。
![](./assets/8.png)
* 测试是否安装成功:
* 随着技术的发展,硬件越来越丰富,功能也越来越强大,性能也越来越好。这种情况下,如果一台计算机只能跑一个程序,实在是太浪费了。而且,底层硬件不断丰富,应用程序需要对接的硬件也将越来越多,如果每个应用程序都这么干,不显示工作很重复吗?于是,操作系统应运而生了。
```shell
gcc --version
```
![](./assets/9.png)
![](./assets/9.gif)
- 操作系统的功能:
- 硬件驱动。
- 进程管理。
- 内存管理。
- 网络管理。
- 安全管理。
- 文件管理。
### 1.2.3 Cygwin 的安装和配置
#### 1.2.3.1 安装
* 下载到本地:略。
> [!NOTE]
>
> 下载地址在[这里](https://www.cygwin.com/install.html)。
* 那么,操作系统的作用,就是这样的,即:
* 对下,管理计算机的硬件资源。
* 对上,提供使用计算机资源的操作方式,有:
* `系统调用`:是一套已经写好的代码接口,应用程序通过调用这些接口来请求操作系统执行特定的硬件操作。它们直接与硬件交互,提供底层功能支持,如:文件操作、进程管理、内存管理等。`开发者`通过系统调用可以实现对底层资源的直接控制,确保程序能够高效、安全地运行。
* `终端命令`:是一种文本命令接口,通过命令行输入各种指令来控制操作系统和软件的行为。终端命令可以执行文件操作、系统配置、网络管理等各种任务。主要针对`开发人员`和`高级用户`,他们通过命令行可以快速、精确地完成各种操作,提高工作效率。
* `图形用户界面`GUI是通过图形元素窗口、图标、按钮等与用户进行交互的界面。供直观、易用的操作方式使用户能够通过鼠标点击、拖拽等简单操作完成复杂任务。主要面向`普通用户`,降低了计算机操作的门槛,提高了用户体验和工作效率。
![](./assets/10.png)
* 点击安装:
### 1.4.2 用户态和内核态
* 在现代操作系统中,`用户态User Mode`和`内核态Kernel Mode`是两种不同的执行模式,它们对系统资源的访问权限有着本质的区别。这种区分是为了提供一个稳定和安全的运行环境,防止用户程序直接操作硬件设备和关键的系统资源,从而可能引起系统的不稳定或安全问题。
![](./assets/11.png)
![](./assets/12.png)
- 内核态Kernel Mode VS 用户态User Mode
| 类型 | 内核态Kernel Mode | 用户态User Mode |
| :----- | :----------------------------------------------------------- | :----------------------------------------------------------- |
| 权限 | 内核态是操作系统代码运行的模式,拥有访问系统全部资源和执行硬件操作的`最高权限`。在这种模式下,操作系统的核心部分可以直接访问内存、硬件设备控制、管理文件系统和网络通信等。 | 用户态是普通应用程序运行的模式,具有`较低`的系统资源访问权限。在用户态,程序不能直接执行硬件操作,必须通过操作系统提供的接口(即系统调用)来请求服务。 |
| 安全性 | 由于内核态具有如此高的权限,因此只有可信的、经过严格审查的操作系统核心组件才被允许在此模式下运行。这样可以保护系统不被恶意软件破坏。 | 用户态为系统提供了一层保护,确保用户程序不能直接访问关键的系统资源,防止系统崩溃和数据泄露。 |
| 功能 | 内核态提供了`系统调用`的接口,允许用户态程序安全地请求使用操作系统提供的服务,比如:文件操作、网络通信、内存管理等。 | 用户态保证了操作系统的稳定性和安全性,同时也使得多个程序可以在相互隔离的环境中同时运行,避免相互干扰。 |
> [!NOTE]
>
> - ① 操作系统通过用户态和内核态的分离,实现了对系统资源的保护和控制。
> - ② 当用户程序需要进行文件读写、网络通信或其他需要操作系统介入的操作时会发生从用户态到内核态的切换。这通过系统调用System Call实现系统调用是用户程序与操作系统内核通信的桥梁。
> - ③ 执行完毕后,系统从内核态返回用户态,继续执行用户程序。
> - ④ 用户态和内核态的这种分离设计是现代操作系统中实现安全、稳定运行的关键机制之一。
* 示例:
```java {25}
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
public class Demo {
public static void writeFile(String filePath, String content) {
Path path = Paths.get(filePath);
try {
Files.write(path, content.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args){
// 用户态
int a = 10;
int b = 20;
int c = a + b;
string filePath = "c:/demo.txt";
string txt = a + b + c;
// 从用户态切换到内核态完成文件写入
writeFile(filePath, a);
// 从内核态切换回用户态
System.out.println(a);
System.out.println(b);
System.out.println(c);
}
}
```
### 1.4.3 ISA、ABI 和 API
- ISA 、ABI 和 API 的参考模型如下:
![](./assets/12.jpg)
* 在底层,硬件模型以指令集架构 ISA 表示该架构定义了处理器、寄存器、存储器和中断管理的指令集。ISA 是硬件和软件之间的接口,对于操作系统 OS 开发人员 System ISA 和直接管理底层硬件的应用程序 User ISA 的开发人员来说非常重要。
> [!NOTE]
>
> - ① ISA 是计算机体系结构中定义的一组指令它规定了处理器能够执行的操作。ISA 包括指令的编码、寄存器的使用、内存访问模式等。不同的处理器可能有不同的 ISA例如x86、ARM、MIPS 等。
> - ② 在设计一个新的操作系统时,开发者需要确保操作系统能够支持特定的 ISA ,以便在特定的硬件上运行。例如:如果操作系统旨在运行在 ARM 架构的处理器上,那么它必须能够理解和执行 ARM ISA 定义的指令集。
- 应用程序二进制接口 ABI 将`操作系统层`与由操作系统管理的`应用程序`和`库`分开。ABI 涵盖了低级数据类型、对齐方式和调用约定等详细信息,并定义了可执行程序的格式。系统调用在此级别定义。此接口允许应用程序和库在实现相同 ABI 的操作系统之间移植。
> [!NOTE]
>
> - ① ABI 是指在二进制级别上应用程序与操作系统、库或应用程序的不同部分之间的接口。它定义了数据类型的大小、布局、对齐方式以及函数调用的约定如参数如何传递、返回值如何处理等。ABI 确保了编译后的二进制文件能够在特定的操作系统和硬件平台上正确地运行。
> - ② 在 windows 上的应用程序的运行格式是:`PE`portable executable格式、`.dll` dynamic link library格式和 `.lib` 格式;而在 Linux 上的应用程序的运行格式是:`ELF`executable and linking format格式、`.so` shared object格式和 `.a` 格式。
> - ③ 在 Linux 中可以通过 `file /bin/ls` 命令查看指定可执行应用程序的 ABI 格式;从而也可以论证,在 Windows 上可以运行的程序,在 Linux 上运行不了。
> - ④ 当开发者在 Linux 系统上编写 C 语言程序并使用特定的编译器GCC编译时编译器会遵循 Linux 平台的 ABI 规范来生成二进制文件。这样,生成的可执行文件就可以在任何遵循相同 ABI 规范的 Linux 系统上运行。
> - ⑤ 如果一个应用程序需要跨平台(操作系统)运行,就需要使用`一套代码,多平台编译`的方式(针对 C 或 C++ 等相同的源代码在不同平台操作系统上使用特定平台的编译器GCC来分别编译成符合自己平台的 ABI 规范的二进制文件。
- 最高级别的抽象由应用程序编程接口 API 表示,它将`应用程序`连接到`库`或`底层操作系统`。
> [!NOTE]
>
> - ① API 是一组预定义的函数、协议和工具用于构建软件和应用程序。API 允许不同的软件系统相互交互它定义了软件组件之间如何相互通信。API 可以是库、框架、协议或服务。
> - ② 在 Web 开发中,开发者可能会使用 JavaScript 的 Fetch API 来与服务器进行通信,获取数据或提交表单。这个 API 提供了一种标准化的方式来发送 HTTP 请求和处理响应,而不需要开发者关心底层的网络协议细节。
### 1.4.4 系统调用System Call和函数库Library Call
* 在现代操作系统中,应用程序都不能直接作用于硬件,而是运行在操作系统之上。
![](./assets/13.png)
- 并且,在上文的图示中,我们也会看到`系统调用System Call`和`函数库Library Call`的身影,如下:
![](./assets/14.png)
![](./assets/15.png)
![](./assets/16.png)
![](./assets/17.png)
* 选择需要安装的软件包:
* 其实,`系统调用System Call`和`函数库Library Call`的区别如下:
| 类型 | 系统调用System Call | 函数库Library Call |
| :------- | :----------------------------------------------------------- | :----------------------------------------------------------- |
| 定义 | 系统调用是操作系统提供给程序员的一组接口,这些接口允许用户空间的程序请求操作系统内核提供的服务,比如文件操作、进程控制、通信和内存管理等。 | 函数库调用是指使用高级语言编写的一组预先编译好的函数,这些函数实现了一些常用的功能,比如:字符串处理、数学计算等。程序员可以在自己的程序中直接调用这些函数,而无需重新实现它们。 |
| 权限 | 执行系统调用时,会从用户态切换到内核态。这是因为系统调用涉及到访问受保护的系统资源,这些操作必须由操作系统控制以确保系统的稳定性和安全性。 | 函数库调用通常在用户态执行,不涉及到用户态与内核态之间的切换。它们直接使用操作系统通过系统调用提供的服务,或者完全在用户空间内完成计算,不需要操作系统介入。 |
| 性能开销 | 由于涉及到用户态与内核态之间的切换,系统调用的执行成本相对较高。因此,频繁的系统调用可能会影响程序的性能。 | 相对于系统调用,函数库调用的性能开销较小。因为它们通常不涉及到模式切换,且执行的操作多在用户空间完成。 |
| 示例 | open()read()write()fork()exec() 等 UNIX/Linux 系统调用。 | C 标准库中的 printf() 等函数;数学库中的 sin()cos() 等函数。 |
> [!NOTE]
>
> 默认是最小化安装,没有 GCC需要选择 gcc-core、gcc-g++、make、gdb、binutils 。
> - ① **执行层级**:系统调用直接与操作系统内核交互,执行更底层的操作;而函数库调用运行在用户空间,通常使用系统调用来实现其功能。
> - ② **性能开销**:系统调用由于涉及到用户态与内核态的切换,性能开销相对较大;函数库调用则因为主要在用户态执行,性能开销较小。
> - ③ **使用目的**:系统调用提供了访问操作系统资源和服务的能力;函数库调用则提供了方便、高效执行常见任务的手段。
![](./assets/18.png)
* 安装 gcc-core
![](./assets/19.png)
# 第二章:初识计算机语言
![](./assets/20.png)
## 2.1 计算机语言是什么?
* 安装 gcc-g++
![](./assets/21.png)
* 安装 make
![](./assets/22.png)
* 安装 gdb
![](./assets/23.png)
* 安装 binutils
![](./assets/24.png)
![](./assets/25.png)
![](./assets/26.png)
![](./assets/27.png)
#### 1.2.3.2 配置 path 环境变量
* 和 `1.2.2.2 配置 path 环境变量` 步骤相同:略。
### 1.2.4 MSYS2推荐
#### 1.2.4.1 安装
* 下载到本地:略。
* `人类语言`是人和人之间用于沟通的一种方式,例如:中国人和中国人之间使用普通话沟通,而中国人和美国人交流,则可以使用英语。
> [!NOTE]
>
> 下载地址在[这里](https://www.msys2.org/)。
> * ① 中文有自己的`固定格式`和`固定词汇`(即:`语法规则`),英文也是自己的`固定格式`和`固定词汇`(即:`语法规则`);同样的道理,法语、韩国等各种`人类语言`都有自己的`固定格式`和`固定词汇`(即:`语法规则`)。
> * ② 在和别的国家的人进行交流的时候,我们必须正确的表达,对方才会理解我们;否则,如果不熟悉对方国家的语言的语法规则,乱用语法规则,可能会贻笑大方,如:中文中的`望其项背`原指看见对方的背影,形容差距不大,能赶上;但是,很多人却认为这是形容遥不可及或难以企及的目标。
> * ③ 就算和本国家的人进行交流的时候,我们也必须正确的表达,对方才会理解我们;否则,如果乱用语法规则,可能也会让对方感觉奇怪,听不懂我们的意思,如:`借我 5000 RMB 买 iphone` 或者 `5000 RMB 我买 iphone 借`
![](./assets/28.png)
* 点击安装:
![](./assets/29.png)
![](./assets/30.png)
![](./assets/31.png)
* `计算机编程语言`是人和计算机交流的方式。人们可以使用`编程语言`对计算机下达`命令(指令)`,让计算机完成人们需要的功能。
> [!NOTE]
>
> 可能很多人,会遇到安装到 `50%` 就一直卡死在那边,不用慌,关闭它,再次重新安装即可。
> * ① 计算机语言也有自己`固定格式`和`固定词汇`(即:`语法规则`),我们必须学习其语法规则,才能控制计算机,让计算机完成我们所需要的功能。
> * ② 计算机语言有很多种C、C++、Java、Go、JavaScript、Python、Scala 等。
![](./assets/32.png)
## 2.2 为什么要学习计算机语言?
* 点击运行 MSYS2
![](./assets/33.png)
* 出现命令终端:
> [!NOTE]
>
> 如果没有出现命令终端,也不要慌,去 Win11 操作系统的`开始`菜单,那边找一下,就能找到。
![](./assets/34.png)
* 替换清华镜像源(可选):
```shell
sed -i \
"s#https\?://mirror.msys2.org/#https://mirrors.tuna.tsinghua.edu.cn/msys2/#g" \
/etc/pacman.d/mirrorlist*
```
![](./assets/35.gif)
* 安装 gcc 等相关开发包:
```shell
pacman -Syu --noconfirm # 更新包管理器
```
```shell
pacman -Sy base-devel --noconfirm # 安装开发工具包
```
> [!NOTE]
>
> 也许,你会看到其他人的安装命令是 `pacman -Sy mingw-w64-x86_64-toolchain --noconfirm`,此处解释下两者的区别:
>
> * ① `mingw-w64-x86_64-toolchain` 使用更传统的 MSVCRT适合需要经典 MinGW 环境的项目或依赖较老 C 运行时的应用。
> * ② `mingw-w64-ucrt-x86_64-toolchain` 使用 Microsoft 的 UCRT更适合现代 Windows 开发,提供更好的兼容性和性能。
```shell
pacman -Sy mingw-w64-ucrt-x86_64-toolchain --noconfirm # 安装开发 gcc 相关工具链
```
![](./assets/36.gif)
#### 1.2.4.2 配置 path 环境变量
* 和 `1.2.2.2 配置 path 环境变量` 步骤相同:略。
> [!NOTE]
>
> 本人需要配置的 path 环境变量是 `C:\msys64\ucrt64\bin`
## 1.3 什么是 IDE集成开发环境
* 在实际开发中,除了`编译器`是必须安装的工具之外,我们往往还需要很多其他的辅助软件,如下所示:
* 编辑器:用来编写代码,并且给代码着色,以方便阅读。
* 代码提示器:输入部分代码,即可提示全部代码,加速代码的编写过程。
* 调试器:观察程序的每一个运行步骤,发现程序的逻辑错误。
* 项目管理工具:对程序涉及到的所有资源进行管理,包括:源文件、图片、视频、第三方库等。
* 漂亮的界面:各种按钮、面板、菜单、窗口等控件整齐排布,操作更方便。
* 这些工具通常被打包在一起统一安装和发布Visual Studio、CLion 以及 VS Code 通常统称为集成开发环境IDEIntegrated Development Environment
> [!NOTE]
>
> * ① IDE集成开发环境就是一系列开发工具的组合套装。这就好比台式机核心部件是主机。主机就相当于 IDE 的代码编辑器和编译器有了它们开发者就可以进行基本的编程工作。然而正如我们在购买台式机时通常还会附带显示器、键盘、鼠标、U盘、摄像头等外围设备IDE 也同样提供了一系列额外的工具和插件,比如:调试器、版本控制集成、代码补全、代码重构工具等。这些“外围设备”让开发过程更加高效、直观,并且能够满足更多的开发需求,使得 IDE 成为一个完整的开发环境。
> * ② 严格来讲, VS Code 属于编辑器,而不是 IDE但是可以通过安装各种插件来完成 IDE 的功能;而 Visual Studio 和 CLion 属于 IDE。
> * ③ 在实际开发中,使用 IDE 进行编程并不是一种非常丢人的事情。而使用编辑器,如:`记事本`、`vi/vim` 等,进行编程,也并不是一件非常骄傲的事情。可能有些人会在网上发布这样的言论:“学习编程,刚开始需要使用`记事本`或 `vi/vim`等简单的编辑器软件,不要使用 IDE ”,目的可能是为了让初学者熟悉编程的基础概念和语法,并避免依赖 IDE 的辅助功能。但是,这种方法或许可以起到锻炼基础技能的功能,但这并不意味着 IDE 就不适合初学者。事实上,许多 IDE 还提供了初学者友好的界面和工具,可以帮助新手更快地入门和理解编程。
## 1.4 IDE 的安装和配置
### 1.4.1 CLion
#### 1.4.1.1 概述
* [CLion](https://www.jetbrains.com/clion/) 是一款由 JetBrains 推出的跨平台 C/C++ 集成开发环境IDE它具有智能编辑器、CMake 构建支持、调试器、单元测试、代码分析等功能,可以极大提高 C/C++ 开发效率。
> [!NOTE]
>
> * ① 本次,演示的 CLion 的安装版本是 `2024.2.1` ,后续版本可能会更新,但是操作几乎不会发生太多变化!!!
> * ② CLion 作为一个 IDE本身就携带了各个平台操作系统的 C 语言编译器Windows 中就是 MinGW但是CLion 中自带的 C 语言编译器版本可能并非我们实际开发所想要的(版本不是很高),这也是在 Windows 中,为什么推荐使用 `MSYS2` 的原因所在。
#### 1.4.1.2 安装
* 鼠标双击,进入安装:
![](./assets/37.png)
* 下一步:
![](./assets/38.png)
* 下一步:
![](./assets/39.png)
* 下一步:
![](./assets/40.png)
* 安装:
![](./assets/41.png)
* 安装完成:
> [!NOTE]
>
> 通常安装完成之后,桌面上会出现 CLion 的快捷方式,可以点击此快捷方式,以便快速启动 CLion 。
![](./assets/42.png)
#### 1.4.1.3 配置
* 打开 CLion
![](./assets/43.png)
* 切换中文界面(可选):
> [!NOTE]
>
> 对于以中文、韩语和日语为母语的开发者,`CLion` 在 `2024.2` 版本之后就绑定了本地化插件,即不需要再安装本地化插件了。
![](./assets/44.gif)
* 配置 UI
![](./assets/45.png)
![](./assets/46.png)
* 配置自定义字体(可选):
![](./assets/47.png)
* 配置 `系统设置`相关功能:
![](./assets/48.png)
![](./assets/49.png)
* 配置`文件编码` 为 UTF-8
![](./assets/50.png)
* 配置`控制台编码`为 UTF-8
![](./assets/51.png)
* 配置`显示方法分隔符`功能:
![](./assets/52.png)
* 配置`编辑器`的字体(可选):
> [!NOTE]
>
> 本人是安装了 `Fira Code` 字体,如果你也需要安装此字体,可以去 [GitHub](https://github.com/) 搜索并下载。
![](./assets/53.png)
* 检测 GCC 工具链是否安装成功:
![](./assets/54.png)
### 1.4.2 VS Code
#### 1.4.2.1 概述
* [Visual Studio Code (VS Code)](https://code.visualstudio.com/) 是一个免费的开源代码编辑器,适用于 Windows、MacOS 和 Linux 平台。它支持语法高亮、智能代码补全IntelliSense、内置调试工具和Git集成。用户可以通过扩展来添加更多功能支持新的编程语言、主题和调试工具。VS Code 还支持在微软 Azure 上进行部署和托管,适用于各种编程语言和框架。
> [!NOTE]
>
> * ① Visual Studio Code 需要安装对应的插件,才能运行 C/C++ 代码。
> * ② Visual Studio Code 除了开源免费的优点之外,还有一个优点就是插件巨多(几乎所有主流的编程语言都提供有对应的插件),这也是很多程序员喜欢使用它的原因。
#### 1.4.2.2 安装
* 鼠标双击,进入安装:
![](./assets/55.png)
* 同意协议:
![](./assets/56.png)
* 下一步:
![](./assets/57.png)
* 下一步:
![](./assets/58.png)
* 下一步:
![](./assets/59.png)
* 安装:
![](./assets/60.png)
* 安装过程:
![](./assets/61.png)
* 安装完成:
![](./assets/62.png)
#### 1.4.2.3 配置
* 安装`中文`插件:
![](./assets/63.png)
![](./assets/64.png)
* 安装 `C/C++` 插件:
![](./assets/65.png)
![](./assets/66.png)
### 1.4.3 Microsoft Visual Studio
#### 1.4.3.1 概述
* [Visual Studio](https://visualstudio.microsoft.com/)(简称 VS是由微软公司发布的集成开发环境。它包括了整个软件生命周期中所需要的大部分工具UML 工具、代码管控工具、项目版本控制 Git 等。
* Visual Studio 支持 C/C++、C#、F#、VB 等多种程序语言的开发和测试,可以用于生成 Web 应用程序,也可以生成桌面应用程序,功能十分强大,但下载和安装很可能耗时数小时,还可能会塞满磁盘。
* Visual Studio 有三种版本:社区版(免费,不支持企业使用),专业版(收费)和企业版(收费)。企业版拥有面向架构师的功能、高级调试和测试,这些功能是另外两种版本所没有的。
* Visual Studio 旨在成为世界上最好的 IDE集成开发环境号称“宇宙第一强大 IDE”。
> [!NOTE]
>
> 本人安装的 Visual Studio 的安装版本是 Visual Studio 2022 ,后续版本可能会更新,但是操作几乎不会发生太多变化!!!
#### 1.4.3.2 安装
* 鼠标双击,进入安装:
![](./assets/67.png)
* 继续:
![](./assets/68.png)
* 等待:
![](./assets/69.png)
* 工作负荷(使用 C++ 的桌面开发):
![](./assets/70.png)
* 单个组件:
![](./assets/71.png)
* 语言包:
![](./assets/72.png)
* 安装位置(修改默认的安装位置):
![](./assets/73.png)
* 如果不是第一次安装,可能会出现`共享组件、工具和 SDK`不可以修改,即:
![](./assets/74.png)
* 此时,就需要打开`注册表编辑器`,将如下图中的除了第一个选项,全部删除,然后关闭再重新安装,即:
![](./assets/75.png)
* 开始安装:
![](./assets/76.png)
* 安装中:
![](./assets/77.png)
* 安装完成,然后关闭:
![](./assets/78.png)
#### 1.4.3.3 配置
* 在开始菜单处,启动 VS
![](./assets/79.png)
* 登录或跳过该选项(有微软账号就注册,没有就暂时跳过):
![](./assets/80.png)
* 继续:
![](./assets/81.png)
* 注册 VS
![](./assets/82.png)
* 填写注册码:
> [!NOTE]
>
> * ① Pro 版本:`TD244-P4NB7-YQ6XK-Y8MMM-YWV2J`
> * ② Enterprise 版本:`VHF9H-NXBBB-638P6-6JHCY-88JWH`
![](./assets/83.png)
![](./assets/84.png)
## 1.5 什么是工程/项目
### 1.5.1 概述
* 一个真正的软件往往包含多项功能,每一项功能都需要几十行、几千行甚至几万行的代码,如果我们将这些代码都放到一个源文件中,不但打开的速度极慢,代码的编写和维护也会变得非常困难。
* 在实际开发中,随着软件规模的增加,代码的复杂性也会显著提升,为了提高代码的易读性、维护性等,程序员会将代码按照功能分别放到不同的源文件中。
> [!NOTE]
>
> 需要说明的是,一个真正的软件除了源代码之外,往往还会包括图片、视频、音频、库(框架)等其它资源,这些也是一个个的文件。
* 为了有效的管理这些种类繁杂、数目众多的文件,我们会将这些文件按照功能放到不同的目录中进行统一管理,并且这个目录下只存放与当前程序有关的资源。其实,这就是工程或项目。
> [!NOTE]
>
> 总结:
>
> * ① 随着软件规模的增加,代码的复杂性也会显著提升。将代码分割成多个模块或文件并分别管理,可以减少每个文件的复杂度,使代码更易读、易理解、易维护。工程提供了一个结构化的环境,将这些文件组织在一个系统化的目录结构中。
> * ② 除了代码,软件开发还涉及到各种资源的管理,如:图片、音频、视频、配置文件等。工程能够帮助开发者将这些资源合理地分类存放,并与代码一同管理,确保它们在开发、编译和运行时能被正确引用。
* 许多 IDE 都提供了工程或项目的概念,其目的就是为了帮助开发者合理的管理软件开发中所需要的资源,如:图片、视频、音频、库(框架)等。
![](./assets/85.png)
### 1.5.2 工程类型/项目类型
* 程序或软件是一个非常宽泛的概念,它可以细分为很多种类,如下所示:
* 控制台程序Console Application控制台程序是一种不具备图形用户界面的程序它通过文本方式与用户交互通常运行在命令行窗口黑底白字的终端Unix/Linux 中的`ls`命令、Windows 中的`cmd.exe`等。
* GUI 程序Graphical User Interface ProgramGUI 程序是一种具有图形用户界面的程序通过窗口、按钮、菜单等图形控件与用户交互微信、QQ 等。
* 静态库和动态库:不单独出现,而是作为其它程序的一个组成部分,普通用户很难接触到它们。
* 静态库指的是在编译时包含到程序中的库,程序不依赖外部文件运行,如:在 C/C++ 中,静态库通常以`.lib`Windows或`.a`Unix/Linux为扩展名。
* 动态库指的是在运行时加载的库,允许多个程序共享,并且程序在运行时依赖这些库,如: 在Windows中动态库通常以`.dll`为扩展名;在 Unix/Linux 中,以`.so`为扩展名。
* 不同类型的程序控制台程序、GUI 程序、静态库、动态库等)需要不同的配置和文件结构,因此在 IDE 中创建项目时,选择正确的工程类型非常重要。不同的工程类型决定了 IDE 为我们生成的初始文件、目录结构,以及预设的一些编译和链接参数。
> [!IMPORTANT]
>
> * ① 控制台程序适合初学者,因为它更简单,没有复杂的界面元素,开发时可以专注于逻辑和代码本身。
> * ② 而 GUI 程序则涉及到用户界面设计和事件驱动编程,更适合有一定编程基础的人进行学习和开发。
# 第二章C 语言入门HelloWorld
## 2.1 手动版
* ① 新建一个 `HelloWorld.c` 的文件:
![](./assets/86.png)
* ② 通过`记事本`等软件打开该文件,输入如下的代码,并保存:
```c
#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}
```
![](./assets/87.gif)
* ③ 通过 `gcc` 命令编译该文件:
```shell
gcc HelloWorld.c -o HelloWorld.exe
```
![](./assets/88.gif)
* ④ 执行:
```shell
./HelloWorld.exe
```
![](./assets/89.gif)
## 2.2 VS Code 版
* ① 新建一个`空`文件夹(目录),用于存放代码:
![](./assets/90.png)
* ② 通过 `vscode` 打开该目录:
![](./assets/91.gif)
* ③ 在 `vscode` 中新建 `HelloWorld.c` 文件:
![](./assets/92.gif)
* ④ 设置 VSCode 中 C/C++ 的代码格式为行尾风格(可选):
![](./assets/93.png)
![](./assets/94.png)
* ⑤ 编写如下的代码,并保存:
```c
#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}
```
![](./assets/95.gif)
* ⑥ 通过 `gcc` 命令编译该文件:
```shell
gcc HelloWorld.c -o HelloWorld.exe
```
![](./assets/96.gif)
* ⑦ 执行:
```shell
./HelloWorld.exe
```
![](./assets/97.gif)
* ⑧ 安装 Code Runner 插件(步骤略),实现右键直接编译执行(可选):
![](./assets/98.gif)
## 2.3 VS 版
* ① 新建空项目:
![](./assets/99.png)
![](./assets/100.png)
![](./assets/101.png)
![](./assets/102.png)
* ② 打开`解决方案资源管理器`
![](./assets/103.png)
![](./assets/104.png)
* ③ 新建 `HelloWorld.c` 文件:
![](./assets/105.gif)
* ④ 编写如下代码,并保存:
```c
#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}
```
![](./assets/106.gif)
* ⑤ 编译和执行:
![](./assets/107.gif)
## 2.4 CLion 版
* ① 新建空项目:
![](./assets/108.png)
![](./assets/109.png)
![](./assets/110.png)
* ② 编写如下代码,并保存:
```c
#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}
```
![](./assets/111.gif)
* ③ 编译和运行:
![](./assets/112.gif)
* ④ 默认情况下,一个项目只能有一个 c 源文件包含 main 函数,但是 CLion 可以有多个,如下:
![](./assets/113.gif)
* ⑤ 如果之后,有中文乱码问题,那么请做如下步骤:
![](./assets/114.png)
![](./assets/115.gif)
> [!NOTE]
>
> 内容如下所示:
>
> ```txt
> -Dfile.encoding=UTF-8
> -Dconsole.encoding=UTF-8
> ```
# 第三章:五花八门的 C 语言编译器(⭐)
## 3.1 概述
* 由于 C 语言的历史比较久,而且早期没有规范,整个计算机产业也都处于拓荒的年代,所以就涌现了很多款 C 语言编译器,它们各有特点,适用于不同的平台。
## 3.2 桌面操作系统
* 目前而言,主流的桌面操作系统就是 Windows、Linux 和 MacOS 。
* 对于 Windows 而言,使用的最多的 C/C++ 编译器是 `MSVC` Microsoft Visual C++),被集成在 Visual Studio 开发环境中,其特点如下:
* ① 兼容性: 与 Windows 操作系统和 Windows API 深度集成,生成的二进制文件为 PE 格式。
* ② 调试工具: 提供强大的调试工具Visual Studio Debugger。
* ③ 优化: 支持各种编译器优化,特别是针对 Windows 平台的优化。
* ④ 库支持: 提供丰富的 Windows 专用库,如:~~MFCMicrosoft Foundation Class Library~~。
> [!NOTE]
>
> MSVC 不开源,我们可以使用 Visual Studio Community 社区版,但是如果想使用 Visual Studio Community 社区版生成出来的应用进行商用,就需要好好阅读微软的许可证和说明书了。
* 对于 Linux 而言,使用的最多的 C/C++ 编译器是 `GCC`(支持多种架构和语言),并且很多 Linux 发行版本都将 GCC 作为默认的编译器,其特点如下所示:
* ① 广泛支持: 支持各种 Linux 发行版,是大多数开源项目的默认编译器。
* ② 强大的优化: 提供各种编译优化选项,适合多种性能需求的开发。
* ③ 丰富的工具链: 和 GDBGNU 调试器、Make、Autoconf 等工具无缝集成。
> [!NOTE]
>
> 目前而言GCC 已经属于跨平台的项目了,支持 Windows、Linux 和 MacOS ,在 Windows 上 GCC 的移植项目MinGW、Cygwin 以及 MSYS2其差别如下所示
>
> * ① MinGW 提供了 GCC 编译器的 Windows 版本,可以生成 Windows 兼容的本地代码。
> * ② Cygwin 是一个在 Windows 上运行的类 Unix 环境,它提供了一套完整的 POSIX 兼容工具,包括 GCC 编译器。
> * ③ MSYS2 是一个在 Windows 上运行的轻量级、开源的 Unix-like 环境,它为 Windows 用户提供了类似于 Linux 的开发环境。MSYS2 是 MinGW 和 Cygwin 的后继者,旨在提供更现代化和更强大的开发工具集。
* 对于 MacOS 而言,使用的最多的 C/C++ 编译器是 `Clang/LLVM`,其特点如下:
* ① Xcode 集成: 深度集成到 Xcode 中,支持 Apple 的所有平台macOS、iOS、tvOS、watchOS的开发。
* ② 优化和兼容: 生成的代码针对 Apple 的硬件进行优化,并兼容 GCC 的大部分功能。
* ③ 现代化: Clang 提供了对 C 语言标准的全面支持,并且以其快速的编译速度和易读的错误报告而著称。
> [!NOTE]
>
> 在 MacOS 中,尽管 Clang 是默认编译器;但是,也可以 Homebrew 等包管理器来安装 GCC ,以便开发 C/C++ 项目。
## 3.3 嵌入式系统
* 在嵌入式系统开发中,可用的 C 语言编译器以及工具链非常丰富, 有很多是免费或开源的,如下所示:
* `GCC (GNU Compiler Collection)`
* 简介GCC 是最广泛使用的开源编译器集合之一,支持多种处理器架构,包括 ARM、AVR、MIPS、RISC-V 等。
* 开源或免费:完全开源且免费,受到广泛的社区支持。
* `Clang/LLVM`
* 简介Clang 是基于 LLVM 架构的开源编译器,支持多种架构,并且与 GCC 兼容。
* 开源或免费:开源且免费,具有快速的编译速度和现代化的代码分析工具。
* `SDCC (Small Device C Compiler)`
* 简介: SDCC 是一个开源的跨平台 C 编译器,主要用于 8 位和 16 位微控制器8051、Z80、PIC 等。
* 开源或免费:完全开源且免费,适合教育和小型项目开发。
* `MPLAB XC Compilers`
* 简介MPLAB XC 是由 Microchip 提供的编译器系列,专门用于其 PIC 和 dsPIC 微控制器。
* 开源或免费:提供免费版本(使用标准优化级别),但也有付费版本提供更高级的优化。
* `ARM GCC`
* 简介ARM GCC 是 GCC 的一个专门版本,针对 ARM Cortex-M 系列微控制器进行了优化。
* 开源或免费:完全开源且免费,广泛用于工业、教育和开源项目中。
* `PlatformIO`
* 简介PlatformIO 是一个开源的嵌入式开发生态系统,支持多种开发板、框架和编译器。
* 开源或免费:基本功能免费,部分高级功能和插件需要订阅服务。
* `Eclipse`
* 简介Eclipse 是一个开源的集成开发环境IDE可以通过插件支持嵌入式开发。
* 开源或免费Eclipse 和 GCC 都是开源免费的,适合跨平台开发。
* `Arduino IDE`
* 简介Arduino IDE 是一个简单易用的开源开发环境,广泛用于 Arduino 开发板和其他兼容开发板。
* 开源或免费:完全开源且免费,非常适合教育和入门级开发。
* ...
* 这些编译器以及工具链各有优势,开发者应根据目标硬件平台、项目需求和开发环境选择最适合的编译器。
## 3.4 C 语言为什么有那么多的编译器?
* C 语言并没有一个官方机构,也不属于哪个公司,它只有一个制定标准的委员会,任何其他组织或者个人都可以开发 C 语言的编译器,而这个编译器要遵守哪个 C 语言标准,是 100% 遵守还是部分遵守,并没有强制性的措施,也没有任何约束。
> [!NOTE]
>
> * ① 各个厂商可以根据自己的利益和喜好来开发编译器。
> * ② 市场和用户的选择通常是推动编译器开发者遵循标准的主要动力。
* 并且,不同硬件平台之间也存在差异,这会导致内存管理方式、寄存器、指令集等都有所不同,为了确保 C 语言程序能在这些硬件平台运行,就得针对该平台开发/定制不同的编译器。
> [!NOTE]
>
> * ① 上述的情况,在单片机和嵌入式领域更加常见。
> * ② 总体而言C 语言具有开放性,并且要适应不同的硬件平台,这使得不同厂商可以根据自己的需求来进行个性化开发/定制。
* 这也导致了一个非常棘手的问题,有的编译器遵守较新的 C 语言标准,有的编译器只能遵守较老的 C 语言标准,有的编译器还进行了很多扩展,比如:
* GCC、LLVM/Clang 更新非常及时,能够支持最新的 C 语言标准。
* MSVC 更新比较缓慢迟迟不能支持新标准例如VC6.0、VS2010 都在使用 C89 标准VS2015 部分支持 C99 标准。
> [!NOTE]
>
> 微软官方给出的答复:最新的标准已经在 C++ 中支持了C 语言就没必要再重复了。
* 初学者经常会遇到这种情况,有些代码在 MSVC 下能够正常运行,拿到 GCC 下就不行了,一堆报错信息; 或者反过来,在 GCC 上能运行的代码在 MSVC 下不能运行。这是因为不同的编译器支持的标准不同,每个编译器都进行了自己的扩展,假如你使用了 MSVC 自己的扩展函数,那么拿到 GCC 下肯定是不支持的。
> [!IMPORTANT]
>
> * ① 在学习的时候,无所谓使用那个 C 语言编译器了。
> * ② 但是,如果要开发实际项目(开源或商业),最好使用 `GCC` 编译器,因为其功能最强大、开源、跨平台、免费,支持最新的 C 语言标准。
# 第四章:注释(⭐)
## 4.1 概述
* 编程语言中,`注释`是一种`特殊`的文本,它不会被编译器执行,而仅用于代码的解释和文档说明。
>[!NOTE]
>
>* ① 注释是一个程序员必须有具有的良好编程习惯。
>* ② 在实际开发中,程序员可以将自己的思路通过`注释`整理出来,然后再用`代码`去实现。
## 4.2 单行注释
* C 语言中的单行注释的格式,如下所示:
```c
// 单行注释
```
> [!NOTE]
>
> 在 CLion 中的快捷键是 `Ctrl + /`
* 示例:
* 编程语言到底是什么?编程语言就是由文字和符号组成的,如:
```c
#include <stdio.h> // 这是编译预处理指令
@ -850,217 +307,645 @@ int main() { // 定义主函数
}
```
## 4.3 多行注释
* 编程语言就是用于控制计算机,让其完成我们需要的功能。而我们学习编程语言,其实就是学习这些文字和符号编写的规则。
* 因为 CPU 只能识别二进制的指令,而我们`编写`的程序叫做`源代码`,是人类能看懂;但是,计算机却不能识别。那么,我们就需要让计算机能识别我们编写的源程序,就需要将我们编写的源代码交给编译器程序,其会帮助我们将所编写的源代码转换为计算机能够识别的二进制指令。
* C 语言中的多行注释的格式,如下所示:
> [!NOTE]
>
> 编译器就是运行在操作系统之上的程序,其作用就是用来将程序员编写的源代码转换为计算机能够识别的二进制指令。
```c
/*
 这是第一行注释
 这是第二行注释
 这是第三行注释
*/
* 如果我们用 Java 语言编写了程序(源代码),那么编写的程序也是不能直接运行的,需要通过 Java 语言的编译器将 Java 程序编译为计算机能够识别的二进制指令。
* 如果我们用 Python 语言编写了程序(源代码),那么编写的程序也是不能直接运行的,需要通过 Python 语言的编译器将 Python 程序编译为计算机能够识别的二进制指令。
* ……
> [!NOTE]
>
> 总结:无论我们学习任何一门编程语言,想要将程序运行起来,都必须做如下的两件事情:
>
> * ① 学习该语言的文字和符号编写的规则,即:`语法规则`。
> * ② 需要在操作系统上安装对应编程语言的`编译器`程序,将源程序编译为计算机能够识别的二进制指令。
## 2.3 计算机语言简史
### 2.3.1 机器语言(相当于人类的石器时代)
* 1946 年 2 月 14 日,世界上第一台计算机 `ENIAC` 诞生,使用的是最原始的`穿透卡片`。
![](./assets/16.png)
* 这种卡片使用的是用`二进制代码`表示的语言,和人类语言差别极大,这种语言就称为`机器语言`,如:
```txt
0000,0000,000000010000 代表 LOAD A, 16
0000,0001,000000000001 代表 LOAD B, 1
0001,0001,000000010000 代表 STORE B, 16
```
* 这种语言本质上是计算机能识别的`唯一语言`,人类很难理解;换言之,当时的程序员 99.9% 都是异类!!!
> [!WARNING]
>
> * ① 不同类型CPU 架构x86_64、arm 等的处理器有不同的机器语言指令集指令集架构ISA决定了机器语言的具体形式。
> * ② 换言之,机器语言与特定硬件架构紧密相关,机器语言程序几乎没有可移植性。
### 2.3.2 汇编语言(相当于人类的青铜&铁器时代)
* `汇编语言`使用`助记符`MOV、ADD、SUB代替二进制操作码使程序更易于人类编写和理解因此`汇编语言`也被称为`符号语言`。
![](./assets/17.png)
* 汇编语言的`优点`是能编写`高效率`的程序;但是,`缺点`和机器语言没什么不同,汇编语言同样`依赖于具体的计算机架构(面向机器)`,程序不具备跨平台的可移植性。
> [!WARNING]
>
> * ① 汇编语言,目前仍然应用于工业电子编程领域、软件的加密解密、计算机病毒分析等。
> * ② 汇编语言是编程语言的拓荒年代,它非常底层,直接和计算机硬件打交道,开发效率低,学习成本高。
### 2.3.3 高级语言(相当于人类的信息时代)
* `高级语言`是一种`接近于人们使用习惯`的程序设计语言。`它允许程序员使用接近日常英语的指令来编写程序`,程序中的符号和算式也和`日常使用的数学公式`差不多,接近于自然语言和数学语言,容易被人们掌握。
![](./assets/18.png)
* 高级语言`独立于计算机硬件`,有一定的通用性;计算机不能直接识别和执行用高级语言编写的程序,需要使用`编译器`或`解释器`转换为机器语言,才能被计算机识别和执行。
![](./assets/19.png)
> [!NOTE]
>
> * ① 普遍使用的高级编程语言C、C++、Java、Python、C#、JavaScript、Go、SQL 等。
> * ② C 语言是“[面向过程](https://zh.wikipedia.org/wiki/%E8%BF%87%E7%A8%8B%E5%BC%8F%E7%BC%96%E7%A8%8B)”的编程语言,已经脱离了计算机硬件,可以用来设计和开发`中等`规模的程序。
> * ③ Java、C++、Python、C# 等都是“[面向对象](https://zh.wikipedia.org/wiki/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1)”的编程语言(它们在“[面向过程](https://zh.wikipedia.org/wiki/%E8%BF%87%E7%A8%8B%E5%BC%8F%E7%BC%96%E7%A8%8B)”的基础上又增加了很多概念),可以用来设计和开发`中大型`规模的程序。
>
> [!IMPORTANT]
>
> * ① C 语言出现的时候,已经度过了编程语言的拓荒年代,具备了现代编程语言的特性,但是这个时候还没有出现“[软件危机](https://zh.wikipedia.org/wiki/%E8%BD%AF%E4%BB%B6%E5%8D%B1%E6%9C%BA)”,人们没有动力去开发更加高级的语言,所以也没有太复杂的编程思想。
> * ② 之后出现的“[面向对象](https://zh.wikipedia.org/wiki/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1)”的编程思想解决了一部分在“[软件危机](https://zh.wikipedia.org/wiki/%E8%BD%AF%E4%BB%B6%E5%8D%B1%E6%9C%BA)”上的窘境。
### 2.3.4 总结
* 编写语言的对比,如下所示:
| 类别 | 特征 | 优点 | 缺点 | 示例 |
| :----------- | :--------------------------------- | :----------------------------------------------- | :----------------------------------------------------------- | :------------------ |
| **机器语言** | 直接由计算机执行的二进制代码 | 执行速度快 | 编写困难,可读性差,与具体硬件强绑定 | 二进制代码 |
| **汇编语言** | 用助记符代替二进制代码的低级语言 | 相对机器语言更易编写和理解,允许直接控制硬件资源 | 依然需要了解硬件,不够抽象,与具体硬件或平台相关 | MOVADD 等助记符 |
| **高级语言** | 接近人类语言,提供了更高层次的抽象 | 易于编写和维护,可移植性好,支持多种编程范式 | 需要通过编译器或解释器转换为机器语言,可能存在一定的性能损失 | CJava Python 等 |
> [!IMPORTANT]
>
> - ① 使用机器语言进行编程,对于程序员来说,简直就是噩梦,尤其当功能比较多,程序比较大的时候,不但编写麻烦,需要频繁查询指令手册,而且排查错误非常麻烦,要直接面对一堆二进制数据,想想都令人崩溃(上古程序员,可能真的不是“人”,而是“异类”)。此外,使用二进制指令编程,步骤非常繁琐,要考虑各种边界情况和底层问题,开发效率十分低下。
> - ② 这就倒逼程序员开发出了编程语言提供自己的生产力汇编语言、C 语言、C++ 语言、Java 语言、Go 语言等等,都是在逐步`提高开发效率`。至此,编程终于不再是只有极客才能做的事情,不怎么了解计算机的初学者在经过一定时间的训练后也可以编写出有模有样的程序。
> - ③ 在实际开发中随着计算机科学的发展现代化的高级编程语言C++ 语言、Java 语言、Go 语言等,因其强大的表达能力、良好的可移植性和易用性,成为了日常软件开发的主流选择。
# 第三章:初识 C 语言
## 3.1 C 语言究竟是一门怎样的编程语言?
* 对于绝大多数程序员而言C 语言是学习编程的第一门语言,很少有不了解 C 语言的程序员。
> [!NOTE]
>
> 许多著名编程语言的创造者和计算机科学领域的大佬都有学习和使用过 C 语言詹姆斯·高斯林James GoslingJava 之父比雅尼·斯特劳斯特鲁普Bjarne StroustrupC++ 之父吉多·范罗苏姆Guido van RossumPython 之父林纳斯·托瓦兹Linus TorvaldsLinux 之父)等。
* C 语言除了能让我们了解编程的相关概念,带领我们走进编程的大门,还能让我们明白程序的运行原理,如:
* ① 计算机的各个部件是如何协同工作的?
* ② 程序在内存中是一种怎样的状态?
* ③ 程序在计算机中到底是如何执行的,以及它的生命周期是什么?
* ④ 操作系统和应用程序之间,又有着怎样的爱恨情仇?
* ⑤ ...
* 这些底层知识决定了我们的发展高度,也决定了我们的职业生涯。如果我们希望能在计算机行业中长远的发展,就必须学习这些相关的底层知识。
> [!IMPORTANT]
>
> 这些底层知识,包括不限于:数据结构和算法、计算机组成原理、操作系统、计算机网络、设计模式以及编译原理(了解即可):
>
> * 数据结构和算法:主要研究数据的组织方式和处理方法,包括:线性表、树、图等数据结构,以及基本的算法思想和分析方法。
>
> * 计算机组成原理:主要研究计算机系统的硬件组成和工作原理,包括:数字电路、存储器、中央处理器等内容。
> * 操作系统:主要研究计算机系统的软件组成和工作原理,包括:进程/线程/并发(重点)、内存布局和内存管理(重点)、文件系统和磁盘 IO 等内容,以及基本的操作系统概念和设计思路。
> * 计算机网络主要研究计算机之间的通信原理和协议包括网络体系结构OSI 模型)、传输协议、网络安全等内容,以及基本的网络概念和技术。重点学习 TCP/IP 协议栈、socket 通信(三/四次握手、select、poll、epoll、HTTPS/HTTP、长链接等内容。
> * 设计模式:是软件设计中常见问题的通用解决方案。不过,在 C 语言中通常是没有的,因为其是面向过程的编程语言,而 C++ 和 Java 等面向对象的编程语言是有设计模式的。
> * 编译原理:主要研究如何将高级编程语言转换为机器语言,有词法分析、语法分析、语义分析、中间代码生成、优化、代码生成等步骤,了解即可。
* C 语言的概念少,词汇少,只包含了基本的编程元素,相对比较简单。对于初学者来说,学习 C 语言的时间短,成本小。
> [!NOTE]
>
> * ① 很多人之所以觉得 C 语言难,就是因为栽倒在 C 语言的指针那边相对比其他的现代化编程语言而言C 语言的指针确实足够底层和麻烦,需要掌握的细节很多)。
> * ② 但是,一旦有所突破,学习之路就会一马平川。
## 3.2 C 语言的由来
* 1969 年,美国贝尔实验室的`肯·汤姆森`Ken Thompson和`丹尼斯·里奇`Dennis Ritchie一起开发了 Unix 操作系统。Unix 最初是使用`汇编语言`编写的,依赖于计算机硬件。为了程序的`可读性`和`可移植性`,它们决定使用高级语言重写。但是。当时的高级语言无法满足他们的要求,`肯·汤姆森`就在 BCPL 语言的基础上发明了 `B` 语言。
* 1972 年,`丹尼斯·里奇`Dennis Ritchie`B` 语言的基础上重新设计了一种新的语言,这种新语言取代了 `B` 语言,即 `C` 语言。
![](./assets/20.png)
* 1973 年,`整个 Unix 系统都使用 C 语言重写(重构)`,大大增强了 Unix 在不同硬件平台的可移植性,这标志着 Unix 的初步成熟,也标志着 C 语言的初步成熟(因为 C 语言可以用于大型项目开发了)。
> [!NOTE]
>
> C 语言最初是作为 Unix 系统的开发工具而发明的,它的初衷非常简单,就是被设计成一种非常高效的、可以操作硬件的系统级编程语言,然后帮助团队开发 Unix 操作系统。
* 此后这种语言快速流传广泛用于各种操作系统和系统软件的开发Unix、MS-DOS、Microsoft Windows 以及 Linux 等。
![](./assets/21.png)
* 1988 年美国国家标准协会ANSI正式将 `C 语言标准化`,标志着 C 语言开始稳定和规范化。
## 3.3 为什么要学习 C 语言?
* ① `C 语言具有可移植好、跨平台的特点`:用 C 语言编写的代码可以在不同的操作系统和硬件平台上编译和运行。
> [!NOTE]
>
> * ① C 语言的最原始的设计目的,就是为了将 Unix 操作系统移植到其他的计算机架构上,这使得它从一开始就非常注重可移植性。
> * ② 这边所说的 C 语言的可移植性,是和汇编语言相比的;如果 C 语言和现代化的高级编程语言相比可移植性还是很差的Java 的口号是“一次编译到处运行”Go 的口号是“一次编译,到处执行”。
* ② `C 语言在许多领域应用广泛`
* `操作系统`C 广泛用于开发操作系统Unix、Linux 和 Windows。
* `嵌入式系统`C 是一种用于开发嵌入式系统(如:微控制器、微处理器和其它电子设备)的流程语言。
* `系统软件`C 用于开发设备驱动程序、编译器和汇编器等系统软件。
* `网络`C 语言广泛用于开发网络应用程序例如Web 服务器、网络协议和网络驱动程序。
* `数据库系统`C 用于开发数据库系统例如Oracle、MySQL 和 PostgreSQL 。
* `游戏`:由于 C 能够处理低级硬件交互,因此经常用于开发计算机游戏。
* `人工智能`C 用于开发人工智能和机器学习的应用程序,例如:神经网络和深度学习算法。
* `科学应用`C 用于开发科学应用程序,例如:仿真软件和数值分析工具。
* `金融应用`C 用于开发股票市场分析和交易系统等金融应用。
* ③ C 语言能够直接对硬件进行操作、管理内存以及和操作系统对话,这使得它是一种非常接近底层的语言,非常适合`写需要和硬件交互、有极高性能要求`的程序。
> [!NOTe]
>
> * C 语言毕竟诞生的时间非常早20 世纪 70 年代),属于 70 后了,有点落后于现在的时代,虽然执行效率高(仅次于汇编语言),但是开发效率低。
> * 随着时间的推移,人们在 C 语言的基础上增加了面向对象的机制([软件危机](https://zh.wikipedia.org/wiki/%E8%BD%AF%E4%BB%B6%E5%8D%B1%E6%9C%BA)),形成了一门新的编程语言,它的名字是 C++ 。
> * 但是C++ 的特性实在是太多了(因为 C++ 兼容 C并增加了很多自己独有的特性可以是当今最复杂的编程语言没有之一于是人们在 C++ 的基础上,删减了一些非必要的特性,就形成了 Java 和 C# ,也可以认为 Java 和 C# 是 C++--。
> * 当然近年来Go 语言也很火,它的设计者之一就是 `Unix` 操作系统的的开发者`肯·汤姆森`Ken ThompsonGo 诞生的背景据说是`肯·汤姆森`Ken Thompson在 C++ 委员会在为其演讲 C++ 新特性的时候,觉得 C++ 新特性太多太复杂,于是就开发了 Go 语言所以Go 语言中有很多 C 语言的身影。
> * 其实,在 C 语言之后的许多编程语言,或多或少都参考了 C 语言;所以,编程界流传了一句话:汇编生 C C 生万物。
* ④ `学习 C 语言有助于快速上手其他编程语言`C++(原先是 C 语言的一个扩展,在 C 语言的基础上嫁接了面向对象编程思想、C#、Java 等,这些语言都继承或深受 C 语言的影响和启发。
* ⑤ C 语言长盛不衰。`C 语言至今,依然是最广泛使用、最流行的编程语言之一`,包括很多大学将 C 语言作为计算机教学的入门语言,拥有庞大而活跃的用户社区,这意味着有许多资源和库可供开发人员使用。
* ⑥ C 语言`容易学习`,适合入门。和 Java、C++、Python 等更加现代化的高级编程语言相比C 语言涉及的概念少,附带的标准库小,整体比较简单,容易学习,非常适合初学者入门。
## 3.4 计算机语言排行榜
* [TIOBE](https://www.tiobe.com/tiobe-index/) 是一个流行编程语言排行每月更新。排名权重基于世界范围内工程师数量Google、Bing、Yahoo! 、Wikipedia、Amazon、Youtube 和百度这些主流的搜索引擎,也将作为排名权重的参考指标。
![](./assets/22.png)
* 计算机语言走势图:
![](./assets/23.png)
## 3.5 C 语言到底能做什么?
### 3.5.1 概述
* 我们知道,这个世界上的编程语言太多太多。其中,最流行的编程语言就是 [TOBIE](https://www.tiobe.com/tiobe-index/) 榜单上的前 50 名。
* 从应用的范围上来讲,编程语言大致可以分为以下两类,如下所示:
* ① `通用型编程语言`这些语言设计的目的是能够用于开发多种类型的应用程序不局限于某个特定领域。它们通常拥有丰富的库和框架支持可以应用于系统编程、应用开发、Web开发、数据分析等多个领域。
* ② `专用型编程语言`:这些语言是为了解决特定领域的问题而设计的,通常在该领域中表现出色。虽然它们可以在一定程度上用于其他领域,但其核心设计和优化是针对某个特定应用场景的。
* 常见的`专用型编程语言`,如下所示:
| 专用型编程语言 | 描述 |
| -------------- | ------------------------------------------------------------ |
| SQL | 专门用于数据库查询和管理,尽管可以嵌入其他编程语言中,但其主要应用是数据库操作。 |
| R | 主要用于统计分析和数据可视化,广泛应用于数据科学和研究领域。 |
| MATLAB | 主要用于数值计算、算法开发和数据可视化,广泛应用于工程和科学领域。 |
| VHDL/Verilog | 用于硬件描述和数字电路设计,主要应用于电子工程领域。 |
* 常见的`通用型编程语言`,如下所示:
| 通用型编程语言 | 描述 |
| -------------- | ------------------------------------------------------------ |
| Python | 广泛应用于Web开发、数据科学、自动化脚本、人工智能等。 |
| Java | 用于企业级应用、移动应用开发Android、Web 开发等。 |
| C++ | 可用于系统编程、游戏开发、嵌入式系统等多个领域。 |
| JavaScript | 最初用于 Web 前端开发现在也广泛用于服务器端Node.js、桌面应用开发Electron等。 |
> [!NOTE]
>
> C 语言是一门通用型的编程语言并没有针对某个领域进行优化。但是C 语言也有很多常见的应用领域。
### 3.5.2 C 语言的常见应用领域
* C 语言的常见应用领域,如下所示:
* ① 单片机或嵌入式开发单片机通常是一个集成度较高的微控制器资源有限家电控制、自动化设备、传感器数据采集、智能玩具、汽车电子。嵌入式系统是一个计算机系统嵌入在更大设备中用来执行专门的任务。它通常包含硬件和软件两部分具有专用性、实时性、资源受限等特点消费电子智能手机、智能手表等、医疗设备、工业控制系统、汽车电子系统ECU、ABS、物联网设备等。
* ② ~~上位机开发(桌面软件开发)~~其实这部分的市场已经被其它编程语言蚕食了C++ 的 QT、GTKJava 的 Swing 、JavaFxDart 的 Flutter ,因为 C 语言太过于底层,本身不直接支持跨平台,而且没有丰富的库和框架支持。
* ③ 系统组件开发:
* 基础组件文件系统、进程管理、用户界面CLI + GUI等。
* 核心算法,如:加密/安全算法MD5、SHA、AES、RSA、SSL、调度算法进程和线程调度、内存页面置换、LZ 压缩算法、CRC 等数据校验算法、随机数生成算法等。
* 硬件驱动声卡驱动、显卡驱动、网卡驱动、蓝牙驱动、键盘和鼠标驱动、扫描仪驱动、打印机驱动、USB 驱动等。
* 通信协议TCP/IP 协议族UDP、DNS、路由选择、HTTP/HTTPS 、SMTP/POP3/IMAP、FTP、NFC 等。
* ④ 开发操作系统:这是 C 语言的初衷它就是为开发操作系统而生的UNIX 内核、Linux 内核以及 Windows 内核,主要就是使用 C 语言开发。
* ⑤ 开发其它编程语言:
* 有些编程语言的编译器(解释器)和标准库就使用 C 语言开发,如: Python、PHP、Rust、Perl。
* 有些编程语言是在 C 语言的基础上进行的扩展,如: C++、Objective-C、Swift。
* ⑥ 信号处理C 语言在电气工程领域也有很多用途,它可以使用信号处理算法来管理微处理器、微控制器等集成电路。
* ⑦ 音视频处理C 语言的速度非常快,能够快速地对音频和视频数据进行处理。音频和视频数据通常比较大,需要高效的算法和数据结构来处理,而 C 语言运行速度非常快能够及时处理这些数据。C 语言提供了丰富的底层库和工具FFmpeg、OpenCV 等,这些库和工具可以方便地对音频和视频数据进行编码、解码、剪辑、处理和转换等操作。
* ⑧ 数据库开发:数据库是软件领域的基础设施,它的性能直接影响整个应用程序的运行效率,所以必须使用一种高效的语言进行开发。使用 C 语言开发的数据库有MySQL、SQLite、PostgreSQL 等。
* ⑨ ...
> [!IMPORTANT]
>
> 虽然 C 语言的开发场景貌似看起来很多;但是,使用 C 语言的场景几乎就是:底层/系统开发 + 关键组件/模块的开发 + 贴近硬件的开发,这些开发场景非常关注运行效率,或者响应时间,或者硬件资源。
## 3.6 不学 C 语言,行吗?
* C 语言相比于其它的现代化高级编程语言而言Java、Go 等,实在是太老了;如果不打算搞嵌入式开发,即便学了,一时半会也用不上。但是,几乎所有大学的计算机/软件专业都将 C 语言作为必修课,这又是为什么呢?
* 其实,是因为 C 语言是一门基础语言,很多其它的课程都依赖 C 语言。如果你不了解 C 语言,很多课程你是理解不了的,如下所示:
* ① 编译原理课程通常是以 C 语言为例进行讲解,因为 C 语言的编译过程相对简单、规范和透明,适合教学。
* ② 数据结构课程通常也是使用 C 语言进行编程,因为 C 语言比较底层能够让大家看到数据结构的各种细节。另外数据结构是一种被频繁调用的组件必须要追求效率C 语言再合适不过了。
* ③ 学习操作系统原理(内存、进程、线程、通信等)也要具备 C 语言基础,否则是学不明白的。
* ...
* C 语言是一门面向计算机的语言它能帮助我们快速了解底层而其它的高级语言Python、Java、C# 等是面向用户的它能让我们快速上手搞出点实用的工具来比如桌面软件、网站、APP 等。
> [!NOTE]
>
> * ① 借助 C 语言学习原理,相当于修炼内功;使用其它语言开发程序,相当于精通招式。
> * ② 一个既有扎实“内功”又精通“招式”的程序员,无疑能够更好地应对各种编程挑战。
* 从整体上讲,计算机软件大概可以分为两种:
- 一种是基础设施,如:操作系统、数据库、浏览器、云计算系统、大数据系统、编译器/编程语言、通信协议、区块链、标准库/运行库、算法实现等。
- 一种是应用软件桌面软件、APP、网站、小程序等。
> [!IMPORTANT]
>
> * ① 内功不扎实的话,开发一般的应用软件可能没什么问题。但是,如果想要开发高性能的软件,或者开发基础设施,那是绝对是不行的。
> * ② 万丈高楼平地起,勿在浮沙筑高台!!!
## 3.7 C 语言的版本选择
### 3.7.1 概述
* C 语言是在 B 语言的基础上改进而来的,目的是为了更好地开发 Unix 操作系统。到了 1973 年Unix 的大部分功能都被 C 语言重写,这标志着 C 语言的初步成熟,因为它可以用于大型项目了。
* 后来C 语言被多次改进,越来越强大,为了规范 C 语言的特性和功能,人们发布了多个 C 语言标准。
### 3.7.2 版本 1KR C
* K&R C 指的是 C 语言的原始版本。1978 年C 语言的发明者布莱恩·柯林Brian `K`ernighan和丹尼斯·里奇Dennis `R`itchie合写了一本著名的教材《C 编程语言》The C programming language
>[!NOTE]
>
>* ① 多行注释不能嵌套使用!!!
>* ② 在 CLion 中的快捷键是 `Ctrl + Alt + /`
>由于 C 语言还没有成文的语法标准,这本书就成了公认标准,以两位作者的姓氏首字母作为版本简称 “K&R C”。
### 3.7.3 版本 2ANSI C又称 C89 或 C90
* 示例:
```c
#include <stdio.h>
int main() {
/*
printf(1);
printf(2);
*/
printf("你好,世界!!!");
return 0;
}
```
# 第五章HelloWorld 解读(⭐)
## 5.1 规范的代码风格
### 5.1.1 正确的缩进和空白
* ① 使用一次 `tab` 操作,实现缩进,默认整体向右边移动;如果使用 `shift + tab` 则整体向左移动。
* ② 运算符两边习惯各加一个空格,如:`2 + 4 = 6`。
* 到了 80 年代C 语言越来越流行,广泛被业界使用,从大型主机到小型微机,各个厂商群雄并起,推出了多款 C 语言的编译器。这些编译器根据行业和厂商自己的需求进行了各种扩展C 语言进入了春秋战国时代,逐渐演变成一个松散杂乱的大家族。
* 为统一 C 语言版本1989 年美国国家标准协会ANSI制定了一套 C 语言标准并于次年被国际标准化组织ISO通过。它被称为 “ANSI C”也可以按照发布年份称为 “C89 或 C90”。
> [!NOTE]
>
> 各种 IDE 都有格式化的快捷键CLion 的格式化快捷键是 `Ctrl + Alt + L`
> 目前常用的编译器MSVCMicrosoft Visual C++、GCC、LLVM Clang 等,都能很好地支持 ANSI C 的内容。
### 3.7.4 版本 3C99
* 示例:
```c
#include <stdio.h>
int main() {
int a = 1;
int b = 2;
int c = a + b;
printf("c = %d", c);
return 0;
}
```
### 5.1.2 代码风格
* 在 C 语言中,有两种代码风格:`行尾风格`和`次行风格`。
>[!NOTE]
>
>看个人爱好,任选一种即可,本人喜欢`行尾分格`
* 示例:行尾风格
```c
int main(){
if(a > b) {
return a;
} else {
return b;
}
  return 0;
}
```
* 示例:次行风格
```c
int main()
{
if(a > b)
{
return a;
}
else
{
return b;
}
  return 0;
}
```
## 5.2 代码细节剖析
### 5.2.1 main() 函数
* 在 C 语言中,一个程序或工程可以定义很多函数,但是有且只有一个 main() 函数,作为程序执行的入口,并且在 main() 函数结尾结束整个程序的运行,即:
```c
int main(){
return 0;
}
```
* 如果 main() 函数是空括号,即表示 main() 函数不接收任何参数。
* 在 main() 函数之前的 `int` 称为关键字,代表数据类型是`整型`,它是 main() 函数的返回值的类型,即在执行 main() 函数之后一定会得到一个整数类型的值,即函数值。
* C 语言标准的第一次`大型修订`,发生在 1999 年,增加了许多语言特性,如:双斜杠( `//` )的注释语法,可变长度数组、灵活的数组成员、复数、内联函数和指定的初始值设定项,这个版本称为 C99`是目前最流行的 C 版本`。
> [!NOTE]
>
> * ① 在 C 语言中,人们约定,如果 `return 0`,就表示 main() 函数终止运行,且运行成功;如果返回其它非零整数,则表示运行失败。
> * ② 默认情况下,如果 main() 函数中省略 `return 0` ,则编译器会自动加上。但是,为了保持统一的代码风格,不建议省略。
> 这个时候的 C 语言编译器基本已经成熟,各个组织对 C99 的支持所表现出来的兴趣不同:
>
> * 当 GCC 和其它一些商业编译器支持 C99 的大部分特性的时候。
> * 微软和 Borland 却似乎对此不感兴趣,或者说没有足够的资源和动力来改进编译器。
>
> 最终导致不同的编译器在部分语法上存在差异。典型的例子就是ANSI C 规定,只能用常量表示数组的长度,而 C99 取消了这个限制,数组的长度也可以用变量表示(可变长度数组)。对于 C99 的这个新改动GCC 和 Clang 是支持的,而 MSVC 却不支持。
### 5.2.2 函数体
### 3.7.5 版本 4C11
* ① 一对花括号 `{}` 定义了函数的主体,所有函数都必须以大括号开头和结尾,成对出现。
* ② C 程序中的函数体指的是作为该函数一部分的语句。它可以是任何操作,比如:搜索、排序、打印等。
* ③ 每一个执行语句后面都会有一个英文分号`;`作为语句结束的标志。
* ④ 一行内可写几条语句,一条语句也可写在几行上。
### 5.2.3 printf() 函数
* printf() 函数的格式,如下所示:
```c
printf (const char *__format, ...)
```
* printf() 函数是产生格式化输出的函数作用是将参数文本输出到屏幕f 表示 format格式化表示可以指定输出文本的格式
```c
printf ("Hello World"); // 将字符串输出到控制台,行尾不换行
```
* 如果想让光标移动到下一行的开头,可以在输出文本的结尾,可以添加一个换行符 `\n`,即:
```c
printf("Hello World\n");
```
### 5.2.4 标准库和头文件
#### 5.2.4.1 概述
* printf() 函数是在标准库的头文件 `stdio.h` 中定义的,要想在程序中使用这个函数,必须在源文件的头部引入该头文件,即:
```c
#include <stdio.h>
```
#### 5.2.4.2 标准库Standard Library
* C 语言的`标准库`是由一组函数组成,这些函数提供了许多常用的操作和功能,如:输入输出、字符串处理、内存管理、数学计算等。标准库中的函数由编译器提供,遵循 ANSI C 标准C89/C90、C99、C11等
* 换言之C 语言的`标准库`就是包含函数的实际代码这些代码在编译的时候被链接到我们的程序中无需手动包含。C 语言的`标准库`提供了可重用的函数实现,使得程序员不必编写常用的功能。
* 2011 年,标准化组织再一次对 C 语言进行修订增加了_Generic、static_assert 和原子类型限定符,这个版本称为 C11。
> [!NOTE]
>
> 实际的 printf() 函数的实现代码通常位于标准库的实现文件中,如:在 Linux 中的标准库`libc.so.6` 就包含了 printf() 函数的实现。
> * ① 需要强调的是,修订标准的原因并不是因为原标准不能用,而是需要跟进新的技术。
> * ② 支持此标准的主流 C 语言编译器有 GCC、LLVM Clang、Intel C++ Compile 等。
#### 5.2.4.3 头文件Header Files
### 3.7.6 版本 5C17
* `头文件`是包含函数声明、宏定义、数据类型定义等内容的文件。头文件的作用是为源代码提供必要的声明和定义,以便编译器能够正确解析和链接函数调用。头文件通常以`.h`作为文件扩展名。
* 换言之,头文件包含函数声明、宏定义和数据类型定义,但不包含函数的实现。头文件告知编译器如何使用标准库中的函数和定义,确保编译时的正确性。头文件需要在源代码文件中使用`#include`指令显式包含,如:`#include <stdio.h>`。
* C11 标准在 2017 年进行了修补,但发布是在 2018 年。新版本只是解决了 C11 的一些缺陷,没有引入任何新功能,这个版本称为 C17。
* 常见的 C 语言头文件及其功能和常用函数、宏等,如下所示:
### 3.7.7 版本 6C23
| 头文件 | 功能说明 | 常用函数和宏 |
| ------------- | ---------------------------------------------------------- | ------------------------------------------------------------ |
| **stdio.h** | 标准输入输出库 | `printf` `scanf` `fprintf``fscanf``fopen` `fclose``fgets` `fputs` |
| **stdlib.h** | 标准库,提供内存分配、程序控制、类型转换、随机数生成等功能 | `malloc` `free` `exit` `atoi` `atof``rand``srand` |
| **string.h** | 字符串处理库 | `strlen` `strcpy` `strncpy` `strcat` `strcmp``strstr` `memset` `memcpy` |
| **math.h** | 数学库 | `sin` `cos` `tan` `exp` `log` `sqrt` `pow` |
| **time.h** | 时间和日期库 | `time` `clock` `difftime` `mktime` `strftime` `localtime``gmtime` |
| **ctype.h** | 字符处理库 | `isalnum` `isalpha` `isdigit` `islower` `isupper` `tolower` `toupper` |
| **stdbool.h** | 布尔类型库 | `bool` `true` `false` |
| **assert.h** | 断言库 | `assert` |
#### 5.2.4.4 预处理命令
* `#include` 命令的作用是将指定文件的内容插入到包含该命令的源文件中。这通常用于包含头文件,以便使用头文件中声明的函数、宏和数据类型。
* 语法:
```c
// 用于包含标准库头文件或系统头文件。
// 编译器将在系统的标准头文件目录中查找文件。
#include <filename>
```
```c
// 用于包含用户自定义的头文件。
// 编译器首先在当前目录中查找文件,如果未找到,再在标准头文件目录中查找。
#include "filename"
```
* 2023 年发布,计划进一步增强安全性,消除实现定义的行为,引入模块化语言概念等新特性,使 C 语言在安全和可靠性方面有重大提高。
# 第四章C 语言的学习技巧
## 4.1 如何学习 C 语言?
* 对于大部分的初学者, 学习 C 语言的目的可能是为了成为一名合格的程序员,开发出优秀的软件。但是,在学习了 C 语言的基本语法后,却发现只能在`控制台``黑底白字`)上玩玩,没有漂亮的用户界面以及人性化的交互。于是,开始学习数据结构、算法、数据库、操作系统,越陷越深,越来越迷茫,不知道学习 C 语言能做什么,认为学习编程很难,开始怀疑自己,直到放弃!!!
* 其实C 语言本身就是一门非常简单的语言,提供的实用功能不多,大部分的时候需要`借助`操作系统、第三方库以及以及一些硬件,如:单片机等,才能发挥它的威力!!!
> [!IMPORTANT]
>
> * ① 学习 C 语言仅仅是让你踏上程序员之路的第一步而已,只学习 C 语言也做不了什么。
> * ② 系统、扎实的学习 C 语言可以让你了解底层硬件、一些简单的数据结构和算法,并培养计算机思维。
* 前文我们提过C 语言是一门通用性的语言并没有针对某个领域进行优化。并且在实际项目中C 语言主要用于比较底层的开发,例如:
* Windows、Linux、Unix 等操作系统的内核 90% 以上都使用 C 语言开发Rust 语言有望未来,在操作系统开发中占据一席之地,特别是在对安全性和性能要求极高的领域)。
* 开发硬件驱动,让硬件和操作系统连接起来,这样用户才能更有效的使用硬件。
* 单片机和嵌入式属于软硬件的结合,是使用 C 语言最多的地方。
* 开发系统组件或服务,用于支撑上层应用。
* 如果对软件某个模块,例如:算法和搜索部分的效率要求较高,也可以使用 C 语言来开发。
* ……
* 貌似感觉 C 语言的应用还是很多的啊,那为什么感觉学习 C 语言还是做不了什么?原因除了 C 语言通常都是开发最`底层`的应用之外,最重要的一点就是`生态`。
> [!IMPORTANT]
>
> 现代化的高级编程语言的流行程度,除了和编程语言的设计是否优秀有关,最主要的原因就是`生态`。
>
> * ① 很多编程语言都自带`标准库`语言本身提供的开箱即用Java、Go 等。
> * ② 很多编程语言都有自己的`包管理器`用于管理第三方库解决方案Java 中的 Maven、Gradle、Go 中的 go modules JavaScript 的 npm 等。
>
> 遗憾的是C 语言的`标准库`非常简单,只有`输入输出`、`文件操作`、`日期时间`、`字符串处理`、`内存管理`,对于`网络编程`、`GUI`、`数据库`、`并发`等`需要`大量的`第三方库`或`操作系统的功能`来扩展 C 语言的功能Java 语言、Go 语言等其他的现代化高级编程语言都是直接将这些常见的开发场景内置到标准库中开箱即用极大的降低了软件开发的难度。C 语言的`第三方库`也不是很多和其它现代化高级编程语言相比C 语言的大多数的第三方库都是`底层库`,支持应用开发的库寥寥无几,只有一个 GTK 库能够开发出桌面软件,几乎没有网站开发以及 APP 开发相关的库),社区也不是很活跃(和其它现代化高级编程语言相比),更别提缺少自己的包管理器。
> [!IMPORTANT]
>
> * ① 换言之在实际工作中C 语言几乎是不用做软件、网站、APP 等这些应用层开发,其它的编程语言能够更好地完成任务,没必要非得使用 C 语言C 语言基本都是用来做底层开发,也就是看不见摸不着的、在后台默默提供服务的那些项目,而这样的项目对初学者来说基本没有实用价值,初学者也不知道它们该怎么使用。
>
> * ② 初学者想要的 C 语言没有C 语言能做的初学者用不到,就是这种矛盾,导致初学者非常迷茫。
>
> * ③ 有人可能会问C 语言不是还可以用来开发单片机或者嵌入式吗是的没错但是这个方向是软硬件结合的不是在我们的电脑上进行开发而是在特殊的板子上进行开发并且还需要学习数字电路、模拟电路、8051/ARM、RTOS、嵌入式 Linux 等方面的知识,只学 C 语言也没有用武之地。
>
> * ④ 如果你觉得学了 C 语言没用,那么恭喜你,你是对的,应用层的开发一般真的用不上它。
> * ⑤ 但是,没用也要学,学习 C 语言并不一定是要应用它C 语言可以夯实你的编程基础,尤其是数据结构、算法、内存、线程、进程、通信、操作系统、编译原理等底层的计算机知识,没有 C 语言基础是学不好的。
> * ⑥ 这些底层知识并不一定能够直接应用在实际开发中,但是它们会让你有底气,会让你透彻地理解编程概念,会让你站的“低”看得远,会让你避免很多低级错误,会让你心中有“架构师”的思维。
> [!IMPORTANT]
>
> 不过,现在 C 语言社区也开始诞生了一些包管理器Conan 和 vcpkg 也有自己的项目构建工具cmake 、xmake 等。
> [!NOTE]
>
> JavaScript 的作者 Brendan Eich布兰登·艾奇 曾经这么说:“与其说我爱 JavaScript不如说我恨它。它是 C 语言和 Self 语言一夜情的产物(`致敬 C 语言`)。十八世纪英国文学家约翰逊博士说得好:"它的优秀之处并非原创,它的原创之处并不优秀。"”
>
## 4.2 项目构建工具和包管理器
### 4.2.1 概述
* `项目构建工具`和`包管理器`在软件开发中扮演着不同的角色,它们虽然有时会有重叠的功能,但主要关注的点是不同的。
### 4.2.2 项目构建工具
* `项目构建工具`是用于`自动化编译、测试、打包、部署`等一系列任务的软件工具。它们帮助开发者简化和管理整个软件开发生命周期中的各个步骤,尤其是在构建过程中的复杂性管理上。
* 其功能有:
* 编译代码:自动编译源代码(如 `.java`、`.c` 等)为可执行文件或中间文件(如:`.class` 文件)。
* 运行测试:集成单元测试、集成测试,自动运行测试用例并生成报告。
* 打包将编译后的代码、依赖库、资源文件等打包成可分发的格式JAR、WAR、可执行文件等
* 依赖管理:自动下载、更新和管理项目所需的第三方库(这部分功能有时与包管理器重叠)。
* 部署:将打包后的应用程序自动部署到测试环境、生产环境等。
* 任务自动化:除了基本的构建流程外,还可以自动化执行一些常见任务,如:代码检查、文档生成等。
* 常用的项目构建工具:
* MavenJava一个流行的构建工具和依赖管理工具广泛用于 Java 项目。
* GradleJava、Kotlin、Groovy一个灵活的构建工具支持声明式的构建脚本和多种语言。
* MakeC/C++):一个经典的构建工具,使用 `Makefile` 来定义构建规则和依赖关系。
* CMakeC/C++一个跨平台的构建系统帮助生成标准的构建文件Makefile 或 Visual Studio 项目文件。
* ……
### 4.2.3 包管理器
* `包管理器`是用于`自动化安装、更新、配置`和`管理软件包及其依赖关系`的工具。它主要关注于获取和管理项目所需的第三方库或工具包,并确保它们正确地集成到项目中。
* 其功能有:
* 依赖管理:根据项目配置文件(如:`package.json`、`requirements.txt`)自动下载和安装项目所需的依赖包。
* 版本控制:管理包的版本,允许开发者指定某个特定版本或版本范围,确保项目中的库版本一致性。
* 包的发布和共享:开发者可以通过包管理器发布自己的库,并且共享给社区或组织内部的其他项目使用。
* 环境隔离:有些包管理器提供虚拟环境功能,可以将不同项目的依赖隔离开,避免版本冲突。
* 更新和卸载:包管理器可以自动更新依赖包到最新的兼容版本或卸载不再需要的包。
* 常见的包管理器:
* npmNode.js用于管理 JavaScript 和 Node.js 项目的包和模块。
* pipPython用于安装和管理 Python 的软件包。
* CargoRustRust 编程语言的包管理器和构建工具。
* YarnJavaScript是 npm 的替代品,提供更快和更可靠的包管理体验。
* HomebrewmacOS用于 macOS 系统下的命令行工具和库的管理。
* ……
### 4.2.4 注意事项
* 对于 `Java` 项目中的 `Maven``Gradle` 而言,其不仅是`项目构建工具`也是`包管理工具`。
> [!NOTE]
>
> * ① Gradle 也支持原生项目的开发C 和 C++ 。
> * ② 不过,目前而言,业界开发 C/C++ 项目时,使用最多的项目构建工具和包管理器是 Cmake 和 Conan 。
## 4.3 C 语言为什么没有应用层开发的库?
* C 语言是一门“古老”的语言了它只支持面向过程编程不支持面向对象编程和泛型编程在中大型的应用层项目开发中C 语言已经显得捉襟见肘了C++、Java、Python、C#、JavaScript 等其他编程语言能够更好地胜任,为 C 语言开发应用层的库简直是费力不讨好,所以几乎没人这么做。
* GTK 算是一个应用层的库,它使用 C 语言开发但是为了适应市场GTK 也提供了其它编程语言的接口C++、Python 等。
* 先不用管面向过程、面向对象、泛型这些晦涩的编程概念。简单地理解就是C 语言支持的特性少,用起来费劲,开发效率低,而 C++、Java、Python、C#、JavaScript 等支持的特性多,用起来方便,开发效率高。
> [!IMPORTANT]
>
> * ① C 语言的优势是运行效率极高,这正是底层开发所看重的。
> * ② 底层开发有时候就是一个模块,或者是一个服务,规模不算大,但是对效率有严格的要求,此时用 C 语言就非常合适,所以针对底层开发的 C 语言库较多,因为它们有非常大的实用价值。
# 第五章:附录
## 5.1 嵌入式领域中的 C 语言
### 5.1.1 概述
* C 语言在 C51、STM32 和 ARM 平台上的应用场景非常广泛,涵盖了各种嵌入式系统的开发需求。
### 5.1.2 C518051 系列微控制器)
* `背景`8051 是由 Intel 于 1980 年设计的一种 8 位微控制器架构。它具有指令集简单、结构紧凑的特点,广泛应用于低端嵌入式系统中。
* `开发工具`C51 是指针对 8051 系列微控制器的 C 语言编译器Keil C51。这种编译器将 C 语言代码编译为适合 8051 架构的汇编代码。
* `C 语言的作用`C 语言在 8051 微控制器上的应用使得开发更加高效和可维护。尽管 8051 的硬件资源有限,但 C 语言仍然能够在不损失性能的前提下提供高级编程的便利。
* `应用场景`
* **简单的控制系统**:家用电器(微波炉、洗衣机、空调)的控制板等。这些设备通常不需要复杂的运算能力,但要求可靠和稳定的控制。
* **低功耗传感器接口**C51 微控制器常用于低功耗传感器的数据采集和传输,如:温度、湿度、压力传感器。
* **工业自动化设备**:用于简单的工业自动化控制,如:小型电机驱动、工业传感器数据处理和传输。
* **电子玩具**:许多简单的电子玩具使用 8051 系列微控制器来控制声音、LED 灯光、显示屏等。
> [!NOTE]
>
> 总结C51 微控制器适用于资源受限、需要低成本的简单控制系统,非常适合使用 C 语言来进行开发!!!
### 5.1.3 STM32STM32 系列微控制器)
* `背景`STM32 是意法半导体STMicroelectronics推出的一系列基于 ARM Cortex-M 内核的 32 位微控制器。它们广泛用于需要高性能和低功耗的嵌入式应用中,如:工业控制、消费电子和物联网设备。
* `开发工具`:开发 STM32 微控制器通常使用 Keil、IAR Embedded Workbench 或 STM32CubeIDE 等开发环境。这些环境中使用的编程语言主要是 C有时也包括 C++)。
* `C 语言的作用`C 语言在 STM32 上的应用非常广泛,开发者可以利用它直接控制硬件寄存器,同时也能方便地使用 STM32 提供的 HAL硬件抽象层库或 LL低层库进行开发。C 语言在这个平台上不仅能实现底层控制,还能编写复杂的应用逻辑。
* `应用场景`
* **物联网IoT设备**STM32 微控制器常用于各种物联网设备智能家居控制系统、环境监测设备、可穿戴设备等。这些设备通常需要低功耗和强大的处理能力并且需要支持多种通信协议Wi-Fi、Bluetooth、LoRa。
* **消费电子**:智能手表、健身追踪器、电子书阅读器、无人机等,这些设备需要具备实时处理能力、低功耗和良好的外设支持。
* **医疗设备**STM32 微控制器被广泛应用于便携式医疗设备中,如:血糖监测仪、心率监测器、便携式超声设备等,这些设备需要精确的传感器数据采集和处理。
* **工业自动化控制**PLC可编程逻辑控制器、工业机器人、伺服电机控制等STM32 能够处理复杂的控制算法和实时任务。
* **汽车电子**:用于汽车中的传感器管理、车载信息娱乐系统、车身控制系统(车窗、电动座椅调节等)。
> [!NOTE]
>
> 总结STM32 微控制器在物联网、消费电子、医疗设备和工业控制等领域表现出色,非常适合使用 C 语言来进行开发,因为 C 语言允许直接进行硬件控制并支持复杂的应用开发。
### 5.1.3 ARM 架构(特别是 ARM Cortex 系列)
* `背景`ARM 是一种广泛使用的处理器架构特别是在嵌入式系统中ARM Cortex 系列处理器(如 Cortex-M、Cortex-R 和 Cortex-A非常流行。Cortex-M 系列主要用于微控制器Cortex-R 用于实时系统Cortex-A 则用于高性能嵌入式系统。
* `开发工具`:针对 ARM 架构的开发,常用工具包括 ARM Keil MDK、IAR、GCC for ARM 和 ARM Development Studio。这些工具均支持使用 C 语言进行开发。
* `C 语言的作用`C 语言在 ARM 架构上的应用广泛。它被用于操作系统内核(如 FreeRTOS、Zephyr、设备驱动、应用层逻辑等。在 ARM Cortex-M 和 Cortex-R 系列中C 语言的高效性和低级别硬件访问能力是开发实时、低延迟系统的关键。
* `应用场景`
* **高级嵌入式操作系统**ARM Cortex-A 系列处理器广泛用于运行 Linux、Android 等操作系统的嵌入式设备,如:智能手机、平板电脑、智能电视和车载娱乐系统。
* **实时系统**ARM Cortex-R 系列处理器用于实时系统,如:汽车的 ABS防抱死制动系统、ESC电子稳定控制系统以及航空电子设备这些系统要求极低的延迟和高可靠性。
* **高性能物联网网关**Cortex-A 系列处理器可以用来开发支持多协议、多设备管理的物联网网关,这些网关通常需要强大的计算能力和多线程处理能力。
* **边缘计算设备**在边缘计算场景中ARM Cortex-A 处理器用于执行本地数据处理和决策,如:视频分析、图像处理、语音识别等。
* **智能家居设备**ARM Cortex-M 系列微控制器广泛应用于智能家居产品,如:智能灯泡、智能音箱、家庭安全系统,这些设备需要高效的处理能力和低功耗。
* **机器人控制系统**ARM Cortex-M 和 Cortex-A 系列处理器用于机器人系统的控制和通信,如:无人机、工业机器人、服务机器人等,处理复杂的运动控制、路径规划和传感器数据融合。
> [!NOTE]
>
> 总结ARM Cortex 系列适用于从实时系统到高级嵌入式操作系统的各类应用,支持从低功耗控制到高性能计算的多种需求,非常适合使用 C 语言来进行开发,因为 C 语言不仅用于控制硬件,还广泛应用于操作系统和应用程序的开发。
## 5.2 C 和 C++ 的关系
* 起源和发展:
* C 语言C 语言由丹尼斯·里奇Dennis Ritchie在 1972 年开发,最初用于开发操作系统,特别是 UNIX。它是一种结构化的编程语言提供了对硬件的低级访问且效率高因此在系统编程中广泛使用。
* C++ 语言C++ 由比雅尼·斯特劳斯特鲁普Bjarne Stroustrup在 20 世纪 80 年代初开发,作为 C 语言的扩展。它引入了面向对象编程的概念,同时保留了 C 语言的高效性和底层操作能力。C++ 最初被称为 “C with Classes”带类的 C后来发展成一种独立的编程语言。
* 兼容型:
* 语法兼容C++ 几乎完全兼容 C 语言的语法,这意味着大多数 C 代码可以在 C++ 编译器下直接编译运行。C++ 可以看作是 C 语言的超集(虽然不完全是 100% 兼容,但差异较少)。
* 扩展性C++ 在 C 的基础上增加了许多新特性,如:类和对象、继承、多态、模板和异常处理等。这使得 C++ 不仅适合系统编程,还可以用于开发复杂的应用程序。
* 编程范式:
* C 语言:主要是面向过程编程,强调的是函数调用和控制结构。
* C++ 语言:支持多种编程范式,包括:面向过程、面向对象和泛型编程,使得开发者可以更灵活地选择适合的编程风格。
* 性能与应用:
* 性能:由于 C++ 包含了更多的高级特性,它的编译时间和运行时开销可能比 C 稍高。但得益于其优化机制C++ 仍然可以实现与 C 语言相近的性能。
* 应用场景C 语言仍然在嵌入式系统、驱动程序开发和其他对性能要求极高的场合中占据重要地位。而 C++ 则被广泛用于游戏开发、图形处理、大型软件系统等需要复杂结构和抽象的领域。
> [!NOTE]
>
> C++ 是 C 语言的继承者和扩展,它保留了 C 语言的优势,并引入了更多现代编程语言的特性,使得它在更广泛的应用领域中得到应用。
## 5.3 C 语言和 Java 语言的对比
* C 语言 和 Java 语言的软件设计思想对比,如下所示:
> [!NOTE]
>
> * ① Java 语言编写的应用是直接运行在 JVMJava 虚拟机)上,体现了`分层`的软件设计思想;并且,这种`分层`的软件设计思想给 Java 语言带来了`跨平台性`和`自动内存管理`等方面的功能。
> * ② C 语言编写的应用是直接运行在操作系统之上,其设计哲学是:`简洁、高效、直接控制底层`。
![](./assets/10.svg)
* 虽然 C 语言和 Java 语言有很多相似之处,但在设计理念、运行环境、内存管理等方面有显著差异,如下所示:
| 对比角度 | C 语言 | Java 语言 |
| ------------ | ----------------------------------------------------- | ------------------------------------------------------- |
| 编译与解释 | 编译型语言,源代码编译为机器码直接执行,效率高。 | 解释与编译结合,源代码编译为字节码,通过 JVM 解释运行。 |
| 平台依赖性 | 与操作系统和硬件平台紧密结合,跨平台能力较弱。 | 通过 JVM 实现跨平台,"编写一次,到处运行"。 |
| 内存管理 | 手动管理内存,需显式分配与释放,容易出现内存泄漏。 | 自动垃圾回收,简化内存管理,降低内存泄漏风险。 |
| 指针 | 支持指针,能直接操作内存,但可能导致安全问题。 | 不支持指针操作,避免内存安全隐患。 |
| 编程范式 | 过程化编程,主要通过函数调用组织程序。 | 完全的面向对象编程,一切皆为对象。 |
| 异常处理 | 不支持异常处理,错误处理通过返回值或全局变量实现。 | 提供强大的异常处理机制,通过 try-catch-finally 块处理。 |
| 标准库与生态 | 标准库简洁,需大量使用第三方库开发复杂应用。 | 标准库丰富,拥有庞大生态系统和社区支持。 |
| 运行时性能 | 直接编译成机器码,性能高,适合高性能要求的场景。 | 运行在虚拟机上,性能稍低,但通过 JIT 等优化提升效率。 |
| 多线程支持 | 依赖操作系统 APIpthread多线程编程较为复杂。 | 内置多线程支持,提供 Thread 类和并发工具类。 |
| 应用场景 | 系统级编程,如操作系统、嵌入式系统、驱动程序等。 | 企业级应用、Web 开发、Android 开发、大数据处理等。 |
> [!IMPORTANT]
>
> 总结而言:
>
> * ① C 语言适合底层编程、系统级开发,性能高、控制力强,但对程序员的要求也更高。而 Java 语言则更适合应用层开发,拥有丰富的库和工具支持,开发效率更高,并且由于自动内存管理和异常处理,编写的代码通常更加安全和健壮。
> * ② 两者各有优势,选择使用哪种语言应根据项目需求和开发环境来决定。
# 5.4 C 语言的优缺点
### 5.4.1 概述
* 当今繁荣昌盛的互联网世界是建立在 C 语言基础之上的,如:
* ① 操作系统C 语言设计的初衷就是用来编写操作系统。目前所有的主流操作系统内核,所采用的编程语言都是 C 语言。
* ② 网络协议栈:一般和内核采用相同的语言实现,也就是 C 语言。
* ③ WEB 服务器和 HTTP 服务器Apahce、NGINX 是采用 C 语言编写的Tomcat 是采用 Java 语言编写的,只能运行 Java 应用程序。
* ④ 关系型数据库MySQL 是采用 C/C++ 语言编写的,而 PostgreSQL 和 SQLite 主要由 C 语言编写的。
* ⑤ 非关系型数据库Redis 主要由 C 语言编写的MongoDB 是由 C++ 语言编写的,而 Elasticsearch 由 Java 语言编写的。
* ...
> [!NOTE]
>
> * ① C/C++ 非常适合做基建开发,而 Java 和 Go 等非常适合做业务开发。
> * ② 近几年Rust 语言很火,有望替代 C/C++ 做基建开发,因为其是内存安全的,并且没有 GC垃圾收集器
* C 语言是一门接近“低级语言”的“中级语言”,如下所示:
![](./assets/24.svg)
### 5.4.2 C 语言的优点
* ① C 语言是一门接近硬件层面的编程语言,提供了接近硬件的操作能力。
> [!NOTE]
>
> * ① C 语言通过提供直接内存访问、指针操作和系统调用等功能,允许程序员以接近硬件的方式编写代码。
> * ② 这种能力使得 C 语言特别适合用于嵌入式系统开发以及系统编程。
* ② C 语言拥有在所有编程语言中几乎独一档的高运行效率。
> [!NOTE]
>
> * ① C 语言提供了接近硬件的操作能力,这意味着 C 程序可以更好的利用硬件资源。
> * ② C 语言编译器将源代码直接编译成机器代码(或非常接近的汇编语言),而非解释执行或编译为中间代码。
> * ③ C 语言还提供了各种特性使得程序员可以控制优化 C 程序的几乎每一个细节。如C 语言的手动内存分配控制,这意味着可以精确地管理资源使用,从而提高效率。
> * ④ 在平均一般情况下如果一个C程序的执行速度是 100%,那么:
> * C++ 可能由于复杂性的影响,性能会轻微下降,一般是 95% 左右。
> * Java 由于JVM等特性的影响运行效率一般低于 C/C++,大概在 50%-80% 范围内。
> * Python 是一种脚本语言,需要解释执行,效率很差,可能在 5%-30% 之间。
* ③ C 语言的语法非常简洁,没有太多复杂的特性。
> [!NOTE]
>
> * ① 作为一门接近硬件的编程语言C 语言的抽象层次很低,没有很多高级的抽象特性。
> * ② 这使得 C 语言简单易学同时对于想要深入理解计算机原理的人来说C 语言的低抽象性也可以使得学习者可以更好的忽略语言特性,关注具体原理。
* ④ C 语言作为一门"中级语言"市面上流行的编程语言基本都可以找到它的影子C-like 语言),所以学习 C 语言对于学习其他编程语言也有很大的帮助。
> [!NOTE]
>
> 很多类 C 的编程语言都有 C 语言的身影C++、Java 等。
* ⑤ C 语言的生命力旺盛,从诞生到如今始终都是最流行的开发语言之一。
### 5.4.3 C 语言的缺点
* ① C 语言是一门非常容易出错,却非常不容易发现错误的编程语言。
> [!NOTE]
>
> * ① C 语言的语法相对`非常宽松`和`灵活`,给予了程序员很大的自由度,但出错的概率也大大增加了。
> * ② C 语言给程序员很大的自主性和控制权限,但即便是熟练的 C 程序员也无法保证能够完美的控制程序,如:内存管理,手动的内存管理不仅增加了编程的复杂性,而且非常容易出错,非常容易造成内存泄漏和使用未初始化的内存导致未定义行为。
> * ③ C 语言追求效率所以没有专门的异常机制来指示程序出现的问题。C 程序甚至不会去检查数组下标越界的问题。
> * ...
* ② C 语言缺少一些必要的高级特性,使得 C 程序的可维护性很差。
> [!NOTE]
>
> 一个复杂的 C 程序,如果在设计之初没有考虑到维护问题,那么将很难对它做出修改或扩展。(这也是 C++ 出现的目的)
* ③ 相比较现代的高级语言C 语言经常可以写出可读性十分差的代码。
> [!NOTE]
>
> * ① 一方面是由于 C 语言作为一门小型语言,它缺乏一些现代高级语言中的特性,并且其标准库相对较小。因此,程序员往往需要自行编写复杂的代码实现,这些代码对于其他人可能难以理解。
> * ② 另一方面C 语言诞生于编程发展的初期,那时的编程哲学倾向于代码的极致简洁性。这种风格有时会导致代码过于晦涩难懂,尤其是当程序员过分追求聪明的技巧时,可能会编写出只有自己能理解的代码。
> * ③ 在现代编程中,尤其是团队协作环境下,高度重视代码的可读性和维护性。因此,强调编写清晰易读的 C 代码,而不是单纯追求代码的简洁。
* ④ C 语言的核心语法特性在不同编译器和平台之间是保持一致的,这是因为不同平台的编译器实现都必须遵循 C 语言的标准ISO C标准。这确保了无论在 Windows 还是 Linux 平台上,基本的语法、控制结构和数据类型等基础元素都是相同的。
> [!NOTE]
>
> * ① 由于 C 语言提供了接近操作系统层面的编程能力,当涉及到与操作系统直接交互的特定功能,如:系统调用或使用特定平台的库函数时,就会出现不同平台间的实现差异。
> * ② `这些差异并不是由 C 语言本身的语法差异引起的,而是由不同操作系统平台的不同特点所引起的`
> * ③ 这种差异性一方面影响了 C 语言的跨平台性,也增加了 C 语言的学习成本,例如:尽管 Windows 和 Linux 平台上的 C 语言核心编程逻辑相似,但在实现平台特定功能时所需的 API 调用和编程细节可能大不相同。
> * ④ 需要注意的是,尽管存在平台间的差异,但这并不意味着 Windows 和 Linux 下的 C 语言是两种截然不同的语言。适应不同的平台环境和编程接口对程序员而言,不算什么特别困难的事情。

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

Before

Width:  |  Height:  |  Size: 505 KiB

After

Width:  |  Height:  |  Size: 505 KiB

View File

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 818 KiB

After

Width:  |  Height:  |  Size: 818 KiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 308 KiB

After

Width:  |  Height:  |  Size: 308 KiB

View File

Before

Width:  |  Height:  |  Size: 359 KiB

After

Width:  |  Height:  |  Size: 359 KiB

View File

Before

Width:  |  Height:  |  Size: 590 KiB

After

Width:  |  Height:  |  Size: 590 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

Before

Width:  |  Height:  |  Size: 675 KiB

After

Width:  |  Height:  |  Size: 675 KiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 288 KiB

View File

Before

Width:  |  Height:  |  Size: 821 KiB

After

Width:  |  Height:  |  Size: 821 KiB

View File

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View File

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 424 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 MiB

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 803 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 786 KiB

View File

Before

Width:  |  Height:  |  Size: 509 KiB

After

Width:  |  Height:  |  Size: 509 KiB

View File

Before

Width:  |  Height:  |  Size: 501 KiB

After

Width:  |  Height:  |  Size: 501 KiB

View File

Before

Width:  |  Height:  |  Size: 494 KiB

After

Width:  |  Height:  |  Size: 494 KiB

View File

Before

Width:  |  Height:  |  Size: 500 KiB

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View File

Before

Width:  |  Height:  |  Size: 502 KiB

After

Width:  |  Height:  |  Size: 502 KiB

View File

Before

Width:  |  Height:  |  Size: 488 KiB

After

Width:  |  Height:  |  Size: 488 KiB

View File

Before

Width:  |  Height:  |  Size: 508 KiB

After

Width:  |  Height:  |  Size: 508 KiB

View File

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 261 KiB

View File

Before

Width:  |  Height:  |  Size: 341 KiB

After

Width:  |  Height:  |  Size: 341 KiB

View File

Before

Width:  |  Height:  |  Size: 317 KiB

After

Width:  |  Height:  |  Size: 317 KiB

View File

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 280 KiB

View File

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 191 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

View File

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

View File

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 142 KiB

View File

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

View File

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 193 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

Before

Width:  |  Height:  |  Size: 571 KiB

After

Width:  |  Height:  |  Size: 571 KiB

View File

Before

Width:  |  Height:  |  Size: 505 KiB

After

Width:  |  Height:  |  Size: 505 KiB

View File

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 151 KiB

View File

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 164 KiB

View File

Before

Width:  |  Height:  |  Size: 818 KiB

After

Width:  |  Height:  |  Size: 818 KiB

View File

Before

Width:  |  Height:  |  Size: 242 KiB

After

Width:  |  Height:  |  Size: 242 KiB

View File

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 190 KiB

View File

Before

Width:  |  Height:  |  Size: 461 KiB

After

Width:  |  Height:  |  Size: 461 KiB

View File

Before

Width:  |  Height:  |  Size: 571 KiB

After

Width:  |  Height:  |  Size: 571 KiB

View File

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 187 KiB

View File

@ -1,806 +1,1066 @@
# 第一章:CLion 高级配置(⭐)
# 第一章:开发环境的安装和配置(⭐)
## 1.1 安装和配置 WSL2
## 1.1 什么是编译器?
### 1.1.1 概述
* 要开发 C/C++ 程序,需要安装 C/C++ 编译器,目前有两种主流实现,即:
* GCCGNU Compiler Collection全平台实现即支持 Windows、MacOS、Linux 等。
* MSVCMicrosoft Visual C++):只支持 Windows 系统。
* GCC 在 Windows 上的版本有很多,如:[MinGW-w64](https://www.mingw-w64.org/)、[Gygwin](https://cygwin.com/)、[MSYS2](https://www.msys2.org/)。它们之间的区别,如下所示:
* WSL2全称为 Windows Subsystem for Linux 2是微软提供的一种技术允许用户在 Windows 操作系统上运行 Linux 内核。WSL2 是 WSL1 的升级版,它引入了一个真正的 Linux 内核来代替 WSL1 中使用的兼容层,从而提供更高的性能和更广泛的系统调用支持。
* 和传统的虚拟化技术的对比,如下所示:
![](./assets/1.svg)
| 特性 | MinGW-w64 | Cygwin | MSYS2 |
| ------------ | ----------------------------- | ------------------------------------- | ----------------------------------------------------- |
| **简介** | Minimalist GNU for Windows | POSIX 兼容环境和工具集 | 结合了 MinGW 和 Cygwin 的工具集 |
| **编译器** | 提供 GCC 编译器 | 提供 POSIX 兼容环境,包含大量工具 | 提供 MinGW-w64 工具链和 Cygwin 环境 |
| **生成文件** | Windows 原生可执行文件 | POSIX 兼容的可执行文件 | 可以生成 Windows 原生可执行文件或 POSIX 文件 |
| **依赖** | 无需额外依赖 | 依赖 Cygwin DLL | 根据使用工具链决定MinGW-w64 无依赖Cygwin 有依赖) |
| **工具和库** | 基本的编译工具 | 丰富的 Unix 工具和库 | 丰富的工具和库,强大的包管理系统 |
| **性能** | 性能较好 | 可能较低,因为通过兼容层调用系统 | 取决于使用的工具链MinGW-w64 性能较好) |
| **复杂度** | 简单易用 | 设置和使用相对复杂 | 较为灵活,复杂度介于 MinGW-w64 和 Cygwin 之间 |
| **适用场景** | 开发 Windows 原生应用 | 运行和开发 Unix 程序 | 混合使用 Unix 工具和开发 Windows 原生应用 |
| **优点** | 轻量级,直接生成 Windows 应用 | 完整的 POSIX 兼容环境,丰富的工具 | 灵活的环境,强大的包管理系统 |
| **缺点** | 工具和库较少 | 生成文件依赖 Cygwin DLL性能可能较低 | 环境较大,占用更多空间,复杂性比 MinGW-w64 高 |
> [!NOTE]
>
> WSL2 的功能,如下所示:
>
> * ① **真实的 Linux 内核**WSL2 使用了微软开发的轻量级虚拟机,它包含了一个完整的 Linux 内核。这意味着 WSL2 能够运行更多的 Linux 应用程序,并且支持更多的系统调用。
> * ② **文件系统性能提升**WSL2 的文件系统性能比 WSL1 有显著提升。对于 I/O 密集型的操作编译代码或数据库操作WSL2 能够提供更快的速度。
> * ③ **兼容性增强**:由于使用了真实的 Linux 内核WSL2 对 Linux 应用程序的兼容性大幅提高。许多在 WSL1 上不能运行或需要调整的应用程序,可以在 WSL2 上直接运行。
> * ④ **网络功能改进**WSL2 提供了更好的网络集成,能够更容易地与 Windows 上的其他网络资源进行交互。
> * ⑤ **资源使用优化**WSL2 使用轻量级虚拟机,比传统的虚拟机占用更少的资源,同时提供了类似的隔离和安全性。
> * ① MinGW-w64 、Cygwin 以及 MSYS2 任选其一安装即可。
> * ② 目前的 Win10 和 Win11 版本支持 WSL2 Windows Sub Linux 2 ,即 Windows 的子系统 Linux可以实现在 Windows 系统上安装一个 Linux ,然后再运行 Linux 中的 GCC 工具链。
> * ③ 本人的操作系统是 Win11 ,安装和配置都将以该系统为基础作为演示,后续不再赘述!!!
## 1.2 编译器的安装和配置
### 1.2.2 MinGW-w64 的安装和配置
#### 1.2.2.1 安装
* 下载到本地:略。
> [!NOTE]
>
> WSL2 的用途,如下所示:
> 下载地址在[这里](https://github.com/niXman/mingw-builds-binaries/releases/download/13.2.0-rt_v11-rev1/x86_64-13.2.0-release-posix-seh-ucrt-rt_v11-rev1.7z)。
![](./assets/1.png)
* 解压到指定的目录,即:
> [!NOTE]
>
> * ① **开发环境**WSL2 为开发者提供了一个原生的 Linux 开发环境,而无需离开 Windows 。这对于需要在 Linux 上开发、测试或运行应用程序的开发者非常有帮助。
> * ② **学习和实验**:用户可以使用 WSL2 在 Windows 上学习和实验 Linux 命令行工具和应用程序,而无需设置双重引导系统或安装虚拟机。
> * ③ **多平台开发**对于跨平台开发者来说WSL2 允许他们在一个操作系统上同时进行 Windows 和 Linux 平台的开发和测试,提高工作效率。
> * ④ **运行 Linux 工具和应用程序**WSL2 支持在 Windows 上直接运行各种 Linux 工具和应用程序Docker、数据库、编程语言环境等。
### 1.1.2 WSL2 的安装
* ① BIOS 或 UEFI 中,开启虚拟化:步骤略。
> 本人的解压目录是:`D:\develop\mingw64` 。
![](./assets/2.png)
* ② 查看是否开启了虚拟化:
#### 1.2.2.2 配置 path 环境变量
* 配置环境变量,以便任意目录都可以执行 gcc 命令,即:
![](./assets/3.png)
* ③ 启用适用于 Linux 的 Windows 子系统:
![](./assets/4.png)
> [!IMPORTANT]
![](./assets/5.png)
![](./assets/6.png)
> [!NOTE]
>
> 以管理员身份打开 PowerShell 并运行,执行完下面命令之后,如果提示需要重启计算机,那就重启计算机!!!
> 因为,本人安装 MinGW-w64 的目录是 `D:\develop\mingw64`,所以本人需要配置的 path 环境变量就是`D:\develop\mingw64\bin`。
```powershell
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
```
![](./assets/7.png)
![](./assets/4.gif)
![](./assets/8.png)
* ④ 启用虚拟机功能
* 测试是否安装成功
> [!IMPORTANT]
>
> 以管理员身份打开 PowerShell 并运行,执行完下面命令之后,如果提示需要重启计算机,那就重启计算机!!!
```powershell
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
```
![](./assets/5.gif)
* ⑤ 更新 Linux 内核包:
> [!IMPORTANT]
>
> WSL2 的最新 Linux 内核包托管在 GitHub 上,某些国家可能会污染 Github 相关的域名,那么就需要手动下载,然后安装即可,下载地址在[这里](https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi)。
```powershell
wsl --update
```
![](./assets/6.gif)
* ⑥ 将 wsl2 设置为默认版本:
```powershell
wsl --set-default-version 2
```
![](./assets/7.gif)
* ⑦ 查看官方在线支持的 Linux 版本:
```powershell
wsl --list --online
```
![](./assets/8.gif)
* ⑧ 安装指定版本的 Linux
> [!IMPORTANT]
>
> 官方支持的 Linux 版本,托管在 Github 上,某些国家可能会污染 Github 的域名,有如下两种解决方案:
>
> * ① 科学上网。
> * ② 在 `Microsoft Store` 中搜索并安装。
```powershell
wsl --install Ubuntu-24.04
```shell
gcc --version
```
![](./assets/9.gif)
* ⑨ 在 Microsoft Store 中搜索并安装(可选):
### 1.2.3 Cygwin 的安装和配置
#### 1.2.3.1 安装
* 下载到本地:略。
> [!NOTE]
>
> 下载地址在[这里](https://www.cygwin.com/install.html)。
![](./assets/10.png)
* ⑩ 查询本地安装的 Linux 版本:
* 点击安装:
```powershell
wsl --list
```
![](./assets/11.png)
![](./assets/11.gif)
![](./assets/12.png)
### 1.1.3 配置 WSL2
![](./assets/13.png)
* 本人的安装的是 AlmaLinux9 ,所以需要执行如下命令,以便安装 cmake 相关工具链:
![](./assets/14.png)
```shell
sudo dnf update -y # 更新包管理器
sudo dnf groupinstall "Development Tools" -y # 安装开发工具包
sudo dnf install gcc gcc-c++ -y # 安装GCC相关工具链
sudo dnf install cmake -y # 安装 cmake
sudo dnf install make -y # 安装 make
sudo dnf install gdb -y # 安装 gdb
```
![](./assets/15.png)
![](./assets/12.gif)
* 可以通过 CLion 测试是否安装成功:
![](./assets/13.gif)
### 1.1.4 配置 WSL2
* 本人的安装的是 Ubuntu 24.04,所以需要执行如下命令,以便安装 cmake 相关工具链:
```shell
sudo apt update && sudo apt upgrade -y # 更新包管理器
sudo apt install build-essential -y # 安装开发工具包
sudo apt install gcc g++ -y # 安装 GCC 相关工具链
sudo apt install cmake -y # 安装 cmake
sudo apt install gdb -y # 安装 gdb
```
![](./assets/14.gif)
* 可以通过 CLion 测试是否安装成功:
![](./assets/15.gif)
## 1.2 切换 CLion 中的 cmake 的工具链
* 可以在 CLoin 中切换 cmake 的工具链,以便支持不同平台的 cmake ,即:
![](./assets/16.gif)
## 1.3 修改 CMakeLists.txt 文件
* 前文也提到了,在一个 C 语言项目中,只能有一个 main() 函数;但是,我们可以修改 `CMakeLists.txt` 文件的内容,以便其支持在一个 C 语言项目中,可以同时运行多个包含 main() 函数的文件。
> [!NOTE]
>
> * ① 其实,这样设置的目的:就是为了让每个 `.c` 文件都可以编译为一个独立的可执行文件,而不是所有的 `.c` 文件编译为一个可执行文件。
> * ② 在实际开发中,对于 C 语言项目而言,当然必须只能有一个 `main()` 函数(只有一个 `.c` 文件包含 `main()` 函数,其余的 `.c` 文件中包含函数声明或函数实现),因为程序有且仅有一个入口。
* `CMakeLists.txt` 文件的位置,如下所示:
![](./assets/16.png)
![](./assets/17.png)
* `CMakeLists.txt` 文件的内容,如下所示
* 选择需要安装的软件包:
```txt
cmake_minimum_required(VERSION 3.22.1)
# 项目名称和版本号
project(c-study VERSION 1.0 LANGUAGES C)
# 设置 C 标准
set(CMAKE_C_STANDARD 23)
set(CMAKE_C_STANDARD_REQUIRED True)
set(CMAKE_C_EXTENSIONS OFF)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g")
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
add_definitions(-D_DEBUG)
elseif (CMAKE_BUILD_TYPE STREQUAL "Release")
add_definitions(-DNDEBUG)
elseif (CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
add_definitions(-DRELWITHDEBINFO)
elseif (CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
add_definitions(-DMINSIZEREL)
endif ()
# 辅助函数,用于递归查找所有源文件
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 ()
```
## 1.4 配置 .clang-format 文件
* 配置 `.clang-format` 格式化文件,以便写代码的时候,可以自动保存并格式化 C 程序代码,如下所示:
> [!NOTE]
>
> 默认是最小化安装,没有 GCC需要选择 gcc-core、gcc-g++、make、gdb、binutils 。
![](./assets/18.png)
* `.clang-format` 的内容,如下所示
* 安装 gcc-core
```txt
BasedOnStyle: LLVM
IndentWidth: 4
UseTab: Never
ColumnLimit: 0
# 控制大括号的位置
BreakBeforeBraces: Attach
# 控制空行的使用
EmptyLineBeforeAccessModifier: Never
KeepEmptyLinesAtTheStartOfBlocks: true
# 控制短函数、短 if 语句和循环的格式
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
# 控制其他格式选项
AlignConsecutiveAssignments: true
AlignConsecutiveDeclarations: true
# 控制注释的格式化
ReflowComments: true
# 控制包含指令的格式化
IncludeBlocks: Regroup
SortIncludes: CaseInsensitive
SpaceBeforeParens: ControlStatements
SpacesInParentheses: false
SpacesInAngles: false
SpacesInContainerLiterals: false
SpacesInCStyleCastParentheses: false
# 控制 switch-case 格式
IndentCaseLabels: true
```
* CLion 中配置`保存`的时候`自动格式化`,即:
![](./assets/19.gif)
## 1.5 配置 .gitignore 文件
* 需要在项目中,配置 `.gitignore` 文件,以便在提交代码到 Git 仓库的时候,忽略某些文件或目录,如下所示:
![](./assets/19.png)
![](./assets/20.png)
* `.gitignore` 文件的内容,如下所示
* 安装 gcc-g++
```txt
.vscode
.idea
cmake-build-*
build
```
![](./assets/21.png)
## 1.6 演示
* 我们可以在项目中,临时创建或复制一个文件,看上述配置是否生效,即:
> [!NOTE]
>
> 如果是复制并粘贴一个文件到项目中,请点击`重新加载 CMake 项目`
![](./assets/21.gif)
# 第二章C 语言的编译过程(⭐)
## 2.1 概述
* C 程序的编译过程,如下所示:
* 安装 make
![](./assets/22.png)
* 过程 ① :编写(编辑)源代码,即:编写 C 语言源程序代码,并以文件的形式存储在磁盘中。
* 安装 gdb
![](./assets/23.png)
* 安装 binutils
![](./assets/24.png)
![](./assets/25.png)
![](./assets/26.png)
![](./assets/27.png)
#### 1.2.3.2 配置 path 环境变量
* 和 `1.2.2.2 配置 path 环境变量` 步骤相同:略。
### 1.2.4 MSYS2推荐
#### 1.2.4.1 安装
* 下载到本地:略。
> [!NOTE]
>
> 源程序需要以 `.c` 作为扩展名。
> 下载地址在[这里](https://www.msys2.org/)。
* 过程 ② :编译,即:将 C 语言源程序转换为`目标程序(或目标文件)`。如果程序没有错误,没有任何提示,就会生成一个扩展名为 `.obj``.o` 的二进制文件。C 语言中的每条可执行语句经过编译之后,最终都会转换为二进制的机器指令。
![](./assets/28.png)
* 点击安装:
![](./assets/29.png)
![](./assets/30.png)
![](./assets/31.png)
> [!NOTE]
>
> * ① 其实,`编译阶段`包含了`预处理`、`编译`和`汇编`。
>
> * ② `预处理`是编译过程的第一个阶段。在这个阶段,预处理器处理源代码中的指令(例如:`#include`、`#define`等),主要任务包括:
>
> * 头文件包含:将头文件的内容插入到源文件中。例如:`#include <stdio.h>`会被替换为`stdio.h`文件的内容。
> * 宏展开:替换宏定义。例如:`#define PI 3.14`会将代码中的`PI`替换为`3.14`。
> * 条件编译:根据条件指令(如:`#ifdef`、`#ifndef`)有选择地编译代码。
>
> * 删除代码中的注释,但是不会进行语法检查。
>
> * 预处理完成后,生成一个扩展名为`.i`的中间文件。
>
> * ③ `编译`是将预处理后的源代码转换为汇编代码的过程。在这个阶段,编译器会检查代码的语法和语义,将其转换为目标机器的汇编语言,生成一个扩展名为`.s`的汇编文件。
>
> * ④ `汇编`是将汇编代码转换为机器代码(也称为目标代码或目标文件)的过程。在这个阶段,汇编器将汇编指令转换为二进制机器指令,生成一个扩展名为`.o`或 `.obj`的目标文件。
> 可能很多人,会遇到安装到 `50%` 就一直卡死在那边,不用慌,关闭它,再次重新安装即可。
* 过程 ③ :链接(连接),即:将编译形成的目标文件 `*.obj``*.o`和库函数以及其他目录文件`链接`,形成一个统一的二进制文件 `*.exe`
![](./assets/32.png)
>[!NOTE]
>
>* 为什么需要链接库文件?
>* 因为我们的 C 程序会使用 C 程序库中的内容,如:`<stdio.h>` 中的 `printf()` 函数,这些函数不是程序员自己写的,而是 C 程序库中提供的因此需要链接。其实在链接过程中还会加入启动代码这个启动代码和系统相关Linux 下主要有 crt0.c、crti.c 等,它们设置堆栈后,再调用 main() 函数)负责初始化程序运行时的环境。
* 点击运行 MSYS2
* 过程 ④ :执行,即:有了可执行的 `*.exe`文件,我们就可以在控制台上执行运行此 `*.exe` 文件。
![](./assets/33.png)
* 出现命令终端:
> [!NOTE]
>
> 如果`修改`了源代码,还需要重新`编译`、`链接`,并生成新的 `*.exe`文件,再执行,方能生效
> 如果没有出现命令终端,也不要慌,去 Win11 操作系统的`开始`菜单,那边找一下,就能找到。
## 2.2 GCC 编译器的介绍
![](./assets/34.png)
* 编辑器vim 、vscode 等,是指我们用它来编写源程序的(编辑代码),而我们写的代码语句,电脑是不懂的,我们需要把它转成电脑能懂的语句,编译器就是这样的转化工具。换言之,我们用编辑器编写程序,由编译器编译后才可以运行!
* 编译器是将易于编写、阅读和维护的高级计算机语言翻译为计算机能解读、运行的低级机器语言的程序。
* gccGNU Compiler CollectionGNU 编译器套件),是由 GNU 开发的编程语言编译器。gcc 原本作为 GNU 操作系统的官方编译器,现已被大多数类 Unix 操作系统Linux、BSD、Mac OS X 等采纳为标准的编译器gcc 同样适用于微软的 Windows 。
* gcc 最初用于编译 C 语言,随着项目的发展, gcc 已经成为了能够编译 C、C++、Java、Ada、fortran、Object C、Object C++、Go 语言的编译器大家族。
* 替换清华镜像源(可选):
## 2.3 通过 gcc 直接生成可执行文件
```shell
sed -i \
"s#https\?://mirror.msys2.org/#https://mirrors.tuna.tsinghua.edu.cn/msys2/#g" \
/etc/pacman.d/mirrorlist*
```
* 示例:进行预处理、编译、汇编和链接
![](./assets/35.gif)
* 安装 gcc 等相关开发包:
```shell
pacman -Syu --noconfirm # 更新包管理器
```
```shell
pacman -Sy base-devel --noconfirm # 安装开发工具包
```
> [!NOTE]
>
> 也许,你会看到其他人的安装命令是 `pacman -Sy mingw-w64-x86_64-toolchain --noconfirm`,此处解释下两者的区别:
>
> * ① `mingw-w64-x86_64-toolchain` 使用更传统的 MSVCRT适合需要经典 MinGW 环境的项目或依赖较老 C 运行时的应用。
> * ② `mingw-w64-ucrt-x86_64-toolchain` 使用 Microsoft 的 UCRT更适合现代 Windows 开发,提供更好的兼容性和性能。
```shell
pacman -Sy mingw-w64-ucrt-x86_64-toolchain --noconfirm # 安装开发 gcc 相关工具链
```
![](./assets/36.gif)
#### 1.2.4.2 配置 path 环境变量
* 和 `1.2.2.2 配置 path 环境变量` 步骤相同:略。
> [!NOTE]
>
> 本人需要配置的 path 环境变量是 `C:\msys64\ucrt64\bin`
## 1.3 什么是 IDE集成开发环境
* 在实际开发中,除了`编译器`是必须安装的工具之外,我们往往还需要很多其他的辅助软件,如下所示:
* 编辑器:用来编写代码,并且给代码着色,以方便阅读。
* 代码提示器:输入部分代码,即可提示全部代码,加速代码的编写过程。
* 调试器:观察程序的每一个运行步骤,发现程序的逻辑错误。
* 项目管理工具:对程序涉及到的所有资源进行管理,包括:源文件、图片、视频、第三方库等。
* 漂亮的界面:各种按钮、面板、菜单、窗口等控件整齐排布,操作更方便。
* 这些工具通常被打包在一起统一安装和发布Visual Studio、CLion 以及 VS Code 通常统称为集成开发环境IDEIntegrated Development Environment
> [!NOTE]
>
> * ① IDE集成开发环境就是一系列开发工具的组合套装。这就好比台式机核心部件是主机。主机就相当于 IDE 的代码编辑器和编译器有了它们开发者就可以进行基本的编程工作。然而正如我们在购买台式机时通常还会附带显示器、键盘、鼠标、U盘、摄像头等外围设备IDE 也同样提供了一系列额外的工具和插件,比如:调试器、版本控制集成、代码补全、代码重构工具等。这些“外围设备”让开发过程更加高效、直观,并且能够满足更多的开发需求,使得 IDE 成为一个完整的开发环境。
> * ② 严格来讲, VS Code 属于编辑器,而不是 IDE但是可以通过安装各种插件来完成 IDE 的功能;而 Visual Studio 和 CLion 属于 IDE。
> * ③ 在实际开发中,使用 IDE 进行编程并不是一种非常丢人的事情。而使用编辑器,如:`记事本`、`vi/vim` 等,进行编程,也并不是一件非常骄傲的事情。可能有些人会在网上发布这样的言论:“学习编程,刚开始需要使用`记事本`或 `vi/vim`等简单的编辑器软件,不要使用 IDE ”,目的可能是为了让初学者熟悉编程的基础概念和语法,并避免依赖 IDE 的辅助功能。但是,这种方法或许可以起到锻炼基础技能的功能,但这并不意味着 IDE 就不适合初学者。事实上,许多 IDE 还提供了初学者友好的界面和工具,可以帮助新手更快地入门和理解编程。
## 1.4 IDE 的安装和配置
### 1.4.1 CLion
#### 1.4.1.1 概述
* [CLion](https://www.jetbrains.com/clion/) 是一款由 JetBrains 推出的跨平台 C/C++ 集成开发环境IDE它具有智能编辑器、CMake 构建支持、调试器、单元测试、代码分析等功能,可以极大提高 C/C++ 开发效率。
> [!NOTE]
>
> * ① 本次,演示的 CLion 的安装版本是 `2024.2.1` ,后续版本可能会更新,但是操作几乎不会发生太多变化!!!
> * ② CLion 作为一个 IDE本身就携带了各个平台操作系统的 C 语言编译器Windows 中就是 MinGW但是CLion 中自带的 C 语言编译器版本可能并非我们实际开发所想要的(版本不是很高),这也是在 Windows 中,为什么推荐使用 `MSYS2` 的原因所在。
#### 1.4.1.2 安装
* 鼠标双击,进入安装:
![](./assets/37.png)
* 下一步:
![](./assets/38.png)
* 下一步:
![](./assets/39.png)
* 下一步:
![](./assets/40.png)
* 安装:
![](./assets/41.png)
* 安装完成:
> [!NOTE]
>
> 通常安装完成之后,桌面上会出现 CLion 的快捷方式,可以点击此快捷方式,以便快速启动 CLion 。
![](./assets/42.png)
#### 1.4.1.3 配置
* 打开 CLion
![](./assets/43.png)
* 切换中文界面(可选):
> [!NOTE]
>
> 对于以中文、韩语和日语为母语的开发者,`CLion` 在 `2024.2` 版本之后就绑定了本地化插件,即不需要再安装本地化插件了。
![](./assets/44.gif)
* 配置 UI
![](./assets/45.png)
![](./assets/46.png)
* 配置自定义字体(可选):
![](./assets/47.png)
* 配置 `系统设置`相关功能:
![](./assets/48.png)
![](./assets/49.png)
* 配置`文件编码` 为 UTF-8
![](./assets/50.png)
* 配置`控制台编码`为 UTF-8
![](./assets/51.png)
* 配置`显示方法分隔符`功能:
![](./assets/52.png)
* 配置`编辑器`的字体(可选):
> [!NOTE]
>
> 本人是安装了 `Fira Code` 字体,如果你也需要安装此字体,可以去 [GitHub](https://github.com/) 搜索并下载。
![](./assets/53.png)
* 检测 GCC 工具链是否安装成功:
![](./assets/54.png)
### 1.4.2 VS Code
#### 1.4.2.1 概述
* [Visual Studio Code (VS Code)](https://code.visualstudio.com/) 是一个免费的开源代码编辑器,适用于 Windows、MacOS 和 Linux 平台。它支持语法高亮、智能代码补全IntelliSense、内置调试工具和Git集成。用户可以通过扩展来添加更多功能支持新的编程语言、主题和调试工具。VS Code 还支持在微软 Azure 上进行部署和托管,适用于各种编程语言和框架。
> [!NOTE]
>
> * ① Visual Studio Code 需要安装对应的插件,才能运行 C/C++ 代码。
> * ② Visual Studio Code 除了开源免费的优点之外,还有一个优点就是插件巨多(几乎所有主流的编程语言都提供有对应的插件),这也是很多程序员喜欢使用它的原因。
#### 1.4.2.2 安装
* 鼠标双击,进入安装:
![](./assets/55.png)
* 同意协议:
![](./assets/56.png)
* 下一步:
![](./assets/57.png)
* 下一步:
![](./assets/58.png)
* 下一步:
![](./assets/59.png)
* 安装:
![](./assets/60.png)
* 安装过程:
![](./assets/61.png)
* 安装完成:
![](./assets/62.png)
#### 1.4.2.3 配置
* 安装`中文`插件:
![](./assets/63.png)
![](./assets/64.png)
* 安装 `C/C++` 插件:
![](./assets/65.png)
![](./assets/66.png)
### 1.4.3 Microsoft Visual Studio
#### 1.4.3.1 概述
* [Visual Studio](https://visualstudio.microsoft.com/)(简称 VS是由微软公司发布的集成开发环境。它包括了整个软件生命周期中所需要的大部分工具UML 工具、代码管控工具、项目版本控制 Git 等。
* Visual Studio 支持 C/C++、C#、F#、VB 等多种程序语言的开发和测试,可以用于生成 Web 应用程序,也可以生成桌面应用程序,功能十分强大,但下载和安装很可能耗时数小时,还可能会塞满磁盘。
* Visual Studio 有三种版本:社区版(免费,不支持企业使用),专业版(收费)和企业版(收费)。企业版拥有面向架构师的功能、高级调试和测试,这些功能是另外两种版本所没有的。
* Visual Studio 旨在成为世界上最好的 IDE集成开发环境号称“宇宙第一强大 IDE”。
> [!NOTE]
>
> 本人安装的 Visual Studio 的安装版本是 Visual Studio 2022 ,后续版本可能会更新,但是操作几乎不会发生太多变化!!!
#### 1.4.3.2 安装
* 鼠标双击,进入安装:
![](./assets/67.png)
* 继续:
![](./assets/68.png)
* 等待:
![](./assets/69.png)
* 工作负荷(使用 C++ 的桌面开发):
![](./assets/70.png)
* 单个组件:
![](./assets/71.png)
* 语言包:
![](./assets/72.png)
* 安装位置(修改默认的安装位置):
![](./assets/73.png)
* 如果不是第一次安装,可能会出现`共享组件、工具和 SDK`不可以修改,即:
![](./assets/74.png)
* 此时,就需要打开`注册表编辑器`,将如下图中的除了第一个选项,全部删除,然后关闭再重新安装,即:
![](./assets/75.png)
* 开始安装:
![](./assets/76.png)
* 安装中:
![](./assets/77.png)
* 安装完成,然后关闭:
![](./assets/78.png)
#### 1.4.3.3 配置
* 在开始菜单处,启动 VS
![](./assets/79.png)
* 登录或跳过该选项(有微软账号就注册,没有就暂时跳过):
![](./assets/80.png)
* 继续:
![](./assets/81.png)
* 注册 VS
![](./assets/82.png)
* 填写注册码:
> [!NOTE]
>
> * ① Pro 版本:`TD244-P4NB7-YQ6XK-Y8MMM-YWV2J`
> * ② Enterprise 版本:`VHF9H-NXBBB-638P6-6JHCY-88JWH`
![](./assets/83.png)
![](./assets/84.png)
## 1.5 什么是工程/项目
### 1.5.1 概述
* 一个真正的软件往往包含多项功能,每一项功能都需要几十行、几千行甚至几万行的代码,如果我们将这些代码都放到一个源文件中,不但打开的速度极慢,代码的编写和维护也会变得非常困难。
* 在实际开发中,随着软件规模的增加,代码的复杂性也会显著提升,为了提高代码的易读性、维护性等,程序员会将代码按照功能分别放到不同的源文件中。
> [!NOTE]
>
> 需要说明的是,一个真正的软件除了源代码之外,往往还会包括图片、视频、音频、库(框架)等其它资源,这些也是一个个的文件。
* 为了有效的管理这些种类繁杂、数目众多的文件,我们会将这些文件按照功能放到不同的目录中进行统一管理,并且这个目录下只存放与当前程序有关的资源。其实,这就是工程或项目。
> [!NOTE]
>
> 总结:
>
> * ① 随着软件规模的增加,代码的复杂性也会显著提升。将代码分割成多个模块或文件并分别管理,可以减少每个文件的复杂度,使代码更易读、易理解、易维护。工程提供了一个结构化的环境,将这些文件组织在一个系统化的目录结构中。
> * ② 除了代码,软件开发还涉及到各种资源的管理,如:图片、音频、视频、配置文件等。工程能够帮助开发者将这些资源合理地分类存放,并与代码一同管理,确保它们在开发、编译和运行时能被正确引用。
* 许多 IDE 都提供了工程或项目的概念,其目的就是为了帮助开发者合理的管理软件开发中所需要的资源,如:图片、视频、音频、库(框架)等。
![](./assets/85.png)
### 1.5.2 工程类型/项目类型
* 程序或软件是一个非常宽泛的概念,它可以细分为很多种类,如下所示:
* 控制台程序Console Application控制台程序是一种不具备图形用户界面的程序它通过文本方式与用户交互通常运行在命令行窗口黑底白字的终端Unix/Linux 中的`ls`命令、Windows 中的`cmd.exe`等。
* GUI 程序Graphical User Interface ProgramGUI 程序是一种具有图形用户界面的程序通过窗口、按钮、菜单等图形控件与用户交互微信、QQ 等。
* 静态库和动态库:不单独出现,而是作为其它程序的一个组成部分,普通用户很难接触到它们。
* 静态库指的是在编译时包含到程序中的库,程序不依赖外部文件运行,如:在 C/C++ 中,静态库通常以`.lib`Windows或`.a`Unix/Linux为扩展名。
* 动态库指的是在运行时加载的库,允许多个程序共享,并且程序在运行时依赖这些库,如: 在Windows中动态库通常以`.dll`为扩展名;在 Unix/Linux 中,以`.so`为扩展名。
* 不同类型的程序控制台程序、GUI 程序、静态库、动态库等)需要不同的配置和文件结构,因此在 IDE 中创建项目时,选择正确的工程类型非常重要。不同的工程类型决定了 IDE 为我们生成的初始文件、目录结构,以及预设的一些编译和链接参数。
> [!IMPORTANT]
>
> * ① 控制台程序适合初学者,因为它更简单,没有复杂的界面元素,开发时可以专注于逻辑和代码本身。
> * ② 而 GUI 程序则涉及到用户界面设计和事件驱动编程,更适合有一定编程基础的人进行学习和开发。
# 第二章C 语言入门HelloWorld
## 2.1 手动版
* ① 新建一个 `HelloWorld.c` 的文件:
![](./assets/86.png)
* ② 通过`记事本`等软件打开该文件,输入如下的代码,并保存:
```c
#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}
```
![](./assets/87.gif)
* ③ 通过 `gcc` 命令编译该文件:
```shell
gcc HelloWorld.c -o HelloWorld.exe
```
![](./assets/23.gif)
![](./assets/88.gif)
## 2.4 通过 gcc 分步编译
### 2.4.1 概述
* 预处理命令:
* ④ 执行:
```shell
# 通常以 .i 结尾表示这个文件是一个中间状态
gcc -E 源文件.c -o 源文件.i
./HelloWorld.exe
```
* 编译(预处理和编译)命令:
![](./assets/89.gif)
```shell
# 在 Linux 中,通常以 .s 结尾;在 Windows 中,通常以 .asm 结尾
gcc -S 源文件.i -o 源文件.s
```
## 2.2 VS Code 版
* 汇编(预处理、编译和汇编)命令:
* ① 新建一个`空`文件夹(目录),用于存放代码:
```shell
# 在 Linux 中,通常以 .o 结尾;在 Windows 中,通常以 .obj 结尾
gcc -c 源文件.s -o 源文件.o
```
![](./assets/90.png)
* 链接(预处理、编译、汇编和链接)命令:
* ② 通过 `vscode` 打开该目录:
```shell
# 在 Linux 中,通常以 .out 结尾;在 Windows 中,通常以 .exe 结尾
gcc 源文件.o -o 源文件.exe
```
![](./assets/91.gif)
### 2.4.2 应用示例
* ③ 在 `vscode` 中新建 `HelloWorld.c` 文件:
* 示例:只进行预处理
![](./assets/92.gif)
```shell
gcc -E HelloWorld.c -o HelloWorld.i
```
* ④ 设置 VSCode 中 C/C++ 的代码格式为行尾风格(可选):
![](./assets/24.gif)
![](./assets/93.png)
![](./assets/94.png)
* 示例:只进行预处理和编译
```shell
gcc -S HelloWorld.i -o HelloWorld.s
```
![](./assets/25.gif)
* 示例:只进行预处理、编译和汇编
```shell
gcc -c HelloWorld.s -o HelloWorld.o
```
![](./assets/26.gif)
* 示例:进行预处理、编译、汇编和链接
```shell
gcc HelloWorld.o -o HelloWorld.exe
```
![](./assets/27.gif)
# 第三章:附录
## 3.1 WSL2 代理问题
* 在安装和配置 WSL2 之后,可能会出现如下的提示,即:
![](./assets/28.png)
* 那么,只需要修改 `%USERPROFILE%\.wslconfig`文件,内容如下:
> [!NOTE]
>
> 如果没有该文件,则需要自己新建该文件!!!
```txt
[wsl2]
networkingMode=mirrored
dnsTunneling=true
firewall=true
autoProxy=true
[experimental]
# requires dnsTunneling but are also OPTIONAL
bestEffortDnsParsing=true
useWindowsDnsCache=true
```
![](./assets/29.png)
* 在命令行中,执行如下的命令:
```shell
wsl --shutdown
```
![](./assets/30.gif)
* 此时,再打开终端,就没有这种提示了:
![](./assets/31.png)
## 3.2 CLion 调试问题
* 在 CLion 中进行 run运行程序的时候对于 `printf` 函数或 `scanf` 函数很正常,如下所示:
![](./assets/32.gif)
* 但是,当我们 debug调试 的时候,对于 `printf` 函数或 `scanf` 函数会一直没有输出,如下所示:
![](./assets/33.gif)
* 原因是 `scanf` 函数并不是直接让用户从键盘输入数据,而是先检查缓冲区,处理缓冲区中的数据;当遇到 `scanf` 函数时,程序会先检查输入缓冲区中是否有数据,所以解决方案就是`禁用缓冲区`,如下所示:
```c {5}
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
int a, b, c;
printf("请输入整数 a 、b 和 c 的值:");
scanf("%d %d %d", &a, &b, &c);
int result = a * b * c;
printf("%d × %d × %d = %d", a, b, c, result);
return 0;
}
```
* 那么,就会达到我们想要的效果了,如下所示:
![](./assets/34.gif)
## 3.3 Win 中文乱码问题
* 之前,我们提及到,在 Win 中,如果出现`中文乱码`问题,就需要去`语言和区别`设置`系统区域`的编码为 UTF-8 但是这样可能会造成其它的软件出现中文乱码问题Xshell 等。
> [!NOTE]
>
> * ① 之所以,修改系统的编码为 UTF-8 会出现问题,是因为早期的 Win 系统的中文默认编码是 GBK目前也是Win 并没有强制第三方软件使用 UTF-8 编码) ,而 Xshell 等也使用的这些编码,一旦我们修改为 UTF-8 之后,可能会造成这些第三方软件出现中文乱码问题(第三方软件适配问题,相信将来应该都会切换为 UTF-8 编码),体验较差!!!
> * ② 在 Linux 或 MacOS 之所以不会出现中文乱码的问题,是因为这些系统默认的编码就是 UTF-8 。
* 其实,还有一种解决方案,如下所示:
![](./assets/35.png)
![](./assets/36.png)
![](./assets/37.png)
* 测试一下,是否配置成功:
![](./assets/38.gif)
## 3.4 CLion 中自动导入头文件
* 在 CLion 中,最为强大的功能就是直接输入函数,然后让 IDE 帮我们自动导入头文件,包括自定义的头文件,相当实用。
> [!NOTE]
>
> * ① CLion 中的`自动导入头文件`的`快捷键`是 `Alt + Enter`
> * ② CLion 中的`自动提取变量的类型`的`快捷键`是 `Ctrl + Alt + V`
![](./assets/39.gif)
* 开启自动导入头文件的步骤,如下所示:
![](./assets/40.png)
![](./assets/41.png)
## 3.5 WSL2 启用 systemd
### 3.5.1 概述
* 根据 [systemd.io](https://systemd.io/)“systemd 是 Linux 系统的基本构建基块套件。 它提供一个系统和服务管理器,该管理器作为 PID 1 运行并启动系统的其余部分。”
* Systemd 主要是一个 init 系统和服务管理器,它包括按需启动守护程序、装载和自动装载点维护、快照支持以及使用 Linux 控制组进行跟踪等功能。
* 大多数主要的 Linux 发行版现在都运行 systemd因此在 WSL2 上启用它可使体验更接近于使用裸机 Linux。
> [!CAUTION]
>
> * ① 默认情况下,在 WSL2 中,只有 Ubuntu 才会将 systemd 作为 pid-1 的守护进程(微软维护和定制的 Ubuntu 版本,在 GitHub 的 Codespace 中默认的 Linux 环境就是 Ubuntu。而其余基于 WSL2 为内核的 Linux 发行版本并不会将 systemd 作为 pid-1 的守护进程,而是会使用 init 作为 pid-1 的守护进程。
> * ② 需要注意的是,很多 Linux 软件都需要 systemd 来进行管理Docker 。
> * ③ 本次以 AlmaLinux9 作为演示!!!
* 检查进程树,判断 systemd 是否正在运行:
```shell
ps -p 1 -o comm= # 如果显示 systemd ,则表示 systemd 正在运行
```
![](./assets/42.gif)
### 3.5.2 操作步骤
* ① 查询 WSL2 的版本,确保 WSL2 的版本为 `0.67.6` 或更高版本:
```shell
# 如果未满足要求,则使用 wsl --update 更新 WSL2 版本
wsl --version # 在 win 中的 cmd 或 PowerShell 执行该命令
```
![](./assets/43.png)
* ② 向 `/etc/wsl.conf` 配置文件中写入以下内容:
```shell
cat <<EOF | tee /etc/wsl.conf
[boot]
systemd=true
EOF
```
![](./assets/44.gif)
* ③ 重启 WSL 实例:
```shell
wsl --shutdown # 在 win 中的 cmd 或 PowerShell 执行该命令
```
![](./assets/45.gif)
* ④ 查看是否启用成功:
```shell
ps -p 1 -o comm=
```
![](./assets/46.png)
## 3.6 GCC 查看支持的 C 语言标准版本
### 3.6.1 概述
* GCC 是个跨平台的项目,支持 Windows、Linux 和 MacOS ,那么查看它支持的 C 语言标准版本就非常重要,以防止我们使用了新的 C 语言语法,本地却还是旧的 GCC 支持的 C 语言标准。
### 3.6.2 查看支持 C 语言标准版本的方法
* 可以执行如下的命令,查看 GCC 支持的 C 语言标准的版本:
```c
gcc -E -dM - </dev/null | grep "STDC_VERSION"
```
> [!NOTE]
>
> 其实就是通过 `__STDC_VERSION__` 的值,来查看支持的版本:
>
> * 如果没有查到,则默认是 c89 的标准。
> * 如果是 `#define __STDC_VERSION__ 199901L`,则默认支持的是 C99 标准。
> * 如果是 `#define __STDC_VERSION__ 201112L`,则默认支持是的 C11 标准。
> * 如果是 `#define __STDC_VERSION__ 201710L`,则默认支持的是 C17 标准。
> * 如果是 `#define __STDC_VERSION__ 2023xxL`,则默认支持的是 C23 标准。
>
> 需要说明的是在本文撰写之前C23 标准目前还是草案,并没有完全确定下来。
![](./assets/47.png)
### 3.6.3 切换 GCC 默认支持的 C 语言标准版本
#### 3.6.3.1 环境变量方式
* 可以通过设置一个环境变量,来更改默认的 C 语言的标准版本:
```shell
echo 'export CFLAGS="-std=c11"' >> ~/.bashrc
```
```shell
source ~/.bashrc
```
![](./assets/48.gif)
* 验证是否有效:
```shell
echo $CFLAGS
```
![](./assets/49.png)
#### 3.6.3.2 CMake 方式
* CMake 方式最简单了,只需要修改配置文件 CMakeLists.txt 文件,如下所示:
```txt {6}
cmake_minimum_required(VERSION 3.22.1)
project(c-study VERSION 1.0 LANGUAGES C)
# 设置 C 标准
set(CMAKE_C_STANDARD 23)
...
```
#### 3.6.3.3 命令行方式
* 有的时候,我们临时想验证某个版本的新特性,就可以只用在命令行中添加参数,来改变支持的 C 语言标准版本,如下所示:
```shell
gcc -std=c89 ...
```
```shell
gcc -std=c99 ...
```
```shell
gcc -std=c11 ...
```
```shell
gcc -std=c17 ...
```
## 3.7 CLion 如何集成 MSYS2?
### 3.7.1 概述
* CLion 在 Windows 中默认集成的是 `MinGW`,可能无法满足我们的需求,我们需要使用 `MSYS2` ,因为其提供的包管理器太好用了。
> [!NOTE]
>
> 需要说明的是,`MSYS2` 包含了 `MinGW`,这也是我们为什么在 `Windows` 上为什么使用 `MSYS2` 的其中一个原因。
### 3.7.2 集成方法
* ① 所有设置:
![](./assets/50.png)
* ② 工具链:
![](./assets/51.png)
## 3.8 CLion 中代码模板的使用
### 3.8.1 概述
* 在学习 C 语言的过程中,可能会不停的写这样的模板代码,如下所示:
* ⑤ 编写如下的代码,并保存:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, nullptr);
int main(){
printf("Hello World");
return 0;
}
```
* 刚开始写,还感觉比较新鲜,非常好玩。但是,随着时间的深入,我们会感觉特别繁琐,又很无聊。那么,能否在 CLion 中配置一下,让其为我们自动生成呢?
![](./assets/95.gif)
![](./assets/52.gif)
* ⑥ 通过 `gcc` 命令编译该文件:
### 3.8.2 配置方法
```shell
gcc HelloWorld.c -o HelloWorld.exe
```
* ① 点击`设置`
![](./assets/96.gif)
![](./assets/53.png)
* ⑦ 执行:
* ② `编辑器` --> `文件和代码模板`
```shell
./HelloWorld.exe
```
![](./assets/54.png)
![](./assets/97.gif)
* ③ 点击`+`,配置对应的内容:
* ⑧ 安装 Code Runner 插件(步骤略),实现右键直接编译执行(可选):
![](./assets/98.gif)
## 2.3 VS 版
* ① 新建空项目:
![](./assets/99.png)
![](./assets/100.png)
![](./assets/101.png)
![](./assets/102.png)
* ② 打开`解决方案资源管理器`
![](./assets/103.png)
![](./assets/104.png)
* ③ 新建 `HelloWorld.c` 文件:
![](./assets/105.gif)
* ④ 编写如下代码,并保存:
```c
#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}
```
![](./assets/106.gif)
* ⑤ 编译和执行:
![](./assets/107.gif)
## 2.4 CLion 版
* ① 新建空项目:
![](./assets/108.png)
![](./assets/109.png)
![](./assets/110.png)
* ② 编写如下代码,并保存:
```c
#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}
```
![](./assets/111.gif)
* ③ 编译和运行:
![](./assets/112.gif)
* ④ 默认情况下,一个项目只能有一个 c 源文件包含 main 函数,但是 CLion 可以有多个,如下:
![](./assets/113.gif)
* ⑤ 如果之后,有中文乱码问题,那么请做如下步骤:
![](./assets/114.png)
![](./assets/115.gif)
> [!NOTE]
>
> 模板的内容,如下所示:
> 内容如下所示:
>
> ```c
> #[[#include]]# <stdio.h>
>
> int main() {
>
> // 禁用 stdout 缓冲区
> setbuf(stdout, nullptr);
>
>
>
> return 0;
> }
> ```txt
> -Dfile.encoding=UTF-8
> -Dconsole.encoding=UTF-8
> ```
![](./assets/55.png)
# 第三章:五花八门的 C 语言编译器(⭐)
## 3.1 概述
* 由于 C 语言的历史比较久,而且早期没有规范,整个计算机产业也都处于拓荒的年代,所以就涌现了很多款 C 语言编译器,它们各有特点,适用于不同的平台。
## 3.2 桌面操作系统
* 目前而言,主流的桌面操作系统就是 Windows、Linux 和 MacOS 。
* 对于 Windows 而言,使用的最多的 C/C++ 编译器是 `MSVC` Microsoft Visual C++),被集成在 Visual Studio 开发环境中,其特点如下:
* ① 兼容性: 与 Windows 操作系统和 Windows API 深度集成,生成的二进制文件为 PE 格式。
* ② 调试工具: 提供强大的调试工具Visual Studio Debugger。
* ③ 优化: 支持各种编译器优化,特别是针对 Windows 平台的优化。
* ④ 库支持: 提供丰富的 Windows 专用库,如:~~MFCMicrosoft Foundation Class Library~~。
> [!NOTE]
>
> MSVC 不开源,我们可以使用 Visual Studio Community 社区版,但是如果想使用 Visual Studio Community 社区版生成出来的应用进行商用,就需要好好阅读微软的许可证和说明书了。
* 对于 Linux 而言,使用的最多的 C/C++ 编译器是 `GCC`(支持多种架构和语言),并且很多 Linux 发行版本都将 GCC 作为默认的编译器,其特点如下所示:
* ① 广泛支持: 支持各种 Linux 发行版,是大多数开源项目的默认编译器。
* ② 强大的优化: 提供各种编译优化选项,适合多种性能需求的开发。
* ③ 丰富的工具链: 和 GDBGNU 调试器、Make、Autoconf 等工具无缝集成。
> [!NOTE]
>
> 目前而言GCC 已经属于跨平台的项目了,支持 Windows、Linux 和 MacOS ,在 Windows 上 GCC 的移植项目MinGW、Cygwin 以及 MSYS2其差别如下所示
>
> * ① MinGW 提供了 GCC 编译器的 Windows 版本,可以生成 Windows 兼容的本地代码。
> * ② Cygwin 是一个在 Windows 上运行的类 Unix 环境,它提供了一套完整的 POSIX 兼容工具,包括 GCC 编译器。
> * ③ MSYS2 是一个在 Windows 上运行的轻量级、开源的 Unix-like 环境,它为 Windows 用户提供了类似于 Linux 的开发环境。MSYS2 是 MinGW 和 Cygwin 的后继者,旨在提供更现代化和更强大的开发工具集。
* 对于 MacOS 而言,使用的最多的 C/C++ 编译器是 `Clang/LLVM`,其特点如下:
* ① Xcode 集成: 深度集成到 Xcode 中,支持 Apple 的所有平台macOS、iOS、tvOS、watchOS的开发。
* ② 优化和兼容: 生成的代码针对 Apple 的硬件进行优化,并兼容 GCC 的大部分功能。
* ③ 现代化: Clang 提供了对 C 语言标准的全面支持,并且以其快速的编译速度和易读的错误报告而著称。
> [!NOTE]
>
> 在 MacOS 中,尽管 Clang 是默认编译器;但是,也可以 Homebrew 等包管理器来安装 GCC ,以便开发 C/C++ 项目。
## 3.3 嵌入式系统
* 在嵌入式系统开发中,可用的 C 语言编译器以及工具链非常丰富, 有很多是免费或开源的,如下所示:
* `GCC (GNU Compiler Collection)`
* 简介GCC 是最广泛使用的开源编译器集合之一,支持多种处理器架构,包括 ARM、AVR、MIPS、RISC-V 等。
* 开源或免费:完全开源且免费,受到广泛的社区支持。
* `Clang/LLVM`
* 简介Clang 是基于 LLVM 架构的开源编译器,支持多种架构,并且与 GCC 兼容。
* 开源或免费:开源且免费,具有快速的编译速度和现代化的代码分析工具。
* `SDCC (Small Device C Compiler)`
* 简介: SDCC 是一个开源的跨平台 C 编译器,主要用于 8 位和 16 位微控制器8051、Z80、PIC 等。
* 开源或免费:完全开源且免费,适合教育和小型项目开发。
* `MPLAB XC Compilers`
* 简介MPLAB XC 是由 Microchip 提供的编译器系列,专门用于其 PIC 和 dsPIC 微控制器。
* 开源或免费:提供免费版本(使用标准优化级别),但也有付费版本提供更高级的优化。
* `ARM GCC`
* 简介ARM GCC 是 GCC 的一个专门版本,针对 ARM Cortex-M 系列微控制器进行了优化。
* 开源或免费:完全开源且免费,广泛用于工业、教育和开源项目中。
* `PlatformIO`
* 简介PlatformIO 是一个开源的嵌入式开发生态系统,支持多种开发板、框架和编译器。
* 开源或免费:基本功能免费,部分高级功能和插件需要订阅服务。
* `Eclipse`
* 简介Eclipse 是一个开源的集成开发环境IDE可以通过插件支持嵌入式开发。
* 开源或免费Eclipse 和 GCC 都是开源免费的,适合跨平台开发。
* `Arduino IDE`
* 简介Arduino IDE 是一个简单易用的开源开发环境,广泛用于 Arduino 开发板和其他兼容开发板。
* 开源或免费:完全开源且免费,非常适合教育和入门级开发。
* ...
* 这些编译器以及工具链各有优势,开发者应根据目标硬件平台、项目需求和开发环境选择最适合的编译器。
## 3.4 C 语言为什么有那么多的编译器?
* C 语言并没有一个官方机构,也不属于哪个公司,它只有一个制定标准的委员会,任何其他组织或者个人都可以开发 C 语言的编译器,而这个编译器要遵守哪个 C 语言标准,是 100% 遵守还是部分遵守,并没有强制性的措施,也没有任何约束。
> [!NOTE]
>
> * ① 各个厂商可以根据自己的利益和喜好来开发编译器。
> * ② 市场和用户的选择通常是推动编译器开发者遵循标准的主要动力。
* 并且,不同硬件平台之间也存在差异,这会导致内存管理方式、寄存器、指令集等都有所不同,为了确保 C 语言程序能在这些硬件平台运行,就得针对该平台开发/定制不同的编译器。
> [!NOTE]
>
> * ① 上述的情况,在单片机和嵌入式领域更加常见。
> * ② 总体而言C 语言具有开放性,并且要适应不同的硬件平台,这使得不同厂商可以根据自己的需求来进行个性化开发/定制。
* 这也导致了一个非常棘手的问题,有的编译器遵守较新的 C 语言标准,有的编译器只能遵守较老的 C 语言标准,有的编译器还进行了很多扩展,比如:
* GCC、LLVM/Clang 更新非常及时,能够支持最新的 C 语言标准。
* MSVC 更新比较缓慢迟迟不能支持新标准例如VC6.0、VS2010 都在使用 C89 标准VS2015 部分支持 C99 标准。
> [!NOTE]
>
> 微软官方给出的答复:最新的标准已经在 C++ 中支持了C 语言就没必要再重复了。
* 初学者经常会遇到这种情况,有些代码在 MSVC 下能够正常运行,拿到 GCC 下就不行了,一堆报错信息; 或者反过来,在 GCC 上能运行的代码在 MSVC 下不能运行。这是因为不同的编译器支持的标准不同,每个编译器都进行了自己的扩展,假如你使用了 MSVC 自己的扩展函数,那么拿到 GCC 下肯定是不支持的。
> [!IMPORTANT]
>
> * ① 在学习的时候,无所谓使用那个 C 语言编译器了。
> * ② 但是,如果要开发实际项目(开源或商业),最好使用 `GCC` 编译器,因为其功能最强大、开源、跨平台、免费,支持最新的 C 语言标准。
# 第四章:注释(⭐)
## 4.1 概述
* 编程语言中,`注释`是一种`特殊`的文本,它不会被编译器执行,而仅用于代码的解释和文档说明。
>[!NOTE]
>
>* ① 注释是一个程序员必须有具有的良好编程习惯。
>* ② 在实际开发中,程序员可以将自己的思路通过`注释`整理出来,然后再用`代码`去实现。
## 4.2 单行注释
* C 语言中的单行注释的格式,如下所示:
```c
// 单行注释
```
> [!NOTE]
>
> 在 CLion 中的快捷键是 `Ctrl + /`
* 示例:
```c
#include <stdio.h> // 这是编译预处理指令
int main() { // 定义主函数
printf("你好,世界!!!"); // 输出所指定的一行信息
return 0; // 函数执行完毕时返回函数值0
}
```
## 4.3 多行注释
* C 语言中的多行注释的格式,如下所示:
```c
/*
 这是第一行注释
 这是第二行注释
 这是第三行注释
*/
```
>[!NOTE]
>
>* ① 多行注释不能嵌套使用!!!
>* ② 在 CLion 中的快捷键是 `Ctrl + Alt + /`
* 示例:
```c
#include <stdio.h>
int main() {
/*
printf(1);
printf(2);
*/
printf("你好,世界!!!");
return 0;
}
```
# 第五章HelloWorld 解读(⭐)
## 5.1 规范的代码风格
### 5.1.1 正确的缩进和空白
* ① 使用一次 `tab` 操作,实现缩进,默认整体向右边移动;如果使用 `shift + tab` 则整体向左移动。
* ② 运算符两边习惯各加一个空格,如:`2 + 4 = 6`。
> [!NOTE]
>
> 各种 IDE 都有格式化的快捷键CLion 的格式化快捷键是 `Ctrl + Alt + L`
* 示例:
```c
#include <stdio.h>
int main() {
int a = 1;
int b = 2;
int c = a + b;
printf("c = %d", c);
return 0;
}
```
### 5.1.2 代码风格
* 在 C 语言中,有两种代码风格:`行尾风格`和`次行风格`。
>[!NOTE]
>
>看个人爱好,任选一种即可,本人喜欢`行尾分格`
* 示例:行尾风格
```c
int main(){
if(a > b) {
return a;
} else {
return b;
}
  return 0;
}
```
* 示例:次行风格
```c
int main()
{
if(a > b)
{
return a;
}
else
{
return b;
}
  return 0;
}
```
## 5.2 代码细节剖析
### 5.2.1 main() 函数
* 在 C 语言中,一个程序或工程可以定义很多函数,但是有且只有一个 main() 函数,作为程序执行的入口,并且在 main() 函数结尾结束整个程序的运行,即:
```c
int main(){
return 0;
}
```
* 如果 main() 函数是空括号,即表示 main() 函数不接收任何参数。
* 在 main() 函数之前的 `int` 称为关键字,代表数据类型是`整型`,它是 main() 函数的返回值的类型,即在执行 main() 函数之后一定会得到一个整数类型的值,即函数值。
> [!NOTE]
>
> * ① 在 C 语言中,人们约定,如果 `return 0`,就表示 main() 函数终止运行,且运行成功;如果返回其它非零整数,则表示运行失败。
> * ② 默认情况下,如果 main() 函数中省略 `return 0` ,则编译器会自动加上。但是,为了保持统一的代码风格,不建议省略。
### 5.2.2 函数体
* ① 一对花括号 `{}` 定义了函数的主体,所有函数都必须以大括号开头和结尾,成对出现。
* ② C 程序中的函数体指的是作为该函数一部分的语句。它可以是任何操作,比如:搜索、排序、打印等。
* ③ 每一个执行语句后面都会有一个英文分号`;`作为语句结束的标志。
* ④ 一行内可写几条语句,一条语句也可写在几行上。
### 5.2.3 printf() 函数
* printf() 函数的格式,如下所示:
```c
printf (const char *__format, ...)
```
* printf() 函数是产生格式化输出的函数作用是将参数文本输出到屏幕f 表示 format格式化表示可以指定输出文本的格式
```c
printf ("Hello World"); // 将字符串输出到控制台,行尾不换行
```
* 如果想让光标移动到下一行的开头,可以在输出文本的结尾,可以添加一个换行符 `\n`,即:
```c
printf("Hello World\n");
```
### 5.2.4 标准库和头文件
#### 5.2.4.1 概述
* printf() 函数是在标准库的头文件 `stdio.h` 中定义的,要想在程序中使用这个函数,必须在源文件的头部引入该头文件,即:
```c
#include <stdio.h>
```
#### 5.2.4.2 标准库Standard Library
* C 语言的`标准库`是由一组函数组成,这些函数提供了许多常用的操作和功能,如:输入输出、字符串处理、内存管理、数学计算等。标准库中的函数由编译器提供,遵循 ANSI C 标准C89/C90、C99、C11等
* 换言之C 语言的`标准库`就是包含函数的实际代码这些代码在编译的时候被链接到我们的程序中无需手动包含。C 语言的`标准库`提供了可重用的函数实现,使得程序员不必编写常用的功能。
> [!NOTE]
>
> 实际的 printf() 函数的实现代码通常位于标准库的实现文件中,如:在 Linux 中的标准库`libc.so.6` 就包含了 printf() 函数的实现。
#### 5.2.4.3 头文件Header Files
* `头文件`是包含函数声明、宏定义、数据类型定义等内容的文件。头文件的作用是为源代码提供必要的声明和定义,以便编译器能够正确解析和链接函数调用。头文件通常以`.h`作为文件扩展名。
* 换言之,头文件包含函数声明、宏定义和数据类型定义,但不包含函数的实现。头文件告知编译器如何使用标准库中的函数和定义,确保编译时的正确性。头文件需要在源代码文件中使用`#include`指令显式包含,如:`#include <stdio.h>`。
* 常见的 C 语言头文件及其功能和常用函数、宏等,如下所示:
| 头文件 | 功能说明 | 常用函数和宏 |
| ------------- | ---------------------------------------------------------- | ------------------------------------------------------------ |
| **stdio.h** | 标准输入输出库 | `printf` `scanf` `fprintf``fscanf``fopen` `fclose``fgets` `fputs` |
| **stdlib.h** | 标准库,提供内存分配、程序控制、类型转换、随机数生成等功能 | `malloc` `free` `exit` `atoi` `atof``rand``srand` |
| **string.h** | 字符串处理库 | `strlen` `strcpy` `strncpy` `strcat` `strcmp``strstr` `memset` `memcpy` |
| **math.h** | 数学库 | `sin` `cos` `tan` `exp` `log` `sqrt` `pow` |
| **time.h** | 时间和日期库 | `time` `clock` `difftime` `mktime` `strftime` `localtime``gmtime` |
| **ctype.h** | 字符处理库 | `isalnum` `isalpha` `isdigit` `islower` `isupper` `tolower` `toupper` |
| **stdbool.h** | 布尔类型库 | `bool` `true` `false` |
| **assert.h** | 断言库 | `assert` |
#### 5.2.4.4 预处理命令
* `#include` 命令的作用是将指定文件的内容插入到包含该命令的源文件中。这通常用于包含头文件,以便使用头文件中声明的函数、宏和数据类型。
* 语法:
```c
// 用于包含标准库头文件或系统头文件。
// 编译器将在系统的标准头文件目录中查找文件。
#include <filename>
```
```c
// 用于包含用户自定义的头文件。
// 编译器首先在当前目录中查找文件,如果未找到,再在标准头文件目录中查找。
#include "filename"
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

View File

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 310 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

View File

Before

Width:  |  Height:  |  Size: 572 KiB

After

Width:  |  Height:  |  Size: 572 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 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: 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: 681 KiB

After

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

Before

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

View File

Before

Width:  |  Height:  |  Size: 614 KiB

After

Width:  |  Height:  |  Size: 614 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 486 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 495 KiB

View File

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 209 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 451 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 522 KiB

View File

Before

Width:  |  Height:  |  Size: 456 KiB

After

Width:  |  Height:  |  Size: 456 KiB

View File

Before

Width:  |  Height:  |  Size: 388 KiB

After

Width:  |  Height:  |  Size: 388 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 74 KiB

View File

Before

Width:  |  Height:  |  Size: 379 KiB

After

Width:  |  Height:  |  Size: 379 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 172 KiB

View File

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 278 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 156 KiB

View File

Before

Width:  |  Height:  |  Size: 316 KiB

After

Width:  |  Height:  |  Size: 316 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 188 KiB

View File

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 290 KiB

View File

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 169 KiB

View File

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

View File

Before

Width:  |  Height:  |  Size: 393 KiB

After

Width:  |  Height:  |  Size: 393 KiB

View File

Before

Width:  |  Height:  |  Size: 758 KiB

After

Width:  |  Height:  |  Size: 758 KiB

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

View File

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

Before

Width:  |  Height:  |  Size: 684 KiB

After

Width:  |  Height:  |  Size: 684 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -1,575 +1,534 @@
# 第一章:变量(⭐)
# 第一章:CLion 高级配置(⭐)
## 1.1 程序中变化的数据
## 1.1 安装和配置 WSL2
- 在生活中,我们使用最多的不是固定的数据,而是会变化的数据:
- ① 购物车商品的`数量`、`价格`等。
- ② 一首歌`播放的时间`、`进度条`、`歌词的展示`等。
- ③ 微信聊天中`消息条数`、`时间`、`语音的长度`、`头像`、`名称`等。
- ④ 游戏中技能的`冷却时间`、`血量`、`蓝量`、`buff 时间`、`金币的数量`等。
- ……
### 1.1.1 概述
* 下图是一个`购物车`中`变化`的`数据`,即:
* WSL2全称为 Windows Subsystem for Linux 2是微软提供的一种技术允许用户在 Windows 操作系统上运行 Linux 内核。WSL2 是 WSL1 的升级版,它引入了一个真正的 Linux 内核来代替 WSL1 中使用的兼容层,从而提供更高的性能和更广泛的系统调用支持。
* 和传统的虚拟化技术的对比,如下所示:
![](./assets/1.png)
* 那么,在实际开发中,我们就会使用`变量`来`保存`和`操作`这些`变化`的`数据`。
## 1.2 变量
* 变量的定义:变量是程序中不可或缺的组成单位,最基本的存储单元。其实,变量就是一个存储数据的临时空间,可以向其中存储不同类型的数据,如:整数、小数、字符、字符串等,并且变量中的数据在程序运行的时候可以动态改变。
![](./assets/1.svg)
> [!NOTE]
>
> * `变量`:用来`存储数据`的`容器`。
> * `数据`:可以是一个用来计算的`数字`,如:上文购物车中的`价格`等;也可以是一句话中的`关键词`或`其它任意格式的数据`。
> * 变量的`特别`之处就在于`它存放的数据是可以改变`的。
> WSL2 的功能,如下所示:
>
> * ① **真实的 Linux 内核**WSL2 使用了微软开发的轻量级虚拟机,它包含了一个完整的 Linux 内核。这意味着 WSL2 能够运行更多的 Linux 应用程序,并且支持更多的系统调用。
> * ② **文件系统性能提升**WSL2 的文件系统性能比 WSL1 有显著提升。对于 I/O 密集型的操作编译代码或数据库操作WSL2 能够提供更快的速度。
> * ③ **兼容性增强**:由于使用了真实的 Linux 内核WSL2 对 Linux 应用程序的兼容性大幅提高。许多在 WSL1 上不能运行或需要调整的应用程序,可以在 WSL2 上直接运行。
> * ④ **网络功能改进**WSL2 提供了更好的网络集成,能够更容易地与 Windows 上的其他网络资源进行交互。
> * ⑤ **资源使用优化**WSL2 使用轻量级虚拟机,比传统的虚拟机占用更少的资源,同时提供了类似的隔离和安全性。
* 我们可以将`变量`想象为一个`容器`,盒子中`装的`就是我们想要的`数据`,并且我们需要`给`盒子`取`一个`特别的名称`;通过这个`特别的名称`,我们可以`给`盒子`添加数据`或`移除数据`,这个`特别的名称`就是`变量名`。
> [!NOTE]
>
> WSL2 的用途,如下所示:
>
> * ① **开发环境**WSL2 为开发者提供了一个原生的 Linux 开发环境,而无需离开 Windows 。这对于需要在 Linux 上开发、测试或运行应用程序的开发者非常有帮助。
> * ② **学习和实验**:用户可以使用 WSL2 在 Windows 上学习和实验 Linux 命令行工具和应用程序,而无需设置双重引导系统或安装虚拟机。
> * ③ **多平台开发**对于跨平台开发者来说WSL2 允许他们在一个操作系统上同时进行 Windows 和 Linux 平台的开发和测试,提高工作效率。
> * ④ **运行 Linux 工具和应用程序**WSL2 支持在 Windows 上直接运行各种 Linux 工具和应用程序Docker、数据库、编程语言环境等。
### 1.1.2 WSL2 的安装
* ① BIOS 或 UEFI 中,开启虚拟化:步骤略。
![](./assets/2.png)
> [!NOTE]
>
> * ① `变量`是内存中的一个`存储区域`,该区域的数据可以在`同一类型`范围内`不断变化`。
> * ② 通过`变量名`,可以`操作`这块内存区域,向其中`存储数据`或`获取数据`以及`移除数据`。
> * ③ 变量的构成包含三个要素:`数据类型`、`变量名`、`需要存储的数据`。
> * ④ 在生活中,我们会经常说:这件衣服的价格是 `100整型` 元,这双鞋子的价格是 `250.5(小数,浮点类型)` 元,`今天天气真好(字符串类型)`之类的话;在计算机科学中,这些都是数据,并且它们是有类型,即:数据类型。(数据类型用于定义变量所能存储的数据的种类以及可以对这些数据进行的操作的一种分类,每种数据类型都有特定的属性和用途,它们决定了变量在内存中如何表示和存储,以及变量可以执行哪些操作)
* ② 查看是否开启了虚拟化:
## 1.3 变量的声明和使用
![](./assets/3.png)
* ① 变量必须先声明,后使用。
* ② 可以先声明变量再赋值,也可以在声明变量的同时进行赋值。
* ③ 变量的值可以在同一类型范围内不断变化。
>[!NOTE]
>
>* ① 在实际开发中,我们通常都会在声明变量的同时,给其赋值,这被称为初始化。
>* ② 如果不在声明变量的同时,进行初始化,默认情况下,系统会赋予的随机值,我们也称为垃圾值。
>* ③ 其实,变量既可以声明在 `main()` 函数的外面,称为`全局变量`;也可以声明在 `main()` 函数的立马,称为`局部变量`。使用未初始化的局部变量有很多风险,很多编译器会给出警告,提醒程序员注意。
>[!IMPORTANT]
>
>* ① C 语言的编译器,在程序员在使用未初始化的局部变量会有警告的原因就是:变量声明的时候,会给变量分配一块内存空间,如果不对变量进行初始化,那么就意味着不对这块内存空间进行写入操作,那么这块内存空间的数据将保持不变。但是,这个内存空间的数据是哪里来的?是当前程序之前运行产生的,还是其它程序之前运行产生的,我们一无所知。由此可知,如果不进行初始化,那么变量对应的内存空间的数据是毫无意义的,是随机值,是垃圾值,没有任何价值。所以,建议在声明局部变量的同时进行初始化操作。
>* ② 在实际开发中,声明局部变量的时候,必须进行初始化操作,以便能够减少潜在的错误并提高代码的稳定性。
>* ③ 在很多编程语言Java ,如果局部变量没有进行初始化操作,将会在编译阶段报错。
* 示例:先声明,再使用
```c
#include <stdio.h>
int main() {
// 声明一个整型变量,取名为 a
int a;
// 给变量赋值
a = 10;
printf("a = %d\n", a);
return 0;
}
```
* 示例:初始化(声明变量的同时给其赋值)
```c
#include <stdio.h>
int main() {
// 声明一个整型变量,取名为 b ,并直接赋值(初始化,实际开发中最为常用)
int b = 200;
// 修改变量 b 的值,将变量 a 的值赋值给变量 b
b = 300;
printf("b= %d\n", b);
return 0;
}
```
* 示例:同时声明多个变量并赋值
```c
#include <stdio.h>
int main() {
// 同时声明多个整型的变量并赋值
int c1 = 10, c2 = 20, c3 = 30;
printf("c1 = %d\n", c1);
printf("c2 = %d\n", c2);
printf("c3 = %d\n", c3);
return 0;
}
```
## 1.4 浅谈变量周边概念
### 1.4.1 数据类型
* 数据是放在内存中的,变量是给这块内存起的名字,有了变量就可以找到并使用这份数据。但是,该如何使用?
* 我们知道,对于数字、文字、符号、图形、音频、视频等数据都是以二进制的形式被加载进内存中,进而被计算机中的 CPU 所识别,它们本质上没有任何区别。那么,对于 `00010000` 这个二进制数字,我们是理解为数字 16 ?还是理解为图像中的某个像素的颜色?如果没有特别指明,我们并不清楚。
* 换言之,内存中的数据有多种解释方式;那么,我们在存储之前就必须明确指定,如: `int num = 10;` 中的 `int` 就是数据类型,用来限定 `num` (内存中的某个区域)中存储的是整数,而不是图像中某个像素的颜色。
> [!NOTE]
>
> 总结:
>
> * ① 数据类型用来说明数据的类型,确定了数据的解释方式,让计算机和程序员不会产生歧义。
> * ② C 语言中很多基本的数据类型char、short、int、long 等;如果需要,也可以组成更加复杂的数据类型(后续讲解)。
### 1.4.2 连续定义多个变量
* 为了让程序的书写更加简洁C 语言支持多个变量的连续定义,如下所示:
```c
int a,b,c;
```
```c
float m=3.14,n=4.14;
```
> [!NOTE]
>
> * ① 连续定义的多个变量以逗号`,`分隔,并且要拥有相同的数据类型。变量可以初始化,也可以不初始化。
> * ② 很多 C 语言程序员喜欢这么写;但是,本人不是很喜欢,因为连续定义可能会导致代码的可读性降低,特别是在声明时变量之间用逗号分隔,容易导致混淆。
### 1.4.3 数据的长度
* 所谓数据长度,就是指的是数据占用多少个字节。占用的字节越多,能存储的数据就越多;对于数字而言,值就会越大。反之,能存储的数字就有限。
* 多个数据在内存中是连续存储的,彼此之间是没有明显的界限的。如果不指明数据的长度,那么计算机就不知道何时才能存取结束。假设我们保存了一个整数 1000 ,它占用 4 个字节的内存,而读取它的时候却读取了 3 个字节或5 个字节;那么,显示是不正确的。
* 所以,在定义变量的时候还要指明数据的长度,而这恰恰是数据类型的另外一个作用,即:`数据类型除了指明数据的解释方式,还指明了数据的长度`。
> [!NOTE]
>
> 总结:在 C 语言中,每一种数据类型所占用的字节数都是固定的,知道了数据类型,也就知道了数据的长度。
* 在 32 位环境中,各种数据类型的长度,如下所示:
| 数据类型 | 长度(字节) |
| ----------------- | ------------ |
| `char` | 1 |
| `short` | 2 |
| `int` | 4 |
| `long` | 4 |
| `long long` | 8 |
| `float` | 4 |
| `double` | 8 |
| `long double` | 8 |
| `pointer`(指针) | 4 |
> [!NOTE]
>
> * ① C 语言有多少种数据类型,每种数据类型长度是多少、该如何使用,这是每一位 C 程序员都必须要掌握的。
> * ② 当然,不必担心,后续还会一一讲解的。
* ③ 启用适用于 Linux 的 Windows 子系统:
> [!IMPORTANT]
>
> * ① 数据类型只需要在定义变量时指明,而且必须指明。
> * ② 使用变量时无需再指明,因为此时的数据类型已经确定了。
> 以管理员身份打开 PowerShell 并运行,执行完下面命令之后,如果提示需要重启计算机,那就重启计算机!!!
## 1.5 从计算机底层看变量
```powershell
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
```
### 1.5.1 内存条的内部结构
![](./assets/4.gif)
* 如果只看内存条的外观,无非就是一些集成电路和颗粒而已,如下所示
* ④ 启用虚拟机功能:
![](./assets/3.jpeg)
* 并且,我们只需要将内存条插入到计算机主板对应的内存条插槽上,就可以正常工作,如下所示:
![](./assets/4.jpg)
* 在家用的台式机主板上,通常有 4 个插槽或 2 个插槽,例如:本人的计算机就支持 4 个插槽,如下所示:
![](./assets/5.png)
>[!NOTE]
> [!IMPORTANT]
>
>* ① 上图中的外形规格是 DIMM所以我们通常也以 DIMM 也表示内存条。
>* ② DIMM 是内存条的物理形式,安装在主板的内存插槽中。
>* ③ 常见的 DIMM 类型包括 UDIMM非缓冲 DIMM、RDIMM缓冲 DIMM和 LRDIMM负载减少DIMM
> 以管理员身份打开 PowerShell 并运行,执行完下面命令之后,如果提示需要重启计算机,那就重启计算机!!!
* 我们可以通过 [CPU-Z](https://www.cpuid.com/) 这个软件,查看 CPU 的一些指标信息,如下所示:
```powershell
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
```
![](./assets/6.png)
![](./assets/5.gif)
> [!NOTE]
* ⑤ 更新 Linux 内核包:
> [!IMPORTANT]
>
> * ① 通过 CPU-Z 表明本人的台式机是支持双通道的,`channel` 在`计算机`中可以`翻译`为`信道`或`通道`。
> * ② 通道是内存控制器与内存模块之间的通信路径。
> * ③ 多通道内存可以提高数据传输带宽。例如:双通道内存系统同时使用两个通道来传输数据,从而提高性能。
> * ④ 现代主板通常支持双通道Dual Channel、四通道Quad Channel甚至八通道Octa Channel
> WSL2 的最新 Linux 内核包托管在 GitHub 上,某些国家可能会污染 Github 相关的域名,那么就需要手动下载,然后安装即可,下载地址在[这里](https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi)。
* 对于家用台式机而言,如果将内存条的插槽从左到右依次编号,如下所示:
```powershell
wsl --update
```
![](./assets/7.png)
![](./assets/6.gif)
* 其中,`槽1` 和 `槽2` 是一个通道,`槽3` 和 `槽4` 是一个通道;所以,通常是这么建议的:
* 如果只有 1 根内存条,就插到 `槽2` 中。
* 如果有 2 根内存条,就分别插入到 `槽2``槽4` 中。
* 如果有 4 根内存条,就全插满即可。
* ⑥ 将 wsl2 设置为默认版本:
> [!NOTE]
```powershell
wsl --set-default-version 2
```
![](./assets/7.gif)
* ⑦ 查看官方在线支持的 Linux 版本:
```powershell
wsl --list --online
```
![](./assets/8.gif)
* ⑧ 安装指定版本的 Linux
> [!IMPORTANT]
>
> 组成双通道配置的内存条需要遵循一些基本要求来确保它们能够正常以双通道模式运行:
> 官方支持的 Linux 版本,托管在 Github 上,某些国家可能会污染 Github 的域名,有如下两种解决方案:
>
> - ① **相同容量**:理想情况下,组成双通道的内存条应该具有相同的容量。这样可以确保它们在处理数据时的一致性和兼容性。
> - ② **匹配的速度规格**内存条应该具有相同的速度规格即它们的频率DDR4-2400、DDR4-3200等应该相同。不同速度的内存条可以一起工作但系统会以所有内存条中最慢的那个的速度运行。
> - ③ **相同的时序**内存条的时序CL16-18-18-38应该匹配。时序参数影响内存的响应速度和稳定性不匹配的时序可能会降低性能或导致系统不稳定。
> - ④ **相同的制造商和型号**(推荐):虽然不是强制性要求,但选择相同制造商和型号的内存条可以最大限度地减少兼容性问题。不同制造商的内存条可能在微小的规格和性能上有差异,这有可能影响双通道配置的效能。
> * ① 科学上网。
> * ② 在 `Microsoft Store` 中搜索并安装。
```powershell
wsl --install Ubuntu-24.04
```
![](./assets/9.gif)
* 内存条表面会有内存颗粒,如下所示:
![](./assets/8.png)
> [!NOTE]
>
> 上图中的内存条有 8 个内存颗粒;但是,高端服务器上的内存条通常会存在 9 个内存颗粒,最后 1 个内存颗粒专门用来做 ECC 校验。
* 一个内存条有两面,高端的内存条两面都有内存颗粒,我们将每个面称为 Rank 。那么,如果内存条有两个面,就是存在 Rank0 和 Rank1 ,即:
![](./assets/9.png)
* 内存条表面的黑色颗粒,我们称为 chip芯片 ,即:
* ⑨ 在 Microsoft Store 中搜索并安装(可选):
![](./assets/10.png)
> [!NOTE]
>
> * ① 内存颗粒是内存条上的 DRAM 芯片,每个芯片包含多个存储单元。
> * ② 内存颗粒存储数据并与内存控制器进行数据交换。
* ⑩ 查询本地安装的 Linux 版本:
* 在 chip 中还有 8 个 bank每个 bank 就是数据存储的实体,这些 bank 组成了一个二维矩阵,只要声明了 column 和 row 就可以从每个 bank 中取出 8bit 1 Bytes的数据如下所示
![img](./assets/11.png)
* 综上所示,内存条的分层结构就是 `Channel > DIMM > Rank -> Chip -> Bank -> Row/Column`
### 1.5.2 变量的作用
* 如果我们希望计算 10 和 20 的和;那么,在计算机中需要怎么做?
* ① 首先,计算 10 和 20 的运算,一定在 CPU 中进行,因为在计算机中的各个部件中,只有 CPU 有运算器ALU
* ② 其次,我们需要将 10 和 20 交给 CPU ;由于 CPU 只能和内存进行交互,那么我们必须将 10 和 20 存储到内存中。
> [!NOTE]
>
> 即使 10 和 20 是存储在文件中的,也需要先加载进内存,然后再交给 CPU 进行运算。
* ③ 最后,只需要告诉 CPU 做何种运算,如:加、减、乘、除等。
* 其中,最为重要的问题就是如何将数据存储到内存中?答案就是通过`变量`。
![](./assets/12.png)
* 我们知道,计算机底层是使用二进制来表示指令和数据的;但是,如果我们的代码都是这样的,即:
```txt
0000,0000,000000010000 代表 LOAD A, 16
0000,0001,000000000001 代表 LOAD B, 1
0001,0001,000000010000 代表 STORE B, 16
```powershell
wsl --list
```
* 这样,直接使用`内存地址`来编写代码(机器语言)实现是太难阅读、修改和维护了;于是,我们就使用了汇编语言来编写代码,并通过编译器来将汇编语言翻译为机器语言,即:
![](./assets/11.gif)
```txt
LOAD A, 16 -- 编译 --> 0000,0000,000000010000
LOAD B, 1 -- 编译 --> 0000,0001,000000000001
STORE B, 16 -- 编译 --> 0001,0001,000000010000
### 1.1.3 配置 WSL2
* 本人的安装的是 AlmaLinux9 ,所以需要执行如下命令,以便安装 cmake 相关工具链:
```shell
sudo dnf update -y # 更新包管理器
sudo dnf groupinstall "Development Tools" -y # 安装开发工具包
sudo dnf install gcc gcc-c++ -y # 安装GCC相关工具链
sudo dnf install cmake -y # 安装 cmake
sudo dnf install make -y # 安装 make
sudo dnf install gdb -y # 安装 gdb
```
* 但是,这样的汇编语言还是面向机器的,编程时仍然需要记住和管理大量内存地址,不具备程序的移植性;于是,我们就是使用了高级语言来编写代码,并引入了变量的概念,即:
![](./assets/12.gif)
```c
int num = 10;
* 可以通过 CLion 测试是否安装成功:
![](./assets/13.gif)
### 1.1.4 配置 WSL2
* 本人的安装的是 Ubuntu 24.04,所以需要执行如下命令,以便安装 cmake 相关工具链:
```shell
sudo apt update && sudo apt upgrade -y # 更新包管理器
sudo apt install build-essential -y # 安装开发工具包
sudo apt install gcc g++ -y # 安装 GCC 相关工具链
sudo apt install cmake -y # 安装 cmake
sudo apt install gdb -y # 安装 gdb
```
* 我们使用`变量名`来`关联`内存`地址`,这样我们在编写代码的时候,就可以不用直接操作内存地址,极大地提高了代码的可读性和开发效率。并且,当程序运行完毕之后,程序所占用的内存还会交还给操作系统,以便其它程序使用。
* 综上所述,高级语言编译器的作用就是:
* ① 编写源代码时使用变量名。
* ② 程序在经过编译器的编译之后,所有变量名被替换为具体地址。
* ③ ……
![](./assets/14.gif)
* 此时,我们就可以知道,`变量`就是内存中用于`存储数据`的`临时空间`,并且变量中的值是可以变化的。
* `内存`中空间的`最小单位`是`字节`Bytes即 8 个 0 或 1 ,如下所示:
* 可以通过 CLion 测试是否安装成功:
```txt
00011001 00100110 00100110 00100110 00100110 ...
```
![](./assets/15.gif)
## 1.2 切换 CLion 中的 cmake 的工具链
* 可以在 CLoin 中切换 cmake 的工具链,以便支持不同平台的 cmake ,即:
![](./assets/16.gif)
## 1.3 修改 CMakeLists.txt 文件
* 前文也提到了,在一个 C 语言项目中,只能有一个 main() 函数;但是,我们可以修改 `CMakeLists.txt` 文件的内容,以便其支持在一个 C 语言项目中,可以同时运行多个包含 main() 函数的文件。
> [!NOTE]
>
> 计算机中存储单位的换算,如下所示:
>
> * 1 B = 8 bit。
> * 1 KB = 1024 B。
> * 1 MB = 1024 KB。
> * 1 GB = 1024 MB。
> * 1 TB = 1024 GB 。
> * ……
> * ① 其实,这样设置的目的:就是为了让每个 `.c` 文件都可以编译为一个独立的可执行文件,而不是所有的 `.c` 文件编译为一个可执行文件。
> * ② 在实际开发中,对于 C 语言项目而言,当然必须只能有一个 `main()` 函数(只有一个 `.c` 文件包含 `main()` 函数,其余的 `.c` 文件中包含函数声明或函数实现),因为程序有且仅有一个入口。
* 在内存中,每一个字节都有一个编号,这个编号我们称之为地址。一个变量至少占用 1 个字节1 个或多个字节我们将变量的第一个字节所占用的地址变量的首地址就称之为该变量的地址。CPU 就可以通过变量地址找到某个变量的值,然后拿到具体的数据进行计算了。
* `CMakeLists.txt` 文件的位置,如下所示:
![](./assets/17.png)
* `CMakeLists.txt` 文件的内容,如下所示:
```txt
cmake_minimum_required(VERSION 3.22.1)
# 项目名称和版本号
project(c-study VERSION 1.0 LANGUAGES C)
# 设置 C 标准
set(CMAKE_C_STANDARD 23)
set(CMAKE_C_STANDARD_REQUIRED True)
set(CMAKE_C_EXTENSIONS OFF)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g")
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
add_definitions(-D_DEBUG)
elseif (CMAKE_BUILD_TYPE STREQUAL "Release")
add_definitions(-DNDEBUG)
elseif (CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
add_definitions(-DRELWITHDEBINFO)
elseif (CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
add_definitions(-DMINSIZEREL)
endif ()
# 辅助函数,用于递归查找所有源文件
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 ()
```
## 1.4 配置 .clang-format 文件
* 配置 `.clang-format` 格式化文件,以便写代码的时候,可以自动保存并格式化 C 程序代码,如下所示:
![](./assets/18.png)
* `.clang-format` 的内容,如下所示:
```txt
BasedOnStyle: LLVM
IndentWidth: 4
UseTab: Never
ColumnLimit: 0
# 控制大括号的位置
BreakBeforeBraces: Attach
# 控制空行的使用
EmptyLineBeforeAccessModifier: Never
KeepEmptyLinesAtTheStartOfBlocks: true
# 控制短函数、短 if 语句和循环的格式
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
# 控制其他格式选项
AlignConsecutiveAssignments: true
AlignConsecutiveDeclarations: true
# 控制注释的格式化
ReflowComments: true
# 控制包含指令的格式化
IncludeBlocks: Regroup
SortIncludes: CaseInsensitive
SpaceBeforeParens: ControlStatements
SpacesInParentheses: false
SpacesInAngles: false
SpacesInContainerLiterals: false
SpacesInCStyleCastParentheses: false
# 控制 switch-case 格式
IndentCaseLabels: true
```
* CLion 中配置`保存`的时候`自动格式化`,即:
![](./assets/19.gif)
## 1.5 配置 .gitignore 文件
* 需要在项目中,配置 `.gitignore` 文件,以便在提交代码到 Git 仓库的时候,忽略某些文件或目录,如下所示:
![](./assets/20.png)
* `.gitignore` 文件的内容,如下所示:
```txt
.vscode
.idea
cmake-build-*
build
```
## 1.6 演示
* 我们可以在项目中,临时创建或复制一个文件,看上述配置是否生效,即:
> [!NOTE]
>
> 变量就是保存程序运行过程中临时产生的值。
> 如果是复制并粘贴一个文件到项目中,请点击`重新加载 CMake 项目`
* 其实,到这里还是有疑惑的?我们说过,一个变量至少会占用 1 个字节,如果一个变量占用了 4 个字节,而 CPU 只会通过变量的地址(首地址)获取数据,那么 CPU 是如何获取完整的数据的?答案就是通过`数据类型`,数据类型除了限制数据的种类,还限制了数据在内存中所占空间的大小,如上图所示:
* ① 假设变量 `a` 的首地址是 `01` ,变量的数据类型是 `4` 个字节。
* ② 那么CPU 就会依次,从 `01 ~ 04` 中获取数据。
![](./assets/21.gif)
* 再次,剖析下变量的语法格式:
```txt
数据类型 变量名 = 值;
```
# 第二章C 语言的编译过程(⭐)
* `变量名`的`作用`,如下所示:
* ① 当我们`编写`代码的时候,使用`变量名`来`关联`某块内存的`地址`。
* ② 当 CPU `执行`的时候,会将变量名`替换`为具体的地址,再进行具体的操作。
## 2.1 概述
* C 程序的编译过程,如下所示:
> [!IMPORTANT]
![](./assets/22.png)
* 过程 ① :编写(编辑)源代码,即:编写 C 语言源程序代码,并以文件的形式存储在磁盘中。
> [!NOTE]
>
> 变量名(标识符)需要符合命名规则和命名规范!!!
> 源程序需要以 `.c` 作为扩展名。
* `数据类型`的`作用`,如下所示:
* ① 变量的数据类型`决定了`变量所占空间的大小。当我们在声明变量的时候写了数据数据类型CPU 就知道从变量的首地址位置开始取多少字节。
* ② 变量的数据类型`决定了`两个变量是否能够运行以及能够做何种运算。例如JavaScript 就没有 char 类型的变量,都是 string 类型,可以和任意数据类型的数据拼接,并转换为 string 类型Java 中有 char 类型的变量,底层都会转换 unicode 编码,然后再计算。
* 过程 ② :编译,即:将 C 语言源程序转换为`目标程序(或目标文件)`。如果程序没有错误,没有任何提示,就会生成一个扩展名为 `.obj``.o` 的二进制文件。C 语言中的每条可执行语句经过编译之后,最终都会转换为二进制的机器指令。
> [!NOTE]
>
> * ① 其实,`编译阶段`包含了`预处理`、`编译`和`汇编`。
>
> * ② `预处理`是编译过程的第一个阶段。在这个阶段,预处理器处理源代码中的指令(例如:`#include`、`#define`等),主要任务包括:
>
> * 头文件包含:将头文件的内容插入到源文件中。例如:`#include <stdio.h>`会被替换为`stdio.h`文件的内容。
> * 宏展开:替换宏定义。例如:`#define PI 3.14`会将代码中的`PI`替换为`3.14`。
> * 条件编译:根据条件指令(如:`#ifdef`、`#ifndef`)有选择地编译代码。
>
> * 删除代码中的注释,但是不会进行语法检查。
>
> * 预处理完成后,生成一个扩展名为`.i`的中间文件。
>
> * ③ `编译`是将预处理后的源代码转换为汇编代码的过程。在这个阶段,编译器会检查代码的语法和语义,将其转换为目标机器的汇编语言,生成一个扩展名为`.s`的汇编文件。
>
> * ④ `汇编`是将汇编代码转换为机器代码(也称为目标代码或目标文件)的过程。在这个阶段,汇编器将汇编指令转换为二进制机器指令,生成一个扩展名为`.o`或 `.obj`的目标文件。
* `值`的`作用`,如下所示:
* ① `值`就是`内存`中`实际存储`的`数据`。
* ② `=` 是赋值操作符,就是将等号右侧的数据存储到等号左侧的变量名所代表的内存空间。
* 那么,如下代码的含义就是:
```c
// int 数据类型4 个字节
// num 变量名 -- 关联内存中的一块存储空间
// = 10 将 10 存储到 num 所代表的 4 个字节的存储空间中
int num = 10;
```
## 1.6 变量的重要操作
### 1.6.1 变量的输出
* 在计算机中,所谓的`输入`和`输出`都是以计算机CPU 和内存)为主体而言的,即:
* 过程 ③ :链接(连接),即:将编译形成的目标文件 `*.obj``*.o`和库函数以及其他目录文件`链接`,形成一个统一的二进制文件 `*.exe`
>[!NOTE]
>
>* ① 输入:从输入设备(键盘、鼠标、扫描仪)向计算机输入数据。
>
>* ② 输出:从计算机向外部输出设备(显示器、打印机)输出数据。
>* 为什么需要链接库文件?
>* 因为我们的 C 程序会使用 C 程序库中的内容,如:`<stdio.h>` 中的 `printf()` 函数,这些函数不是程序员自己写的,而是 C 程序库中提供的因此需要链接。其实在链接过程中还会加入启动代码这个启动代码和系统相关Linux 下主要有 crt0.c、crti.c 等,它们设置堆栈后,再调用 main() 函数)负责初始化程序运行时的环境。
![](./assets/13.png)
* 在 C 语言中,提供了 `printf()` 函数用于输出信息,其函数声明是:
```c
int printf (const char *__format, ...) {
...
}
```
* `printf` 的标准含义是格式化输出文本,来源于 `print formatted格式化打印`的缩写,其语法规则,如下所示:
![](./assets/14.svg)
* 过程 ④ :执行,即:有了可执行的 `*.exe`文件,我们就可以在控制台上执行运行此 `*.exe` 文件。
> [!NOTE]
>
> * ① 格式化字符串:是使用双引号括起来的字符串,里面包含了普通的字符串和格式占位符。
> * ② 格式占位符(格式声明符):由 `%` 和`格式字符`组成,作用是将输出的数据转换为指定的格式后输出,这里的 `%d` 表示整数。
> * ③ 输出列表:是程序要输出的一些数据,可以是常量、变量或表达式,需要和格式占位符一一对应。
> 如果`修改`了源代码,还需要重新`编译`、`链接`,并生成新的 `*.exe`文件,再执行,方能生效。
* 在计算机中,二进制、八进制、十进制以及十六进制的英文名称和缩写,如下所示:
* 二进制binary缩写是 bin。
* 八进制octal缩写是 oct。
* 十进制decimal缩写是 dec。
* 十六进制Hexadecimal缩写是 hex。
## 2.2 GCC 编译器的介绍
* 其实,我们也可以在 Windows 系统中的计算器中来看到,即:
* 编辑器vim 、vscode 等,是指我们用它来编写源程序的(编辑代码),而我们写的代码语句,电脑是不懂的,我们需要把它转成电脑能懂的语句,编译器就是这样的转化工具。换言之,我们用编辑器编写程序,由编译器编译后才可以运行!
* 编译器是将易于编写、阅读和维护的高级计算机语言翻译为计算机能解读、运行的低级机器语言的程序。
* gccGNU Compiler CollectionGNU 编译器套件),是由 GNU 开发的编程语言编译器。gcc 原本作为 GNU 操作系统的官方编译器,现已被大多数类 Unix 操作系统Linux、BSD、Mac OS X 等采纳为标准的编译器gcc 同样适用于微软的 Windows 。
* gcc 最初用于编译 C 语言,随着项目的发展, gcc 已经成为了能够编译 C、C++、Java、Ada、fortran、Object C、Object C++、Go 语言的编译器大家族。
![](./assets/15.png)
## 2.3 通过 gcc 直接生成可执行文件
> [!IMPORTANT]
>
> * ① 在生活中的 decimal 是小数的意思。
> * ② 但是在计算机中decimal 的完整含义是 decimal integer ,即十进制整数。
* 示例:进行预处理、编译、汇编和链接
* 示例:
```c
#include <stdio.h>
int main() {
// 声明变量并赋值
int num = 18;
// 使用输出语句,将变量 num 的值输出,其中 %d 表示输出的是整数
printf("我今年%d岁\n", num);
return 0;
}
```shell
gcc HelloWorld.c -o HelloWorld.exe
```
### 1.6.2 计算变量的大小
![](./assets/23.gif)
* 我们可以使用 `sizeof`关键字(运算符)来计算变量或类型所占内存空间的大小。
## 2.4 通过 gcc 分步编译
### 2.4.1 概述
* 预处理命令:
* 示例:
```c
#include <stdio.h>
int main() {
int num = 10;
printf("变量所占内存空间的大小:%zd字节\n", sizeof(num));
// 数据类型所占内存空间的大小
printf("数据类型所占内存空间的大小:%zd字节\n", sizeof(int));
return 0;
}
```shell
# 通常以 .i 结尾表示这个文件是一个中间状态
gcc -E 源文件.c -o 源文件.i
```
### 1.6.3 获取变量的地址
* 编译(预处理和编译)命令:
* 在 C 语言中,我们可以使用`取地址运算符 &` 来获取变量的地址。
* 示例:
```c
#include <stdio.h>
int main() {
int num = 10;
printf("变量 num 的值是:%d\n", num);
printf("变量 num 的地址(指针)是:%#p\n", &num);
return 0;
}
```shell
# 在 Linux 中,通常以 .s 结尾;在 Windows 中,通常以 .asm 结尾
gcc -S 源文件.i -o 源文件.s
```
### 1.6.4 变量的输入
* 汇编(预处理、编译和汇编)命令:
* 在 C 语言中,提供了 `scanf()` 函数用于从标准输入(通常是键盘)中读取数据并根据变量的地址赋值给变量(变量需要提前声明),其函数声明是:
```c
int scanf(const char *__format, ...) {
...
}
```shell
# 在 Linux 中,通常以 .o 结尾;在 Windows 中,通常以 .obj 结尾
gcc -c 源文件.s -o 源文件.o
```
* 其语法规则,如下所示
* 链接(预处理、编译、汇编和链接)命令:
![](./assets/16.png)
```shell
# 在 Linux 中,通常以 .out 结尾;在 Windows 中,通常以 .exe 结尾
gcc 源文件.o -o 源文件.exe
```
### 2.4.2 应用示例
* 示例:只进行预处理
```shell
gcc -E HelloWorld.c -o HelloWorld.i
```
![](./assets/24.gif)
* 示例:只进行预处理和编译
```shell
gcc -S HelloWorld.i -o HelloWorld.s
```
![](./assets/25.gif)
* 示例:只进行预处理、编译和汇编
```shell
gcc -c HelloWorld.s -o HelloWorld.o
```
![](./assets/26.gif)
* 示例:进行预处理、编译、汇编和链接
```shell
gcc HelloWorld.o -o HelloWorld.exe
```
![](./assets/27.gif)
# 第三章:附录
## 3.1 WSL2 代理问题
* 在安装和配置 WSL2 之后,可能会出现如下的提示,即:
![](./assets/28.png)
* 那么,只需要修改 `%USERPROFILE%\.wslconfig`文件,内容如下:
> [!NOTE]
>
> `&age`、`&num` 中的 `&`是寻址操作符,`&age` 表示变量 `age` 在内存中的地址。
> 如果没有该文件,则需要自己新建该文件!!!
> [!CAUTION]
>
> * ① scanf() 函数中的 `%d`,如果是连着写,即:`%d%d`,那么在输入数据的时候,数据之间不可以使用逗号`,`分隔只能使用空白字符空格、tab 键或回车键),即:`2空格3tab`或`2tab3回车`等。
>
> * ② 如果是 `%d,%d`,则输入的时候需要加上逗号`,`,即:`2,3`。
> * ③ 如果是 `%d %d`,则输入的时候需要加上空格,即:`2空格3`。
```txt
[wsl2]
networkingMode=mirrored
dnsTunneling=true
firewall=true
autoProxy=true
[experimental]
# requires dnsTunneling but are also OPTIONAL
bestEffortDnsParsing=true
useWindowsDnsCache=true
```
![](./assets/29.png)
* 示例:计算圆的面积,半径由用户指定
* 在命令行中,执行如下的命令:
```c
```shell
wsl --shutdown
```
![](./assets/30.gif)
* 此时,再打开终端,就没有这种提示了:
![](./assets/31.png)
## 3.2 CLion 调试问题
* 在 CLion 中进行 run运行程序的时候对于 `printf` 函数或 `scanf` 函数很正常,如下所示:
![](./assets/32.gif)
* 但是,当我们 debug调试 的时候,对于 `printf` 函数或 `scanf` 函数会一直没有输出,如下所示:
![](./assets/33.gif)
* 原因是 `scanf` 函数并不是直接让用户从键盘输入数据,而是先检查缓冲区,处理缓冲区中的数据;当遇到 `scanf` 函数时,程序会先检查输入缓冲区中是否有数据,所以解决方案就是`禁用缓冲区`,如下所示:
```c {5}
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
// CLion debug 独有,后文不再提及,如果 debug 有问题,就添加如下代码
setbuf(stdout, NULL);
float radius;
printf("请输入一个半径:");
scanf("%f", &radius);
double area = 3.1415926 * radius * radius;
printf("半径是%f的圆的面积是%.2lf", radius, area);
return 0;
}
```
* 示例:输入一个整数值,求其绝对值
```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;
}
```
* 示例:输入多个变量的值,求其乘积
```c
#include <stdio.h>
int main() {
int a, b, c;
printf("请输入整数 a 、b 和 c 的值:");
@ -583,880 +542,265 @@ int main() {
}
```
## 1.7 标识符
* 那么,就会达到我们想要的效果了,如下所示:
### 1.7.1 概述
![](./assets/34.gif)
* 在 C 语言中,变量、函数、数组名、结构体等要素命名的时候使用的字符序列,称为标识符。
## 3.3 Win 中文乱码问题
* 之前,我们提及到,在 Win 中,如果出现`中文乱码`问题,就需要去`语言和区别`设置`系统区域`的编码为 UTF-8 但是这样可能会造成其它的软件出现中文乱码问题Xshell 等。
> [!NOTE]
>
> * ① 在上世纪 60 - 70 年代的时候,因为国家贫穷,人民生活不富裕等原因,家长虽然会给孩子取名为:`张建国`、`李华强`等。
> * ② 但是,也会给孩子取小名,如:`二狗子`、`狗剩`等,目的是希望孩子能健康成长(养活),像 `张建国`、`李华强`、`二狗子`、`狗剩`都是名字(标识符),伴随人的一生
> * ① 之所以,修改系统的编码为 UTF-8 会出现问题,是因为早期的 Win 系统的中文默认编码是 GBK目前也是Win 并没有强制第三方软件使用 UTF-8 编码) ,而 Xshell 等也使用的这些编码,一旦我们修改为 UTF-8 之后,可能会造成这些第三方软件出现中文乱码问题(第三方软件适配问题,相信将来应该都会切换为 UTF-8 编码),体验较差!!!
> * ② 在 Linux 或 MacOS 之所以不会出现中文乱码的问题,是因为这些系统默认的编码就是 UTF-8
### 1.7.2 标识符的命名规范
* 其实,还有一种解决方案,如下所示:
* 强制规范:
* ① 只能由`小写`或`大写英文字母``0-9` 或 `_` 组成。
* ② 不能以`数字`开头。
* ③ 不可以是`关键字`。
* ④ 标识符具有`长度`限制,不同编译器和平台会有所不同,一般限制在 63 个字符内。
* ⑤ 严格`区分大小写字母`Hello、hello 是不同的标识符。
* 建议规范:
* ① 为了提高阅读性使用有意义的单词见名知意sumnamemaxyear 等。
* ② 使用下划线连接多个单词组成的标识符max_classes_per_student 等。
* ③ 多个单词组成的标识符,除了使用下划线连接,也可以使用小驼峰命名法,除第一个单词外,后续单词的首字母大写,如: studentId、student_name 等。
* ④ 不要出现仅靠大小写区分不同的标识符name、Name 容易混淆。
* ⑤ 系统内部使用了一些下划线开头的标识符C99 标准添加的类型 `_Bool`,为防止冲突,建议开发者尽量避免使用下划线开头的标识符。
![](./assets/35.png)
![](./assets/36.png)
![](./assets/37.png)
* 示例:合法(不一定建议)的标识符
* 测试一下,是否配置成功:
```txt
a、BOOK_sun、MAX_SIZE、Mouse、student23、
Football、FOOTBALL、max、_add、num_1、sum_of_numbers
```
![](./assets/38.gif)
## 3.4 CLion 中自动导入头文件
* 示例:非法的标识符
```txt
$zj、3sum、ab#cd、23student、Foot-baii、
s.com、bc、j**p、book-1、tax rate、don't
```
### 1.7.3 关键字
* C 语言中的关键字是编译器`预定义`的`保留字`,它们有`特定`的`含义`和`用途`,用于控制程序的结构和执行。
* C80 和 C90 ANSI C定义的关键字如下所示
| 类型(功能) | 具体关键字 |
| -------------------------- | ------------------------------------------------------------ |
| 数据类型关键字 | `char`、`double`、`float`、`int`、`long`、`short`、`signed`、`unsigned`、`void` |
| 存储类说明符关键字 | `auto`、`extern`、`register`、`static`、`typedef`、`volatile`、`const` |
| 控制语句关键字 | `break`、`case`、`continue`、`default`、`do`、`else`、`for`、`goto`、`if`、`return`、`switch`、`while` |
| 结构体、联合体和枚举关键字 | `enum`、`struct`、`union` |
| 其他关键字 | `sizeof` |
* C99 新增的关键字,如下所示:
| 类型(功能) | 具体关键字 |
| ------------------ | --------------------------------- |
| 数据类型关键字 | `_Bool`、`_Complex`、`_Imaginary` |
| 存储类说明符关键字 | `inline`、`restrict` |
| 其他关键字 | `_Complex``_Imaginary` |
* C11 新增的关键字,如下所示:
| 类型(功能) | 具体关键字 |
| ------------------ | ------------------------------------------------------------ |
| 存储类说明符关键字 | `_Atomic` |
| 其他关键字 | `_Alignas``_Alignof``_Generic``_Noreturn``_Static_assert``_Thread_local` |
> [!IMPORTANT]
>
> * ① 关键字不能用作标识符(如:变量名、函数名等)。
> * ② 不要死记硬背这些关键字,在实际开发中,并不一定全部使用到;而且,在学到后面的时候,会自动记住这些关键字以及对应的含义。
# 第二章:常量(⭐)
## 2.1 概述
* 在程序运行过程中,不能改变的量就是常量。
>[!NOTE]
>
>* ① 在数学中的 `π`,就是一个常量,其值为 3.1415926 。
>* ② 在生活中,人类的性别只有`男`和`女`;其中,`男`和`女`也是常量。
>* ③ ...
## 2.2 常量的分类
* 在 C 语言中的变量的分类,如下所示:
* ① 字面量常量。
* ② 标识符常量:
* `#define` 宏定义的标识符常量。
* `const` 关键字修饰的标识符常量。
* 枚举常量。
>[!NOTE]
>
>* ① 所谓的`字面量常量`,就是可以直接使用的常量,不需要声明或定义,包括:整数常量、浮点数常量以及字符常量。
>* ② 所谓的`标识符常量`,就是使用标识符来作为常量名,包括: `#define` 宏定义的标识符常量、`const` 关键字修饰的标识符常量、枚举常量。
* 示例:字面量常量
```c
#include <stdio.h>
int main() {
1;
'A';
12.3;
"你好";
return 0;
}
```
* 示例:字面量常量
```c
#include <stdio.h>
int main() {
printf("整数常量 =》%d\n", 1);
printf("字符常量 =》%c\n", 'A');
printf("浮点数常量 =》%f\n", 12.3);
printf("字符串常量 =》%s\n", "你好");
return 0;
}
```
## 2.3 使用 #define 定义常量
* `#define` 来定义常量,也叫作宏定义,就是用一个标识符来表示一个常量值,如果在后面的代码中出现了该标识符,那么编译时就全部替换成指定的常量值,即用宏体替换所有宏名,简称`宏替换`。
* 格式是:
```c
#define 常量名 常量值
```
> [!IMPORTANT]
>
> * ① 其实`宏定义`的常量的`执行时机`是在`预处理`阶段,将所有`宏常量`替换完毕,才会继续编译代码。
> * ② 不要以 `;` 结尾,如果有 `;` ,分号也会成为常量值的一部分。
> * ③ `# define` 必须写在 `main` 函数的外面!!!
> * ④ `常量名`习惯用`大写字母`表示,如果多个单词,使用 `_` 来分隔,以便和变量区分。
* 示例:
```c
#include <stdio.h>
#define PI 3.1415926
int main() {
double radius = 2.5;
double area = PI * radius * radius;
printf("半径为%lf的圆的面积是%.2lf", radius, area);
return 0;
}
```
## 2.4 const 关键字
* C99 标准新增,这种方式跟定义一个变量是类似的;只不过,需要在变量的数据类型前加上 `const` 关键字。
* 和使用 `#define定义宏常量`相比const 定义的常量有详细的数据类型,而且会在编译阶段进行安全检查,在运行时才完成替换,所以会更加安全和方便。
* 格式是:
```c
const 数据类型 常量名 = 常量值;
```
* 示例:
```c
#include <stdio.h>
const double PI = 3.1415926;
int main() {
double radius = 2.5;
double area = PI * radius * radius;
printf("半径为%lf的圆的面积是%.2lf", radius, area);
return 0;
}
```
## 2.5 枚举常量
* 格式:
```c
enum 枚举常量 {
xxx = 1;
yyy;
...
}
```
* 在 CLion 中,最为强大的功能就是直接输入函数,然后让 IDE 帮我们自动导入头文件,包括自定义的头文件,相当实用。
> [!NOTE]
>
> * ① 默认情况下,枚举常量是从 0 开始递增的
> * ② 也可以在定义枚举常量的时候,自定义它们的值
> * ① CLion 中的`自动导入头文件`的`快捷键`是 `Alt + Enter`
> * ② CLion 中的`自动提取变量的类型`的`快捷键`是 `Ctrl + Alt + V`
![](./assets/39.gif)
* 开启自动导入头文件的步骤,如下所示:
* 示例:
![](./assets/40.png)
```c
#include <stdio.h>
![](./assets/41.png)
enum sex {
MALE = 1,
FEMALE = 2,
};
int main() {
printf("%d\n", MALE);
printf("%d\n", FEMALE);
return 0;
}
```
* 示例:
```c
#include <stdio.h>
enum Sex {
MALE = 1,
FEMALE = 2,
};
int main() {
enum Sex sex;
printf("请输入性别(1 表示男性, 2 表示女性)");
scanf("%d", &sex);
printf("您的性别是:%d\n", sex);
return 0;
}
```
## 2.6 `#define` 定义常量 VS `const` 定义常量
### 2.6.1 概述
* `#define``const` 都可以用来定义常量,但它们的工作方式和应用场景有所不同。
### 2.6.2 语法和定义方式
* `#define` 是一个预处理指令,用来定义宏。在编译时,所有的宏会被预处理器展开为它们定义的值,类似于文本替换。
```c
#define PI 3.14159
```
* `const`是一个编译时常量,用来定义具有类型的常量变量。它是由编译器处理的,并且在运行时仍然可以保留类型信息。
```c
const float PI = 3.14159;
```
### 2.6.3 类型检查
* `#define`没有类型,它只是简单的文本替换,不会进行类型检查。因此,如果在宏中定义了错误的类型,可能导致编译错误或运行时错误。
```c
#define MAX 10 + 20 // 实际展开后可能是 MAX = 10 + 20而不是 30
```
* `const`具有类型,编译器会进行类型检查。如果定义时类型不匹配,会报编译错误。
```c
const int MAX = 30; // 定义时指定了类型,类型检查严格
```
### 2.6.4 作用域
* `#define`宏没有作用域的概念,它是在预处理时进行全局替换的。因此,可能会引发意外的替换问题,尤其是在复杂项目中。
```c
#define SIZE 10 // SIZE 可能在其他文件中也被不小心替换
```
* `const`具有作用域,它遵循 C 语言的作用域规则(比如局部作用域、全局作用域)。这使得 `const` 定义的常量更安全,因为它们只能在指定的范围内使用。
```c
const int SIZE = 10; // 可以局部或全局定义,不会引发冲突
```
### 2.6.5 调试
* `#define`在调试时,宏常量被替换为字面值,因此调试工具中无法看到它的原始名称,只能看到被替换后的值。
* `const`常量在编译后依然存在,因此在调试时可以直接看到常量的名称和它的值,调试体验更好。
### 2.6.6 内存分配
* `#define`宏在预处理阶段替换,不占用内存。
* `const` 常量会被分配内存,特别是在全局或静态情况下,但它也可以被优化为编译时常量,有时也不会占用额外的内存。
### 2.6.7 适用场景
* `#define`通常用于定义简单的常量值、条件编译或宏函数(用于文本替换)。适合不需要类型、安全性检查的场合。
* `const`用于定义类型安全的常量,适合需要进行类型检查或确保作用域的场合。
### 2.6.8 总结
* `const` 更加安全,尤其是在需要类型检查和局部作用域的时候;而 `#define` 常用于需要简洁的文本替换或宏定义。`const` 和 `#define` 对比的表格,如下所示:
| 特性 | `#define` | `const` |
| -------- | ------------------ | --------------------- |
| 类型检查 | 无类型检查 | 有类型检查 |
| 作用域 | 无(全局替换) | 有作用域(局部/全局) |
| 调试支持 | 差(替换为字面值) | 好(保留名称) |
| 内存开销 | 无 | 可能会有 |
| 使用场景 | 宏、条件编译 | 类型安全的常量 |
# 第三章:进制
## 3.1 概述
* 计算机的底层只有`二进制`,即计算机中`运算`和`存储`的`所有数据`都需要转换为`二进制`,包括:数字、字符、图片、视频等。
![](./assets/17.jpg)
* 之前,我们也提到现代的计算机(量子计算机除外)几乎都遵循`冯·诺依曼`体系结构,其理论要点如下:
* ① **存储程序**`程序指令`和`数据`都存储在计算机的内存中,这使得程序可以在运行时修改。
* ② **二进制逻辑**:所有数据和指令都以`二进制`形式表示。
* ③ **顺序执行**:指令按照它们在内存中的顺序执行,但可以有条件地改变执行顺序。
* ④ **五大部件**:计算机由`运算器`、`控制器`、`存储器`、`输入设备`和`输出设备`组成。
* ⑤ **指令结构**:指令由操作码和地址码组成,操作码指示要执行的操作,地址码指示操作数的位置。
* ⑥ **中心化控制**计算机的控制单元CPU负责解释和执行指令控制数据流。
* 所以,再次论证了为什么计算机只能识别二进制。
## 3.2 进制
### 3.2.1 常见的进制
* 在生活中,我们最为常用的进制就是`十进制`,其规则是`满 10 进 1` ,即:
![](./assets/18.jpeg)
* 在计算机中,常见的进制有`二进制`、`八进制`和`十六进制`,即:
* 二进制:只能 0 和 1 ,满 2 进 1 。
* 八进制0 ~ 7 ,满 8 进 1 。
* 十六进制0 ~ 9 以及 A ~ F ,满 16 进 1 。
> [!NOTE]
>
> 在十六进制中,除了 `0``9` 这十个数字之外,还引入了字母,以便表示超过 `9` 的值。其中,字母 `A` 对应十进制的 `10` ,字母 `B` 对应十进制的 `11` ,字母 `C` 对应十进制的 `12`,字母 `D` 对应十进制的 `13`,字母 `E` 对应十进制的 `14`,字母 `F` 对应十进制的 `15`
* 进制的换算举例,如下所示:
| 十进制 | 二进制 | 八进制 | 十六进制 |
| ------ | ------ | ------ | -------- |
| 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 1 |
| 2 | 10 | 2 | 2 |
| 3 | 11 | 3 | 3 |
| 4 | 100 | 4 | 4 |
| 5 | 101 | 5 | 5 |
| 6 | 110 | 6 | 6 |
| 7 | 111 | 7 | 7 |
| 8 | 1000 | 10 | 8 |
| 9 | 1001 | 11 | 9 |
| 10 | 1010 | 12 | a 或 A |
| 11 | 1011 | 13 | b 或 B |
| 12 | 1100 | 14 | c 或 C |
| 13 | 1101 | 15 | d 或 D |
| 14 | 1110 | 16 | e 或 E |
| 15 | 1111 | 17 | f 或 F |
| 16 | 10000 | 20 | 10 |
| ... | ... | ... | ... |
* 二进制和十六进制的关系:十六进制是以 16 为基数的进制系统16 在二进制中表示为 ( 2^4 ),即:一个十六进制可以表示 4 位二进制。
> [!NOTE]
>
> 十六进制的范围是0 ~ F 0 ~ 15对应的二进制数的范围是0000 ~ 1111 0 ~ 15
* 每个十六进制数都可以映射到一个唯一的 4 位二进制数,即:
| 十六进制 | 二进制 |
| -------- | ------ |
| 0 | 0000 |
| 1 | 0001 |
| 2 | 0010 |
| 3 | 0011 |
| 4 | 0100 |
| 5 | 0101 |
| 6 | 0110 |
| 7 | 0111 |
| 8 | 1000 |
| 9 | 1001 |
| A | 1010 |
| B | 1011 |
| C | 1100 |
| D | 1101 |
| E | 1110 |
| F | 1111 |
>[!NOTE]
>
>由此可见,每个十六进制数字确实由 4 位二进制数表示。
* 二进制和八进制的关系:八进制是以 8 为基数的进制系统8 在二进制中表示为 ( 2^3 );即:一个八进制位可以表示 3 个二进制位。
> [!NOTE]
>
> 八进制的范围是0 ~ 7 对应的二进制数的范围是000 ~ 111。
* 每个八进制数位都可以映射到一个唯一的 3 位二进制数,即:
| 八进制 | 二进制 |
| ------ | ------ |
| 0 | 000 |
| 1 | 001 |
| 2 | 010 |
| 3 | 011 |
| 4 | 100 |
| 5 | 101 |
| 6 | 110 |
| 7 | 111 |
> [!NOTE]
>
> 由此可见,每个八进制数字确实由 3 位二进制数表示。
### 3.2.2 C 语言中如何表示不同进制的整数?
* 规则如下:
* 在 C 语言中,如果是`二进制`(字面常量),则需要在二进制整数前加上 `0b``0B`
* 在 C 语言中,如果是`八进制`(字面常量),则需要在八进制整数前加上 `0`
* 在 C 语言中,如果是`十进制`(字面常量),正常数字表示即可。
* 在 C 语言中,如果是`十六进制`(字面常量),则需要在十六进制整数前加上 `0x`或`0X` 。
* 示例:
```c
#include <stdio.h>
int main() {
int num1 = 0b10100110; // 二进制
int num2 = 0717563; // 八进制
int num3 = 1000; // 十进制
int num4 = 0xaf72; // 十六进制
printf("num1 = %d\n", num1); // num1 = 166
printf("num2 = %d\n", num2); // num2 = 237427
printf("num3 = %d\n", num3); // num3 = 1000
printf("num4 = %d\n", num4); // num4 = 44914
return 0;
}
```
### 3.2.3 输出格式
* 在 C 语言中,可以使用不同的`格式占位符`来`输出`不同`进制`的整数,如下所示:
* `%d`:十进制整数。
* `%o` :八进制整数。
* `%x`:十六进制整数。
* `%#o` :显示前缀 `0` 的八进制整数。
* `%#x` :显示前缀 `0x` 的十六进制整数。
* `%#X` :显示前缀 `0X` 的十六进制整数。
> [!CAUTION]
>
> C 语言中没有输出二进制数的格式占位符!!!
* 示例:
```c
#include <stdio.h>
int main() {
int num = 100;
printf("%d 的十进制整数: %d\n", num, num); // 100 的十进制整数: 100
printf("%d 的八进制整数: %o\n", num, num); // 100 的八进制整数: 144
printf("%d 的十六进制整数: %x\n", num, num); // 100 的十六进制整数: 64
printf("%d 的八进制(前缀)整数: %#o\n", num, num); // 100 的八进制(前缀)整数: 0144
printf("%d 的十六进制(前缀)整数: %#x\n", num, num); // 100 的十六进制(前缀)整数: 0x64
printf("%d 的十六进制(前缀)整数: %#X\n", num, num); // 100 的十六进制(前缀)整数: 0X64
return 0;
}
```
## 3.3 进制的运算规则
### 3.3.1 概述
* `十进制`的运算规则,如下所示:
* 逢`十`进`一`(针对加法而言)。
* 借`一`当`十`(针对减法而言)。
* `二进制`的运算规则,如下所示:
* 逢`二`进`一`(针对加法而言)。
* 借`一`当`二`(针对减法而言)。
* `八进制`的运算规则,如下所示:
* 逢`八`进`一`(针对加法而言)。
* 借`一`当`八`(针对减法而言)。
* `十六进制`的运算规则,如下所示:
* 逢`十六`进`一`(针对加法而言)。
* 借`一`当`十六`(针对减法而言)。
### 3.3.2 二进制的运算
* 二进制的加法:`1 + 0 = 1` 、`1 + 1 = 10`、`11 + 10 = 101`、`111 + 111 = 1110`。
![](./assets/19.svg)
* 二进制的减法:`1 - 0 = 1` 、`10 - 1 = 1`、`101 - 11 = 10`、`1100 - 111 = 101` 。
![](./assets/20.svg)
### 3.3.3 八进制的运算
* 八进制的加法:`3 + 4 = 7` 、`5 + 6 = 13`、`75 + 42 = 137`、`2427 + 567 = 3216`。
![](./assets/21.svg)
* 八进制的减法:`6 - 4 = 2` 、`52 - 27 = 33`、`307 - 141 = 146`、`7430 - 1451 = 5757` 。
![](./assets/22.svg)
### 3.3.4 十六进制的运算
* 十六进制的加法:`6 + 7 = D` 、`18 + BA = D2`、`595 + 792 = D27`、`2F87 + F8A = 3F11`。
![](./assets/23.svg)
* 十六进制的减法:`D - 3 = A` 、`52 - 2F = 23`、`E07 - 141 = CC6`、`7CA0 - 1CB1 = 5FEF` 。
![](./assets/24.svg)
## 3.4 进制的转换
### 3.4.1 概述
* 不同进制的转换,如下所示:
![](./assets/25.png)
* 在计算机中,数据是从右往左的方式排列的;其中,最右边的是低位,最左边的是高位,即:
![](./assets/26.svg)
### 3.4.2 二进制和十进制的转换
#### 3.4.2.1 二进制转换为十进制
* 规则:从最低位开始,将每个位上的数提取出来,乘以 2 的 (位数 - 1 )次方,然后求和。
> [!NOTE]
>
> * ① 在学术界,将这种计算规则,称为`位权相加法`。
> * ② `八进制转换为十进制`、`十六进制转换为十进制`和`二进制转换为十进制`的算法相同!!!
* 示例:十进制转十进制
![](./assets/27.svg)
* 示例:二进制转十进制
![](./assets/28.svg)
#### 3.4.2.2 十进制转换二进制
* 规则:将该数不断除以 2 ,直到商为 0 为止,然后将每步得到的余数倒过来,就是对应的二进制。
> [!NOTE]
>
> * ① 在学术界,将这种计算规则,称为`短除法`或`连续除2取余法`。
> * ② 很好理解,只有不断地除以 2 ,就能保证最大的数字不超过 2 ,这不就是二进制(只能有 0 或 1
> * ③ `八进制转换为二进制`、`十六进制转换为二进制`和`十进制转换为二进制`的算法相同!!!
* 示例:十进制转十进制
![](./assets/29.svg)
* 示例:十进制转二进制
![](./assets/30.svg)
### 3.4.3 二进制转八进制
* 规则:从右向左,每 3 位二进制就是一个八进制,不足补 0分组转换法
* 示例011 101 001 -> 351
![](./assets/31.svg)
### 3.4.4 二进制转十六进制
* 规则:从右向左,每 4 位二进制就是一个十六进制,不足补 0分组转换法
* 示例1110 1001 -> 0xE9
![](./assets/32.svg)
## 3.5 原码、反码和补码
## 3.5 WSL2 启用 systemd
### 3.5.1 概述
* 机器数:一个数在计算机的存储形式是二进制,我们称这些二进制数为机器数。机器数可以是有符号的,用机器数的最高位来存放符号位,`0` 表示正数,`1` 表示负数。
* 根据 [systemd.io](https://systemd.io/)“systemd 是 Linux 系统的基本构建基块套件。 它提供一个系统和服务管理器,该管理器作为 PID 1 运行并启动系统的其余部分。”
* Systemd 主要是一个 init 系统和服务管理器,它包括按需启动守护程序、装载和自动装载点维护、快照支持以及使用 Linux 控制组进行跟踪等功能。
* 大多数主要的 Linux 发行版现在都运行 systemd因此在 WSL2 上启用它可使体验更接近于使用裸机 Linux。
> [!IMPORTANT]
> [!CAUTION]
>
> * ① 这里讨论的适用于`有符号位`的整数int 等。
> * ② 这里讨论的不适用于`无符号位`的整数unsinged int 等。
> * ① 默认情况下,在 WSL2 中,只有 Ubuntu 才会将 systemd 作为 pid-1 的守护进程(微软维护和定制的 Ubuntu 版本,在 GitHub 的 Codespace 中默认的 Linux 环境就是 Ubuntu。而其余基于 WSL2 为内核的 Linux 发行版本并不会将 systemd 作为 pid-1 的守护进程,而是会使用 init 作为 pid-1 的守护进程。
> * ② 需要注意的是,很多 Linux 软件都需要 systemd 来进行管理Docker 。
> * ③ 本次以 AlmaLinux9 作为演示!!!
![](./assets/33.svg)
* 检查进程树,判断 systemd 是否正在运行:
* 真值(数据位):因为机器数带有符号位,所以机器数的形式值不等于其真实表示的值(真值),以机器数 1000 0001 为例,其真正表示的值(首位是符号位)为 -1而形式值却是 129 ,因此将带有符号位的机器数的真正表示的值称为机器数的真值。
> [!IMPORTANT]
>
> * ① 这里讨论的适用于`有符号位`的整数int 等。
> * ② 这里讨论的不适用于`无符号位`的整数unsinged int 等。
![](./assets/34.svg)
### 3.5.2 原码
* 原码的表示与机器数真值表示的一样,即用第一位表示符号,其余位表示数值。
* 规则:
* 正数的`原码`是它本身对应的二进制数,符号位是 0 。
* 负数的`原码`是它本身绝对值对应的二进制数,但是符号位是 1 。
* `+1` 的原码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 |
| -------- | --------------------- |
| +1 | `0`000 0000 0000 0001 |
* `-1` 的原码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 |
| -------- | --------------------- |
| -1 | `1`000 0000 0000 0001 |
> [!IMPORTANT]
>
> * ① 按照原码的规则,会出现 `+0``-0` 的情况,即:`0`000 0000 0000 0001+0、`1`000 0000 0000 0001-0显然不符合实际情况。
>* ② 所以,计算机底层虽然存储和计算的都是二进数,但显然不是原码。
### 3.5.3 反码
* 规则:
* 正数的反码和它的原码相同。
* 负数的反码是在其原码的基础上,符号位不变,其余各位取反。
* `+1` 的反码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 | 反码16位二进制数 |
| -------- | --------------------- | --------------------- |
| +1 | `0`000 0000 0000 0001 | `0`000 0000 0000 0001 |
* `-1` 的反码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 | 反码16位二进制数 |
| -------- | --------------------- | --------------------- |
| -1 | `1`000 0000 0000 0001 | `1`111 1111 1111 1110 |
> [!IMPORTANT]
>
> * ① 按照反码的规则,如果是 `+0`,对应的原码是 `0`000 0000 0000 0000那么其反码还是 `0`000 0000 0000 0000如果是 `-0`,对应的原码是 `1`000 0000 0000 0000其反码是 `1`111 1111 1111 1111显然不符合实际情况。
>* ② 所以,计算机底层虽然存储和计算的都是二进数,但显然不是反码。
### 3.5.4 补码
* 规则:
* 正数的补码和它的原码相同。
* 负数的补码是在其反码的基础上 + 1 。
* `+1` 的补码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 | 反码16位二进制数 | 补码16位二进制数 |
| -------- | --------------------- | --------------------- | --------------------- |
| +1 | `0`000 0000 0000 0001 | `0`000 0000 0000 0001 | `0`000 0000 0000 0001 |
* `-1` 的补码,使用 `16` 位二进数来表示,就是:
| 十进制数 | 原码16位二进制数 | 反码16位二进制数 | 补码16位二进制数 |
| -------- | --------------------- | --------------------- | --------------------- |
| -1 | `1`000 0000 0000 0001 | `1`111 1111 1111 1110 | `1`111 1111 1111 1111 |
* 如果 `0` ,按照 `+0` 的情况进行处理,如下所示:
![](./assets/35.svg)
* 如果 `0` ,按照 `-0` 的情况进行处理,如下所示:
![](./assets/36.svg)
* `+1``-1` 的`原码`和`补码`的转换过程,如下所示:
![](./assets/37.svg)
> [!IMPORTANT]
>
> * ① 补码表示法解决了`原码`和`反码`存在的`两种`零(`+0` 和 `-0`)的问题,即:在补码表示法中,只有`一个`零,即 `0000 0000`
>* ②补码使得`加法运算`和`减法运算`可以统一处理,通过将减法运算`转换`为加法运算,可以简化硬件设计,提高了运算效率。
> * ③ 计算机底层`存储`和`计算`的都是`二进数的补码`。换言之,当`读取`整数的时候,需要采用`逆向`的转换,即:将补码转换为原码。正数的原码、反码、补码都是一样的,三码合一。负数的补码转换为原码的方法就是先减去 `1` ,得到反码,再按位取反,得到原码(符号位是不能借位的)。
### 3.5.5 总结
* ① 计算机底层`存储`和`计算`的都是`二进数的补码`。换言之,当`读取`整数的时候,需要采用`逆向`的转换,即:将补码转换为原码。
* ② 正数的原码、反码和补码都是一样的,三码合一。
* ③ 负数的反码是在其原码的基础上按位取反0 变 1 1 变 0 ),符号位不变;负数的补码是其反码 + 1 。
* ④ 0 的补码是 0 。
* ⑤ 负数的补码转换为原码的方法就是先减去 `1` ,得到反码,再按位取反,得到原码(符号位是不能借位的)。
## 3.6 计算机底层为什么使用补码?
* `加法`和`减法`是计算机中最基本的运算,计算机时时刻刻都离不开它们,所以它们由硬件直接支持。为了提高加法和减法的运行效率,硬件电路必须设计得尽量简单。
* 对于有符号位的数字来说,内存需要区分符号位和数值位:对于人类来说,很容易识别(最高位是 0 还是 1但是对于计算机来说需要设计专门的电路这无疑增加了硬件的复杂性增加了计算时间。如果能将符号位和数值位等同起来让它们一起参与运算不再加以区分这样硬件电路就可以变得非常简单。
* 此外,加法和减法也可以合并为一种运算,即:加法运算。换言之,减去一个数就相当于加上这个数的相反数,如:`5 - 3` 相当于 `5 +-3``10 --9`相当于 `10 + 9`
* 如果能够实现上述的两个目标,那么只需要设计一种简单的、不用区分符号位和数值位的加法电路,就能同时实现加法运算和减法运算,而且非常高效。其实,这两个目标已经实现了,真正的计算机的硬件电路就是这样设计的。
* 但是,简化硬件电路是有代价的,这个代价就是`有符号数`在存储和读取的时候都要继续转换。这也是对于有符号数的运算来说,计算机底层为什么使用`补码`的原因所在。
## 3.7 补码到底是如何简化硬件电路的?
* 假设 6 和 18 都是 short 类型,现在我们要计算 `6 - 18` 的结果,根据运算规则,它等价于 `6 +-18`。如果按照采用`原码`来计算,那么运算过程是这样的,如下所示:
> [!NOTE]
>
> 直接使用原码表示整数,让符号位也参与运算,那么对于减法来说,结果显然是不正确的。
![](./assets/38.svg)
* 于是,人们开始继续探索,不断试错,终于设计出了`反码`,如下所示:
> [!NOTE]
>
> 直接使用反码表示整数,让符号位也参与运算,对于 6 +-18来说结果貌似正确。
![](./assets/39.svg)
* 如果我们将`被减数`和`减数`对调一下,即:计算 `18 - 6` 的结果,也就是 `18 +-6`的结果,继续采用`反码`来进行运算,如下所示:
> [!NOTE]
>
> * ① 6 - 186+-18如果采用`反码`计算结果是正确的但是18 - 618 +-6如果采用`反码`计算,结果相差 1 。
> * ② 可以推断:如果按照`反码`来计算,小数 - 大数,结果正确;而大数 - 小数,结果相差 1 。
![](./assets/40.svg)
* 对于这个相差的 `1` 必须进行纠正,但是又不能影响`小数-大数`的结果。于是,人们又绞尽脑汁设计出了`补码`,给`反码`打了一个`“补丁”`,终于把相差的 `1` 给纠正过来了。那么,`6 - 18` 按照`补码`的运算过程,如下所示:
![](./assets/41.svg)
* 那么,`18 - 6` 按照`补码`的运算过程,如下所示:
![](./assets/42.svg)
> [!IMPORTANT]
>
> 总结:采用`补码`的形式正好将相差的 `1`纠正过来,也没有影响到小数减大数,这个“补丁”非常巧妙。
>
> * ① 小数减去大数,结果为负,之前(负数从反码转换为补码需要 +1加上的 1 ,后来(负数从补码转换为反码要 -1还需要减去正好抵消掉所以不会受到影响。
> * ② 大数减去小数,结果为正,之前(负数从反码转换为补码需要 +1加上的 1 ,后来(正数的补码和反码相同,从补码转换为反码不用 -1就没有再减去不能抵消掉这就相当于给计算结果多加了一个 1。
>
> `补码`这种天才般的设计,一举达成了之前加法运算和减法运算提到的两个目标,简化了硬件电路。
## 3.8 问题抛出
* 在 C 语言中,对于`有符号位`的整数,是使用 `0` 作为正数,`1` 作为负数,来表示`符号位`,并使用`数据位`来表示的是数据的`真值`,如下所示:
```c
int a = 10;
int b = -10;
```shell
ps -p 1 -o comm= # 如果显示 systemd ,则表示 systemd 正在运行
```
![](./assets/43.svg)
![](./assets/42.gif)
* 但是,对于`无符号位`的整数而言,是`没有`符号位和数据位,即:没有原码、反码、补码的概念。无符号位的整数的数值都是直接使用二进制来表示的(也可以理解为,对于无符号位的整数,计算机底层存储的就是其原码),如下所示:
### 3.5.2 操作步骤
```c
unsigned int a = 10;
unsigned int b = -10;
* ① 查询 WSL2 的版本,确保 WSL2 的版本为 `0.67.6` 或更高版本:
```shell
# 如果未满足要求,则使用 wsl --update 更新 WSL2 版本
wsl --version # 在 win 中的 cmd 或 PowerShell 执行该命令
```
![](./assets/44.svg)
![](./assets/43.png)
* 这就是导致了一个结果就是:如果我定义一个`有符号`的负数,却让其输出`无符号`,必然造成结果不对,如下所示:
* ② 向 `/etc/wsl.conf` 配置文件中写入以下内容:
```shell
cat <<EOF | tee /etc/wsl.conf
[boot]
systemd=true
EOF
```
![](./assets/44.gif)
* ③ 重启 WSL 实例:
```shell
wsl --shutdown # 在 win 中的 cmd 或 PowerShell 执行该命令
```
![](./assets/45.gif)
* ④ 查看是否启用成功:
```shell
ps -p 1 -o comm=
```
![](./assets/46.png)
## 3.6 GCC 查看支持的 C 语言标准版本
### 3.6.1 概述
* GCC 是个跨平台的项目,支持 Windows、Linux 和 MacOS ,那么查看它支持的 C 语言标准版本就非常重要,以防止我们使用了新的 C 语言语法,本地却还是旧的 GCC 支持的 C 语言标准。
### 3.6.2 查看支持 C 语言标准版本的方法
* 可以执行如下的命令,查看 GCC 支持的 C 语言标准的版本:
```c
gcc -E -dM - </dev/null | grep "STDC_VERSION"
```
> [!NOTE]
>
> 其实就是通过 `__STDC_VERSION__` 的值,来查看支持的版本:
>
> * 如果没有查到,则默认是 c89 的标准。
> * 如果是 `#define __STDC_VERSION__ 199901L`,则默认支持的是 C99 标准。
> * 如果是 `#define __STDC_VERSION__ 201112L`,则默认支持是的 C11 标准。
> * 如果是 `#define __STDC_VERSION__ 201710L`,则默认支持的是 C17 标准。
> * 如果是 `#define __STDC_VERSION__ 2023xxL`,则默认支持的是 C23 标准。
>
> 需要说明的是在本文撰写之前C23 标准目前还是草案,并没有完全确定下来。
![](./assets/47.png)
### 3.6.3 切换 GCC 默认支持的 C 语言标准版本
#### 3.6.3.1 环境变量方式
* 可以通过设置一个环境变量,来更改默认的 C 语言的标准版本:
```shell
echo 'export CFLAGS="-std=c11"' >> ~/.bashrc
```
```shell
source ~/.bashrc
```
![](./assets/48.gif)
* 验证是否有效:
```shell
echo $CFLAGS
```
![](./assets/49.png)
#### 3.6.3.2 CMake 方式
* CMake 方式最简单了,只需要修改配置文件 CMakeLists.txt 文件,如下所示:
```txt {6}
cmake_minimum_required(VERSION 3.22.1)
project(c-study VERSION 1.0 LANGUAGES C)
# 设置 C 标准
set(CMAKE_C_STANDARD 23)
...
```
#### 3.6.3.3 命令行方式
* 有的时候,我们临时想验证某个版本的新特性,就可以只用在命令行中添加参数,来改变支持的 C 语言标准版本,如下所示:
```shell
gcc -std=c89 ...
```
```shell
gcc -std=c99 ...
```
```shell
gcc -std=c11 ...
```
```shell
gcc -std=c17 ...
```
## 3.7 CLion 如何集成 MSYS2?
### 3.7.1 概述
* CLion 在 Windows 中默认集成的是 `MinGW`,可能无法满足我们的需求,我们需要使用 `MSYS2` ,因为其提供的包管理器太好用了。
> [!NOTE]
>
> 需要说明的是,`MSYS2` 包含了 `MinGW`,这也是我们为什么在 `Windows` 上为什么使用 `MSYS2` 的其中一个原因。
### 3.7.2 集成方法
* ① 所有设置:
![](./assets/50.png)
* ② 工具链:
![](./assets/51.png)
## 3.8 CLion 中代码模板的使用
### 3.8.1 概述
* 在学习 C 语言的过程中,可能会不停的写这样的模板代码,如下所示:
```c
#include <stdio.h>
char *getBinary(int num) {
static char binaryString[33];
int i, j;
for (i = sizeof(num) * 8 - 1, j = 0; i >= 0; i--, j++) {
const int bit = (num >> i) & 1;
binaryString[j] = bit + '0';
}
binaryString[j] = '\0';
return binaryString;
}
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
int num = -10;
printf("b=%s\n", getBinary(num)); // b=11111111111111111111111111110110
printf("b=%d\n", num); // b=-10
printf("b=%u\n", num); // b=4294967286
setbuf(stdout, nullptr);
return 0;
}
```
* 其实C 语言的底层逻辑很简单C 语言压根不关心你定义的是有符号数还是无符号数,它只关心内存(如果定义的是有符号数,那就按照有符号数的规则来存储;如果定义的是无符号数,那就按照无符号数的规则来存储)。换言之,有符号数可以按照无符号数的规则来输出,无符号数也可以按照有符号数的规则来输出,至于输出结果对不对,那是程序员的事情,和 C 语言没有任何关系。
* 刚开始写,还感觉比较新鲜,非常好玩。但是,随着时间的深入,我们会感觉特别繁琐,又很无聊。那么,能否在 CLion 中配置一下,让其为我们自动生成呢?
> [!IMPORTANT]
![](./assets/52.gif)
### 3.8.2 配置方法
* ① 点击`设置`
![](./assets/53.png)
* ② `编辑器` --> `文件和代码模板`
![](./assets/54.png)
* ③ 点击`+`,配置对应的内容:
> [!NOTE]
>
> * ① 实际开发中,`printf` 函数中的常量、变量或表达式,需要和格式占位符一一对应;否则,将会出现数据错误的现象。
> * ② 正因为上述的原因很多现代化的编程语言Java 等,直接取消了无符号的概念。但是,很多数据库是使用 C 语言开发的MySQL 等,就提供了创建数据表的字段为无符号类型的功能,即:`UNSIGNED`(正整数) ,不要感觉困惑!!!
> * ③ 对于 `1000 0000 …… 0000 0000` 这个特殊的补码,无法按照上述的方法转换为原码,所以计算机直接规定这个补码对应的值就是 `-2³¹`,至于为什么,下节我们会详细分析。
> 模板的内容,如下所示:
>
> ```c
> #[[#include]]# <stdio.h>
>
> int main() {
>
> // 禁用 stdout 缓冲区
> setbuf(stdout, nullptr);
>
>
>
> return 0;
> }
> ```
![](./assets/55.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 819 KiB

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 212 KiB

View File

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 217 KiB

View File

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 486 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 426 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 495 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 363 KiB

After

Width:  |  Height:  |  Size: 451 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 522 KiB

View File

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 74 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 489 KiB

After

Width:  |  Height:  |  Size: 75 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 287 KiB

After

Width:  |  Height:  |  Size: 88 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 287 KiB

After

Width:  |  Height:  |  Size: 161 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 212 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 118 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 444 KiB

After

Width:  |  Height:  |  Size: 172 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 156 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 461 KiB

After

Width:  |  Height:  |  Size: 188 KiB

View File

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 211 KiB

View File

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 253 KiB

View File

Before

Width:  |  Height:  |  Size: 317 KiB

After

Width:  |  Height:  |  Size: 317 KiB

View File

Before

Width:  |  Height:  |  Size: 438 KiB

After

Width:  |  Height:  |  Size: 438 KiB

View File

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 253 KiB

View File

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 380 KiB

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 398 KiB

After

Width:  |  Height:  |  Size: 398 KiB

View File

Before

Width:  |  Height:  |  Size: 507 KiB

After

Width:  |  Height:  |  Size: 507 KiB

View File

Before

Width:  |  Height:  |  Size: 534 KiB

After

Width:  |  Height:  |  Size: 534 KiB

View File

Before

Width:  |  Height:  |  Size: 443 KiB

After

Width:  |  Height:  |  Size: 443 KiB

View File

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 156 KiB

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 217 KiB

View File

Before

Width:  |  Height:  |  Size: 509 KiB

After

Width:  |  Height:  |  Size: 509 KiB

View File

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

View File

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 167 KiB

View File

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 162 KiB

View File

@ -1,130 +1,534 @@
# 第一章:数据类型(⭐)
# 第一章:变量(⭐)
## 1.1 概述
## 1.1 程序中变化的数据
* 根据`变量`中`存储`的`值`的`不同`,我们可以将`变量`分为两类:
* `普通变量`:变量所对应的内存中存储的是`普通值`。
* `指针变量`:变量所对应的内存中存储的是`另一个变量的地址`。
- 在生活中,我们使用最多的不是固定的数据,而是会变化的数据:
- ① 购物车商品的`数量`、`价格`等。
- ② 一首歌`播放的时间`、`进度条`、`歌词的展示`等。
- ③ 微信聊天中`消息条数`、`时间`、`语音的长度`、`头像`、`名称`等。
- ④ 游戏中技能的`冷却时间`、`血量`、`蓝量`、`buff 时间`、`金币的数量`等。
- ……
* 如下图所示:
* 下图是一个`购物车`中`变化`的`数据`,即
![](./assets/1.png)
* 那么,在实际开发中,我们就会使用`变量`来`保存`和`操作`这些`变化`的`数据`。
## 1.2 变量
* 变量的定义:变量是程序中不可或缺的组成单位,最基本的存储单元。其实,变量就是一个存储数据的临时空间,可以向其中存储不同类型的数据,如:整数、小数、字符、字符串等,并且变量中的数据在程序运行的时候可以动态改变。
> [!NOTE]
>
> 普通变量和指针变量的相同点:
>
> * 普通变量有内存空间,指针变量也有内存空间。
> * 普通变量有内存地址,指针变量也有内存地址。
> * 普通变量所对应的内存空间中有值,指针变量所对应的内存空间中也有值。
>
> 普通变量和指针变量的不同点:
>
> * 普通变量所对应的内存空间存储的是普通的值,如:整数、小数、字符等;指针变量所对应的内存空间存储的是另外一个变量的地址。
> * 普通变量有普通变量的运算方式,而指针变量有指针变量的运算方式(后续讲解)。
> * `变量`:用来`存储数据`的`容器`。
> * `数据`:可以是一个用来计算的`数字`,如:上文购物车中的`价格`等;也可以是一句话中的`关键词`或`其它任意格式的数据`。
> * 变量的`特别`之处就在于`它存放的数据是可以改变`的。
* 那么,在 C 语言中变量的数据类型就可以这么划分,如下所示:
* 我们可以将`变量`想象为一个`容器`,盒子中`装的`就是我们想要的`数据`,并且我们需要`给`盒子`取`一个`特别的名称`;通过这个`特别的名称`,我们可以`给`盒子`添加数据`或`移除数据`,这个`特别的名称`就是`变量名`。
![](./assets/2.png)
> [!NOTE]
>
> * 根据`普通变量`中`存储`的`值`的类型不同,可以将`普通变量类型`划分为`基本数据类型`(整型、字符类型、浮点类型、布尔类型)和`复合数据类型`(数组类型、结构体类型、共用体类型、枚举类型)。
> * 根据`指针变量`所`指向空间`中`存储`的`值`的类型不同,可以将`指针类型`分为`基本数据类型指针`、`复合数据类型指针`、`函数指针`、`数组指针`等,例如:如果指针所指向的空间保存的是 int 类型,那么该指针就是 int 类型的指针。
> * ① `变量`是内存中的一个`存储区域`,该区域的数据可以在`同一类型`范围内`不断变化`。
> * ② 通过`变量名`,可以`操作`这块内存区域,向其中`存储数据`或`获取数据`以及`移除数据`。
> * ③ 变量的构成包含三个要素:`数据类型`、`变量名`、`需要存储的数据`。
> * ④ 在生活中,我们会经常说:这件衣服的价格是 `100整型` 元,这双鞋子的价格是 `250.5(小数,浮点类型)` 元,`今天天气真好(字符串类型)`之类的话;在计算机科学中,这些都是数据,并且它们是有类型,即:数据类型。(数据类型用于定义变量所能存储的数据的种类以及可以对这些数据进行的操作的一种分类,每种数据类型都有特定的属性和用途,它们决定了变量在内存中如何表示和存储,以及变量可以执行哪些操作)
## 1.2 整数类型
## 1.3 变量的声明和使用
### 1.2.1 概述
* ① 变量必须先声明,后使用。
* ② 可以先声明变量再赋值,也可以在声明变量的同时进行赋值。
* ③ 变量的值可以在同一类型范围内不断变化。
* 整数类型简称整型用于存储整数值12、20、50 等。
* 根据所占`内存空间`大小的不同,可以将整数类型划分为:
* ① 短整型:
>[!NOTE]
>
>* ① 在实际开发中,我们通常都会在声明变量的同时,给其赋值,这被称为初始化。
>* ② 如果不在声明变量的同时,进行初始化,默认情况下,系统会赋予的随机值,我们也称为垃圾值。
>* ③ 其实,变量既可以声明在 `main()` 函数的外面,称为`全局变量`;也可以声明在 `main()` 函数的立马,称为`局部变量`。使用未初始化的局部变量有很多风险,很多编译器会给出警告,提醒程序员注意。
| 类型 | 存储空间(内存空间) | 取值范围 |
| ------------------------------------ | -------------------- | ----------------------------------- |
| unsigned short (无符号短整型) | 2 字节 | 0 ~ 65,535 (2^16 - 1) |
| [signed] short有符号短整型默认 | 2 字节 | -32,768 (- 2^15) ~ 32,767 (2^15 -1) |
>[!IMPORTANT]
>
>* ① C 语言的编译器,在程序员在使用未初始化的局部变量会有警告的原因就是:变量声明的时候,会给变量分配一块内存空间,如果不对变量进行初始化,那么就意味着不对这块内存空间进行写入操作,那么这块内存空间的数据将保持不变。但是,这个内存空间的数据是哪里来的?是当前程序之前运行产生的,还是其它程序之前运行产生的,我们一无所知。由此可知,如果不进行初始化,那么变量对应的内存空间的数据是毫无意义的,是随机值,是垃圾值,没有任何价值。所以,建议在声明局部变量的同时进行初始化操作。
>* ② 在实际开发中,声明局部变量的时候,必须进行初始化操作,以便能够减少潜在的错误并提高代码的稳定性。
>* ③ 在很多编程语言Java ,如果局部变量没有进行初始化操作,将会在编译阶段报错。
* ② 整型:
| 类型 | 存储空间(内存空间) | 取值范围 |
| -------------------------------- | -------------------- | ------------------------------------------- |
| unsigned int无符号整型 | 4 字节(通常) | 0 ~ 4294967295 (0 ~2^32 -1) |
| [signed] int有符号整型默认 | 4 字节(通常) | -2147483648- 2^31 ~ 2147483647 (2^31-1) |
* ③ 长整型:
* 示例:先声明,再使用
| 类型 | 存储空间(内存空间) | 取值范围 |
| ----------------------------------- | -------------------- | --------------- |
| unsigned long无符号长整型 | 4 字节(通常) | 0 ~2^32 -1 |
| [signed] long有符号长整型默认 | 4 字节(通常) | - 2^31 ~ 2^31-1 |
```c
#include <stdio.h>
* ④ 长长整型:
int main() {
| 类型 | 存储空间(内存空间) | 取值范围 |
| ---------------------------------------- | -------------------- | --------------- |
| unsigned long long无符号长整型 | 8 字节(通常) | 0 ~2^64 -1 |
| [signed] long long有符号长整型默认 | 8 字节(通常) | - 2^63 ~ 2^63-1 |
// 声明一个整型变量,取名为 a
int a;
// 给变量赋值
a = 10;
printf("a = %d\n", a);
return 0;
}
```
* 示例:初始化(声明变量的同时给其赋值)
```c
#include <stdio.h>
int main() {
// 声明一个整型变量,取名为 b ,并直接赋值(初始化,实际开发中最为常用)
int b = 200;
// 修改变量 b 的值,将变量 a 的值赋值给变量 b
b = 300;
printf("b= %d\n", b);
return 0;
}
```
* 示例:同时声明多个变量并赋值
```c
#include <stdio.h>
int main() {
// 同时声明多个整型的变量并赋值
int c1 = 10, c2 = 20, c3 = 30;
printf("c1 = %d\n", c1);
printf("c2 = %d\n", c2);
printf("c3 = %d\n", c3);
return 0;
}
```
## 1.4 浅谈变量周边概念
### 1.4.1 数据类型
* 数据是放在内存中的,变量是给这块内存起的名字,有了变量就可以找到并使用这份数据。但是,该如何使用?
* 我们知道,对于数字、文字、符号、图形、音频、视频等数据都是以二进制的形式被加载进内存中,进而被计算机中的 CPU 所识别,它们本质上没有任何区别。那么,对于 `00010000` 这个二进制数字,我们是理解为数字 16 ?还是理解为图像中的某个像素的颜色?如果没有特别指明,我们并不清楚。
* 换言之,内存中的数据有多种解释方式;那么,我们在存储之前就必须明确指定,如: `int num = 10;` 中的 `int` 就是数据类型,用来限定 `num` (内存中的某个区域)中存储的是整数,而不是图像中某个像素的颜色。
> [!NOTE]
>
> * ① 数据类型在内存中占用的存储单元字节数就称为该数据类型的长度步长short 占用 2 个字节的内存,就称 short 的长度(步长)是 2。
> 总结:
>
> * ② C 语言并没有严格规定各种整数数据类型在内存中所占存储单元的长度,只做了宽泛的限制:
> * ① 数据类型用来说明数据的类型,确定了数据的解释方式,让计算机和程序员不会产生歧义。
> * ② C 语言中很多基本的数据类型char、short、int、long 等;如果需要,也可以组成更加复杂的数据类型(后续讲解)。
### 1.4.2 连续定义多个变量
* 为了让程序的书写更加简洁C 语言支持多个变量的连续定义,如下所示:
```c
int a,b,c;
```
```c
float m=3.14,n=4.14;
```
> [!NOTE]
>
> * short 至少占用 2 个字节的存储单元。
> * int 建议为一个机器字长(指计算机的处理器在一次操作中能够处理的二进制数据的位数,机器字长是处理器的“字”长度,它决定了处理器在一个时钟周期内能够处理的数据量,如:早期的计算机的处理器通常是 8 位的机器字长,意味着处理器一次只能处理 8 位(二进制)数据;之后的计算机的处理器有 16 位的机器字长,意味着处理器一次可以处理 16 位的数据;再之后计算机的处理器有 32 位或 64 位的机器字长,意味着处理器一次可以处理 32 位或 64位的数据。即32 位环境下 int 占用 4 个字节的存储单元64 位环境下 int 占用 8 个字节的存储单元。
> * short 的长度(步长)不能大于 intlong 的长度(步长)不能小于 intlong long 不能小于 long。
> * ① 连续定义的多个变量以逗号`,`分隔,并且要拥有相同的数据类型。变量可以初始化,也可以不初始化。
> * ② 很多 C 语言程序员喜欢这么写;但是,本人不是很喜欢,因为连续定义可能会导致代码的可读性降低,特别是在声明时变量之间用逗号分隔,容易导致混淆。
### 1.4.3 数据的长度
* 所谓数据长度,就是指的是数据占用多少个字节。占用的字节越多,能存储的数据就越多;对于数字而言,值就会越大。反之,能存储的数字就有限。
* 多个数据在内存中是连续存储的,彼此之间是没有明显的界限的。如果不指明数据的长度,那么计算机就不知道何时才能存取结束。假设我们保存了一个整数 1000 ,它占用 4 个字节的内存,而读取它的时候却读取了 3 个字节或5 个字节;那么,显示是不正确的。
* 所以,在定义变量的时候还要指明数据的长度,而这恰恰是数据类型的另外一个作用,即:`数据类型除了指明数据的解释方式,还指明了数据的长度`。
> [!NOTE]
>
> * ③ 那么,各种整数数据类型在内存中所占存储单元的长度的公式就是 `2 ≤ sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long)`,具体的存储空间由编译系统自行决定。其中,`sizeof` 是测量类型或变量、常量长度的`运算符`。
> 总结:在 C 语言中,每一种数据类型所占用的字节数都是固定的,知道了数据类型,也就知道了数据的长度。
* 在 32 位环境中,各种数据类型的长度,如下所示:
| 数据类型 | 长度(字节) |
| ----------------- | ------------ |
| `char` | 1 |
| `short` | 2 |
| `int` | 4 |
| `long` | 4 |
| `long long` | 8 |
| `float` | 4 |
| `double` | 8 |
| `long double` | 8 |
| `pointer`(指针) | 4 |
> [!NOTE]
>
> * ① C 语言有多少种数据类型,每种数据类型长度是多少、该如何使用,这是每一位 C 程序员都必须要掌握的。
> * ② 当然,不必担心,后续还会一一讲解的。
> [!IMPORTANT]
>
> * ① 之所以这么规定,是为了可以让 C 语言长久使用,因为目前主流的 CPU 都是 64 位,但是在 C 语言刚刚出现的时候CPU 还是以 8 位和 16 位为主。如果当时就将整型定死为 8 位或 16 位,那么现在我们肯定不会再学习 C 语言了。
> * ② 整型分为有符号 signed 和无符号 unsigned 两种,默认是 signed。
> * ③ 在实际开发中,`最常用的整数类型`就是 `int` 类型了,如果取值范围不够,就使用 long 或 long long 。
> * ④ C 语言中的`格式占位符`非常多,只需要大致了解即可;因为,我们在实际开发中,一般都会使用 C++ 或 Rust 以及其它的高级编程语言Java 等,早已经解决了必须通过`格式占位符`来才能将变量进行输入和输出。
> * ① 数据类型只需要在定义变量时指明,而且必须指明。
> * ② 使用变量时无需再指明,因为此时的数据类型已经确定了。
### 1.2.2 短整型(了解)
## 1.5 从计算机底层看变量
* 语法:
### 1.5.1 内存条的内部结构
```c
unsigned short x = 10 ; // 无符号短整型
* 如果只看内存条的外观,无非就是一些集成电路和颗粒而已,如下所示:
![](./assets/3.jpeg)
* 并且,我们只需要将内存条插入到计算机主板对应的内存条插槽上,就可以正常工作,如下所示:
![](./assets/4.jpg)
* 在家用的台式机主板上,通常有 4 个插槽或 2 个插槽,例如:本人的计算机就支持 4 个插槽,如下所示:
![](./assets/5.png)
>[!NOTE]
>
>* ① 上图中的外形规格是 DIMM所以我们通常也以 DIMM 也表示内存条。
>* ② DIMM 是内存条的物理形式,安装在主板的内存插槽中。
>* ③ 常见的 DIMM 类型包括 UDIMM非缓冲 DIMM、RDIMM缓冲 DIMM和 LRDIMM负载减少DIMM
* 我们可以通过 [CPU-Z](https://www.cpuid.com/) 这个软件,查看 CPU 的一些指标信息,如下所示:
![](./assets/6.png)
> [!NOTE]
>
> * ① 通过 CPU-Z 表明本人的台式机是支持双通道的,`channel` 在`计算机`中可以`翻译`为`信道`或`通道`。
> * ② 通道是内存控制器与内存模块之间的通信路径。
> * ③ 多通道内存可以提高数据传输带宽。例如:双通道内存系统同时使用两个通道来传输数据,从而提高性能。
> * ④ 现代主板通常支持双通道Dual Channel、四通道Quad Channel甚至八通道Octa Channel
* 对于家用台式机而言,如果将内存条的插槽从左到右依次编号,如下所示:
![](./assets/7.png)
* 其中,`槽1` 和 `槽2` 是一个通道,`槽3` 和 `槽4` 是一个通道;所以,通常是这么建议的:
* 如果只有 1 根内存条,就插到 `槽2` 中。
* 如果有 2 根内存条,就分别插入到 `槽2``槽4` 中。
* 如果有 4 根内存条,就全插满即可。
> [!NOTE]
>
> 组成双通道配置的内存条需要遵循一些基本要求来确保它们能够正常以双通道模式运行:
>
> - ① **相同容量**:理想情况下,组成双通道的内存条应该具有相同的容量。这样可以确保它们在处理数据时的一致性和兼容性。
> - ② **匹配的速度规格**内存条应该具有相同的速度规格即它们的频率DDR4-2400、DDR4-3200等应该相同。不同速度的内存条可以一起工作但系统会以所有内存条中最慢的那个的速度运行。
> - ③ **相同的时序**内存条的时序CL16-18-18-38应该匹配。时序参数影响内存的响应速度和稳定性不匹配的时序可能会降低性能或导致系统不稳定。
> - ④ **相同的制造商和型号**(推荐):虽然不是强制性要求,但选择相同制造商和型号的内存条可以最大限度地减少兼容性问题。不同制造商的内存条可能在微小的规格和性能上有差异,这有可能影响双通道配置的效能。
* 内存条表面会有内存颗粒,如下所示:
![](./assets/8.png)
> [!NOTE]
>
> 上图中的内存条有 8 个内存颗粒;但是,高端服务器上的内存条通常会存在 9 个内存颗粒,最后 1 个内存颗粒专门用来做 ECC 校验。
* 一个内存条有两面,高端的内存条两面都有内存颗粒,我们将每个面称为 Rank 。那么,如果内存条有两个面,就是存在 Rank0 和 Rank1 ,即:
![](./assets/9.png)
* 内存条表面的黑色颗粒,我们称为 chip芯片 ,即:
![](./assets/10.png)
> [!NOTE]
>
> * ① 内存颗粒是内存条上的 DRAM 芯片,每个芯片包含多个存储单元。
> * ② 内存颗粒存储数据并与内存控制器进行数据交换。
* 在 chip 中还有 8 个 bank每个 bank 就是数据存储的实体,这些 bank 组成了一个二维矩阵,只要声明了 column 和 row 就可以从每个 bank 中取出 8bit 1 Bytes的数据如下所示
![img](./assets/11.png)
* 综上所示,内存条的分层结构就是 `Channel > DIMM > Rank -> Chip -> Bank -> Row/Column`
### 1.5.2 变量的作用
* 如果我们希望计算 10 和 20 的和;那么,在计算机中需要怎么做?
* ① 首先,计算 10 和 20 的运算,一定在 CPU 中进行,因为在计算机中的各个部件中,只有 CPU 有运算器ALU
* ② 其次,我们需要将 10 和 20 交给 CPU ;由于 CPU 只能和内存进行交互,那么我们必须将 10 和 20 存储到内存中。
> [!NOTE]
>
> 即使 10 和 20 是存储在文件中的,也需要先加载进内存,然后再交给 CPU 进行运算。
* ③ 最后,只需要告诉 CPU 做何种运算,如:加、减、乘、除等。
* 其中,最为重要的问题就是如何将数据存储到内存中?答案就是通过`变量`。
![](./assets/12.png)
* 我们知道,计算机底层是使用二进制来表示指令和数据的;但是,如果我们的代码都是这样的,即:
```txt
0000,0000,000000010000 代表 LOAD A, 16
0000,0001,000000000001 代表 LOAD B, 1
0001,0001,000000010000 代表 STORE B, 16
```
* 这样,直接使用`内存地址`来编写代码(机器语言)实现是太难阅读、修改和维护了;于是,我们就使用了汇编语言来编写代码,并通过编译器来将汇编语言翻译为机器语言,即:
```txt
LOAD A, 16 -- 编译 --> 0000,0000,000000010000
LOAD B, 1 -- 编译 --> 0000,0001,000000000001
STORE B, 16 -- 编译 --> 0001,0001,000000010000
```
* 但是,这样的汇编语言还是面向机器的,编程时仍然需要记住和管理大量内存地址,不具备程序的移植性;于是,我们就是使用了高级语言来编写代码,并引入了变量的概念,即:
```c
short x = -10; // 有符号短整型
int num = 10;
```
* 我们使用`变量名`来`关联`内存`地址`,这样我们在编写代码的时候,就可以不用直接操作内存地址,极大地提高了代码的可读性和开发效率。并且,当程序运行完毕之后,程序所占用的内存还会交还给操作系统,以便其它程序使用。
* 综上所述,高级语言编译器的作用就是:
* ① 编写源代码时使用变量名。
* ② 程序在经过编译器的编译之后,所有变量名被替换为具体地址。
* ③ ……
* 此时,我们就可以知道,`变量`就是内存中用于`存储数据`的`临时空间`,并且变量中的值是可以变化的。
* `内存`中空间的`最小单位`是`字节`Bytes即 8 个 0 或 1 ,如下所示:
```txt
00011001 00100110 00100110 00100110 00100110 ...
```
> [!NOTE]
>
> * ① 有符号表示的是正数、负数和 0 ,即有正负号。无符号表示的是 0 和正数,即正整数,没有符号。
> * ② 在 `printf` 中`无符号短整型unsigned short`的`格式占位符`是 `%hu` `有符号短整型signed short`的`格式占位符`是 `%hd`
> * ③ 可以通过 `sizeof` 运算符获取`无符号短整型unsigned short` 和 `有符号短整型signed short` 的`存储空间(所占内存空间)`。
> * ③ 可以通过 `#include <limits.h>` 来获取 `无符号短整型unsigned short` 和`有符号短整型signed short`的`取值范围`。
> 计算机中存储单位的换算,如下所示:
>
> * 1 B = 8 bit。
> * 1 KB = 1024 B。
> * 1 MB = 1024 KB。
> * 1 GB = 1024 MB。
> * 1 TB = 1024 GB 。
> * ……
* 在内存中,每一个字节都有一个编号,这个编号我们称之为地址。一个变量至少占用 1 个字节1 个或多个字节我们将变量的第一个字节所占用的地址变量的首地址就称之为该变量的地址。CPU 就可以通过变量地址找到某个变量的值,然后拿到具体的数据进行计算了。
> [!NOTE]
>
> 变量就是保存程序运行过程中临时产生的值。
* 其实,到这里还是有疑惑的?我们说过,一个变量至少会占用 1 个字节,如果一个变量占用了 4 个字节,而 CPU 只会通过变量的地址(首地址)获取数据,那么 CPU 是如何获取完整的数据的?答案就是通过`数据类型`,数据类型除了限制数据的种类,还限制了数据在内存中所占空间的大小,如上图所示:
* ① 假设变量 `a` 的首地址是 `01` ,变量的数据类型是 `4` 个字节。
* ② 那么CPU 就会依次,从 `01 ~ 04` 中获取数据。
* 再次,剖析下变量的语法格式:
```txt
数据类型 变量名 = 值;
```
* `变量名`的`作用`,如下所示:
* ① 当我们`编写`代码的时候,使用`变量名`来`关联`某块内存的`地址`。
* ② 当 CPU `执行`的时候,会将变量名`替换`为具体的地址,再进行具体的操作。
> [!IMPORTANT]
>
> 变量名(标识符)需要符合命名规则和命名规范!!!
* `数据类型`的`作用`,如下所示:
* ① 变量的数据类型`决定了`变量所占空间的大小。当我们在声明变量的时候写了数据数据类型CPU 就知道从变量的首地址位置开始取多少字节。
* ② 变量的数据类型`决定了`两个变量是否能够运行以及能够做何种运算。例如JavaScript 就没有 char 类型的变量,都是 string 类型,可以和任意数据类型的数据拼接,并转换为 string 类型Java 中有 char 类型的变量,底层都会转换 unicode 编码,然后再计算。
* `值`的`作用`,如下所示:
* ① `值`就是`内存`中`实际存储`的`数据`。
* ② `=` 是赋值操作符,就是将等号右侧的数据存储到等号左侧的变量名所代表的内存空间。
* 那么,如下代码的含义就是:
```c
// int 数据类型4 个字节
// num 变量名 -- 关联内存中的一块存储空间
// = 10 将 10 存储到 num 所代表的 4 个字节的存储空间中
int num = 10;
```
## 1.6 变量的重要操作
### 1.6.1 变量的输出
* 在计算机中,所谓的`输入`和`输出`都是以计算机CPU 和内存)为主体而言的,即:
>[!NOTE]
>
>* ① 输入:从输入设备(键盘、鼠标、扫描仪)向计算机输入数据。
>
>* ② 输出:从计算机向外部输出设备(显示器、打印机)输出数据。
![](./assets/13.png)
* 在 C 语言中,提供了 `printf()` 函数用于输出信息,其函数声明是:
```c
int printf (const char *__format, ...) {
...
}
```
* `printf` 的标准含义是格式化输出文本,来源于 `print formatted格式化打印`的缩写,其语法规则,如下所示:
![](./assets/14.svg)
> [!NOTE]
>
> * ① 格式化字符串:是使用双引号括起来的字符串,里面包含了普通的字符串和格式占位符。
> * ② 格式占位符(格式声明符):由 `%` 和`格式字符`组成,作用是将输出的数据转换为指定的格式后输出,这里的 `%d` 表示整数。
> * ③ 输出列表:是程序要输出的一些数据,可以是常量、变量或表达式,需要和格式占位符一一对应。
* 在计算机中,二进制、八进制、十进制以及十六进制的英文名称和缩写,如下所示:
* 二进制binary缩写是 bin。
* 八进制octal缩写是 oct。
* 十进制decimal缩写是 dec。
* 十六进制Hexadecimal缩写是 hex。
* 其实,我们也可以在 Windows 系统中的计算器中来看到,即:
![](./assets/15.png)
> [!IMPORTANT]
>
> * ① 在生活中的 decimal 是小数的意思。
> * ② 但是在计算机中decimal 的完整含义是 decimal integer ,即十进制整数。
* 示例:定义和打印短整型变量
* 示例:
```c
#include <stdio.h>
int main() {
// 定义有符号 short 类型
signed short s1 = -100;
// 声明变量并赋值
int num = 18;
printf("s1 = %hd \n", s1); // s1 = -100
// 使用输出语句,将变量 num 的值输出,其中 %d 表示输出的是整数
printf("我今年%d岁\n", num);
// 定义无符号 short 类型
unsigned short s2 = 100;
printf("s2 = %hu \n", s2); // s2 = 100
return 0;
}
```
// 定义 short 类型,默认是有符号
short s3 = -200;
printf("s3 = %hd \n", s3); // s3 = -200
### 1.6.2 计算变量的大小
* 我们可以使用 `sizeof`关键字(运算符)来计算变量或类型所占内存空间的大小。
* 示例:
```c
#include <stdio.h>
int main() {
int num = 10;
printf("变量所占内存空间的大小:%zd字节\n", sizeof(num));
// 数据类型所占内存空间的大小
printf("数据类型所占内存空间的大小:%zd字节\n", sizeof(int));
return 0;
}
```
### 1.6.3 获取变量的地址
* 在 C 语言中,我们可以使用`取地址运算符 &` 来获取变量的地址。
* 示例:
```c
#include <stdio.h>
int main() {
int num = 10;
printf("变量 num 的值是:%d\n", num);
printf("变量 num 的地址(指针)是:%#p\n", &num);
return 0;
}
```
### 1.6.4 变量的输入
* 在 C 语言中,提供了 `scanf()` 函数用于从标准输入(通常是键盘)中读取数据并根据变量的地址赋值给变量(变量需要提前声明),其函数声明是:
```c
int scanf(const char *__format, ...) {
...
}
```
* 其语法规则,如下所示:
![](./assets/16.png)
> [!NOTE]
>
> `&age`、`&num` 中的 `&`是寻址操作符,`&age` 表示变量 `age` 在内存中的地址。
> [!CAUTION]
>
> * ① scanf() 函数中的 `%d`,如果是连着写,即:`%d%d`,那么在输入数据的时候,数据之间不可以使用逗号`,`分隔只能使用空白字符空格、tab 键或回车键),即:`2空格3tab`或`2tab3回车`等。
>
> * ② 如果是 `%d,%d`,则输入的时候需要加上逗号`,`,即:`2,3`。
> * ③ 如果是 `%d %d`,则输入的时候需要加上空格,即:`2空格3`。
* 示例:计算圆的面积,半径由用户指定
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
// CLion debug 独有,后文不再提及,如果 debug 有问题,就添加如下代码
setbuf(stdout, NULL);
float radius;
printf("请输入一个半径:");
scanf("%f", &radius);
double area = 3.1415926 * radius * radius;
printf("半径是%f的圆的面积是%.2lf", radius, area);
return 0;
}
@ -132,21 +536,27 @@ int main() {
* 示例:获取类型占用的内存大小(存储空间)
* 示例:输入一个整数值,求其绝对值
```c
#include <stdio.h>
int main() {
size_t s1 = sizeof(unsigned short);
printf("unsigned short 的存储空间是 %zu 字节 \n", s1); // 2
int num;
size_t s2 = sizeof(signed short);
printf("signed short 的存储空间是 %zu 字节 \n", s2); // 2
printf("请输入一个整数:");
scanf("%d", &num);
size_t s3 = sizeof(short);
printf("short 的存储空间是 %zu 字节 \n", s3); // 2
int absNum;
if (num < 0) {
absNum = -num;
} else {
absNum = num;
}
printf("%d的绝对值是%d", num, absNum);
return 0;
}
@ -154,61 +564,267 @@ int main() {
* 示例:获取类型的取值范围
* 示例:输入多个变量的值,求其乘积
```c
#include <limits.h>
#include <stdio.h>
int main() {
printf("unsigned short 类型的范围是[0,%hu]\n", USHRT_MAX); // [0,65535]
printf("short 类型的范围是[%hd,%hd]\n", SHRT_MIN,SHRT_MAX); // [-32768,32767]
int a, b, c;
printf("请输入整数 a 、b 和 c 的值:");
scanf("%d %d %d", &a, &b, &c);
int result = a * b * c;
printf("%d × %d × %d = %d", a, b, c, result);
return 0;
}
```
### 1.2.3 整型
## 1.7 标识符
* 语法:
### 1.7.1 概述
```c
unsigned int x = 10 ; // 无符号整型
* 在 C 语言中,变量、函数、数组名、结构体等要素命名的时候使用的字符序列,称为标识符。
> [!NOTE]
>
> * ① 在上世纪 60 - 70 年代的时候,因为国家贫穷,人民生活不富裕等原因,家长虽然会给孩子取名为:`张建国`、`李华强`等。
> * ② 但是,也会给孩子取小名,如:`二狗子`、`狗剩`等,目的是希望孩子能健康成长(养活),像 `张建国`、`李华强`、`二狗子`、`狗剩`都是名字(标识符),伴随人的一生。
### 1.7.2 标识符的命名规范
* 强制规范:
* ① 只能由`小写`或`大写英文字母``0-9` 或 `_` 组成。
* ② 不能以`数字`开头。
* ③ 不可以是`关键字`。
* ④ 标识符具有`长度`限制,不同编译器和平台会有所不同,一般限制在 63 个字符内。
* ⑤ 严格`区分大小写字母`Hello、hello 是不同的标识符。
* 建议规范:
* ① 为了提高阅读性使用有意义的单词见名知意sumnamemaxyear 等。
* ② 使用下划线连接多个单词组成的标识符max_classes_per_student 等。
* ③ 多个单词组成的标识符,除了使用下划线连接,也可以使用小驼峰命名法,除第一个单词外,后续单词的首字母大写,如: studentId、student_name 等。
* ④ 不要出现仅靠大小写区分不同的标识符name、Name 容易混淆。
* ⑤ 系统内部使用了一些下划线开头的标识符C99 标准添加的类型 `_Bool`,为防止冲突,建议开发者尽量避免使用下划线开头的标识符。
* 示例:合法(不一定建议)的标识符
```txt
a、BOOK_sun、MAX_SIZE、Mouse、student23、
Football、FOOTBALL、max、_add、num_1、sum_of_numbers
```
* 示例:非法的标识符
```txt
$zj、3sum、ab#cd、23student、Foot-baii、
s.com、bc、j**p、book-1、tax rate、don't
```
### 1.7.3 关键字
* C 语言中的关键字是编译器`预定义`的`保留字`,它们有`特定`的`含义`和`用途`,用于控制程序的结构和执行。
* C80 和 C90 ANSI C定义的关键字如下所示
| 类型(功能) | 具体关键字 |
| -------------------------- | ------------------------------------------------------------ |
| 数据类型关键字 | `char`、`double`、`float`、`int`、`long`、`short`、`signed`、`unsigned`、`void` |
| 存储类说明符关键字 | `auto`、`extern`、`register`、`static`、`typedef`、`volatile`、`const` |
| 控制语句关键字 | `break`、`case`、`continue`、`default`、`do`、`else`、`for`、`goto`、`if`、`return`、`switch`、`while` |
| 结构体、联合体和枚举关键字 | `enum`、`struct`、`union` |
| 其他关键字 | `sizeof` |
* C99 新增的关键字,如下所示:
| 类型(功能) | 具体关键字 |
| ------------------ | --------------------------------- |
| 数据类型关键字 | `_Bool`、`_Complex`、`_Imaginary` |
| 存储类说明符关键字 | `inline`、`restrict` |
| 其他关键字 | `_Complex``_Imaginary` |
* C11 新增的关键字,如下所示:
| 类型(功能) | 具体关键字 |
| ------------------ | ------------------------------------------------------------ |
| 存储类说明符关键字 | `_Atomic` |
| 其他关键字 | `_Alignas``_Alignof``_Generic``_Noreturn``_Static_assert``_Thread_local` |
> [!IMPORTANT]
>
> * ① 关键字不能用作标识符(如:变量名、函数名等)。
> * ② 不要死记硬背这些关键字,在实际开发中,并不一定全部使用到;而且,在学到后面的时候,会自动记住这些关键字以及对应的含义。
# 第二章:常量(⭐)
## 2.1 概述
* 在程序运行过程中,不能改变的量就是常量。
>[!NOTE]
>
>* ① 在数学中的 `π`,就是一个常量,其值为 3.1415926 。
>* ② 在生活中,人类的性别只有`男`和`女`;其中,`男`和`女`也是常量。
>* ③ ...
## 2.2 常量的分类
* 在 C 语言中的变量的分类,如下所示:
* ① 字面量常量。
* ② 标识符常量:
* `#define` 宏定义的标识符常量。
* `const` 关键字修饰的标识符常量。
* 枚举常量。
>[!NOTE]
>
>* ① 所谓的`字面量常量`,就是可以直接使用的常量,不需要声明或定义,包括:整数常量、浮点数常量以及字符常量。
>* ② 所谓的`标识符常量`,就是使用标识符来作为常量名,包括: `#define` 宏定义的标识符常量、`const` 关键字修饰的标识符常量、枚举常量。
* 示例:字面量常量
```c
int x = -10; // 有符号整型
#include <stdio.h>
int main() {
1;
'A';
12.3;
"你好";
return 0;
}
```
* 示例:字面量常量
```c
#include <stdio.h>
int main() {
printf("整数常量 =》%d\n", 1);
printf("字符常量 =》%c\n", 'A');
printf("浮点数常量 =》%f\n", 12.3);
printf("字符串常量 =》%s\n", "你好");
return 0;
}
```
## 2.3 使用 #define 定义常量
* `#define` 来定义常量,也叫作宏定义,就是用一个标识符来表示一个常量值,如果在后面的代码中出现了该标识符,那么编译时就全部替换成指定的常量值,即用宏体替换所有宏名,简称`宏替换`。
* 格式是:
```c
#define 常量名 常量值
```
> [!IMPORTANT]
>
> * ① 其实`宏定义`的常量的`执行时机`是在`预处理`阶段,将所有`宏常量`替换完毕,才会继续编译代码。
> * ② 不要以 `;` 结尾,如果有 `;` ,分号也会成为常量值的一部分。
> * ③ `# define` 必须写在 `main` 函数的外面!!!
> * ④ `常量名`习惯用`大写字母`表示,如果多个单词,使用 `_` 来分隔,以便和变量区分。
* 示例:
```c
#include <stdio.h>
#define PI 3.1415926
int main() {
double radius = 2.5;
double area = PI * radius * radius;
printf("半径为%lf的圆的面积是%.2lf", radius, area);
return 0;
}
```
## 2.4 const 关键字
* C99 标准新增,这种方式跟定义一个变量是类似的;只不过,需要在变量的数据类型前加上 `const` 关键字。
* 和使用 `#define定义宏常量`相比const 定义的常量有详细的数据类型,而且会在编译阶段进行安全检查,在运行时才完成替换,所以会更加安全和方便。
* 格式是:
```c
const 数据类型 常量名 = 常量值;
```
* 示例:
```c
#include <stdio.h>
const double PI = 3.1415926;
int main() {
double radius = 2.5;
double area = PI * radius * radius;
printf("半径为%lf的圆的面积是%.2lf", radius, area);
return 0;
}
```
## 2.5 枚举常量
* 格式:
```c
enum 枚举常量 {
xxx = 1;
yyy;
...
}
```
> [!NOTE]
>
> * ① 有符号表示的是正数、负数和 0 ,即有正负号。无符号表示的是 0 和正数,即正整数,没有符号。
> * ② 在 `printf` 中`无符号整型unsigned int`的`格式占位符`是 `%u` `有符号整型signed int`的`格式占位符`是 `%d`
> * ③ 可以通过 `sizeof` 运算符获取`无符号整型unsigned int` 和 `有符号整型signed int` 的`存储空间(所占内存空间)`。
> * ③ 可以通过 `#include <limits.h>` 来获取 `无符号整型unsigned int` 和`有符号整型signed int`的`取值范围`。
> * ① 默认情况下,枚举常量是从 0 开始递增的。
> * ② 也可以在定义枚举常量的时候,自定义它们的值。
* 示例:定义和打印整型变量
* 示例:
```c
#include <stdio.h>
enum sex {
MALE = 1,
FEMALE = 2,
};
int main() {
// 定义有符号 int 类型
signed int i1 = -100;
printf("i1 = %d \n", i1); // i1 = -100
// 定义无符号 int 类型
unsigned int i2 = 100;
printf("i2 = %u \n", i2); // i2 = 100
// 定义 int 类型,默认是有符号
short i3 = -200;
printf("i3 = %d \n", i3); // i3 = -200
printf("%d\n", MALE);
printf("%d\n", FEMALE);
return 0;
}
@ -216,233 +832,259 @@ int main() {
* 示例:获取类型占用的内存大小(存储空间)
* 示例:
```c
#include <stdio.h>
enum Sex {
MALE = 1,
FEMALE = 2,
};
int main() {
enum Sex sex;
size_t i1 = sizeof(unsigned int);
printf("unsigned int 的存储空间是 %zu 字节 \n", i1); // 4
size_t i2 = sizeof(signed int);
printf("signed int 的存储空间是 %zu 字节 \n", i2); // 4
size_t i3 = sizeof(int);
printf("int 的存储空间是 %zu 字节 \n", i3); // 4
printf("请输入性别(1 表示男性, 2 表示女性)");
scanf("%d", &sex);
printf("您的性别是:%d\n", sex);
return 0;
}
```
## 2.6 `#define` 定义常量 VS `const` 定义常量
### 2.6.1 概述
* 示例:获取类型的取值范围
* `#define``const` 都可以用来定义常量,但它们的工作方式和应用场景有所不同。
### 2.6.2 语法和定义方式
* `#define` 是一个预处理指令,用来定义宏。在编译时,所有的宏会被预处理器展开为它们定义的值,类似于文本替换。
```c
#include <limits.h>
#include <stdio.h>
int main() {
printf("unsigned int 类型的范围是[0,%u]\n", UINT_MAX); // [0,4294967295]
printf("int 类型的范围是[%d,%d]\n", INT_MIN,INT_MAX); // [-2147483648,2147483647]
return 0;
}
#define PI 3.14159
```
### 1.2.4 长整型(了解)
* 语法:
* `const`是一个编译时常量,用来定义具有类型的常量变量。它是由编译器处理的,并且在运行时仍然可以保留类型信息。
```c
unsigned long x = 10 ; // 无符号长整型
const float PI = 3.14159;
```
### 2.6.3 类型检查
* `#define`没有类型,它只是简单的文本替换,不会进行类型检查。因此,如果在宏中定义了错误的类型,可能导致编译错误或运行时错误。
```c
long x = -10; // 有符号长整型
#define MAX 10 + 20 // 实际展开后可能是 MAX = 10 + 20而不是 30
```
* `const`具有类型,编译器会进行类型检查。如果定义时类型不匹配,会报编译错误。
```c
const int MAX = 30; // 定义时指定了类型,类型检查严格
```
### 2.6.4 作用域
* `#define`宏没有作用域的概念,它是在预处理时进行全局替换的。因此,可能会引发意外的替换问题,尤其是在复杂项目中。
```c
#define SIZE 10 // SIZE 可能在其他文件中也被不小心替换
```
* `const`具有作用域,它遵循 C 语言的作用域规则(比如局部作用域、全局作用域)。这使得 `const` 定义的常量更安全,因为它们只能在指定的范围内使用。
```c
const int SIZE = 10; // 可以局部或全局定义,不会引发冲突
```
### 2.6.5 调试
* `#define`在调试时,宏常量被替换为字面值,因此调试工具中无法看到它的原始名称,只能看到被替换后的值。
* `const`常量在编译后依然存在,因此在调试时可以直接看到常量的名称和它的值,调试体验更好。
### 2.6.6 内存分配
* `#define`宏在预处理阶段替换,不占用内存。
* `const` 常量会被分配内存,特别是在全局或静态情况下,但它也可以被优化为编译时常量,有时也不会占用额外的内存。
### 2.6.7 适用场景
* `#define`通常用于定义简单的常量值、条件编译或宏函数(用于文本替换)。适合不需要类型、安全性检查的场合。
* `const`用于定义类型安全的常量,适合需要进行类型检查或确保作用域的场合。
### 2.6.8 总结
* `const` 更加安全,尤其是在需要类型检查和局部作用域的时候;而 `#define` 常用于需要简洁的文本替换或宏定义。`const` 和 `#define` 对比的表格,如下所示:
| 特性 | `#define` | `const` |
| -------- | ------------------ | --------------------- |
| 类型检查 | 无类型检查 | 有类型检查 |
| 作用域 | 无(全局替换) | 有作用域(局部/全局) |
| 调试支持 | 差(替换为字面值) | 好(保留名称) |
| 内存开销 | 无 | 可能会有 |
| 使用场景 | 宏、条件编译 | 类型安全的常量 |
# 第三章:进制
## 3.1 概述
* 计算机的底层只有`二进制`,即计算机中`运算`和`存储`的`所有数据`都需要转换为`二进制`,包括:数字、字符、图片、视频等。
![](./assets/17.jpg)
* 之前,我们也提到现代的计算机(量子计算机除外)几乎都遵循`冯·诺依曼`体系结构,其理论要点如下:
* ① **存储程序**`程序指令`和`数据`都存储在计算机的内存中,这使得程序可以在运行时修改。
* ② **二进制逻辑**:所有数据和指令都以`二进制`形式表示。
* ③ **顺序执行**:指令按照它们在内存中的顺序执行,但可以有条件地改变执行顺序。
* ④ **五大部件**:计算机由`运算器`、`控制器`、`存储器`、`输入设备`和`输出设备`组成。
* ⑤ **指令结构**:指令由操作码和地址码组成,操作码指示要执行的操作,地址码指示操作数的位置。
* ⑥ **中心化控制**计算机的控制单元CPU负责解释和执行指令控制数据流。
* 所以,再次论证了为什么计算机只能识别二进制。
## 3.2 进制
### 3.2.1 常见的进制
* 在生活中,我们最为常用的进制就是`十进制`,其规则是`满 10 进 1` ,即:
![](./assets/18.jpeg)
* 在计算机中,常见的进制有`二进制`、`八进制`和`十六进制`,即:
* 二进制:只能 0 和 1 ,满 2 进 1 。
* 八进制0 ~ 7 ,满 8 进 1 。
* 十六进制0 ~ 9 以及 A ~ F ,满 16 进 1 。
> [!NOTE]
>
> * ① 有符号表示的是正数、负数和 0 ,即有正负号。无符号表示的是 0 和正数,即正整数,没有符号。
> * ② 在 `printf` 中`无符号长整型unsigned long`的`格式占位符`是 `%lu` `有符号长整型signed long`的`格式占位符`是 `%ld`
> * ③ 可以通过 `sizeof` 运算符获取`无符号长整型unsigned long` 和 `有符号长整型signed long` 的`存储空间(所占内存空间)`。
> * ③ 可以通过 `#include <limits.h>` 来获取 `无符号长整型unsigned long` 和`有符号长整型signed long`的`取值范围`。
> 在十六进制中,除了 `0``9` 这十个数字之外,还引入了字母,以便表示超过 `9` 的值。其中,字母 `A` 对应十进制的 `10` ,字母 `B` 对应十进制的 `11` ,字母 `C` 对应十进制的 `12`,字母 `D` 对应十进制的 `13`,字母 `E` 对应十进制的 `14`,字母 `F` 对应十进制的 `15`
* 进制的换算举例,如下所示:
| 十进制 | 二进制 | 八进制 | 十六进制 |
| ------ | ------ | ------ | -------- |
| 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 1 |
| 2 | 10 | 2 | 2 |
| 3 | 11 | 3 | 3 |
| 4 | 100 | 4 | 4 |
| 5 | 101 | 5 | 5 |
| 6 | 110 | 6 | 6 |
| 7 | 111 | 7 | 7 |
| 8 | 1000 | 10 | 8 |
| 9 | 1001 | 11 | 9 |
| 10 | 1010 | 12 | a 或 A |
| 11 | 1011 | 13 | b 或 B |
| 12 | 1100 | 14 | c 或 C |
| 13 | 1101 | 15 | d 或 D |
| 14 | 1110 | 16 | e 或 E |
| 15 | 1111 | 17 | f 或 F |
| 16 | 10000 | 20 | 10 |
| ... | ... | ... | ... |
* 示例:定义和打印长整型变量
```c
#include <stdio.h>
int main() {
// 定义有符号 long 类型
signed long l1 = -100;
printf("l1 = %ld \n", l1); // l1 = -100
// 定义无符号 long 类型
unsigned long l2 = 100;
printf("l2 = %lu \n", l2); // l2 = 100
// 定义 long 类型,默认是有符号
long l3 = -200;
printf("l3 = %ld \n", l3); // l3 = -200
return 0;
}
```
* 示例:获取类型占用的内存大小(存储空间)
```c
#include <stdio.h>
int main() {
size_t l1 = sizeof(unsigned long);
printf("unsigned long 的存储空间是 %zu 字节 \n", l1); // 4
size_t l2 = sizeof(signed long);
printf("signed long 的存储空间是 %zu 字节 \n", l2); // 4
size_t l3 = sizeof(long);
printf("long 的存储空间是 %zu 字节 \n", l3); // 4
return 0;
}
```
* 示例:获取类型的取值范围
```c
#include <limits.h>
#include <stdio.h>
int main() {
printf("unsigned long 类型的范围是[0,%lu]\n", ULONG_MAX); // [0,4294967295]
printf("long 类型的范围是[%ld,%ld]\n", LONG_MIN,LONG_MAX); // [-2147483648,2147483647]
return 0;
}
```
### 1.2.5 长长整型(了解)
* 语法:
```c
unsigned long long x = 10 ; // 无符号长长整型
```
```c
long long x = -10; // 有符号长长整型
```
* 二进制和十六进制的关系:十六进制是以 16 为基数的进制系统16 在二进制中表示为 ( 2^4 ),即:一个十六进制可以表示 4 位二进制。
> [!NOTE]
>
> * ① 有符号表示的是正数、负数和 0 ,即有正负号。无符号表示的是 0 和正数,即正整数,没有符号。
> * ② 在 `printf` 中`无符号长长整型unsigned long long`的`格式占位符`是 `%llu` `有符号长长整型signed long long`的`格式占位符`是 `%lld`
> * ③ 可以通过 `sizeof` 运算符获取`无符号长长整型unsigned long long` 和 `有符号长长整型signed long long` 的`存储空间(所占内存空间)`。
> * ③ 可以通过 `#include <limits.h>` 来获取 `无符号长长整型unsigned long long` 和`有符号长长整型signed long long`的`取值范围`。
> 十六进制的范围是0 ~ F 0 ~ 15对应的二进制数的范围是0000 ~ 1111 0 ~ 15
* 每个十六进制数都可以映射到一个唯一的 4 位二进制数,即:
| 十六进制 | 二进制 |
| -------- | ------ |
| 0 | 0000 |
| 1 | 0001 |
| 2 | 0010 |
| 3 | 0011 |
| 4 | 0100 |
| 5 | 0101 |
| 6 | 0110 |
| 7 | 0111 |
| 8 | 1000 |
| 9 | 1001 |
| A | 1010 |
| B | 1011 |
| C | 1100 |
| D | 1101 |
| E | 1110 |
| F | 1111 |
* 示例:定义和打印长长整型变量
>[!NOTE]
>
>由此可见,每个十六进制数字确实由 4 位二进制数表示。
```c
#include <stdio.h>
int main() {
// 定义有符号 long long 类型
signed long long ll1 = -100;
printf("ll1 = %lld \n", ll1); // ll1 = -100
// 定义无符号 long long 类型
unsigned long long ll2 = 100;
printf("ll2 = %llu \n", ll2); // ll2 = 100
// 定义 long long 类型,默认是有符号
long long ll3 = -200;
printf("ll3 = %lld \n", ll3); // ll3 = -200
return 0;
}
```
* 示例:获取类型占用的内存大小(存储空间)
```c
#include <stdio.h>
int main() {
size_t ll1 = sizeof(unsigned long long);
printf("unsigned long long 的存储空间是 %zu 字节 \n", ll1); // 8
size_t ll2 = sizeof(signed long long);
printf("signed long long 的存储空间是 %zu 字节 \n", ll2); // 8
size_t ll3 = sizeof(long long);
printf("long long 的存储空间是 %zu 字节 \n", ll3); // 8
return 0;
}
```
* 示例:获取类型的取值范围
```c
#include <limits.h>
#include <stdio.h>
int main() {
printf("unsigned long long 类型的范围是[0,%llu]\n", ULLONG_MAX); // [0,18446744073709551615]
printf("long long 类型的范围是[%lld,%lld]\n", LLONG_MIN,LLONG_MAX); // [-9223372036854775808,9223372036854775807]
return 0;
}
```
### 1.2.6 字面量后缀
* `字面量`是`源代码`中一个`固定值`的`表示方法`,用于直接表示数据,即:
```c
int num1 = 100; // 100 就是字面量
```
```c
long num2 = 100L; // 100L 就是字面量
```
```c
long long num3 = 100LL; // 100LL 就是字面量
```
* 二进制和八进制的关系:八进制是以 8 为基数的进制系统8 在二进制中表示为 ( 2^3 );即:一个八进制位可以表示 3 个二进制位。
> [!NOTE]
>
> * ① 默认情况下的,整数字面量的类型是 int 类型。
> * ② 如果需要表示 `long` 类型的字面量,需要添加后缀 `l``L` ,建议 `L`
> * ③ 如果需要表示 `long long` 类型的字面量,需要添加后缀 `ll``LL`,建议 `LL`
> * ④ 如果需要表示`无符号`整数类型的字面量,需要添加 `u``U`,建议 `U`
> 八进制的范围是0 ~ 7 对应的二进制数的范围是000 ~ 111。
* 每个八进制数位都可以映射到一个唯一的 3 位二进制数,即:
| 八进制 | 二进制 |
| ------ | ------ |
| 0 | 000 |
| 1 | 001 |
| 2 | 010 |
| 3 | 011 |
| 4 | 100 |
| 5 | 101 |
| 6 | 110 |
| 7 | 111 |
> [!NOTE]
>
> 由此可见,每个八进制数字确实由 3 位二进制数表示。
### 3.2.2 C 语言中如何表示不同进制的整数?
* 规则如下:
* 在 C 语言中,如果是`二进制`(字面常量),则需要在二进制整数前加上 `0b``0B`
* 在 C 语言中,如果是`八进制`(字面常量),则需要在八进制整数前加上 `0`
* 在 C 语言中,如果是`十进制`(字面常量),正常数字表示即可。
* 在 C 语言中,如果是`十六进制`(字面常量),则需要在十六进制整数前加上 `0x`或`0X` 。
* 示例:
```c
#include <stdio.h>
int main() {
int num1 = 0b10100110; // 二进制
int num2 = 0717563; // 八进制
int num3 = 1000; // 十进制
int num4 = 0xaf72; // 十六进制
printf("num1 = %d\n", num1); // num1 = 166
printf("num2 = %d\n", num2); // num2 = 237427
printf("num3 = %d\n", num3); // num3 = 1000
printf("num4 = %d\n", num4); // num4 = 44914
return 0;
}
```
### 3.2.3 输出格式
* 在 C 语言中,可以使用不同的`格式占位符`来`输出`不同`进制`的整数,如下所示:
* `%d`:十进制整数。
* `%o` :八进制整数。
* `%x`:十六进制整数。
* `%#o` :显示前缀 `0` 的八进制整数。
* `%#x` :显示前缀 `0x` 的十六进制整数。
* `%#X` :显示前缀 `0X` 的十六进制整数。
> [!CAUTION]
>
> C 语言中没有输出二进制数的格式占位符!!!
@ -454,1690 +1096,336 @@ long long num3 = 100LL; // 100LL 就是字面量
int main() {
int num = 100;
printf("num = %d\n", num); // num = 100
long num2 = 100L;
printf("num2 = %ld\n", num2); // num2 = 100
long long num3 = 100LL;
printf("num3 = %lld\n", num3); // num3 = 100
unsigned int num4 = 100U;
printf("num4 = %u\n", num4); // num4 = 100
unsigned long num5 = 100LU;
printf("num5 = %lu\n", num5); // num5 = 100
unsigned long long num6 = 100ULL;
printf("num6 = %llu\n", num6); // num6 = 100
printf("%d 的十进制整数: %d\n", num, num); // 100 的十进制整数: 100
printf("%d 的八进制整数: %o\n", num, num); // 100 的八进制整数: 144
printf("%d 的十六进制整数: %x\n", num, num); // 100 的十六进制整数: 64
printf("%d 的八进制(前缀)整数: %#o\n", num, num); // 100 的八进制(前缀)整数: 0144
printf("%d 的十六进制(前缀)整数: %#x\n", num, num); // 100 的十六进制(前缀)整数: 0x64
printf("%d 的十六进制(前缀)整数: %#X\n", num, num); // 100 的十六进制(前缀)整数: 0X64
return 0;
}
```
### 1.2.7 精确宽度类型
* 在前文,我们了解到 C 语言的整数类型short 、int、long、long long在不同计算机上占用的字节宽度可能不一样。但是有的时候我们希望整数类型的存储空间字节宽度是精确的在任意平台计算机上都能一致以提高程序的可移植性。
> [!NOTE]
>
> * Java 语言中的数据类型的存储空间(字节宽度)是一致的,这也是 Java 语言能够跨平台的原因之一(最主要的原因还是 JVM
> * 在嵌入式开发中,使用精确宽度类型可以确保代码在各个平台上的一致性。
* 在 C 语言的标准头文件 `<stdint.h>` 中定义了一些新的类型别名,如下所示:
| 类型名称 | 含义 |
| -------- | --------------- |
| int8_t | 8 位有符号整数 |
| int16_t | 16 位有符号整数 |
| int32_t | 32 位有符号整数 |
| int64_t | 64 位有符号整数 |
| uint8_t | 8 位无符号整数 |
| uint16_t | 16 位无符号整数 |
| uint32_t | 32 位无符号整数 |
| uint64_t | 64 位无符号整数 |
> [!NOTE]
>
> 上面的这些类型都是类型别名,编译器会指定它们指向的底层类型,如:在某个系统中,如果 int 类型是 32 位,那么 int32_t 就会指向 int ;如果 long 类型是 32 位,那么 int32_t 就会指向 long。
* 示例:
```c
#include <stdio.h>
#include <stdint.h>
int main() {
// 变量 x32 声明为 int32_t 类型,可以保证是 32 位(4个字节)的宽度。
int32_t x32 = 45933945;
printf("x32 = %d \n", x32); // x32 = 45933945
return 0;
}
```
### 1.2.8 sizeof 运算符
* 语法:
```c
sizeof(表达式)
```
> [!NOTE]
>
> * ① sizeof 是运算符,不是内置函数。
>
> * ② 表达式可以是任何类型的数据类型、变量或常量。
> * ③ 用来获取某种数据类型、变量或常量占用的字节数量(内存中的存储单元),并且 `sizeof(...)` 的`返回值类型`是 `size_t` ;并且,如果是变量名称,可以省略 `()`;如果是数据类型,则不能省略 `()`
> * ④ 在 `printf` 中使用占位符 `%zu` 来处理 `size_t` 类型的值。
> * ⑤ 之前也提过C 语言没有一个统一的官方机构来制定或强制执行其标准而是由一个标准委员会负责制定标准。不同的编译器可以选择部分或完全遵循这些标准。因此C 语言的编译器实现可能会有所不同,这就要求程序员在编写跨平台代码时特别注意数据类型的大小和布局。
> * ⑥ 与 C 语言不同Java 和 JavaScript 等语言的标准是强制性的。在 Java 语言中,`int` 类型在所有平台上都是 4 个字节,无论是在 Linux、MacOS 还是 Windows 上。因此,这些语言不需要像 C 语言那样依赖 `sizeof` 来处理不同平台上的数据类型大小差异,因为编译器已经在底层处理了这些差异。换言之,`sizeof` 运算符在 C 语言中的重要性在于它为程序员提供了一个处理不同平台上数据类型大小差异的工具。当然,如果你在 C 语言中,使用精确宽度类型,如:`int8_t`、`int16_t`、`int32_t`、`uint8_t`、 `uint16_t`、`uint32_t` 等,也可以确保代码在各个平台上的一致性。
* 示例:参数是数据类型
```c
#include <stdio.h>
#include <stddef.h>
int main() {
size_t s = sizeof(int);
printf("%zu \n", s); // 4
return 0;
}
```
* 示例:参数是变量
```c
#include <stdio.h>
#include <stddef.h>
int main() {
int num = 10;
size_t s = sizeof(num);
printf("%zu \n", s); // 4
return 0;
}
```
* 示例:参数是常量
```c
#include <stdio.h>
#include <stddef.h>
int main() {
size_t s = sizeof(10);
printf("%zu \n", s); // 4
return 0;
}
```
## 1.3 数值溢出
### 1.3.1 概述
* 在生活中,如果一个容器的容量是固定的,我们不停的向其中注入水,那么当容器中充满水之后,再继续注入,水就会从杯子中溢出来,如下所示:
![](./assets/3.jpg)
* 在程序中也是一样的各种整数类型在内存中占用的存储单元是不同的short 在内存中占用 2 个字节的存储单元int 在内存中占用 4 个字节的存储单元。这也就意味着,各种整数类型只能存储有限的数值,当数值过大或多小的时候,超出的部分就会被直接截掉,那么数值就不能被正确的存储,我们就将这种现象就称为`溢出`overflow
> [!NOTE]
>
> * 如果这个数目前是`最大值`,再进行`加法`计算,数据就会超过该类型能够表示的最大值,叫做`上溢出`(如果最大值 + 1 会“绕回”到最小值)。
> * 如果这个数目前是`最小值`,再进行`减法`计算,数据就会超过该类型能够表示的最小值, 叫做`下溢出`(如果最小值 - 1 会“绕回”到最大值)。
>
> [!IMPORTANT]
>
> * ① 在 C 语言中程序产生数值溢出的时候并不会引发错误而使程序自动停止这是因为计算机底层是采用二进制补码的运算规则进行处理的很多编程语言也是这样处理的Java 等)。
> * ② 但是这可能会导致不可预料的后果1996 年的亚利安 5 号运载火箭爆炸、2004 年的 Comair 航空公司航班停飞事故。
> * ③ 在实际开发中,编程时要特别注意,以避免数值溢出问题,特别是在涉及大数或小数的运算(特指整数)。
### 1.3.2 无符号数的取值范围
* 在 C 语言中,`无符号数`unsigned 类型)的取值范围(最大值和最小值)的计算是很容易的,即:将内存中的所有位,设置为 `0` 就是`最小值`,设置为 `1` 就是`最大值`。
> [!IMPORTANT]
>
> 在 C 语言中,无符号整数,最高位不是符号位,它是数值的一部分。
* 以 `unsigned char` 类型为例,它在内存中占用的存储单元是 1 个字节,即 8 位。如果所有位都设置为 `0` ,它的最小值就是 `0` ;如果所有位设置为 `1` ,它的最大值就是 `2⁸ - 1 = 255` ,如下所示:
![](./assets/4.svg)
* 那么,`unsigned char` 的最大值是如何计算出来的?最简单的方法就是这样的,如下所示:
```txt
1 × 2⁰ + 1 × 2¹ + 1 × 2² + 1 × 2³ + 1 × 2⁴ + 1 × 2⁵ + 1 × 2⁶ + 1 × 2⁷
= 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128
= 255
```
* 但是,这种计算方法虽然有效,但是非常麻烦,如果是 8 个字节的 long ,那么计算就非常麻烦了(可能要计算半天)。当然,我们也知道,这就是等比数列(高中知识),等比数列的公式,如下所示:
$S_n = a_1 \times \frac{1 - r^n}{1 - r}$
* 那么,结果就是:$S_8 = 1 \times \frac{1 - 2^8}{1 - 2} = \frac{1 - 256}{-1} = 255$
* 但是,貌似还是很复杂,我们可以换个思路,就是让 `1111 1111``+1` ,然后再 `-1`,这样一增一减正好抵消掉,并且不会影响最终的结果,如下所示:
```txt
1111 1111 + 1 - 1
= 10000 0000 - 1
= 2⁹⁻¹ - 1
= 2⁸ - 1
= 255
```
* 其对应的换算过程,如下所示:
![](./assets/5.svg)
> [!IMPORTANT]
>
> * ① 当内存中所有的位都是 1 的时候,这种“凑整”的技巧非常实用!!!
> * ② 按照上述的技巧,我们可以很容易得计算出:
> * `unsinged char`1 个字节) 的取值范围是:`[0, 2⁸ - 1]`。
> * `unsinged short`2 个字节)的取值范围是:`[0, 2¹⁶ - 1]`。
> * `unsinged int`4 个字节)的取值范围是:`[0, 2³² - 1]`。
> * `unsinged long`8 个字节)的取值范围是:`[0, 2⁶⁴ - 1]`。
### 1.3.3 有符号数的取值范围
* 在 C 语言中,`有符号数`signed 类型)在计算机底层是以`补码`的形式存储的(计算的时候,也是以补码的形式进行计算的,并且符号位参与计算);但是,在读取的时候,需要采用`逆向`的转换,即:将补码转换为原码。
> [!IMPORTANT]
>
> 在 C 语言中,有符号整数,最高位是符号位,用于表示正负数。
* 以 `char` 类型为例,它的取值范围,如下所示:
| 补码 | 反码 | 原码 | 值 |
| ------------- | --------- | --------- | -------- |
| 1111 1111 | 1111 1110 | 1000 0001 | -1 |
| 1111 1110 | 1111 1101 | 1000 0010 | -2 |
| 1111 1101 | 1111 1100 | 1000 0011 | -3 |
| ... | ... | ... | ... |
| 1000 0011 | 1000 0010 | 1111 1101 | -125 |
| 1000 0010 | 1000 0001 | 1111 1110 | -126 |
| 1000 0001 | 1000 0000 | 1111 1111 | -127 |
| **1000 0000** | **---** | **---** | **-128** |
| 0111 1111 | 0111 1111 | 0111 1111 | 127 |
| 0111 1110 | 0111 1110 | 0111 1110 | 126 |
| 0111 1101 | 0111 1101 | 0111 1101 | 125 |
| ... | ... | ... | ... |
| 0000 0010 | 0000 0010 | 0000 0010 | 2 |
| 0000 0001 | 0000 0001 | 0000 0001 | 1 |
| 0000 0000 | 0000 0000 | 0000 0000 | 0 |
* 从上面的列表中,我们可以得知,`char` 类型的取值范围是:`[-2⁸, 2⁸ - 1]`,即:`[-128, 127]`。
* 对于 `-128` 而言,它的补码是 `1000 0000`,是无法按照传统的补码表示法来计算原码的,因为在补码转换到反码的时候需要 `-1` ,而 `1000 0000 - 1`需要向高位借 `1` ,而最高位是符号位是不能借的,这就非常矛盾。
> [!IMPORTANT]
>
> 计算机规定,`1000 0000` 这个特殊的补码就表示 `-128`
* 但是,为什么偏偏是 `-128` ,而不是其它数字?是因为 `-128` 使得 `char` 类型的取值范围保持连贯,中间没有“空隙”。如果我们按照传统的方式来计算 `-128` 的补码,如下所示:
* ① 原码:在原码表示法中,-128 的数据位是 `1000 0000`,但是 char 的数据位只有 `7` 位,那么最高位 `1` 就变为了符号位,剩下的数据位就是 `000 0000`;所以,`-128` 的原码就是 `1000 0000`
* ② 反码:对数据位取反,-128 的反码就是:`1111 1111` 。
* ③ 补码:在反码的基础上 `+1`,得到 `1000 0000`,是因为符号位被覆盖了,补码最终依然是 `1000 0000`
> [!NOTE]
>
> `-128` 从原码转换到补码的过程中,符号位被 `1` 覆盖了两次,而负数的符号位本来就是 `1`,被 `1` 覆盖多少次也不会影响到数字的符号。
* 虽然从 `1000 0000` 这个补码推算不出 `-128`,但是从 `-128` 却能推算出 `1000 0000` 这个补码,即:有符号数在存储之前先要转换为补码。
> [!IMPORTANT]
>
> * ① 通过这种方式,`-128` 就成为了补码的最小值 `1000 0000`,而这个值不会与其他任何正数或负数的补码冲突。
> * 如果采用`原码`存储,那么将会出现 `+0``-0` 的情况,即:`0000 0000`、`1000 0000`,这样在取值范围内,就存在两个相同的值,多此一举。
> * 如果采用`原码`存储,最大值不变是 `127` ,但是最小值只能存储到 `-127` ,不能存储到 `-128`,因为 `-128` 的原码是 `1000 0000`,和 `-0` 的原码冲突。
> * ② 这就是补码系统的强大之处,它能让整数的范围连贯,并且实现了加法和减法的统一处理。
> * ③ 按照上述的方法,我们可以很容易得计算出:
> * `char`1 个字节) 的取值范围是:`[-2⁸, 2⁸ - 1]`。
> * `short`2 个字节)的取值范围是:`[-2¹⁶, 2¹⁶ - 1]`。
> * `int`4 个字节)的取值范围是:`[-2³², 2³² - 1]`。
> * `long`8 个字节)的取值范围是:`[-2⁶⁴, 2⁶⁴ - 1]`。
### 1.3.4 数值溢出
* 对于`无符号`的数值溢出:
* 当数据到达最大值的时候,再 `+1` 就会回到无符号数的最小值。
* 当数据达到最小值的时候,再 `-1` 就会回到无符号数的最大值。
> [!IMPORTANT]
>
> * ① 对于无符号整数的运算,如:加、减、乘、除、取余等,其最小值是 0 ,最大值是 `2^n - 1` 。如果某个计算结果超出了这个范围,计算机会自动将结果对 `2^N` 取余(模),从而丢失高位,只保留低位。
> * ② 以 `8` 位无符号整数而言,最大值是 `255`1111 1111那么 `255 + 1` 的结果就是 `(2^8 -1 + 1) % 2^8 = 0`,商是 `256`
> * ③ 以 `8` 位无符号整数而言,最小值是 `0`0000 0000那么 `0 - 1` 的结果就是 `(0 - 1) % 2^8 = 255`,商是 `-1`
* 那么,`无符号`的`上溢出`,原理就是这样的:
![](./assets/6.svg)
* 那么,`无符号`的`下溢出`,原理就是这样的:
![](./assets/7.svg)
* 对于`有符号`的数值溢出:
* 当数据到达最大值的时候,再 `+1` 就会回到有符号数的最小值。
* 当数据达到最小值的时候,再 `-1` 就会回到有符号数的最大值。
* 那么,`有符号`的`上溢出`,原理就是这样的:
![](./assets/8.svg)
* 那么,`有符号`的`下溢出`,原理就是这样的:
![](./assets/9.svg)
* 示例:无符号的上溢出和下溢出
```c
#include <limits.h>
#include <stdio.h>
int main() {
unsigned short s1 = USHRT_MAX + 1;
printf("无符号的上溢出 = %hu \n", s1); // 0
unsigned short s2 = 0 - 1;
printf("无符号的下溢出 = %hu \n", s2); // 65535
return 0;
}
```
* 示例:有符号的上溢出和下溢出
```c
#include <limits.h>
#include <stdio.h>
int main() {
short s1 = SHRT_MAX + 1;
printf("有符号的上溢出 = %hd \n", s1); // -32768
short s2 = SHRT_MIN - 1;
printf("有符号的下溢出 = %hd \n", s2); // 32767
return 0;
}
```
## 1.4 浮点类型
### 1.4.1 概述
* 在生活中,我们除了使用`整数`18、25 之外,还会使用到`小数`3.1415926、6.18 等,`小数`在计算机中也被称为`浮点数`(和底层存储有关)。
* `整数`在计算机底层的存储被称为`定点存储`,如下所示:
![](./assets/10.svg)
* `小数`在计算机底层的存储被称为`浮点存储`,如下所示:
![](./assets/11.svg)
> [!NOTE]
>
> * ① 计算机底层就是采取类似科学计数法的形式来存储小数的而科学计数法的表现就是这样的3.12 * 10^-2 其中10 是基数,-2 是指数,而 3.12 是尾数。
> * ② 因为尾数区的内存空间的宽度不同,导致了小数的精度也不相同,所以小数在计算机中也称为浮点数。
* 在 C 语言中,变量的浮点类型,如下所示:
| 类型 | 存储大小 | 值的范围 | 有效小数位数 |
| ----------------------- | -------- | --------------------- | ------------ |
| float单精度 | 4 字节 | 1.2E-38 ~ 3.4E+38 | 6 ~ 9 |
| double双精度 | 8 字节 | 2.3E-308 ~ 1.7E+308 | 15 ~ 18 |
| long double长双精度 | 16 字节 | 3.4E-4932 ~ 1.2E+4932 | 18 或更多 |
> [!NOTE]
>
> * ① 各类型的存储大小和精度受到操作系统、编译器、硬件平台的影响。
> * ② 浮点型数据有两种表现形式:
> * 十进制数形式3.12、512.0f、0.512.512,可以省略 0
> * 科学计数法形式5.12e2e 表示基数 10、5.12E-2E 表示基数 10
> * ③ 在实际开发中,对于浮点类型,建议使用 `double` 类型;如果范围不够,就使用 `long double` 类型。
### 1.4.2 格式占位符
* 对于 `float` 类型的格式占位符,是 `%f` ,默认会保留 `6` 位小数,不足 `6` 位以 `0` 补充;可以指定小数位,如:`%.2f` 表示保留 `2` 位小数。
* 对于 `double` 类型的格式占位符,是 `%lf` ,默认会保留 `6` 位小数,不足 `6` 位以 `0` 补充;可以指定小数位,如:`%.2lf` 表示保留 `2` 位小数。
* 对于 `long double` 类型的格式占位符,是 `%Lf` ,默认会保留 `6` 位小数,不足 `6` 位以 `0` 补充;可以指定小数位,如:`%.2Lf` 表示保留 `2` 位小数。
> [!NOTE]
>
> * ① 如果想输出`科学计数法`形式的 `float` 类型的浮点数,则使用 `%e`
> * ② 如果想输出`科学计数法`形式的 `double` 类型的浮点数,则使用 `%le`
> * ③ 如果想输出`科学计数法`形式的 `long double` 类型的浮点数,则使用 `%Le`
> [!NOTE]
>
> * ① 浮点数还有一种更加智能的输出方式,就是使用 `%g``g` 是 `general format` 的缩写,即:通用格式),`%g` 会根据数值的大小自动判断,选择使用普通的浮点数格式(`%f`)进行输出,还是使用科学计数法(`%e`)进行输出,即:`float` 类型的两种输出形式。
> * ② 同理,`%lg` 会根据数值的大小自动判断,选择使用普通的浮点数格式(`%lf`)进行输出,还是使用科学计数法(`%le`)进行输出,即:`double` 类型的两种输出形式。
> * ③ 同理,`%Lg` 会根据数值的大小自动判断,选择使用普通的浮点数格式(`%Lf`)进行输出,还是使用科学计数法(`%Le`)进行输出,即:`long double` 类型的两种输出形式。
* 示例:
```c
#include <stdio.h>
int main() {
float f1 = 10.0;
printf("f1 = %f \n", f1); // f1 = 10.000000
printf("f1 = %.2f \n", f1); // f1 = 10.00
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
double d1 = 13.14159265354;
printf("d1 = %lf \n", d1); // d1 = 13.141593
printf("d1 = %.2lf \n", d1); // d1 = 13.14
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
long double d1 = 13.14159265354;
printf("d1 = %LF \n", d1); // d1 = 13.141593
printf("d1 = %.2LF \n", d1); // d1 = 13.14
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
float f1 = 3.1415926;
double d2 = 3.14e2;
printf("f1 = %.2f \n", f1); // f1 = 3.14
printf("f1 = %.2e \n", f1); // f1 = 3.14e+00
printf("d2 = %.2lf \n", d2); // d2 = 314.00
printf("d2 = %.2e \n", d2); // d2 = 3.14e+02
return 0;
}
```
### 1.4.3 字面量后缀
* 浮点数字面量默认是 double 类型。
* 如果需要表示 `float` 类型的字面量,需要后面添加后缀 `f``F`,建议 `F`
* 如果需要表示 `long double` 类型的字面量,需要后面添加后缀 `l``L`,建议 `L`
* 示例:
```c
#include <stdio.h>
int main() {
float f1 = 3.1415926f;
double d2 = 3.1415926;
long double d3 = 3.1415926L;
printf("f1 = %.2f \n", f1); // f1 = 3.14
printf("d2 = %.3lf \n", d2); // d2 = 3.142
printf("d3 = %.4Lf \n", d3); // d3 = 3.1416
return 0;
}
```
### 1.4.4 类型占用的内存大小(存储空间)
* 可以通过 `sizeof` 运算符来获取 float、double 以及 long double 类型占用的内存大小(存储空间)。
* 示例:
```c
#include <stdio.h>
int main() {
printf("float 的存储空间是 %zu 字节 \n", sizeof(float)); // 4
printf("double 的存储空间是 %zu 字节 \n", sizeof(double)); // 8
printf("long double 的存储空间是 %zu 字节 \n", sizeof(long double)); // 16
return 0;
}
```
### 1.4.5 类型的取值范围
* 可以通过 `#include <float.h>` 来获取类型的取值范围。
* 示例:
```c
#include <float.h>
#include <stdio.h>
int main() {
printf("float 的取值范围是:[%.38f, %f] \n", FLT_MIN, FLT_MAX);
printf("double 的取值范围是:[%lf, %lf] \n", DBL_MIN, DBL_MAX);
printf("double 的取值范围是:[%Lf, %Lf] \n", LDBL_MIN, LDBL_MAX);
return 0;
}
```
### 1.4.6 整数和浮点数的相互赋值
* 在 C 语言中,整数和浮点数是可以相互赋值的,即:
* 将一个整数赋值给小数类型,只需要在小数点后面加 0 就可以了。
* 将一个浮点数赋值给整数类型,就会将小数部分丢掉,只会取整数部分,会改变数字本身的值。
> [!WARNING]
>
> * ① 在 C 语言中浮点数赋值给整数类型会直接截断小数点后面的数编译器一般只会给出警告让我们注意一下C 语言在检查类型匹配方面不太严格,最好不要养成这样的习惯)。
> * ② 但是,在 Java 等编程语言中,这样的写法是不可以的,会在编译阶段直接报错。
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
float a = 123; // 整数赋值给浮点类型,只需要在小数点,后面加 0 即可
printf("a=%f \n", a); // a=123.000000
int b = 123.00; // 浮点赋值给整数类型,会直接截断小数点后面的数
printf("b=%d \n", b); // b=123
return 0;
}
```
## 1.5 字符类型
### 1.5.1 概述
* 在生活中,我们会经常说:今天天气真 `好`,我的性别是 `女`,我今年 `10` 岁等。像这类数据,在 C 语言中就可以用`字符类型`char来表示。`字符类型`表示`单`个字符,使用单引号(`''`)括起来,如:`'1'`、`'A'`、`'&'`。
* 但是,在生活中,也许会听到:`你是好人,只是现阶段,我想学习`、`好的啊,我们在一起`等。像这类数据,在 C 语言中就可以用`字符串`String来表示。`字符串类型`表示`多`个字符的集合,使用双引号(`""`)括起来,如:`"1"`、`"A"`、`"&"`、`"我们"`。
> [!NOTE]
>
> * ① C 语言的出现在 1972 年,由美国人丹尼斯·里奇设计出来;那个时候,只需要 1 个字节的内存空间就可以完美的表示拉丁体系英文文字a-z、A-Z、0-9 以及一些特殊符号所以C 语言中不支持多个字节的字符,如:中文、日文等。
> * ② 像拉丁体系英文文字a-z、A-Z、0-9 以及一些特殊符号,只需要单个字节的内存存储空间就能存储的,我们就称为窄类型;而像中文、日文等单个字节的内存空间存储不了的,我们就称为宽类型。
> * ③ 在 C 语言中是没有字符串类型是使用字符数组char 数组)来模拟字符串的。字符串中的字符在内存中按照次序、紧挨着排列,整个字符串占用一块连续的内存。
> * ④ 在 C 语言中如果想要输出中文、日文等多字节字符就需要使用字符数组char 数组)。
> * ⑤ 在 C++、Java 等高级编程语言中,已经提供了 String (字符串)类型,原生支持 Unicode可以方便地处理多语言和特殊字符。
* 在 C 语言中,可以使用`转义字符 \`来表示特殊含义的字符。
| **转义字符** | **说明** |
| ------------ | -------- |
| `\b` | 退格 |
| `\n` | 换行符 |
| `\r` | 回车符 |
| `\t` | 制表符 |
| `\"` | 双引号 |
| `\'` | 单引号 |
| `\\` | 反斜杠 |
| ... | |
### 1.5.2 格式占位符
* 在 C 语言中,使用 `%c` 来表示 char 类型。
* 示例:
```c
#include <stdio.h>
int main() {
char c = '&';
printf("c = %c \n", c); // c = &
char c2 = 'a';
printf("c2 = %c \n", c2); // c2 = a
char c3 = 'A';
printf("c3 = %c \n", c3); // c3 = A
return 0;
}
```
### 1.5.3 类型占用的内存大小(存储空间)
* 可以通过 `sizeof` 运算符来获取 char 类型占用的内存大小(存储空间)。
* 示例:
```c
#include <stdio.h>
int main() {
printf("char 的存储空间是 %d 字节\n", sizeof(char)); // 1
printf("unsigned char 的存储空间是 %d 字节\n", sizeof(unsigned char)); // 1
return 0;
}
```
### 1.5.4 类型的取值范围
* 可以通过 `#include <limits.h>` 来获取类型的取值范围。
* 示例:
```c
#include <limits.h>
#include <stdio.h>
int main() {
printf("char 范围是[%d,%d] \n", CHAR_MIN,CHAR_MAX); // [-128,127]
printf("unsigned char 范围是[0,%d]\n", UCHAR_MAX); // [0,255]
return 0;
}
```
### 1.5.5 字符类型的本质
* 在 C 语言中char 本质上就是一个整数,是 ASCII 码中对应的数字,占用的内存大小是 1 个字节(存储空间),所以 char 类型也可以进行数学运算。
![](./assets/12.png)
* char 类型同样分为 signed char无符号和 unsigned char有符号其中 signed char 取值范围 -128 ~ 127unsigned char 取值范围 0 ~ 255默认是否带符号取决于当前运行环境。
* `字符类型的数据`在计算机中`存储`和`读取`的过程,如下所示:
![](./assets/13.png)
* 示例:
```c
#include <limits.h>
#include <stdio.h>
int main() {
// char 类型字面量需要使用单引号包裹
char a1 = 'A';
char a2 = '9';
char a3 = '\t';
printf("c1=%c, c3=%c, c2=%c \n", a1, a3, a2);
// char 类型本质上整数可以进行运算
char b1 = 'b';
char b2 = 101;
printf("%c->%d \n", b1, b1);
printf("%c->%d \n", b2, b2);
printf("%c+%c=%d \n", b1, b2, b1 + b2);
// char 类型取值范围
unsigned char c1 = 200; // 无符号 char 取值范围 0 ~255
signed char c2 = 200; // 有符号 char 取值范围 -128~127c2会超出范围
char c3 = 200; // 当前系统char 默认是 signed char
printf("c1=%d, c2=%d, c3=%d", c1, c2, c3);
return 0;
}
```
### 1.5.6 输出字符方式二(了解)
* 在 C 语言中,除了可以使用 `printf()` 函数输出字符之外,还可以使用 `putchar()`函数输出字符。
> [!NOTE]
>
> * ① `putchar()` 函数每次只能输出一个字符,如果需要输出多个字符需要调用多次;而 `printf()` 函数一次可以输出多个字符,并且 `char` 类型对应的格式占位符是 `%c`
> * ② 在实际开发中,使用 `printf()` 函数居多。
* 示例:
```c
#include <stdio.h>
int main() {
char a = '1';
char b = '2';
char c = '&';
/* 12& */
putchar(a);
putchar(b);
putchar(c);
return 0;
}
```
### 1.5.7 初谈字符串(了解)
* 在 C 语言中没有专门的字符串类型,是使用`字符数组`来模拟字符串的,即:可以使用字符数组来存储字符串。
> [!NOTE]
>
> * ① 在 C 语言中,`数组`和`指针`通常会一起出现,所以当`字符数组`可以保存字符串,也就意味着可以使用`指针`来间接存储字符串。
> * ② 在 C 语言中,可以使用 `puts()` 函数输出字符串,每调用一次 `puts()` 函数,除了输出字符串之外,还会在字符串后面加上换行,即:`\n` 。
> * ③ 在 C 语言中,可以使用 `printf()` 函数输出字符串,并且字符串对应的格式占位符是 `%s`。和 `puts()` 函数不同的是,`printf()` 函数不会在字符串后面加上换行,即:`\n`。
> * ④ 在实际开发中,使用 `printf()` 函数居多。
* 示例:
```c
#include <stdio.h>
int main() {
// 存储字符串
char str[] = "我";
char *str2 = "爱你";
puts(str); // 我
puts(str2); // 爱你
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
// 存储字符串
char str[] = "你";
char *str2 = "是好人";
printf("%s\n", str); // 你
printf("%s\n", str2); // 是好人
return 0;
}
```
## 1.6 布尔类型
### 1.6.1 概述
* 布尔值用于表示 true、false两种状态通常用于逻辑运算和条件判断。
### 1.6.2 早期的布尔类型
* 在 C 语言标准C89并没有为布尔值单独设置一个数据类型所以在判断真、假的时候使用 `0` 表示 `false`(假),`非 0` 表示 `true`(真)。
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
// 使用整型来表示真和假两种状态
int handsome = 0;
printf("帅不帅[0 丑1 帅] ");
scanf("%d", &handsome);
if (handsome) {
printf("你真的很帅!!!");
} else {
printf("你真的很丑!!!");
}
return 0;
}
```
### 1.6.3 宏定义的布尔类型
* 判断真假的时候,以 `0``false`(假)、`1` 为 `true`(真),并不直观;所以,我们可以借助 C 语言的宏定义。
* 示例:
```c
#include <stdio.h>
// 宏定义
#define BOOL int
#define TRUE 1
#define FALSE 0
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
BOOL handsome = 0;
printf("帅不帅[FALSE 丑TRUE 帅] ");
scanf("%d", &handsome);
if (handsome) {
printf("你真的很帅!!!");
} else {
printf("你真的很丑!!!");
}
return 0;
}
```
### 1.6.4 C99 标准中的布尔类型
* 在 C99 中提供了 `_Bool` 关键字,用于表示布尔类型;其实,`_Bool`类型的值是整数类型的别名,和一般整型不同的是,`_Bool`类型的值只能赋值为 `0``1` 0 表示假、1 表示真),其它`非 0` 的值都会被存储为 `1`
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
int temp; // 使用 int 类型的变量临时存储输入
_Bool handsome = 0;
printf("帅不帅[0 丑1 帅] ");
scanf("%d", &temp);
// 将输入值转换为 _Bool 类型
handsome = (temp != 0);
if (handsome) {
printf("你真的很帅!!!");
} else {
printf("你真的很丑!!!");
}
return 0;
}
```
### 1.6.5 C99 标准头文件中的布尔类型(推荐)
* 在 C99 中提供了一个头文件 `<stdbool.h>`,定义了 `bool` 代表 `_Bool``false` 代表 `0` `true` 代表 `1`
> [!IMPORTANT]
>
> * ① 在 C++、Java 等高级编程语言中是有 `boolean` 类型的关键字的。
> * ② 在 C23 标准中,将一些 C11 存在的关键字改为小写并去掉前置下划线,如:`_Bool` 改为 `bool`,以前的写法主要是为了避免与旧的代码发生冲突。
> * ③ 在 C23 标准中,加入了 `true``false` 关键字。
* 示例:
```c
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
char input[10];
bool handsome = false;
printf("帅不帅[false 丑true 帅] ");
scanf("%s", input); // 使用 %s 读取字符串
// 将输入字符串转换为布尔值
if (strcmp(input, "true") == 0) {
handsome = true;
} else if (strcmp(input, "false") == 0) {
handsome = false;
} else {
printf("无效输入!\n");
return 1;
}
if (handsome) {
printf("你真的很帅!!!");
} else {
printf("你真的很丑!!!");
}
return 0;
}
```
## 1.7 数据类型转换
### 1.7.1 概述
* 在 C 语言编程中,经常需要对不同类型的数据进行运算,运算前需要先转换为同一类型,再运算。为了解决数据类型不一致的问题,需要对数据的类型进行转换。
### 1.7.2 自动类型转换(隐式转换)
#### 1.7.2.1 运算过程中的自动类型转换
* 不同类型的数据进行混合运算的时候,会发生数据类型转换,`窄类型会自动转换为宽类型`,这样就不会造成精度损失。
![](./assets/14.png)
* 转换规则:
* ① 不同类型的整数进行运算的时候,窄类型整数会自动转换为宽类型整数。
* ② 不同类型的浮点数进行运算的时候,精度小的类型会自动转换为精度大的类型。
* ③ 整数和浮点数进行运算的时候,整数会自动转换为浮点数。
* 转换方向:
![](./assets/15.png)
> [!WARNING]
>
> 最好避免无符号整数与有符号整数的混合运算,因为这时 C 语言会自动将 signed int 转为 unsigned int ,可能不会得到预期的结果。
* 示例:
```c
#include <stdio.h>
/**
* 不同的整数类型混合运算时,宽度较小的类型会提升为宽度较大的类型。
* 比如 short 转为 int int 转为 long 等。
*/
int main() {
short s1 = 10;
int i = 20;
// s1 是 short 类型i 是 int 类型。
// 当 s1 和 i 运算的时候,会自动转为 int 类型后,然后再计算。
int result = s1 + i;
printf("result = %d \n", result);
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
int n2 = -100;
unsigned int n3 = 20;
// n2 是有符号n3 是无符号。
// 当 n2 和 n3 运算的时候,会自动转为无符号类型后,然后再计算。
int result = n2 + n3;
printf("result = %d \n", result);
return 0;
}
```
* 示例:
```c
#include <stdio.h>
/**
* 不同的浮点数类型混合运算时,宽度较小的类型转为宽度较大的类型。
* 比如 float 转为 double double 转为 long double 。
*/
int main() {
float f1 = 1.25f;
double d2 = 4.58667435;
// f1 是 float 类型d2 是 double 类型。
// 当 f1 和 d2 运算的时候,会自动转为 double 类型后,然后再计算。
double result = f1 + d2;
printf("result = %.8lf \n", result);
return 0;
}
```
* 示例:
```c
#include <stdio.h>
/**
* 整型与浮点型运算,整型转为浮点型
*/
int main() {
int n4 = 10;
double d3 = 1.67;
// n4 是 int 类型d3 是 double 类型。
// 当 n4 和 d3 运算的时候,会自动转为 double 类型后,然后再计算。
double result = n4 + d3;
printf("%.2lf", result);
return 0;
}
```
#### 1.7.2.2 赋值时的自动类型转换
* 在赋值运算中,赋值号两边量的数据类型不同时,等号右边的类型将转换为左边的类型。
* 如果窄类型赋值给宽类型,不会造成精度损失;如果宽类型赋值给窄类型,会造成精度损失。
![](./assets/16.png)
> [!WARNING]
>
> C 语言在检查类型匹配方面不太严格,最好不要养成这样的习惯。
* 示例:
```c
#include <stdio.h>
int main() {
// 赋值:窄类型赋值给宽类型
int a1 = 10;
double a2 = a1;
printf("a2: %.2f\n", a2); // a2: 10.00
// 转换:将宽类型转换为窄类型
double b1 = 10.5;
int b2 = b1;
printf("b2: %d\n", b2); // b2: 10
return 0;
}
```
### 1.7.3 强制类型转换
* 隐式类型转换中的宽类型赋值给窄类型,编译器是会产生警告的,提示程序存在潜在的隐患,如果非常明确地希望转换数据类型,就需要用到强制(或显式)类型转换。
* 语法:
```c
数据类型 变量名 = (类型名)变量、常量或表达式;
```
> [!WARNING]
>
> 强制类型转换可能会导致精度损失!!!
* 示例:
```c
#include <stdio.h>
int main(){
double d1 = 1.934;
double d2 = 4.2;
int num1 = (int)d1 + (int)d2; // d1 转为 1d2 转为 4结果是 5
int num2 = (int)(d1 + d2); // d1+d2 = 6.1346.134 转为 6
int num3 = (int)(3.5 * 10 + 6 * 1.5); // 35.0 + 9.0 = 44.0 -> int = 44
printf("num1=%d \n", num1);
printf("num2=%d \n", num2);
printf("num3=%d \n", num3);
return 0;
}
```
### 1.7.4 数据类型转换只是临时性的
* 无论是自动类型转换还是强制类型转换,都是为了本次运算而进行的临时性转换,其转换的结果只会保存在临时的内存空间,并不会改变数据原先的类型或值,如下所示:
```c {8}
#include <stdio.h>
int main() {
double total = 100.12; // 总价
int count = 2; // 总数
double price = 0.0; // 单价
int totalInt = (int)total; // 强制类型转换
price = total / count; // 计算单价
printf("total = %.2lf\n", total); // total = 100.12
printf("totalInt = %d\n", totalInt); // totalInt = 100
printf("price = %.2lf\n", price); // price = 50.06
return 0;
}
```
* 虽然 `total` 变量,通过强制类型转换变为了 `int` 类型,才可以赋值给 `totalInt`变量;但是,这种转换并没有影响 `total` 变量本身的`类型`和`值`。
> [!NOTE]
>
> * ① 如果 `total` 变量的`值`或`类型`变化了,那么 `total` 的显示结果,就应该是 `100.00` ,而不是 `100.12`
> * ② 那么,`price` 的结果,显而易见就应该是 `50.00` ,而不是 `50.06` 了。
### 1.7.5 自动类型转换 VS 强制类型转换
* 在 C 语言中,有些数据类型即可以自动类型转换,也可以强制类型转换,如:`int --> double`、`double --> int` 等。但是,有些数据类型只能强制类型转换,不能自动类型转换,如:`void* --> int*` 。
* 可以自动类型转换的类型一定可以强制类型转换;但是,可以强制类型转换的类型却不一定能够自动类型转换。
> [!NOTE]
>
> * ① 目前学习到的数据类型,既可以自动类型转换,也可以强制类型转换。
> * ② 后面,如果学到指针,就会发生指针有的时候,只能强制类型转换却不能自动类型转换;需要说明的是,并非所有的指针都可以强制类型转换,是有条件的,后文讲解。
* 可以自动类型转换的类型,在发生类型转换的时候,一般风险较低,不会给程序带来严重的后果,如:`int --> double` 就没什么毛病,而 `double --> int` 无非丢失精度而已。但是 ,只能强制类型转换的类型,在发生类型转换的时候,通常风险较高,如:`char* --> int*` 就非常奇怪,会导致取得的值也很奇怪,进而导致程序崩溃。
> [!IMPORTANT]
>
> * ① 在实际开发中,如果使用 C 语言进行开发,在进行强制类型转换的时候,需要小心谨慎,防止出现一些奇怪的问题,进而导致程序崩溃!!!
> * ② 现代化的高级编程语言Java 等,直接屏蔽了指针。所以,在使用这些编程语言的时候,无需担心进行强制类型转换时,会出现一些奇怪的问题,进而导致程序崩溃!!!
## 1.8 再谈数据类型
* 通过之前的知识我们知道CPU 是直接和内存打交道的CPU 在处理数据的时候会将数据临时存放到内存中。内存那么大CPU 是怎么找到对应的数据的?
* 首先CPU 会将内存按照字节1 Bytes = 8 bit我们也称为存储单元进行划分如下所示
> [!NOTE]
>
> * ① 操作系统其实并不会直接操作实际的内存而是会通过内存管理单元MMU来操作内存并通过虚拟地址映射Virtual Address Mapping将程序使用的虚拟地址转换为物理地址。虚拟地址映射可以实现内存保护、内存共享和虚拟内存等功能使得程序能够使用比实际物理内存更大的内存空间同时确保程序间不会相互干扰。
> * ② 为了方便初学者学习,后文一律会描述 CPU 直接操作内存(这种说法不严谨,但足够简单和方便理解)。
> * ③ 这些存储单元中,存储的都是 0 和 1 这样的数据,因为计算机只能识别二进制数。
![](./assets/17.svg)
* 并且,为了方便管理,每个独立的小单元格,即:存储单元,都有自己唯一的编号(内存地址),如下所示:
> [!NOTE]
>
> 之所以,要给每个存储单元加上内存地址,就是为了`加快`数据的`存取速度`,可以类比生活中的`字典`以及`快递单号`。
![](./assets/18.svg)
* 我们在定义变量的时候,是这么定义的,如下所示:
```c
int num = 10;
```
> [!NOTE]
>
> 上述的代码其实透露了三个重要的信息:
>
> * ① 数据存储在哪里。
> * ② 数据的长度是多少。
> * ③ 数据的处理方式。
* 其实,在编译器对程序进行编译的时候,是这样做的,如下所示:
> [!NOTE]
>
> * ① 编译器在编译的时候,就将变量替换为内存中存储单元的内存地址(知道了你家的门牌号),这样就可以方便的进行存取数据了(解答了上述的问题 ① )。
> * ② 变量中其实存储的是初始化值 10 在内存中存储单元的首地址,我们也知道,数据类型 int 的存储空间是 4 个字节,那么根据首地址 + 4 个字节就可以完整的将数据从内存空间中取出来或存进去(解答了上述的问题 ② )。
> * ③ 我们知道,数据在计算机底层的存储方式是不一样的,如:整数在计算机底层的存储就是计算机补码的方式,浮点数在计算机底层的存储类似于科学计数法;但是,字符类型在计算机底层的存储和整数以及浮点数完全不同,需要查码表,即:在存储的时候,需要先查询码表,转换为二进制进行存储;在读取的时候,也需要先查询码表,将二进制转换为对应的字符(解答了上述的问题 ③ )。
> [!IMPORTANT]
>
> * ① 数据类型只在定义变量的时候声明,而且必须声明;在使用变量的时候,就无需再声明,因为此时的数据类型已经确定的。
> * ② 在实际开发中,我们通常将普通变量等价于内存中某个区域的值(底层到底是怎么转换的,那是编译器帮我们完成的,我们通常无需关心,也没必要关心)。
> * ③ 某些动态的编程语言JavaScript ,在定义变量的时候,是不需要给出数据类型的,编译器会根据赋值情况自动推断出变量的数据类型,貌似很智能;但是,这无疑增加了编译器的工作,降低了程序的性能(动态一时爽,重构火葬场,说的就是动态编程语言,不适合大型项目的开发;所以,之后微软推出了 TypeScript ,就是为了给 JavaScript 增加强类型系统,以提高开发和运行效率)。
## 3.3 进制的运算规则
### 3.3.1 概述
* `十进制`的运算规则,如下所示:
* 逢`十`进`一`(针对加法而言)。
* 借`一`当`十`(针对减法而言)。
* `二进制`的运算规则,如下所示:
* 逢`二`进`一`(针对加法而言)。
* 借`一`当`二`(针对减法而言)。
* `八进制`的运算规则,如下所示:
* 逢`八`进`一`(针对加法而言)。
* 借`一`当`八`(针对减法而言)。
* `十六进制`的运算规则,如下所示:
* 逢`十六`进`一`(针对加法而言)。
* 借`一`当`十六`(针对减法而言)。
### 3.3.2 二进制的运算
* 二进制的加法:`1 + 0 = 1` 、`1 + 1 = 10`、`11 + 10 = 101`、`111 + 111 = 1110`。
![](./assets/19.svg)
# 第二章:运算符(⭐)
## 2.1 概述
* 运算符是一种特殊的符号,用于数据的运算、赋值和比较等。
* `表达式`指的是一组运算数、运算符的组合,表达式`一定具有值`,一个变量或一个常量可以是表达式,变量、常量和运算符也可以组成表达式,如:
* 二进制的减法:`1 - 0 = 1` 、`10 - 1 = 1`、`101 - 11 = 10`、`1100 - 111 = 101` 。
![](./assets/20.svg)
* `操作数`指的是`参与运算`的`值`或者`对象`,如:
### 3.3.3 八进制的运算
* 八进制的加法:`3 + 4 = 7` 、`5 + 6 = 13`、`75 + 42 = 137`、`2427 + 567 = 3216`。
![](./assets/21.svg)
* 根据`操作数`的`个数`,可以将运算符分为:
* 一元运算符(一目运算符)。
* 二元运算符(二目运算符)。
* 三元运算符(三目运算符)。
* 根据`功能`,可以将运算符分为:
* 算术运算符。
* 关系运算符(比较运算符)。
* 逻辑运算符。
* 赋值运算符。
* 逻辑运算符。
* 位运算符。
* 三元运算符。
* 八进制的减法:`6 - 4 = 2` 、`52 - 27 = 33`、`307 - 141 = 146`、`7430 - 1451 = 5757` 。
![](./assets/22.svg)
### 3.3.4 十六进制的运算
* 十六进制的加法:`6 + 7 = D` 、`18 + BA = D2`、`595 + 792 = D27`、`2F87 + F8A = 3F11`。
![](./assets/23.svg)
* 十六进制的减法:`D - 3 = A` 、`52 - 2F = 23`、`E07 - 141 = CC6`、`7CA0 - 1CB1 = 5FEF` 。
![](./assets/24.svg)
## 3.4 进制的转换
### 3.4.1 概述
* 不同进制的转换,如下所示:
![](./assets/25.png)
* 在计算机中,数据是从右往左的方式排列的;其中,最右边的是低位,最左边的是高位,即:
![](./assets/26.svg)
### 3.4.2 二进制和十进制的转换
#### 3.4.2.1 二进制转换为十进制
* 规则:从最低位开始,将每个位上的数提取出来,乘以 2 的 (位数 - 1 )次方,然后求和。
> [!NOTE]
>
> 掌握一个运算符,需要关注以下几个方面:
>
> * ① 运算符的含义。
> * ② 运算符操作数的个数。
> * ③ 运算符所组成的表达式。
> * ④ 运算符有无副作用,即:运算后是否会修改操作数的值。
> * ① 在学术界,将这种计算规则,称为`位权相加法`。
> * ② `八进制转换为十进制`、`十六进制转换为十进制`和`二进制转换为十进制`的算法相同!!!
## 2.2 算术运算符
* 算术运算符是对数值类型的变量进行运算的,如下所示:
| 运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 |
| ------ | ------------ | ---------- | ------------------------ | ------ |
| `+` | 正号 | 1 | 操作数本身 | ❎ |
| `-` | 负号 | 1 | 操作数符号取反 | ❎ |
| `+` | 加号 | 2 | 两个操作数之和 | ❎ |
| `-` | 减号 | 2 | 两个操作数之差 | ❎ |
| `*` | 乘号 | 2 | 两个操作数之积 | ❎ |
| `/` | 除号 | 2 | 两个操作数之商 | ❎ |
| `%` | 取模(取余) | 2 | 两个操作数相除的余数 | ❎ |
| `++` | 自增 | 1 | 操作数自增前或自增后的值 | ✅ |
| `--` | 自减 | 1 | 操作数自减前或自减后的值 | ✅ |
* 示例:十进制转十进制
![](./assets/27.svg)
* 示例:二进制转十进制
![](./assets/28.svg)
#### 3.4.2.2 十进制转换二进制
* 规则:将该数不断除以 2 ,直到商为 0 为止,然后将每步得到的余数倒过来,就是对应的二进制。
> [!NOTE]
>
> 自增和自减:
> * ① 在学术界,将这种计算规则,称为`短除法`或`连续除2取余法`。
> * ② 很好理解,只有不断地除以 2 ,就能保证最大的数字不超过 2 ,这不就是二进制(只能有 0 或 1
> * ③ `八进制转换为二进制`、`十六进制转换为二进制`和`十进制转换为二进制`的算法相同!!!
* 示例:十进制转十进制
![](./assets/29.svg)
* 示例:十进制转二进制
![](./assets/30.svg)
### 3.4.3 二进制转八进制
* 规则:从右向左,每 3 位二进制就是一个八进制,不足补 0分组转换法
* 示例011 101 001 -> 351
![](./assets/31.svg)
### 3.4.4 二进制转十六进制
* 规则:从右向左,每 4 位二进制就是一个十六进制,不足补 0分组转换法
* 示例1110 1001 -> 0xE9
![](./assets/32.svg)
## 3.5 原码、反码和补码
### 3.5.1 概述
* 机器数:一个数在计算机的存储形式是二进制,我们称这些二进制数为机器数。机器数可以是有符号的,用机器数的最高位来存放符号位,`0` 表示正数,`1` 表示负数。
> [!IMPORTANT]
>
> * ① 自增、自减运算符可以写在操作数的前面也可以写在操作数后面,不论前面还是后面,对操作数的副作用是一致的。
> * ② 自增、自减运算符在前在后,对于表达式的值是不同的。 如果运算符在前,表达式的值是操作数自增、自减之后的值;如果运算符在后,表达式的值是操作数自增、自减之前的值。
> * ③ `变量前++`:变量先自增 1 ,然后再运算;`变量后++`:变量先运算,然后再自增 1 。
> * ④ `变量前--`:变量先自减 1 ,然后再运算;`变量后--`:变量先运算,然后再自减 1 。
> * ⑤ 对于 `i++``i--` 各种编程语言的用法和支持是不同的例如C/C++、Java 等完全支持Python 压根一点都不支持Go 语言虽然支持 `i++``i--` ,却只支持这些操作符作为独立的语句,并且不能嵌入在其它的表达式中。
> * ① 这里讨论的适用于`有符号位`的整数int 等。
> * ② 这里讨论的不适用于`无符号位`的整数unsinged int 等。
![](./assets/33.svg)
* 真值(数据位):因为机器数带有符号位,所以机器数的形式值不等于其真实表示的值(真值),以机器数 1000 0001 为例,其真正表示的值(首位是符号位)为 -1而形式值却是 129 ,因此将带有符号位的机器数的真正表示的值称为机器数的真值。
* 示例:正号和负号
> [!IMPORTANT]
>
> * ① 这里讨论的适用于`有符号位`的整数int 等。
> * ② 这里讨论的不适用于`无符号位`的整数unsinged int 等。
```c
#include <stdio.h>
![](./assets/34.svg)
int main() {
### 3.5.2 原码
int x = 12;
int x1 = -x, x2 = +x;
* 原码的表示与机器数真值表示的一样,即用第一位表示符号,其余位表示数值。
* 规则:
* 正数的`原码`是它本身对应的二进制数,符号位是 0 。
* 负数的`原码`是它本身绝对值对应的二进制数,但是符号位是 1 。
* `+1` 的原码,使用 `16` 位二进数来表示,就是:
int y = -67;
int y1 = -y, y2 = +y;
| 十进制数 | 原码16位二进制数 |
| -------- | --------------------- |
| +1 | `0`000 0000 0000 0001 |
printf("x1=%d, x2=%d \n", x1, x2); // x1=-12, x2=12
printf("y1=%d, y2=%d \n", y1, y2); // y1=67, y2=-67
* `-1` 的原码,使用 `16` 位二进数来表示,就是:
return 0;
}
```
| 十进制数 | 原码16位二进制数 |
| -------- | --------------------- |
| -1 | `1`000 0000 0000 0001 |
> [!IMPORTANT]
>
> * ① 按照原码的规则,会出现 `+0``-0` 的情况,即:`0`000 0000 0000 0001+0、`1`000 0000 0000 0001-0显然不符合实际情况。
>* ② 所以,计算机底层虽然存储和计算的都是二进数,但显然不是原码。
### 3.5.3 反码
* 示例:加、减、乘、除(整数之间做除法时,结果只保留整数部分而舍弃小数部分)、取模
* 规则:
```c
#include <stdio.h>
* 正数的反码和它的原码相同。
* 负数的反码是在其原码的基础上,符号位不变,其余各位取反。
int main() {
* `+1` 的反码,使用 `16` 位二进数来表示,就是:
int a = 5;
int b = 2;
| 十进制数 | 原码16位二进制数 | 反码16位二进制数 |
| -------- | --------------------- | --------------------- |
| +1 | `0`000 0000 0000 0001 | `0`000 0000 0000 0001 |
printf("%d + %d = %d\n", a, b, a + b); // 5 + 2 = 7
printf("%d - %d = %d\n", a, b, a - b); // 5 - 2 = 3
printf("%d × %d = %d\n", a, b, a * b); // 5 × 2 = 10
printf("%d / %d = %d\n", a, b, a / b); // 5 / 2 = 2
printf("%d %% %d = %d\n", a, b, a % b); // 5 % 2 = 1
* `-1` 的反码,使用 `16` 位二进数来表示,就是:
return 0;
}
```
| 十进制数 | 原码16位二进制数 | 反码16位二进制数 |
| -------- | --------------------- | --------------------- |
| -1 | `1`000 0000 0000 0001 | `1`111 1111 1111 1110 |
> [!IMPORTANT]
>
> * ① 按照反码的规则,如果是 `+0`,对应的原码是 `0`000 0000 0000 0000那么其反码还是 `0`000 0000 0000 0000如果是 `-0`,对应的原码是 `1`000 0000 0000 0000其反码是 `1`111 1111 1111 1111显然不符合实际情况。
>* ② 所以,计算机底层虽然存储和计算的都是二进数,但显然不是反码。
### 3.5.4 补码
* 示例:取模(运算结果的符号与被模数也就是第一个操作数相同。)
* 规则:
```c
#include <stdio.h>
* 正数的补码和它的原码相同。
* 负数的补码是在其反码的基础上 + 1 。
* `+1` 的补码,使用 `16` 位二进数来表示,就是:
int main() {
| 十进制数 | 原码16位二进制数 | 反码16位二进制数 | 补码16位二进制数 |
| -------- | --------------------- | --------------------- | --------------------- |
| +1 | `0`000 0000 0000 0001 | `0`000 0000 0000 0001 | `0`000 0000 0000 0001 |
int res1 = 10 % 3;
printf("10 %% 3 = %d\n", res1); // 10 % 3 = 1
* `-1` 的补码,使用 `16` 位二进数来表示,就是:
int res2 = -10 % 3;
printf("-10 %% 3 = %d\n", res2); // -10 % 3 = -1
| 十进制数 | 原码16位二进制数 | 反码16位二进制数 | 补码16位二进制数 |
| -------- | --------------------- | --------------------- | --------------------- |
| -1 | `1`000 0000 0000 0001 | `1`111 1111 1111 1110 | `1`111 1111 1111 1111 |
int res3 = 10 % -3;
printf("10 %% -3 = %d\n", res3); // 10 % -3 = 1
* 如果 `0` ,按照 `+0` 的情况进行处理,如下所示:
int res4 = -10 % -3;
printf("-10 %% -3 = %d\n", res4); // -10 % -3 = -1
![](./assets/35.svg)
return 0;
}
```
* 如果 `0` ,按照 `-0` 的情况进行处理,如下所示:
![](./assets/36.svg)
* `+1``-1` 的`原码`和`补码`的转换过程,如下所示:
* 示例:自增和自减
![](./assets/37.svg)
```c
#include <stdio.h>
> [!IMPORTANT]
>
> * ① 补码表示法解决了`原码`和`反码`存在的`两种`零(`+0` 和 `-0`)的问题,即:在补码表示法中,只有`一个`零,即 `0000 0000`
>* ②补码使得`加法运算`和`减法运算`可以统一处理,通过将减法运算`转换`为加法运算,可以简化硬件设计,提高了运算效率。
> * ③ 计算机底层`存储`和`计算`的都是`二进数的补码`。换言之,当`读取`整数的时候,需要采用`逆向`的转换,即:将补码转换为原码。正数的原码、反码、补码都是一样的,三码合一。负数的补码转换为原码的方法就是先减去 `1` ,得到反码,再按位取反,得到原码(符号位是不能借位的)。
int main() {
### 3.5.5 总结
int i1 = 10, i2 = 20;
int i = i1++;
printf("i = %d\n", i); // i = 10
printf("i1 = %d\n", i1); // i1 = 11
* ① 计算机底层`存储`和`计算`的都是`二进数的补码`。换言之,当`读取`整数的时候,需要采用`逆向`的转换,即:将补码转换为原码。
* ② 正数的原码、反码和补码都是一样的,三码合一。
* ③ 负数的反码是在其原码的基础上按位取反0 变 1 1 变 0 ),符号位不变;负数的补码是其反码 + 1 。
* ④ 0 的补码是 0 。
* ⑤ 负数的补码转换为原码的方法就是先减去 `1` ,得到反码,再按位取反,得到原码(符号位是不能借位的)。
i = ++i1;
printf("i = %d\n", i); // i = 12
printf("i1 = %d\n", i1); // i1 = 12
## 3.6 计算机底层为什么使用补码?
i = i2--;
printf("i = %d\n", i); // i = 20
printf("i2 = %d\n", i2); // i2 = 19
* `加法`和`减法`是计算机中最基本的运算,计算机时时刻刻都离不开它们,所以它们由硬件直接支持。为了提高加法和减法的运行效率,硬件电路必须设计得尽量简单。
* 对于有符号位的数字来说,内存需要区分符号位和数值位:对于人类来说,很容易识别(最高位是 0 还是 1但是对于计算机来说需要设计专门的电路这无疑增加了硬件的复杂性增加了计算时间。如果能将符号位和数值位等同起来让它们一起参与运算不再加以区分这样硬件电路就可以变得非常简单。
* 此外,加法和减法也可以合并为一种运算,即:加法运算。换言之,减去一个数就相当于加上这个数的相反数,如:`5 - 3` 相当于 `5 +-3``10 --9`相当于 `10 + 9`
i = --i2;
printf("i = %d\n", i); // i = 18
printf("i2 = %d\n", i2); // i2 = 18
* 如果能够实现上述的两个目标,那么只需要设计一种简单的、不用区分符号位和数值位的加法电路,就能同时实现加法运算和减法运算,而且非常高效。其实,这两个目标已经实现了,真正的计算机的硬件电路就是这样设计的。
* 但是,简化硬件电路是有代价的,这个代价就是`有符号数`在存储和读取的时候都要继续转换。这也是对于有符号数的运算来说,计算机底层为什么使用`补码`的原因所在。
return 0;
## 3.7 补码到底是如何简化硬件电路的?
```
* 示例:
```c
#include <stdio.h>
/*
随意给出一个整数,打印显示它的个位数,十位数,百位数的值。
格式如下:
数字xxx的情况如下
个位数:
十位数:
百位数:
例如:
数字153的情况如下
个位数3
十位数5
百位数1
*/
int main() {
int num = 153;
int bai = num / 100;
int shi = num % 100 / 10;
int ge = num % 10;
printf("百位为:%d \n", bai);
printf("十位为:%d \n", shi);
printf("个位为:%d \n", ge);
return 0;
}
```
## 2.3 关系运算符(比较运算符)
* 常见的关系运算符,如下所示:
| 运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 |
| ------ | -------- | ---------- | ---------------- | ------ |
| `==` | 相等 | 2 | 0 或 1 | ❎ |
| `!=` | 不相等 | 2 | 0 或 1 | ❎ |
| `<` | 小于 | 2 | 0 或 1 | ❎ |
| `>` | 大于 | 2 | 0 或 1 | ❎ |
| `<=` | 小于等于 | 2 | 0 或 1 | ❎ |
| `>=` | 大于等于 | 2 | 0 或 1 | ❎ |
* 假设 6 和 18 都是 short 类型,现在我们要计算 `6 - 18` 的结果,根据运算规则,它等价于 `6 +-18`。如果按照采用`原码`来计算,那么运算过程是这样的,如下所示:
> [!NOTE]
>
> * ① C 语言中,没有严格意义上的布尔类型,可以使用 0 或 1表示布尔类型的值。
> * ② 不要将 `==` 写成 `=``==` 是比较运算符,而 `=` 是赋值运算符。
> * ③ `>=``<=`含义是只需要满足 `大于或等于`、`小于或等于`其中一个条件,结果就返回真。
> 直接使用原码表示整数,让符号位也参与运算,那么对于减法来说,结果显然是不正确的。
![](./assets/38.svg)
* 示例:
```c
#include <stdio.h>
int main() {
int a = 8;
int b = 7;
printf("a > b 的结果是:%d \n", a > b); // a > b 的结果是1
printf("a >= b 的结果是:%d \n", a >= b); // a >= b 的结果是1
printf("a < b 的结果是%d \n", a < b); // a < b 的结果是0
printf("a <= b 的结果是:%d \n", a <= b); // a <= b 的结果是0
printf("a == b 的结果是:%d \n", a == b); // a == b 的结果是0
printf("a != b 的结果是:%d \n", a != b); // a != b 的结果是1
return 0;
}
```
## 2.4 逻辑运算符
* 常见的逻辑运算符,如下所示:
| 运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 |
| ------ | ------ | ---------- | ---------------- | ------ |
| `&&` | 逻辑与 | 2 | 0 或 1 | ❎ |
| `\|\|` | 逻辑或 | 2 | 0 或 1 | ❎ |
| `!` | 逻辑非 | 2 | 0 或 1 | ❎ |
* 逻辑运算符提供逻辑判断功能,用于构建更复杂的表达式,如下所示:
| a | b | a && b | a \|\| b | !a |
| ------- | ------- | ------- | -------- | ------- |
| 1 | 1 | 1 | 1 | 0 |
| 1 | 0 | 0 | 1 | 0 |
| 0 | 1 | 0 | 1 | 1 |
| 0 | 0 | 0 | 0 | 1 |
* 于是,人们开始继续探索,不断试错,终于设计出了`反码`,如下所示:
> [!NOTE]
>
> * ① 对于逻辑运算符来说,任何`非零值`都表示`真``零值`表示`假`,如:`5 || 0` 返回 `1` `5 && 0` 返回 `0`
> * ② 逻辑运算符的理解:
> * `&&` 的理解就是:`两边条件,同时满足`。
> * `||`的理解就是:`两边条件,二选一`。
> * `!` 的理解就是:`条件取反`。
> * ③ 短路现象:
> * 对于 `a && b` 操作来说,当 a 为假(或 0 )时,因为 `a && b` 结果必定为 0所以不再执行表达式 b。
> * 对于 `a || b` 操作来说,当 a 为真(或非 0 )时,因为 `a || b` 结果必定为 1所以不再执行表达式 b。
> 直接使用反码表示整数,让符号位也参与运算,对于 6 +-18来说结果貌似正确。
![](./assets/39.svg)
* 示例:
```c
#include <stdio.h>
int main() {
int a = 0;
int b = 0;
printf("请输入整数a的值");
scanf("%d", &a);
printf("请输入整数b的值");
scanf("%d", &b);
if (a > b) {
printf("%d > %d", a, b);
} else if (a < b) {
printf("%d < %d", a, b);
} else {
printf("%d = %d", a, b);
}
return 0;
}
```
* 示例:
```c
#include <stdio.h>
// 短路现象
int main() {
int i = 0;
int j = 10;
if (i && j++ > 0) {
printf("床前明月光\n"); // 这行代码不会执行
} else {
printf("我叫郭德纲\n");
}
printf("%d \n", j); //10
return 0;
}
```
* 示例:
```c
#include <stdio.h>
// 短路现象
int main() {
int i = 1;
int j = 10;
if (i || j++ > 0) {
printf("床前明月光 \n");
} else {
printf("我叫郭德纲 \n"); // 这行代码不会被执行
}
printf("%d\n", j); //10
return 0;
}
```
## 2.5 赋值运算符
* 常见的赋值运算符,如下所示:
| 运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 |
| ------ | ------------ | ---------- | ---------------- | ------ |
| `==` | 赋值 | 2 | 左边操作数的值 | ✅ |
| `+=` | 相加赋值 | 2 | 左边操作数的值 | ✅ |
| `-=` | 相减赋值 | 2 | 左边操作数的值 | ✅ |
| `*=` | 相乘赋值 | 2 | 左边操作数的值 | ✅ |
| `/=` | 相除赋值 | 2 | 左边操作数的值 | ✅ |
| `%=` | 取余赋值 | 2 | 左边操作数的值 | ✅ |
| `<<=` | 左移赋值 | 2 | 左边操作数的值 | ✅ |
| `>>=` | 右移赋值 | 2 | 左边操作数的值 | ✅ |
| `&=` | 按位与赋值 | 2 | 左边操作数的值 | ✅ |
| `^=` | 按位异或赋值 | 2 | 左边操作数的值 | ✅ |
| `\|=` | 按位或赋值 | 2 | 左边操作数的值 | ✅ |
* 如果我们将`被减数`和`减数`对调一下,即:计算 `18 - 6` 的结果,也就是 `18 +-6`的结果,继续采用`反码`来进行运算,如下所示:
> [!NOTE]
>
> * ① 赋值运算符的第一个操作数(左值)必须是变量的形式,第二个操作数可以是任何形式的表达式。
> * ② 赋值运算符的副作用针对第一个操作数
> * ① 6 - 186+-18如果采用`反码`计算结果是正确的但是18 - 618 +-6如果采用`反码`计算,结果相差 1 。
> * ② 可以推断:如果按照`反码`来计算,小数 - 大数,结果正确;而大数 - 小数,结果相差 1 。
![](./assets/40.svg)
* 对于这个相差的 `1` 必须进行纠正,但是又不能影响`小数-大数`的结果。于是,人们又绞尽脑汁设计出了`补码`,给`反码`打了一个`“补丁”`,终于把相差的 `1` 给纠正过来了。那么,`6 - 18` 按照`补码`的运算过程,如下所示:
* 示例:
![](./assets/41.svg)
* 那么,`18 - 6` 按照`补码`的运算过程,如下所示:
![](./assets/42.svg)
> [!IMPORTANT]
>
> 总结:采用`补码`的形式正好将相差的 `1`纠正过来,也没有影响到小数减大数,这个“补丁”非常巧妙。
>
> * ① 小数减去大数,结果为负,之前(负数从反码转换为补码需要 +1加上的 1 ,后来(负数从补码转换为反码要 -1还需要减去正好抵消掉所以不会受到影响。
> * ② 大数减去小数,结果为正,之前(负数从反码转换为补码需要 +1加上的 1 ,后来(正数的补码和反码相同,从补码转换为反码不用 -1就没有再减去不能抵消掉这就相当于给计算结果多加了一个 1。
>
> `补码`这种天才般的设计,一举达成了之前加法运算和减法运算提到的两个目标,简化了硬件电路。
## 3.8 问题抛出
* 在 C 语言中,对于`有符号位`的整数,是使用 `0` 作为正数,`1` 作为负数,来表示`符号位`,并使用`数据位`来表示的是数据的`真值`,如下所示:
```c
#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;
}
int a = 10;
int b = -10;
```
## 2.6 位运算符(了解)
![](./assets/43.svg)
### 2.6.1 概述
* 但是,对于`无符号位`的整数而言,是`没有`符号位和数据位,即:没有原码、反码、补码的概念。无符号位的整数的数值都是直接使用二进制来表示的(也可以理解为,对于无符号位的整数,计算机底层存储的就是其原码),如下所示:
* C 语言提供了一些位运算符能够让我们操作二进制位bit
* 常见的位运算符,如下所示。
```c
unsigned int a = 10;
unsigned int b = -10;
```
| 运算符 | 描述 | 操作数个数 | 运算规则 | 副作用 |
| ------ | ---------- | ---------- | ------------------------------------------------------------ | ------ |
| `&` | 按位与 | 2 | 两个二进制位都为 1 ,结果为 1 ,否则为 0 。 | ❎ |
| `\|` | 按位或 | 2 | 两个二进制位只要有一个为 1包含两个都为 1 的情况),结果为 1 ,否则为 0 。 | ❎ |
| `^` | 按位异或 | 2 | 两个二进制位一个为 0 ,一个为 1 ,结果为 1否则为 0 。 | ❎ |
| `~` | 按位取反 | 2 | 将每一个二进制位变成相反值,即 0 变成 1 1 变 成 0 。 | ❎ |
| `<<` | 二进制左移 | 2 | 将一个数的各二进制位全部左移指定的位数,左 边的二进制位丢弃,右边补 0。 | ❎ |
| `>>` | 二进制右移 | 2 | 将一个数的各二进制位全部右移指定的位数,正数左补 0负数左补 1右边丢弃。 | ❎ |
![](./assets/44.svg)
> [!NOTE]
>
> 操作数在进行位运算的时候,以它的补码形式计算!!!
### 2.6.2 输出二进制位
* 在 C 语言中,`printf` 是没有提供输出二进制位的格式占位符的;但是,我们可以手动实现,以方便后期操作。
* 示例:
* 这就是导致了一个结果就是:如果我定义一个`有符号`的负数,却让其输出`无符号`,必然造成结果不对,如下所示:
```c
#include <stdio.h>
/**
* 获取指定整数的二进制表示
* @param num 整数
* @return 二进制表示的字符串,不包括前导的 '0b' 字符
*/
char* getBinary(int num) {
char *getBinary(int num) {
static char binaryString[33];
int i, j;
@ -2152,680 +1440,23 @@ char* getBinary(int num) {
int main() {
int a = 17;
int b = -12;
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
printf("整数 %d 的二进制表示:%s \n", a, getBinary(a));
printf("整数 %d 的二进制表示:%s \n", b, getBinary(b));
int num = -10;
printf("b=%s\n", getBinary(num)); // b=11111111111111111111111111110110
printf("b=%d\n", num); // b=-10
printf("b=%u\n", num); // b=4294967286
return 0;
}
```
### 2.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)
### 2.6.4 按位或
* 按位与 `|` 的运算规则是:如果二进制对应的位上只要有 1 就是 1 ,否则为 0 ,即:
* `1 | 1` 的结果是 `1`
* `1 | 0` 的结果是 `1`
* `0 | 1` 的结果是 `1`
* `0 | 0` 的结果是 `0`
* 示例:`9 | 7 = 15`
![](./assets/24.svg)
* 示例:`-9 | 7 = -9`
![](./assets/25.svg)
### 2.6.5 按位异或
* 按位与 `^` 的运算规则是:如果二进制对应的位上一个为 1 一个为 0 就为 1 ,否则为 0 ,即:
* `1 ^ 1` 的结果是 `0`
* `1 ^ 0` 的结果是 `1`
* `0 ^ 1` 的结果是 `1`
* `0 ^ 0` 的结果是 `0`
> [!NOTE]
>
> 按位异或的场景有:
>
> * ① 交换两个数值:异或操作可以在不使用临时变量的情况下交换两个变量的值。
> * ② 加密或解密:异或操作用于简单的加密和解密算法。
> * ③ 错误检测和校正异或操作可以用于奇偶校验位的计算和检测错误RAID-3 以及以上)。
> * ……
* 示例:`9 ^ 7 = 14`
![](./assets/26.svg)
* 示例:`-9 ^ 7 = -16`
![](./assets/27.svg)
### 2.6.6 按位取反
* 运算规则:如果二进制对应的位上是 1则结果为 0如果是 0 ,则结果为 1 。
* `~0` 的结果是 `1`
* `~1` 的结果是 `0`
* 示例:`~9 = -10`
![](./assets/28.svg)
* 示例:`~-9 = 8`
![](./assets/29.svg)
### 2.6.7 二进制左移
* 在一定范围内,数据每向左移动一位,相当于原数据 × 2。正数、负数都适用
* 示例:`3 << 4 = 48` 3 × 2^4
![](./assets/30.svg)
* 示例:`-3 << 4 = -48` -3 × 2 ^4
![](./assets/31.svg)
### 2.6.8 二进制右移
* 在一定范围内,数据每向右移动一位,相当于原数据 ÷ 2。正数、负数都适用
> [!NOTE]
>
> * ① 如果不能整除,则向下取整。
> * ② 右移运算符最好只用于无符号整数,不要用于负数。因为不同系统对于右移后如何处理负数的符号位,有不同的做法,可能会得到不一样的结果。
* 示例:`69 >> 4 = 4` 69 ÷ 2^4
![](./assets/32.svg)
* 示例:`-69 >> 4 = -5` -69 ÷ 2^4
![](./assets/33.svg)
## 2.7 三元运算符
* 语法:
```c
条件表达式 ? 表达式1 : 表达式2 ;
```
> [!NOTE]
>
> * 如果条件表达式为非 0 (真),则整个表达式的值是表达式 1 。
> * 如果条件表达式为 0 (假),则整个表达式的值是表达式 2 。
* 示例:
```c
#include <stdio.h>
int main() {
int m = 110;
int n = 20;
int result = m > n ? m : n;
printf("result = %d\n", result); // result = 110
return 0;
}
```
## 2.8 运算符优先级
* 在数学中,如果一个表达式是 `a + b * c` ,我们知道其运算规则就是:先算乘除再算加减。其实,在 C 语言中也是一样的先算乘法再算加减C 语言中乘除的运算符比加减的运算符的优先级要高。
> [!NOTE]
>
> 所谓`优先级`,就是当多个运算符出现在同一个表达式中时,先执行哪个运算符。
* C 语言中运算符的优先级有几十个,有的运算符优先级不同,有的运算符优先级相同,如下所示:
| **优先级** | **运算符** | **名称或含义** | **结合方向** |
| ---------- | ---------- | ---------------- | ------------- |
| **1** | `[]` | 数组下标 | ➡️(从左到右) |
| | `()` | 圆括号 | |
| | `.` | 成员选择(对象) | |
| | `->` | 成员选择(指针) | |
| **2** | `-` | 负号运算符 | ⬅️(从右到左) |
| | `(类型)` | 强制类型转换 | |
| | `++` | 自增运算符 | |
| | `--` | 自减运算符 | |
| | `*` | 取值运算符 | |
| | `&` | 取地址运算符 | |
| | `!` | 逻辑非运算符 | |
| | `~` | 按位取反运算符 | |
| | `sizeof` | 长度运算符 | |
| **3** | `/` | 除 | ➡️(从左到右) |
| | `*` | 乘 | |
| | `%` | 余数(取模) | |
| **4** | `+` | 加 | ➡️(从左到右) |
| | `-` | 减 | |
| **5** | `<<` | 左移 | ➡️(从左到右) |
| | `>>` | 右移 | |
| **6** | `>` | 大于 | ➡️(从左到右) |
| | `>=` | 大于等于 | |
| | `<` | 小于 | |
| | `<=` | 小于等于 | |
| **7** | `==` | 等于 | ➡️(从左到右) |
| | `!=` | 不等于 | |
| **8** | `&` | 按位与 | ➡️(从左到右) |
| **9** | `^` | 按位异或 | ➡️(从左到右) |
| **10** | `\|` | 按位或 | ➡️(从左到右) |
| **11** | `&&` | 逻辑与 | ➡️(从左到右) |
| **12** | `\|\|` | 逻辑或 | ➡️(从左到右) |
| **13** | `?:` | 条件运算符 | ⬅️(从右到左) |
| **14** | `=` | 赋值运算符 | ⬅️(从右到左) |
| | `/=` | 除后赋值 | |
| | `*=` | 乘后赋值 | |
| | `%=` | 取模后赋值 | |
| | `+=` | 加后赋值 | |
| | `-=` | 减后赋值 | |
| | `<<=` | 左移后赋值 | |
| | `>>=` | 右移后赋值 | |
| | `&=` | 按位与后赋值 | |
| | `^=` | 按位异或后赋值 | |
| | `\|=` | 按位或后赋值 | |
| **15** | `,` | 逗号运算符 | ➡️(从左到右) |
> [!WARNING]
>
> * ① 不要过多的依赖运算符的优先级来控制表达式的执行顺序,这样可读性太差,尽量`使用小括号来控制`表达式的执行顺序。
> * ② 不要把一个表达式写得过于复杂,如果一个表达式过于复杂,则把它`分成几步`来完成。
> * ③ 运算符优先级不用刻意地去记忆,总体上:一元运算符 > 算术运算符 > 关系运算符 > 逻辑运算符 > 三元运算符 > 赋值运算符。
# 第三章:附录
## 3.1 字符集和字符集编码
### 3.3.1 概述
* 字符集和字符集编码(简称编码)计算机系统中处理文本数据的两个基本概念,它们密切相关但又有区别。
* 字符集Character Set是一组字符的集合其中每个字符都被分配了一个`唯一的编号`(通常是数字)。字符可以是字母、数字、符号、控制代码(如换行符)等。`字符集定义了可以表示的字符的范围`,但它并不直接定义如何将这些字符存储在计算机中。
> [!NOTE]
>
> ASCII美国信息交换标准代码是最早期和最简单的字符集之一它只包括了英文字母、数字和一些特殊字符共 128 个字符。每个字符都分配给了一个从 0 到 127 的数字。
* 字符集编码Character Encoding简称编码是一种方案或方法`它定义了如何将字符集中的字符转换为计算机存储和传输的数据(通常是一串二进制数字)`。简而言之,编码是字符到二进制数据之间的映射规则。
> [!NOTE]
>
> ASCII 编码方案定义了如何将 ASCII 字符集中的每个字符表示为 7 位的二进制数字。例如:大写字母`'A'`在 ASCII 编码中表示为二进制的`1000001`,十进制的 `65`
* `字符集`和`字符集编码`之间的关系如下:
![](./assets/35.png)
* Linux 中安装帮助手册:
![](./assets/36.gif)
### 3.3.2 ASCII 编码
* 从`冯·诺依曼`体系结构中,我们知道,计算机中所有的`数据`和`指令`都是以`二进制`的形式表示的;所以,计算机中对于文本数据的数据也是以二进制来存储的,那么对应的流程如下:
![](./assets/37.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/38.gif)
* 其对应的 ASCII 编码表,如下所示:
![](./assets/39.gif)
* 但是,随着计算机的发展,计算机开始了东征之路,由美国传播到东方:
![](./assets/40.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/41.gif)
![](./assets/42.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 编码写的文章,通过邮件发送给韩国人,因为韩国和中国在字符集编码上的高位字节有重叠部分,必然会造成歧义。
### 3.3.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/43.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 通常是最常用的编码方式,因为它在兼容性和空间效率之间提供了良好的平衡。
* 其实C 语言的底层逻辑很简单C 语言压根不关心你定义的是有符号数还是无符号数,它只关心内存(如果定义的是有符号数,那就按照有符号数的规则来存储;如果定义的是无符号数,那就按照无符号数的规则来存储)。换言之,有符号数可以按照无符号数的规则来输出,无符号数也可以按照有符号数的规则来输出,至于输出结果对不对,那是程序员的事情,和 C 语言没有任何关系。
> [!IMPORTANT]
>
> * ① Windows 内核、.NET Framework、Java String 内部采用的都是 `UTF-16` 编码,主要原因是为了在兼顾字符处理效率的同时,能够有效处理多种语言的字符集,即:历史遗留问题、兼容性要求和多语言支持的需要。
> * ② 不过UNIX 家族的操作系统Linux、Mac OS、iOS 等)内核都采用 `UTF-8` 编码,主要是为了兼容性和灵活性,因为 UTF-8 编码可以无缝处理 ASCII 字符,同时也能够支持多字节的 Unicode 字符,即:为了最大限度地兼容 ASCII同时保持系统的简单性、灵活性和效率。
> * ① 实际开发中,`printf` 函数中的常量、变量或表达式,需要和格式占位符一一对应;否则,将会出现数据错误的现象。
> * ② 正因为上述的原因很多现代化的编程语言Java 等,直接取消了无符号的概念。但是,很多数据库是使用 C 语言开发的MySQL 等,就提供了创建数据表的字段为无符号类型的功能,即:`UNSIGNED`(正整数) ,不要感觉困惑!!!
> * ③ 对于 `1000 0000 …… 0000 0000` 这个特殊的补码,无法按照上述的方法转换为原码,所以计算机直接规定这个补码对应的值就是 `-2³¹`,至于为什么,下节我们会详细分析。
- `Unicode 字符集`和对应的`UTF-8 字符编码`之间的关系,如下所示:
![](./assets/44.png)
>[!NOTE]
>
>`宽字符`和`窄字符`是编程和计算机系统中对字符类型的一种分类,主要用于描述字符在内存中的表示形式及其与编码方式的关系。
>
>* ① `窄字符`通常指使用单个字节8 位)来表示的字符。在许多传统的编码系统中,窄字符通常代表 ASCII 字符或其它单字节字符集中的字符。换言之,`窄字符`适合处理简单的单字节字符集ASCII适用于处理西方语言的应用。
>* ② `宽字符`指使用多个字节(通常是两个或更多)来表示的字符。这些字符通常用于表示比 ASCII 范围更广的字符集,如 Unicode 字符。换言之,`宽字符`适合处理多字节字符集UTF-32、UTF-16 等,适用于需要处理多种语言和符号的国际化应用。
>
>在现代编程中,`窄字符`通常与 `UTF-8` 编码关联,特别是在处理文本输入、输出和网络传输时。尽管 `UTF-8` 是变长编码,由于其高效的空间利用和对 `ASCII` 的优化,通常与`窄字符`概念关联。而`宽字符`通常与 `UTF-16` 编码或 `UTF-32`编码关联,这些编码使用更大的固定或半固定长度来表示字符,适合处理更大的字符集。
## 3.2 WSL2 中设置默认编码为中文
### 3.2.1 概述
* 查看 WSL2 的 Linux 发行版的默认编码:
```shell
echo $LANG
```
![](./assets/45.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` 时,系统默认语言环境保持简单和高效,同时支持更广泛的字符集,特别是多语言和非英语字符。这样可以在需要兼容性的同时,提供对全球化字符的支持。
### 3.2.2 AlmaLinux9 设置默认编码
* ① 搜索中文语言包:
```shell
dnf search locale zh
```
![](./assets/46.gif)
* ② 安装中文语言包:
```shell
dnf -y install glibc-langpack-zh
```
![](./assets/47.gif)
* ③ 切换语言环境为中文:
```shell
localectl set-locale LANG=zh_CN.UTF-8
```
![](./assets/48.gif)
* ④ 手动加载配置文件,使其生效:
```shell
source /etc/locale.conf
```
![](./assets/49.gif)
### 3.2.3 Ubuntu 22.04 设置默认编码
* ① 安装中文语言包:
```shell
apt update -y && apt install language-pack-zh-hans -y
```
![](./assets/50.gif)
* ② 切换环境为中文:
```shell
update-locale LANG=zh_CN.UTF-8 LANGUAGE=zh_CN:zh
```
![](./assets/51.gif)
* ③ 手动加载配置文件,使其生效:
```shell
source /etc/default/locale
```
![](./assets/52.gif)
## 3.3 在 C 语言中使用中文字符
### 3.3.1 概述
* 大部分 C 语言文章或教材对中文字符的处理讳莫如深,甚至只字不提,导致很多初学者认为 C 语言只能处理英文,而不支持中文。
* 其实这是不对的。C 语言作为一门系统级别的编程语言,理应支持世界上任何一个国家的文字,如:中文、日文、韩文等。
> [!NOTE]
>
> 如果 C 语言不支持中文,那么简体中文 Windows 操作系统将无从谈起,我们只能被迫使用英文 Windows 操作系统,这对计算机的传播而言将会是一种巨大的阻碍。
### 3.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.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.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;
}
```
## 3.4 C 语言到底使用什么编码?
### 3.4.1 概述
* 在 C 语言中,只有 `char` 类型的`窄字符`才会使用 ASCII 编码。而 `char` 类型的`窄字符串`、`wchar_t` 类型的`宽字符`和`宽字符串`都不使用 ASCII 编码。
* `wchar_t` 类型的`宽字符`和`宽字符串`使用 UTF-16 或者 UTF-32 编码,这个在上文已经讲解了,现在只剩下 `char` 类型的`窄字符串`没有讲解了,这也是下文的重点。
> [!NOTE]
>
> * ① 其实,对于`char` 类型的窄字符串C 语言并没有规定使用哪一种特定的编码,只要选用的编码能够适应当前的环境即可。换言之,`char` 类型的窄字符串的编码与操作系统以及编译器有关。
> * ② 但是,`char` 类型的窄字符串一定不是 ASCII 编码,因为 ASCII 编码只能显示拉丁体系的文字,而不能输出中文、日文、韩文等。
> * ③ 讨论窄字符串的编码要从以下两个方面下手。
### 3.4.2 源文件使用什么编码?
* 源文件用来保存我们编写的代码,它最终会被存储到本地硬盘,或者远程服务器,这个时候就要尽量压缩文件体积,以节省硬盘空间或者网络流量,而代码中大部分的字符都是 ASCII 编码中的字符,用一个字节足以容纳,所以 UTF-8 编码是一个不错的选择。
* UTF-8 兼容 ASCII代码中的大部分字符可以用一个字节保存。另外UTF-8 基于 Unicode支持全世界的字符我们编写的代码可以给全球的程序员使用真正做到技术无国界。
* 常见的 IDE 或者编辑器Sublime Text、Vim 等,在创建源文件的时候一般默认就是 UTF-8 编码。就算不是,我们也会推荐设置为 UTF-8 编码,如下所示:
![](./assets/53.png)
* 对于 C 语言编译器来说,它往往支持多种编码格式的源文件。微软的 MSVC 、GCC 和 LLVM/Clang 都支持 UTF-8 和本地编码的源文件。
### 3.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 编码。
### 3.3.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可以轻松处理中文等多字节字符。
### 3.3.5 编码字符集和运行字符集
* 源文件使用的字符集,通常称为`编码字符集`,即:写代码的时候所使用的字符集。
> [!NOTE]
>
> 源文件需要保存到硬盘,或者在网络上传输,使用的编码要尽量节省存储空间,同时要方便跨国交流,所以一般使用 UTF-8这就是选择编码字符集的标准。
* 程序中的字符或者字符串使用的字符集,通常称为`运行字符集`,即:程序运行时所使用的字符集。
> [!NOTE]
>
> 程序中的字符或者字符串在程序运行后必须被载入到内存才能进行后续的处理对于这些字符来说要尽量选用能够提高处理速度的编码UTF-16 和 UTF-32 编码就能够快速定位(查找)字符。
* `编码字符集`是站在`存储`和`传输`的角度,而`运行字符集`是站在`处理`或者`操作`的角度,所以它们并不一定相同。

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 209 KiB

View File

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 819 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

Before

Width:  |  Height:  |  Size: 41 KiB

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

View File

Before

Width:  |  Height:  |  Size: 778 KiB

After

Width:  |  Height:  |  Size: 778 KiB

View File

Before

Width:  |  Height:  |  Size: 794 KiB

After

Width:  |  Height:  |  Size: 794 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

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

View File

Before

Width:  |  Height:  |  Size: 487 KiB

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

View File

Before

Width:  |  Height:  |  Size: 55 KiB

After

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

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 156 KiB

View File

Before

Width:  |  Height:  |  Size: 533 KiB

After

Width:  |  Height:  |  Size: 533 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

Before

Width:  |  Height:  |  Size: 962 KiB

After

Width:  |  Height:  |  Size: 962 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 MiB

After

Width:  |  Height:  |  Size: 6.6 MiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View File

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 168 KiB

View File

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

View File

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 220 KiB

View File

Before

Width:  |  Height:  |  Size: 477 KiB

After

Width:  |  Height:  |  Size: 477 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

Before

Width:  |  Height:  |  Size: 225 KiB

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 348 KiB

View File

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 400 KiB

After

Width:  |  Height:  |  Size: 400 KiB

View File

@ -1,389 +1,448 @@
# 第一章:概述
# 第一章:数据类型(⭐)
* `流程控制结构`是用来控制程序中`各语句执行顺序`的语句,并且可以将语句组合成能`完成一定功能`的`小逻辑模块`。
* 在程序设计中规定了`三种`流程结构,如下所示:
* `顺序结构`:程序从上到下逐行执行,中间没有任何判断和跳转。
* `分支结构`:根据条件,有选择的执行某段代码。在 C 语言中,有 `if...else``switch...case` 两种分支语句。
* `循环结构`:根据循环条件,重复性的执行某段代码。在 C 语言中,有 `for`、`while`、`do...while` 三种循环结构。
## 1.1 概述
* 在生活中的`洗衣工厂`,就包含了上述的三种流程结构,如下所示:
* 根据`变量`中`存储`的`值`的`不同`,我们可以将`变量`分为两类:
* `普通变量`:变量所对应的内存中存储的是`普通值`。
* `指针变量`:变量所对应的内存中存储的是`另一个变量的地址`。
![](./assets/1.jpg)
* 如下图所示:
![](./assets/1.png)
> [!NOTE]
>
> 普通变量和指针变量的相同点:
>
> * 普通变量有内存空间,指针变量也有内存空间。
> * 普通变量有内存地址,指针变量也有内存地址。
> * 普通变量所对应的内存空间中有值,指针变量所对应的内存空间中也有值。
>
> 普通变量和指针变量的不同点:
>
> * 普通变量所对应的内存空间存储的是普通的值,如:整数、小数、字符等;指针变量所对应的内存空间存储的是另外一个变量的地址。
> * 普通变量有普通变量的运算方式,而指针变量有指针变量的运算方式(后续讲解)。
# 第二章:顺序结构
## 2.1 概述
* 程序从上到下逐行地执行,表达式语句都是顺序执行的,并且上一行对某个变量的修改对下一行会产生影响。
* 那么,在 C 语言中变量的数据类型就可以这么划分,如下所示:
![](./assets/2.png)
## 2.2 应用示例
> [!NOTE]
>
> * 根据`普通变量`中`存储`的`值`的类型不同,可以将`普通变量类型`划分为`基本数据类型`(整型、字符类型、浮点类型、布尔类型)和`复合数据类型`(数组类型、结构体类型、共用体类型、枚举类型)。
> * 根据`指针变量`所`指向空间`中`存储`的`值`的类型不同,可以将`指针类型`分为`基本数据类型指针`、`复合数据类型指针`、`函数指针`、`数组指针`等,例如:如果指针所指向的空间保存的是 int 类型,那么该指针就是 int 类型的指针。
* 示例:
## 1.2 整数类型
```c
#include <stdio.h>
### 1.2.1 概述
int main() {
* 整数类型简称整型用于存储整数值12、20、50 等。
* 根据所占`内存空间`大小的不同,可以将整数类型划分为:
* ① 短整型:
int x = 1;
int y = 2;
printf("x = %d \n", x); // x = 1
printf("y = %d \n", y); // y = 2
| 类型 | 存储空间(内存空间) | 取值范围 |
| ------------------------------------ | -------------------- | ----------------------------------- |
| unsigned short (无符号短整型) | 2 字节 | 0 ~ 65,535 (2^16 - 1) |
| [signed] short有符号短整型默认 | 2 字节 | -32,768 (- 2^15) ~ 32,767 (2^15 -1) |
// 对 x 和 y 的值进行修改
x++;
y = 2 * x + y;
x = x * 10;
* ② 整型:
printf("x = %d \n", x); // x = 20
printf("y = %d \n", y); // y = 6
| 类型 | 存储空间(内存空间) | 取值范围 |
| -------------------------------- | -------------------- | ------------------------------------------- |
| unsigned int无符号整型 | 4 字节(通常) | 0 ~ 4294967295 (0 ~2^32 -1) |
| [signed] int有符号整型默认 | 4 字节(通常) | -2147483648- 2^31 ~ 2147483647 (2^31-1) |
return 0;
}
```
* ③ 长整型:
| 类型 | 存储空间(内存空间) | 取值范围 |
| ----------------------------------- | -------------------- | --------------- |
| unsigned long无符号长整型 | 4 字节(通常) | 0 ~2^32 -1 |
| [signed] long有符号长整型默认 | 4 字节(通常) | - 2^31 ~ 2^31-1 |
* ④ 长长整型:
# 第三章:分支结构(⭐)
| 类型 | 存储空间(内存空间) | 取值范围 |
| ---------------------------------------- | -------------------- | --------------- |
| unsigned long long无符号长整型 | 8 字节(通常) | 0 ~2^64 -1 |
| [signed] long long有符号长整型默认 | 8 字节(通常) | - 2^63 ~ 2^63-1 |
## 3.1 概述
> [!NOTE]
>
> * ① 数据类型在内存中占用的存储单元字节数就称为该数据类型的长度步长short 占用 2 个字节的内存,就称 short 的长度(步长)是 2。
>
> * ② C 语言并没有严格规定各种整数数据类型在内存中所占存储单元的长度,只做了宽泛的限制:
>
> * short 至少占用 2 个字节的存储单元。
> * int 建议为一个机器字长(指计算机的处理器在一次操作中能够处理的二进制数据的位数,机器字长是处理器的“字”长度,它决定了处理器在一个时钟周期内能够处理的数据量,如:早期的计算机的处理器通常是 8 位的机器字长,意味着处理器一次只能处理 8 位(二进制)数据;之后的计算机的处理器有 16 位的机器字长,意味着处理器一次可以处理 16 位的数据;再之后计算机的处理器有 32 位或 64 位的机器字长,意味着处理器一次可以处理 32 位或 64位的数据。即32 位环境下 int 占用 4 个字节的存储单元64 位环境下 int 占用 8 个字节的存储单元。
> * short 的长度(步长)不能大于 intlong 的长度(步长)不能小于 intlong long 不能小于 long。
>
> * ③ 那么,各种整数数据类型在内存中所占存储单元的长度的公式就是 `2 ≤ sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long)`,具体的存储空间由编译系统自行决定。其中,`sizeof` 是测量类型或变量、常量长度的`运算符`。
* 根据特定条件执行不同的代码块,从而实现灵活的程序控制和更复杂的逻辑。
> [!IMPORTANT]
>
> * ① 之所以这么规定,是为了可以让 C 语言长久使用,因为目前主流的 CPU 都是 64 位,但是在 C 语言刚刚出现的时候CPU 还是以 8 位和 16 位为主。如果当时就将整型定死为 8 位或 16 位,那么现在我们肯定不会再学习 C 语言了。
> * ② 整型分为有符号 signed 和无符号 unsigned 两种,默认是 signed。
> * ③ 在实际开发中,`最常用的整数类型`就是 `int` 类型了,如果取值范围不够,就使用 long 或 long long 。
> * ④ C 语言中的`格式占位符`非常多,只需要大致了解即可;因为,我们在实际开发中,一般都会使用 C++ 或 Rust 以及其它的高级编程语言Java 等,早已经解决了必须通过`格式占位符`来才能将变量进行输入和输出。
## 3.2 单分支结构
### 3.2.1 概述
### 1.2.2 短整型(了解)
* 语法:
```c
if(条件表达式){
语句;
}
unsigned short x = 10 ; // 无符号短整型
```
```c
short x = -10; // 有符号短整型
```
> [!NOTE]
>
> * ① 在 C 语言中,严格意义上是没有 boolean 类型的,使用`非0` 表示`真true``0` 表示`假false`。
> * ② 当条件表达式为真(`非0` ),就会执行代码块中的语句;否则,就不会执行代码块中的语句。
* 流程图,如下所示:
![](./assets/3.png)
### 3.2.2 应用示例
* 需求:成年人心率的正常范围是每分钟 60~100 次。体检时,如果心率不在此范围内,则提示需要做进一步的检查。
> * ① 有符号表示的是正数、负数和 0 ,即有正负号。无符号表示的是 0 和正数,即正整数,没有符号。
> * ② 在 `printf` 中`无符号短整型unsigned short`的`格式占位符`是 `%hu` `有符号短整型signed short`的`格式占位符`是 `%hd`
> * ③ 可以通过 `sizeof` 运算符获取`无符号短整型unsigned short` 和 `有符号短整型signed short` 的`存储空间(所占内存空间)`。
> * ③ 可以通过 `#include <limits.h>` 来获取 `无符号短整型unsigned short` 和`有符号短整型signed short`的`取值范围`。
* 示例:
* 示例:定义和打印短整型变量
```c
#include <stdio.h>
int main() {
int heartBeats = 0;
printf("请输入您的心率:");
scanf("%d", &heartBeats);
// 定义有符号 short 类型
signed short s1 = -100;
if (heartBeats < 60 || heartBeats > 100) {
printf("您的心率不在正常范围内,请做进一步的检查。\n");
}
printf("s1 = %hd \n", s1); // s1 = -100
printf("体检结束!!!");
// 定义无符号 short 类型
unsigned short s2 = 100;
printf("s2 = %hu \n", s2); // s2 = 100
// 定义 short 类型,默认是有符号
short s3 = -200;
printf("s3 = %hd \n", s3); // s3 = -200
return 0;
}
```
### 3.2.3 应用示例
* 需求:根据年龄判断,如果是未成年人,则提示 "未成年人请在家长陪同下访问!" 。
* 示例:
* 示例:获取类型占用的内存大小(存储空间)
```c
#include <stdio.h>
int main() {
int age = 0;
printf("请输入你的年龄:");
scanf("%d", &age);
size_t s1 = sizeof(unsigned short);
printf("unsigned short 的存储空间是 %zu 字节 \n", s1); // 2
if (age < 18) {
printf("未成年人请在家长陪同下访问!\n");
}
size_t s2 = sizeof(signed short);
printf("signed short 的存储空间是 %zu 字节 \n", s2); // 2
printf("欢迎继续访问!");
size_t s3 = sizeof(short);
printf("short 的存储空间是 %zu 字节 \n", s3); // 2
return 0;
}
```
## 3.3 双分支结构
### 3.3.1 概述
* 示例:获取类型的取值范围
```c
#include <limits.h>
#include <stdio.h>
int main() {
printf("unsigned short 类型的范围是[0,%hu]\n", USHRT_MAX); // [0,65535]
printf("short 类型的范围是[%hd,%hd]\n", SHRT_MIN,SHRT_MAX); // [-32768,32767]
return 0;
}
```
### 1.2.3 整型
* 语法:
```c
if(条件表达式) {
语句块1;
}else {
语句块2;
}
unsigned int x = 10 ; // 无符号整型
```
```c
int x = -10; // 有符号整型
```
> [!NOTE]
>
> * ① 在 C 语言中,严格意义上是没有 boolean 类型的,使用`非0` 表示`真true``0` 表示`假false`。
> * ② 当条件表达式为真(`非0` ),就会执行代码块 1 中的语句;否则,执行代码块 2 中的语句。
* 流程图,如下所示:
![](./assets/4.png)
### 3.3.2 应用示例
* 需求:判断一个整数,是奇数还是偶数。
> * ① 有符号表示的是正数、负数和 0 ,即有正负号。无符号表示的是 0 和正数,即正整数,没有符号。
> * ② 在 `printf` 中`无符号整型unsigned int`的`格式占位符`是 `%u` `有符号整型signed int`的`格式占位符`是 `%d`
> * ③ 可以通过 `sizeof` 运算符获取`无符号整型unsigned int` 和 `有符号整型signed int` 的`存储空间(所占内存空间)`。
> * ③ 可以通过 `#include <limits.h>` 来获取 `无符号整型unsigned int` 和`有符号整型signed int`的`取值范围`。
* 示例:
* 示例:定义和打印整型变量
```c
#include <stdio.h>
int main() {
int num = 0;
printf("请输入一个整数:");
scanf("%d", &num);
// 定义有符号 int 类型
signed int i1 = -100;
if (num % 2 == 0) {
printf("%d 是偶数\n", num);
} else {
printf("%d 是奇数\n", num);
}
printf("i1 = %d \n", i1); // i1 = -100
// 定义无符号 int 类型
unsigned int i2 = 100;
printf("i2 = %u \n", i2); // i2 = 100
// 定义 int 类型,默认是有符号
short i3 = -200;
printf("i3 = %d \n", i3); // i3 = -200
return 0;
}
```
### 3.3.2 应用示例
* 需求输入年龄如果大于18岁则输出 "你年龄大于18要对自己的行为负责!";否则,输出 "你的年龄不大这次放过你了。"
* 示例:
* 示例:获取类型占用的内存大小(存储空间)
```c
#include <stdio.h>
int main() {
int age = 0;
printf("请输入年龄:");
scanf("%d", &age);
size_t i1 = sizeof(unsigned int);
printf("unsigned int 的存储空间是 %zu 字节 \n", i1); // 4
if (age > 18) {
printf("你年龄大于18要对自己的行为负责!\n");
} else {
printf("你的年龄不大,这次放过你了!\n");
}
size_t i2 = sizeof(signed int);
printf("signed int 的存储空间是 %zu 字节 \n", i2); // 4
size_t i3 = sizeof(int);
printf("int 的存储空间是 %zu 字节 \n", i3); // 4
return 0;
}
```
### 3.3.3 应用示例
* 需求:判定某个年份是否为闰年?
>[!NOTE]
>
>* ① year 是 400 的整倍数: year%400==0
>* ② 能被 4 整除,但不能被 100 整除year % 4 == 0 && year % 100 != 0
* 示例:
* 示例:获取类型的取值范围
```c
#include <limits.h>
#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);
}
printf("unsigned int 类型的范围是[0,%u]\n", UINT_MAX); // [0,4294967295]
printf("int 类型的范围是[%d,%d]\n", INT_MIN,INT_MAX); // [-2147483648,2147483647]
return 0;
}
```
## 3.4 多重分支结构
### 3.4.1 概述
### 1.2.4 长整型(了解)
* 语法:
```c
if (条件表达式1) {
语句块1;
} else if (条件表达式2) {
语句块2;
}
...
} else if (条件表达式n) {
语句块n;
} else {
语句块n+1;
}
unsigned long x = 10 ; // 无符号长整型
```
```c
long x = -10; // 有符号长整型
```
> [!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 。
> * ① 有符号表示的是正数、负数和 0 ,即有正负号。无符号表示的是 0 和正数,即正整数,没有符号。
> * ② 在 `printf` 中`无符号长整型unsigned long`的`格式占位符`是 `%lu` `有符号长整型signed long`的`格式占位符`是 `%ld`
> * ③ 可以通过 `sizeof` 运算符获取`无符号长整型unsigned long` 和 `有符号长整型signed long` 的`存储空间(所占内存空间)`。
> * ③ 可以通过 `#include <limits.h>` 来获取 `无符号长整型unsigned long` 和`有符号长整型signed long`的`取值范围`。
* 示例:
* 示例:定义和打印长整型变量
```c
#include <stdio.h>
int main() {
int score = 0;
printf("请输入分数:");
scanf("%d", &score);
// 定义有符号 long 类型
signed long l1 = -100;
// 容错:分数不可能小于 0 或大于 100
if (score < 0 || score > 100) {
printf("输入的分数有误!\n");
return 0;
}
printf("l1 = %ld \n", l1); // l1 = -100
if (score >= 90) {
printf("奖励你一部华为 mate60 pro\n");
} else if (score >= 80) {
printf("奖励你一个 ipad\n");
} else if (score >= 60) {
printf("奖励你一个肉夹馍\n");
} else {
printf("你的成绩不及格,没有任何奖励!");
}
// 定义无符号 long 类型
unsigned long l2 = 100;
printf("l2 = %lu \n", l2); // l2 = 100
// 定义 long 类型,默认是有符号
long l3 = -200;
printf("l3 = %ld \n", l3); // l3 = -200
return 0;
}
```
### 3.4.2 应用示例
* 需求:判断水的温度,如果大于 95℃则打印 "开水";如果大于 70℃ 且小于等于 95℃则打印 "热水";如果大于 40℃ 且小于等于 70℃则打印 "温水";如果小于等于 40℃则打印 "凉水"。
* 示例:
* 示例:获取类型占用的内存大小(存储空间)
```c
#include <stdio.h>
int main() {
int temperature = 0;
printf("请输入水的温度:");
scanf("%d", &temperature);
size_t l1 = sizeof(unsigned long);
printf("unsigned long 的存储空间是 %zu 字节 \n", l1); // 4
if (temperature > 95) {
printf("开水 \n");
} else if (temperature > 70 && temperature <= 95) {
printf("热水 \n");
} else if (temperature > 40 && temperature <= 70) {
printf("温水 \n");
} else {
printf("凉水 \n");
}
size_t l2 = sizeof(signed long);
printf("signed long 的存储空间是 %zu 字节 \n", l2); // 4
size_t l3 = sizeof(long);
printf("long 的存储空间是 %zu 字节 \n", l3); // 4
return 0;
}
```
## 3.5 多重分支结构 switch
### 3.5.1 概述
* 示例:获取类型的取值范围
```c
#include <limits.h>
#include <stdio.h>
int main() {
printf("unsigned long 类型的范围是[0,%lu]\n", ULONG_MAX); // [0,4294967295]
printf("long 类型的范围是[%ld,%ld]\n", LONG_MIN,LONG_MAX); // [-2147483648,2147483647]
return 0;
}
```
### 1.2.5 长长整型(了解)
* 语法:
```c
switch(表达式){
case 常量值1:
语句块1;
//break;
case 常量值2:
语句块2;
//break;
...
case 常量值n:
语句块n;
//break;
[default:
语句块n+1;
]
}
unsigned long long x = 10 ; // 无符号长长整型
```
```c
long long x = -10; // 有符号长长整型
```
> [!NOTE]
>
> * ① switch 后面表达式的值必须是一个整型char、short、int、long 等)或枚举类型
> * ② case 后面的值必须是常量,不能是变量
> * ③ default 是可选的,当没有匹配的 case 的时候,就执行 default
> * ④ break 语句可以使程序跳出 switch 语句块,如果没有 break会执行下一个 case 语句块,直到遇到 break 或者执行到 switch 结尾,这个现象称为穿透
> * ① 有符号表示的是正数、负数和 0 ,即有正负号。无符号表示的是 0 和正数,即正整数,没有符号。
> * ② 在 `printf` 中`无符号长长整型unsigned long long`的`格式占位符`是 `%llu` `有符号长长整型signed long long`的`格式占位符`是 `%lld`
> * ③ 可以通过 `sizeof` 运算符获取`无符号长长整型unsigned long long` 和 `有符号长长整型signed long long` 的`存储空间(所占内存空间)`。
> * ③ 可以通过 `#include <limits.h>` 来获取 `无符号长长整型unsigned long long` 和`有符号长长整型signed long long`的`取值范围`。
* 流程图,如下所示:
![](./assets/6.png)
### 3.5.2 应用示例
* 示例:定义和打印长长整型变量
* 需求编写一个程序该程序可以接收一个字符比如a、b、c、d其中 a 表示星期一b 表示星期二…,根据用户的输入显示相应的信息,要求使用 switch 语句。
```c
#include <stdio.h>
int main() {
// 定义有符号 long long 类型
signed long long ll1 = -100;
printf("ll1 = %lld \n", ll1); // ll1 = -100
// 定义无符号 long long 类型
unsigned long long ll2 = 100;
printf("ll2 = %llu \n", ll2); // ll2 = 100
// 定义 long long 类型,默认是有符号
long long ll3 = -200;
printf("ll3 = %lld \n", ll3); // ll3 = -200
return 0;
}
```
* 示例:获取类型占用的内存大小(存储空间)
```c
#include <stdio.h>
int main() {
size_t ll1 = sizeof(unsigned long long);
printf("unsigned long long 的存储空间是 %zu 字节 \n", ll1); // 8
size_t ll2 = sizeof(signed long long);
printf("signed long long 的存储空间是 %zu 字节 \n", ll2); // 8
size_t ll3 = sizeof(long long);
printf("long long 的存储空间是 %zu 字节 \n", ll3); // 8
return 0;
}
```
* 示例:获取类型的取值范围
```c
#include <limits.h>
#include <stdio.h>
int main() {
printf("unsigned long long 类型的范围是[0,%llu]\n", ULLONG_MAX); // [0,18446744073709551615]
printf("long long 类型的范围是[%lld,%lld]\n", LLONG_MIN,LLONG_MAX); // [-9223372036854775808,9223372036854775807]
return 0;
}
```
### 1.2.6 字面量后缀
* `字面量`是`源代码`中一个`固定值`的`表示方法`,用于直接表示数据,即:
```c
int num1 = 100; // 100 就是字面量
```
```c
long num2 = 100L; // 100L 就是字面量
```
```c
long long num3 = 100LL; // 100LL 就是字面量
```
> [!NOTE]
>
> * ① 默认情况下的,整数字面量的类型是 int 类型。
> * ② 如果需要表示 `long` 类型的字面量,需要添加后缀 `l``L` ,建议 `L`
> * ③ 如果需要表示 `long long` 类型的字面量,需要添加后缀 `ll``LL`,建议 `LL`
> * ④ 如果需要表示`无符号`整数类型的字面量,需要添加 `u``U`,建议 `U`
@ -394,39 +453,53 @@ switch(表达式){
int main() {
char chs;
printf("请输入一个字符a、b、c、d");
scanf("%c", &chs);
int num = 100;
printf("num = %d\n", num); // num = 100
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;
}
long num2 = 100L;
printf("num2 = %ld\n", num2); // num2 = 100
long long num3 = 100LL;
printf("num3 = %lld\n", num3); // num3 = 100
unsigned int num4 = 100U;
printf("num4 = %u\n", num4); // num4 = 100
unsigned long num5 = 100LU;
printf("num5 = %lu\n", num5); // num5 = 100
unsigned long long num6 = 100ULL;
printf("num6 = %llu\n", num6); // num6 = 100
return 0;
}
```
### 3.5.3 应用示例
### 1.2.7 精确宽度类型
* 需求编写程序输入月份输出该月份有多少天。说明1 月、3 月、5 月、7月、8 月、10 月、12 月有 31 天4 月、6 月、9 月、11 月有 30 天2 月有 28 天或 29 天。
* 在前文,我们了解到 C 语言的整数类型short 、int、long、long long在不同计算机上占用的字节宽度可能不一样。但是有的时候我们希望整数类型的存储空间字节宽度是精确的在任意平台计算机上都能一致以提高程序的可移植性。
> [!NOTE]
>
> * Java 语言中的数据类型的存储空间(字节宽度)是一致的,这也是 Java 语言能够跨平台的原因之一(最主要的原因还是 JVM
> * 在嵌入式开发中,使用精确宽度类型可以确保代码在各个平台上的一致性。
* 在 C 语言的标准头文件 `<stdint.h>` 中定义了一些新的类型别名,如下所示:
| 类型名称 | 含义 |
| -------- | --------------- |
| int8_t | 8 位有符号整数 |
| int16_t | 16 位有符号整数 |
| int32_t | 32 位有符号整数 |
| int64_t | 64 位有符号整数 |
| uint8_t | 8 位无符号整数 |
| uint16_t | 16 位无符号整数 |
| uint32_t | 32 位无符号整数 |
| uint64_t | 64 位无符号整数 |
> [!NOTE]
>
> 上面的这些类型都是类型别名,编译器会指定它们指向的底层类型,如:在某个系统中,如果 int 类型是 32 位,那么 int32_t 就会指向 int ;如果 long 类型是 32 位,那么 int32_t 就会指向 long。
@ -434,528 +507,500 @@ int main() {
```c
#include <stdio.h>
#include <stdint.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;
}
// 变量 x32 声明为 int32_t 类型,可以保证是 32 位(4个字节)的宽度。
int32_t x32 = 45933945;
printf("x32 = %d \n", x32); // x32 = 45933945
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。
* 示例:
```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 概述
### 1.2.8 sizeof 运算符
* 语法:
```c
for(初始化条件①;循环条件表达式②;迭代语句④){
循环体语句③
}
sizeof(表达式)
```
> [!NOTE]
>
> * ① 初始化条件,用于初始化循环变量,只会执行一次,且循环开始前就执行(可以声明多个变量,但是必须是同一类型,用逗号 `,` 隔开)。
> * ② 循环条件表达式每次循环都执行,同 while 循环一样,每次先判断后执行循环体语句。
> * ③ 迭代语句每次循环都执行,在大括号中循环体语句之后执行(如果有多个变量更新,用逗号 `,` 隔开)。
> * ① sizeof 是运算符,不是内置函数。
>
> * ② 表达式可以是任何类型的数据类型、变量或常量。
> * ③ 用来获取某种数据类型、变量或常量占用的字节数量(内存中的存储单元),并且 `sizeof(...)` 的`返回值类型`是 `size_t` ;并且,如果是变量名称,可以省略 `()`;如果是数据类型,则不能省略 `()`
> * ④ 在 `printf` 中使用占位符 `%zu` 来处理 `size_t` 类型的值。
> * ⑤ 之前也提过C 语言没有一个统一的官方机构来制定或强制执行其标准而是由一个标准委员会负责制定标准。不同的编译器可以选择部分或完全遵循这些标准。因此C 语言的编译器实现可能会有所不同,这就要求程序员在编写跨平台代码时特别注意数据类型的大小和布局。
> * ⑥ 与 C 语言不同Java 和 JavaScript 等语言的标准是强制性的。在 Java 语言中,`int` 类型在所有平台上都是 4 个字节,无论是在 Linux、MacOS 还是 Windows 上。因此,这些语言不需要像 C 语言那样依赖 `sizeof` 来处理不同平台上的数据类型大小差异,因为编译器已经在底层处理了这些差异。换言之,`sizeof` 运算符在 C 语言中的重要性在于它为程序员提供了一个处理不同平台上数据类型大小差异的工具。当然,如果你在 C 语言中,使用精确宽度类型,如:`int8_t`、`int16_t`、`int32_t`、`uint8_t`、 `uint16_t`、`uint32_t` 等,也可以确保代码在各个平台上的一致性。
* 流程图,如下所示:
![](./assets/7.png)
* 示例:参数是数据类型
```c
#include <stdio.h>
#include <stddef.h>
int main() {
size_t s = sizeof(int);
printf("%zu \n", s); // 4
return 0;
}
```
* 示例:参数是变量
```c
#include <stdio.h>
#include <stddef.h>
int main() {
int num = 10;
size_t s = sizeof(num);
printf("%zu \n", s); // 4
return 0;
}
```
* 示例:参数是常量
```c
#include <stdio.h>
#include <stddef.h>
int main() {
size_t s = sizeof(10);
printf("%zu \n", s); // 4
return 0;
}
```
## 1.3 数值溢出
### 1.3.1 概述
* 在生活中,如果一个容器的容量是固定的,我们不停的向其中注入水,那么当容器中充满水之后,再继续注入,水就会从杯子中溢出来,如下所示:
![](./assets/3.jpg)
* 在程序中也是一样的各种整数类型在内存中占用的存储单元是不同的short 在内存中占用 2 个字节的存储单元int 在内存中占用 4 个字节的存储单元。这也就意味着,各种整数类型只能存储有限的数值,当数值过大或多小的时候,超出的部分就会被直接截掉,那么数值就不能被正确的存储,我们就将这种现象就称为`溢出`overflow
> [!NOTE]
>
> 执行过程是:① --> ② --> ③ --> ④ --> ② --> ③ --> ④ --> ... --> ② 。
> * 如果这个数目前是`最大值`,再进行`加法`计算,数据就会超过该类型能够表示的最大值,叫做`上溢出`(如果最大值 + 1 会“绕回”到最小值)。
> * 如果这个数目前是`最小值`,再进行`减法`计算,数据就会超过该类型能够表示的最小值, 叫做`下溢出`(如果最小值 - 1 会“绕回”到最大值)。
>
> [!IMPORTANT]
>
> * ① 在 C 语言中程序产生数值溢出的时候并不会引发错误而使程序自动停止这是因为计算机底层是采用二进制补码的运算规则进行处理的很多编程语言也是这样处理的Java 等)。
> * ② 但是这可能会导致不可预料的后果1996 年的亚利安 5 号运载火箭爆炸、2004 年的 Comair 航空公司航班停飞事故。
> * ③ 在实际开发中,编程时要特别注意,以避免数值溢出问题,特别是在涉及大数或小数的运算(特指整数)。
### 1.3.2 无符号数的取值范围
### 5.2.2 应用示例
* 在 C 语言中,`无符号数`unsigned 类型)的取值范围(最大值和最小值)的计算是很容易的,即:将内存中的所有位,设置为 `0` 就是`最小值`,设置为 `1` 就是`最大值`。
* 需求:输出 5 行 `Hello World!`
> [!IMPORTANT]
>
> 在 C 语言中,无符号整数,最高位不是符号位,它是数值的一部分。
* 以 `unsigned char` 类型为例,它在内存中占用的存储单元是 1 个字节,即 8 位。如果所有位都设置为 `0` ,它的最小值就是 `0` ;如果所有位设置为 `1` ,它的最大值就是 `2⁸ - 1 = 255` ,如下所示:
![](./assets/4.svg)
* 示例:
* 那么,`unsigned char` 的最大值是如何计算出来的?最简单的方法就是这样的,如下所示:
```c
#include <stdio.h>
int main() {
for (int i = 1; i <= 5; ++i) {
printf("Hello World!\n");
}
return 0;
}
```txt
1 × 2⁰ + 1 × 2¹ + 1 × 2² + 1 × 2³ + 1 × 2⁴ + 1 × 2⁵ + 1 × 2⁶ + 1 × 2⁷
= 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128
= 255
```
### 5.2.3 应用示例
* 但是,这种计算方法虽然有效,但是非常麻烦,如果是 8 个字节的 long ,那么计算就非常麻烦了(可能要计算半天)。当然,我们也知道,这就是等比数列(高中知识),等比数列的公式,如下所示:
* 需求:求 1 ~ 100 之内所有偶数的和,以及偶数的个数。
$S_n = a_1 \times \frac{1 - r^n}{1 - r}$
* 那么,结果就是:$S_8 = 1 \times \frac{1 - 2^8}{1 - 2} = \frac{1 - 256}{-1} = 255$
* 但是,貌似还是很复杂,我们可以换个思路,就是让 `1111 1111``+1` ,然后再 `-1`,这样一增一减正好抵消掉,并且不会影响最终的结果,如下所示:
* 示例:
```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;
}
```txt
1111 1111 + 1 - 1
= 10000 0000 - 1
= 2⁹⁻¹ - 1
= 2⁸ - 1
= 255
```
### 5.2.4 应用示例
* 其对应的换算过程,如下所示:
* 需求:输出所有的水仙花数,所谓水仙花数是指一个 3 位数,其各个位上数字立方和等于其本身,例如:`153 = 1×1×1 + 3×3×3 + 5×5×5`。
![](./assets/5.svg)
> [!IMPORTANT]
>
> * ① 当内存中所有的位都是 1 的时候,这种“凑整”的技巧非常实用!!!
> * ② 按照上述的技巧,我们可以很容易得计算出:
> * `unsinged char`1 个字节) 的取值范围是:`[0, 2⁸ - 1]`。
> * `unsinged short`2 个字节)的取值范围是:`[0, 2¹⁶ - 1]`。
> * `unsinged int`4 个字节)的取值范围是:`[0, 2³² - 1]`。
> * `unsinged long`8 个字节)的取值范围是:`[0, 2⁶⁴ - 1]`。
### 1.3.3 有符号数的取值范围
* 示例:
* 在 C 语言中,`有符号数`signed 类型)在计算机底层是以`补码`的形式存储的(计算的时候,也是以补码的形式进行计算的,并且符号位参与计算);但是,在读取的时候,需要采用`逆向`的转换,即:将补码转换为原码。
```c
#include <stdio.h>
> [!IMPORTANT]
>
> 在 C 语言中,有符号整数,最高位是符号位,用于表示正负数。
int main() {
* 以 `char` 类型为例,它的取值范围,如下所示:
int count = 0;
| 补码 | 反码 | 原码 | 值 |
| ------------- | --------- | --------- | -------- |
| 1111 1111 | 1111 1110 | 1000 0001 | -1 |
| 1111 1110 | 1111 1101 | 1000 0010 | -2 |
| 1111 1101 | 1111 1100 | 1000 0011 | -3 |
| ... | ... | ... | ... |
| 1000 0011 | 1000 0010 | 1111 1101 | -125 |
| 1000 0010 | 1000 0001 | 1111 1110 | -126 |
| 1000 0001 | 1000 0000 | 1111 1111 | -127 |
| **1000 0000** | **---** | **---** | **-128** |
| 0111 1111 | 0111 1111 | 0111 1111 | 127 |
| 0111 1110 | 0111 1110 | 0111 1110 | 126 |
| 0111 1101 | 0111 1101 | 0111 1101 | 125 |
| ... | ... | ... | ... |
| 0000 0010 | 0000 0010 | 0000 0010 | 2 |
| 0000 0001 | 0000 0001 | 0000 0001 | 1 |
| 0000 0000 | 0000 0000 | 0000 0000 | 0 |
for (int i = 100; i <= 999; i++) {
// 获取三位数
int ge = i % 10;
int shi = i / 10 % 10;
int bai = i / 100;
* 从上面的列表中,我们可以得知,`char` 类型的取值范围是:`[-2⁸, 2⁸ - 1]`,即:`[-128, 127]`。
* 对于 `-128` 而言,它的补码是 `1000 0000`,是无法按照传统的补码表示法来计算原码的,因为在补码转换到反码的时候需要 `-1` ,而 `1000 0000 - 1`需要向高位借 `1` ,而最高位是符号位是不能借的,这就非常矛盾。
// 判定是否为水仙花数
if (ge * ge * ge + shi * shi * shi + bai * bai * bai == i) {
printf("水仙花数:%d\n", i);
count++;
}
}
> [!IMPORTANT]
>
> 计算机规定,`1000 0000` 这个特殊的补码就表示 `-128`
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 。
* 但是,为什么偏偏是 `-128` ,而不是其它数字?是因为 `-128` 使得 `char` 类型的取值范围保持连贯,中间没有“空隙”。如果我们按照传统的方式来计算 `-128` 的补码,如下所示:
* ① 原码:在原码表示法中,-128 的数据位是 `1000 0000`,但是 char 的数据位只有 `7` 位,那么最高位 `1` 就变为了符号位,剩下的数据位就是 `000 0000`;所以,`-128` 的原码就是 `1000 0000`
* ② 反码:对数据位取反,-128 的反码就是:`1111 1111` 。
* ③ 补码:在反码的基础上 `+1`,得到 `1000 0000`,是因为符号位被覆盖了,补码最终依然是 `1000 0000`
> [!NOTE]
>
> * 如果数 a 能被数 b 整除,且结果是整数,那么 a 就叫做 b 的倍数b 就叫做 a 的约数(因数)。
> * 如果一个整数同时是几个整数的约数,则称该整数为这些整数的公约数;其中,数值最大的称为最大公约数。
> * 如果一个整数同时为两个或多个整数的倍数的数,则称该整数为这些整数的公倍数;其中,数值最小的称为最小公倍数。
> `-128` 从原码转换到补码的过程中,符号位被 `1` 覆盖了两次,而负数的符号位本来就是 `1`,被 `1` 覆盖多少次也不会影响到数字的符号。
* 虽然从 `1000 0000` 这个补码推算不出 `-128`,但是从 `-128` 却能推算出 `1000 0000` 这个补码,即:有符号数在存储之前先要转换为补码。
> [!IMPORTANT]
>
> * ① 通过这种方式,`-128` 就成为了补码的最小值 `1000 0000`,而这个值不会与其他任何正数或负数的补码冲突。
> * 如果采用`原码`存储,那么将会出现 `+0``-0` 的情况,即:`0000 0000`、`1000 0000`,这样在取值范围内,就存在两个相同的值,多此一举。
> * 如果采用`原码`存储,最大值不变是 `127` ,但是最小值只能存储到 `-127` ,不能存储到 `-128`,因为 `-128` 的原码是 `1000 0000`,和 `-0` 的原码冲突。
> * ② 这就是补码系统的强大之处,它能让整数的范围连贯,并且实现了加法和减法的统一处理。
> * ③ 按照上述的方法,我们可以很容易得计算出:
> * `char`1 个字节) 的取值范围是:`[-2⁸, 2⁸ - 1]`。
> * `short`2 个字节)的取值范围是:`[-2¹⁶, 2¹⁶ - 1]`。
> * `int`4 个字节)的取值范围是:`[-2³², 2³² - 1]`。
> * `long`8 个字节)的取值范围是:`[-2⁶⁴, 2⁶⁴ - 1]`。
### 1.3.4 数值溢出
* 对于`无符号`的数值溢出:
* 当数据到达最大值的时候,再 `+1` 就会回到无符号数的最小值。
* 当数据达到最小值的时候,再 `-1` 就会回到无符号数的最大值。
> [!IMPORTANT]
>
> * ① 对于无符号整数的运算,如:加、减、乘、除、取余等,其最小值是 0 ,最大值是 `2^n - 1` 。如果某个计算结果超出了这个范围,计算机会自动将结果对 `2^N` 取余(模),从而丢失高位,只保留低位。
> * ② 以 `8` 位无符号整数而言,最大值是 `255`1111 1111那么 `255 + 1` 的结果就是 `(2^8 -1 + 1) % 2^8 = 0`,商是 `256`
> * ③ 以 `8` 位无符号整数而言,最小值是 `0`0000 0000那么 `0 - 1` 的结果就是 `(0 - 1) % 2^8 = 255`,商是 `-1`
* 那么,`无符号`的`上溢出`,原理就是这样的:
![](./assets/6.svg)
* 那么,`无符号`的`下溢出`,原理就是这样的:
![](./assets/7.svg)
* 对于`有符号`的数值溢出:
* 当数据到达最大值的时候,再 `+1` 就会回到有符号数的最小值。
* 当数据达到最小值的时候,再 `-1` 就会回到有符号数的最大值。
* 那么,`有符号`的`上溢出`,原理就是这样的:
![](./assets/8.svg)
* 那么,`有符号`的`下溢出`,原理就是这样的:
![](./assets/9.svg)
* 示例:
* 示例:无符号的上溢出和下溢出
```c
#include <limits.h>
#include <stdio.h>
int main() {
int m = 12, n = 20;
unsigned short s1 = USHRT_MAX + 1;
printf("无符号的上溢出 = %hu \n", s1); // 0
// 取出两个数中的较小值
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;
}
}
unsigned short s2 = 0 - 1;
printf("无符号的下溢出 = %hu \n", s2); // 65535
return 0;
}
```
## 5.3 while 循环
### 5.3.1 概述
* 语法:
* 示例:有符号的上溢出和下溢出
```c
初始化条件①;
while (循环条件语句②) {
循环体语句③;
迭代语句④;
#include <limits.h>
#include <stdio.h>
int main() {
short s1 = SHRT_MAX + 1;
printf("有符号的上溢出 = %hd \n", s1); // -32768
short s2 = SHRT_MIN - 1;
printf("有符号的下溢出 = %hd \n", s2); // 32767
return 0;
}
```
## 1.4 浮点类型
### 1.4.1 概述
* 在生活中,我们除了使用`整数`18、25 之外,还会使用到`小数`3.1415926、6.18 等,`小数`在计算机中也被称为`浮点数`(和底层存储有关)。
* `整数`在计算机底层的存储被称为`定点存储`,如下所示:
![](./assets/10.svg)
* `小数`在计算机底层的存储被称为`浮点存储`,如下所示:
![](./assets/11.svg)
> [!NOTE]
>
> * ① `while(循环条件部分)` 中循环条件为`非0`值,表示 `true`、`真`;为`0`值,表示 `false`、`假`。
> * ② 当循环条件表达式成立,就执行循环体语句,直到条件不成立停止循环。
> * ③ 为避免死循环,循环条件表达式不能永远成立,且随着循环次数增加,应该越来越趋向于不成立。
> * ④ for 循环和 while 循环`可以相互转换`,二者没有性能上的差别。
> * ⑤ for 循环与 while 循环的区别:`初始化条件部分的作用域不同`。
> * ① 计算机底层就是采取类似科学计数法的形式来存储小数的而科学计数法的表现就是这样的3.12 * 10^-2 其中10 是基数,-2 是指数,而 3.12 是尾数。
> * ② 因为尾数区的内存空间的宽度不同,导致了小数的精度也不相同,所以小数在计算机中也称为浮点数。
* 流程图,如下所示:
* 在 C 语言中,变量的浮点类型,如下所示:
![](./assets/8.png)
| 类型 | 存储大小 | 值的范围 | 有效小数位数 |
| ----------------------- | -------- | --------------------- | ------------ |
| float单精度 | 4 字节 | 1.2E-38 ~ 3.4E+38 | 6 ~ 9 |
| double双精度 | 8 字节 | 2.3E-308 ~ 1.7E+308 | 15 ~ 18 |
| long double长双精度 | 16 字节 | 3.4E-4932 ~ 1.2E+4932 | 18 或更多 |
> [!NOTE]
>
> 执行过程是:① --> ② --> ③ --> ④ --> ② --> ③ --> ④ --> ... --> ② 。
> * ① 各类型的存储大小和精度受到操作系统、编译器、硬件平台的影响。
> * ② 浮点型数据有两种表现形式:
> * 十进制数形式3.12、512.0f、0.512.512,可以省略 0
> * 科学计数法形式5.12e2e 表示基数 10、5.12E-2E 表示基数 10
> * ③ 在实际开发中,对于浮点类型,建议使用 `double` 类型;如果范围不够,就使用 `long double` 类型。
### 5.3.2 应用示例
### 1.4.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 。
* 对于 `float` 类型的格式占位符,是 `%f` ,默认会保留 `6` 位小数,不足 `6` 位以 `0` 补充;可以指定小数位,如:`%.2f` 表示保留 `2` 位小数。
* 对于 `double` 类型的格式占位符,是 `%lf` ,默认会保留 `6` 位小数,不足 `6` 位以 `0` 补充;可以指定小数位,如:`%.2lf` 表示保留 `2` 位小数。
* 对于 `long double` 类型的格式占位符,是 `%Lf` ,默认会保留 `6` 位小数,不足 `6` 位以 `0` 补充;可以指定小数位,如:`%.2Lf` 表示保留 `2` 位小数。
> [!NOTE]
>
> 思路:
> * ① 如果想输出`科学计数法`形式的 `float` 类型的浮点数,则使用 `%e`
> * ② 如果想输出`科学计数法`形式的 `double` 类型的浮点数,则使用 `%le`
> * ③ 如果想输出`科学计数法`形式的 `long double` 类型的浮点数,则使用 `%Le`
> [!NOTE]
>
> * ① 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 。
> * ① 浮点数还有一种更加智能的输出方式,就是使用 `%g``g` 是 `general format` 的缩写,即:通用格式),`%g` 会根据数值的大小自动判断,选择使用普通的浮点数格式(`%f`)进行输出,还是使用科学计数法(`%e`)进行输出,即:`float` 类型的两种输出形式。
> * ② 同理,`%lg` 会根据数值的大小自动判断,选择使用普通的浮点数格式(`%lf`)进行输出,还是使用科学计数法(`%le`)进行输出,即:`double` 类型的两种输出形式。
> * ③ 同理,`%Lg` 会根据数值的大小自动判断,选择使用普通的浮点数格式(`%Lf`)进行输出,还是使用科学计数法(`%Le`)进行输出,即:`long double` 类型的两种输出形式。
* 示例:
```c
#include <stdio.h>
int main() {
float f1 = 10.0;
printf("f1 = %f \n", f1); // f1 = 10.000000
printf("f1 = %.2f \n", f1); // f1 = 10.00
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
double d1 = 13.14159265354;
printf("d1 = %lf \n", d1); // d1 = 13.141593
printf("d1 = %.2lf \n", d1); // d1 = 13.14
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
long double d1 = 13.14159265354;
printf("d1 = %LF \n", d1); // d1 = 13.141593
printf("d1 = %.2LF \n", d1); // d1 = 13.14
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
float f1 = 3.1415926;
double d2 = 3.14e2;
printf("f1 = %.2f \n", f1); // f1 = 3.14
printf("f1 = %.2e \n", f1); // f1 = 3.14e+00
printf("d2 = %.2lf \n", d2); // d2 = 314.00
printf("d2 = %.2e \n", d2); // d2 = 3.14e+02
return 0;
}
```
### 1.4.3 字面量后缀
* 浮点数字面量默认是 double 类型。
* 如果需要表示 `float` 类型的字面量,需要后面添加后缀 `f``F`,建议 `F`
* 如果需要表示 `long double` 类型的字面量,需要后面添加后缀 `l``L`,建议 `L`
* 示例:
```c
#include <stdio.h>
int main() {
float f1 = 3.1415926f;
double d2 = 3.1415926;
long double d3 = 3.1415926L;
printf("f1 = %.2f \n", f1); // f1 = 3.14
printf("d2 = %.3lf \n", d2); // d2 = 3.142
printf("d3 = %.4Lf \n", d3); // d3 = 3.1416
return 0;
}
```
### 1.4.4 类型占用的内存大小(存储空间)
* 可以通过 `sizeof` 运算符来获取 float、double 以及 long double 类型占用的内存大小(存储空间)。
* 示例:
```c
#include <stdio.h>
int main() {
printf("float 的存储空间是 %zu 字节 \n", sizeof(float)); // 4
printf("double 的存储空间是 %zu 字节 \n", sizeof(double)); // 8
printf("long double 的存储空间是 %zu 字节 \n", sizeof(long double)); // 16
return 0;
}
```
### 1.4.5 类型的取值范围
* 可以通过 `#include <float.h>` 来获取类型的取值范围。
* 示例:
```c
#include <float.h>
#include <stdio.h>
int main() {
printf("float 的取值范围是:[%.38f, %f] \n", FLT_MIN, FLT_MAX);
printf("double 的取值范围是:[%lf, %lf] \n", DBL_MIN, DBL_MAX);
printf("double 的取值范围是:[%Lf, %Lf] \n", LDBL_MIN, LDBL_MAX);
return 0;
}
```
### 1.4.6 整数和浮点数的相互赋值
* 在 C 语言中,整数和浮点数是可以相互赋值的,即:
* 将一个整数赋值给小数类型,只需要在小数点后面加 0 就可以了。
* 将一个浮点数赋值给整数类型,就会将小数部分丢掉,只会取整数部分,会改变数字本身的值。
> [!WARNING]
>
> * ① 在 C 语言中浮点数赋值给整数类型会直接截断小数点后面的数编译器一般只会给出警告让我们注意一下C 语言在检查类型匹配方面不太严格,最好不要养成这样的习惯)。
> * ② 但是,在 Java 等编程语言中,这样的写法是不可以的,会在编译阶段直接报错。
@ -969,31 +1014,48 @@ int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
int n = 0;
printf("请输入一个整数:");
scanf("%d", &n);
while (n > 1 && n % 2 == 0) {
n /= 2;
}
if (n == 1) {
printf("yes");
} else {
printf("no");
}
float a = 123; // 整数赋值给浮点类型,只需要在小数点,后面加 0 即可
printf("a=%f \n", a); // a=123.000000
int b = 123.00; // 浮点赋值给整数类型,会直接截断小数点后面的数
printf("b=%d \n", b); // b=123
return 0;
}
```
### 5.3.6 应用示例
* 需求整数反转123 --> 321 。
## 1.5 字符类型
### 1.5.1 概述
* 在生活中,我们会经常说:今天天气真 `好`,我的性别是 `女`,我今年 `10` 岁等。像这类数据,在 C 语言中就可以用`字符类型`char来表示。`字符类型`表示`单`个字符,使用单引号(`''`)括起来,如:`'1'`、`'A'`、`'&'`。
* 但是,在生活中,也许会听到:`你是好人,只是现阶段,我想学习`、`好的啊,我们在一起`等。像这类数据,在 C 语言中就可以用`字符串`String来表示。`字符串类型`表示`多`个字符的集合,使用双引号(`""`)括起来,如:`"1"`、`"A"`、`"&"`、`"我们"`。
> [!NOTE]
>
> 思路:从右边开始,依次获取每一位数字,再拼接起来。
> * ① C 语言的出现在 1972 年,由美国人丹尼斯·里奇设计出来;那个时候,只需要 1 个字节的内存空间就可以完美的表示拉丁体系英文文字a-z、A-Z、0-9 以及一些特殊符号所以C 语言中不支持多个字节的字符,如:中文、日文等。
> * ② 像拉丁体系英文文字a-z、A-Z、0-9 以及一些特殊符号,只需要单个字节的内存存储空间就能存储的,我们就称为窄类型;而像中文、日文等单个字节的内存空间存储不了的,我们就称为宽类型。
> * ③ 在 C 语言中是没有字符串类型是使用字符数组char 数组)来模拟字符串的。字符串中的字符在内存中按照次序、紧挨着排列,整个字符串占用一块连续的内存。
> * ④ 在 C 语言中如果想要输出中文、日文等多字节字符就需要使用字符数组char 数组)。
> * ⑤ 在 C++、Java 等高级编程语言中,已经提供了 String (字符串)类型,原生支持 Unicode可以方便地处理多语言和特殊字符。
* 在 C 语言中,可以使用`转义字符 \`来表示特殊含义的字符。
| **转义字符** | **说明** |
| ------------ | -------- |
| `\b` | 退格 |
| `\n` | 换行符 |
| `\r` | 回车符 |
| `\t` | 制表符 |
| `\"` | 双引号 |
| `\'` | 单引号 |
| `\\` | 反斜杠 |
| ... | |
### 1.5.2 格式占位符
* 在 C 语言中,使用 `%c` 来表示 char 类型。
@ -1004,196 +1066,23 @@ int main() {
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
char c = '&';
int num = 0;
int original = 0;
int rev = 0;
printf("请输入一个整数:");
scanf("%d", &num);
original = num;
printf("c = %c \n", c); // c = &
// 从右边开始,依次获取每个数字,然后拼接到 rev 中
/**
* 第 1 次123 % 10 = 3rev = 0 * 10 + 3 = 3
* 第 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;
}
char c2 = 'a';
printf("c2 = %c \n", c2); // c2 = a
printf("%d 的反转是 %d\n", original, rev);
char c3 = 'A';
printf("c3 = %c \n", c3); // c3 = A
return 0;
}
```
## 5.4 do-while 循环
### 1.5.3 类型占用的内存大小(存储空间)
### 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 个 `*`
* 可以通过 `sizeof` 运算符来获取 char 类型占用的内存大小(存储空间)。
@ -1204,293 +1093,85 @@ for(初始化语句①; 循环条件语句②; 迭代语句⑦) {
int main() {
for (int i = 1; i <= 5; ++i) {
for (int j = 1; j < 6; ++j) {
printf("* ");
}
printf("\n");
}
printf("char 的存储空间是 %d 字节\n", sizeof(char)); // 1
printf("unsigned char 的存储空间是 %d 字节\n", sizeof(unsigned char)); // 1
return 0;
}
```
### 5.5.3 应用示例
### 1.5.4 类型的取值范围
* 需求:打印 5 行直角三角形
* 可以通过 `#include <limits.h>` 来获取类型的取值范围
* 示例:
```c
#include <limits.h>
#include <stdio.h>
int main() {
for (int i = 1; i <= 5; ++i) {
for (int j = 1; j <= i; ++j) {
printf("* ");
}
printf("\n");
}
printf("char 范围是[%d,%d] \n", CHAR_MIN,CHAR_MAX); // [-128,127]
printf("unsigned char 范围是[0,%d]\n", UCHAR_MAX); // [0,255]
return 0;
}
```
### 5.5.4 应用示例
### 1.5.5 字符类型的本质
* 需求:打印 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;
}
```
* 示例:编写程序,要求输入一个数字,判断该数字是否是质数
```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 一旦执行,就结束(或跳出)当次循环结构;并且,此关键字的后面,不能声明其它语句。
* 流程图,如下所示:
* 在 C 语言中char 本质上就是一个整数,是 ASCII 码中对应的数字,占用的内存大小是 1 个字节(存储空间),所以 char 类型也可以进行数学运算。
![](./assets/12.png)
* char 类型同样分为 signed char无符号和 unsigned char有符号其中 signed char 取值范围 -128 ~ 127unsigned char 取值范围 0 ~ 255默认是否带符号取决于当前运行环境。
* `字符类型的数据`在计算机中`存储`和`读取`的过程,如下所示:
![](./assets/13.png)
* 示例:打印 0 ~ 10 ,如果遇到 `3` ,就继续下一次打印
* 示例:
```c
#include <limits.h>
#include <stdio.h>
int main() {
// char 类型字面量需要使用单引号包裹
char a1 = 'A';
char a2 = '9';
char a3 = '\t';
printf("c1=%c, c3=%c, c2=%c \n", a1, a3, a2);
for (int i = 0; i < 10; ++i) {
if (i == 3) {
continue;
}
printf("%d \n", i);
}
// char 类型本质上整数可以进行运算
char b1 = 'b';
char b2 = 101;
printf("%c->%d \n", b1, b1);
printf("%c->%d \n", b2, b2);
printf("%c+%c=%d \n", b1, b2, b1 + b2);
printf("程序结束!\n");
// char 类型取值范围
unsigned char c1 = 200; // 无符号 char 取值范围 0 ~255
signed char c2 = 200; // 有符号 char 取值范围 -128~127c2会超出范围
char c3 = 200; // 当前系统char 默认是 signed char
printf("c1=%d, c2=%d, c3=%d", c1, c2, c3);
return 0;
}
```
### 1.5.6 输出字符方式二(了解)
* 示例:输出 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 语句的时候,这个方法将被结束。
* 在 C 语言中,除了可以使用 `printf()` 函数输出字符之外,还可以使用 `putchar()`函数输出字符。
> [!NOTE]
>
> 和 break 和 continue 不同的是return 直接结束整个方法,不管这个 return 处于多少层循环之内。
> * ① `putchar()` 函数每次只能输出一个字符,如果需要输出多个字符需要调用多次;而 `printf()` 函数一次可以输出多个字符,并且 `char` 类型对应的格式占位符是 `%c`
> * ② 在实际开发中,使用 `printf()` 函数居多。
@ -1501,16 +1182,1650 @@ int main() {
int main() {
for (int i = 1; i <= 100; i++) {
if (i % 7 == 0 || i % 10 == 7 || i / 10 == 7) {
return 0; // 结束整个函数或方法
}
printf("%d ", i);
}
char a = '1';
char b = '2';
char c = '&';
printf("程序结束!\n");
/* 12& */
putchar(a);
putchar(b);
putchar(c);
return 0;
}
```
### 1.5.7 初谈字符串(了解)
* 在 C 语言中没有专门的字符串类型,是使用`字符数组`来模拟字符串的,即:可以使用字符数组来存储字符串。
> [!NOTE]
>
> * ① 在 C 语言中,`数组`和`指针`通常会一起出现,所以当`字符数组`可以保存字符串,也就意味着可以使用`指针`来间接存储字符串。
> * ② 在 C 语言中,可以使用 `puts()` 函数输出字符串,每调用一次 `puts()` 函数,除了输出字符串之外,还会在字符串后面加上换行,即:`\n` 。
> * ③ 在 C 语言中,可以使用 `printf()` 函数输出字符串,并且字符串对应的格式占位符是 `%s`。和 `puts()` 函数不同的是,`printf()` 函数不会在字符串后面加上换行,即:`\n`。
> * ④ 在实际开发中,使用 `printf()` 函数居多。
* 示例:
```c
#include <stdio.h>
int main() {
// 存储字符串
char str[] = "我";
char *str2 = "爱你";
puts(str); // 我
puts(str2); // 爱你
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
// 存储字符串
char str[] = "你";
char *str2 = "是好人";
printf("%s\n", str); // 你
printf("%s\n", str2); // 是好人
return 0;
}
```
## 1.6 布尔类型
### 1.6.1 概述
* 布尔值用于表示 true、false两种状态通常用于逻辑运算和条件判断。
### 1.6.2 早期的布尔类型
* 在 C 语言标准C89并没有为布尔值单独设置一个数据类型所以在判断真、假的时候使用 `0` 表示 `false`(假),`非 0` 表示 `true`(真)。
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
// 使用整型来表示真和假两种状态
int handsome = 0;
printf("帅不帅[0 丑1 帅] ");
scanf("%d", &handsome);
if (handsome) {
printf("你真的很帅!!!");
} else {
printf("你真的很丑!!!");
}
return 0;
}
```
### 1.6.3 宏定义的布尔类型
* 判断真假的时候,以 `0``false`(假)、`1` 为 `true`(真),并不直观;所以,我们可以借助 C 语言的宏定义。
* 示例:
```c
#include <stdio.h>
// 宏定义
#define BOOL int
#define TRUE 1
#define FALSE 0
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
BOOL handsome = 0;
printf("帅不帅[FALSE 丑TRUE 帅] ");
scanf("%d", &handsome);
if (handsome) {
printf("你真的很帅!!!");
} else {
printf("你真的很丑!!!");
}
return 0;
}
```
### 1.6.4 C99 标准中的布尔类型
* 在 C99 中提供了 `_Bool` 关键字,用于表示布尔类型;其实,`_Bool`类型的值是整数类型的别名,和一般整型不同的是,`_Bool`类型的值只能赋值为 `0``1` 0 表示假、1 表示真),其它`非 0` 的值都会被存储为 `1`
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
int temp; // 使用 int 类型的变量临时存储输入
_Bool handsome = 0;
printf("帅不帅[0 丑1 帅] ");
scanf("%d", &temp);
// 将输入值转换为 _Bool 类型
handsome = (temp != 0);
if (handsome) {
printf("你真的很帅!!!");
} else {
printf("你真的很丑!!!");
}
return 0;
}
```
### 1.6.5 C99 标准头文件中的布尔类型(推荐)
* 在 C99 中提供了一个头文件 `<stdbool.h>`,定义了 `bool` 代表 `_Bool``false` 代表 `0` `true` 代表 `1`
> [!IMPORTANT]
>
> * ① 在 C++、Java 等高级编程语言中是有 `boolean` 类型的关键字的。
> * ② 在 C23 标准中,将一些 C11 存在的关键字改为小写并去掉前置下划线,如:`_Bool` 改为 `bool`,以前的写法主要是为了避免与旧的代码发生冲突。
> * ③ 在 C23 标准中,加入了 `true``false` 关键字。
* 示例:
```c
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
char input[10];
bool handsome = false;
printf("帅不帅[false 丑true 帅] ");
scanf("%s", input); // 使用 %s 读取字符串
// 将输入字符串转换为布尔值
if (strcmp(input, "true") == 0) {
handsome = true;
} else if (strcmp(input, "false") == 0) {
handsome = false;
} else {
printf("无效输入!\n");
return 1;
}
if (handsome) {
printf("你真的很帅!!!");
} else {
printf("你真的很丑!!!");
}
return 0;
}
```
## 1.7 数据类型转换
### 1.7.1 概述
* 在 C 语言编程中,经常需要对不同类型的数据进行运算,运算前需要先转换为同一类型,再运算。为了解决数据类型不一致的问题,需要对数据的类型进行转换。
### 1.7.2 自动类型转换(隐式转换)
#### 1.7.2.1 运算过程中的自动类型转换
* 不同类型的数据进行混合运算的时候,会发生数据类型转换,`窄类型会自动转换为宽类型`,这样就不会造成精度损失。
![](./assets/14.png)
* 转换规则:
* ① 不同类型的整数进行运算的时候,窄类型整数会自动转换为宽类型整数。
* ② 不同类型的浮点数进行运算的时候,精度小的类型会自动转换为精度大的类型。
* ③ 整数和浮点数进行运算的时候,整数会自动转换为浮点数。
* 转换方向:
![](./assets/15.png)
> [!WARNING]
>
> 最好避免无符号整数与有符号整数的混合运算,因为这时 C 语言会自动将 signed int 转为 unsigned int ,可能不会得到预期的结果。
* 示例:
```c
#include <stdio.h>
/**
* 不同的整数类型混合运算时,宽度较小的类型会提升为宽度较大的类型。
* 比如 short 转为 int int 转为 long 等。
*/
int main() {
short s1 = 10;
int i = 20;
// s1 是 short 类型i 是 int 类型。
// 当 s1 和 i 运算的时候,会自动转为 int 类型后,然后再计算。
int result = s1 + i;
printf("result = %d \n", result);
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
int n2 = -100;
unsigned int n3 = 20;
// n2 是有符号n3 是无符号。
// 当 n2 和 n3 运算的时候,会自动转为无符号类型后,然后再计算。
int result = n2 + n3;
printf("result = %d \n", result);
return 0;
}
```
* 示例:
```c
#include <stdio.h>
/**
* 不同的浮点数类型混合运算时,宽度较小的类型转为宽度较大的类型。
* 比如 float 转为 double double 转为 long double 。
*/
int main() {
float f1 = 1.25f;
double d2 = 4.58667435;
// f1 是 float 类型d2 是 double 类型。
// 当 f1 和 d2 运算的时候,会自动转为 double 类型后,然后再计算。
double result = f1 + d2;
printf("result = %.8lf \n", result);
return 0;
}
```
* 示例:
```c
#include <stdio.h>
/**
* 整型与浮点型运算,整型转为浮点型
*/
int main() {
int n4 = 10;
double d3 = 1.67;
// n4 是 int 类型d3 是 double 类型。
// 当 n4 和 d3 运算的时候,会自动转为 double 类型后,然后再计算。
double result = n4 + d3;
printf("%.2lf", result);
return 0;
}
```
#### 1.7.2.2 赋值时的自动类型转换
* 在赋值运算中,赋值号两边量的数据类型不同时,等号右边的类型将转换为左边的类型。
* 如果窄类型赋值给宽类型,不会造成精度损失;如果宽类型赋值给窄类型,会造成精度损失。
![](./assets/16.png)
> [!WARNING]
>
> C 语言在检查类型匹配方面不太严格,最好不要养成这样的习惯。
* 示例:
```c
#include <stdio.h>
int main() {
// 赋值:窄类型赋值给宽类型
int a1 = 10;
double a2 = a1;
printf("a2: %.2f\n", a2); // a2: 10.00
// 转换:将宽类型转换为窄类型
double b1 = 10.5;
int b2 = b1;
printf("b2: %d\n", b2); // b2: 10
return 0;
}
```
### 1.7.3 强制类型转换
* 隐式类型转换中的宽类型赋值给窄类型,编译器是会产生警告的,提示程序存在潜在的隐患,如果非常明确地希望转换数据类型,就需要用到强制(或显式)类型转换。
* 语法:
```c
数据类型 变量名 = (类型名)变量、常量或表达式;
```
> [!WARNING]
>
> 强制类型转换可能会导致精度损失!!!
* 示例:
```c
#include <stdio.h>
int main(){
double d1 = 1.934;
double d2 = 4.2;
int num1 = (int)d1 + (int)d2; // d1 转为 1d2 转为 4结果是 5
int num2 = (int)(d1 + d2); // d1+d2 = 6.1346.134 转为 6
int num3 = (int)(3.5 * 10 + 6 * 1.5); // 35.0 + 9.0 = 44.0 -> int = 44
printf("num1=%d \n", num1);
printf("num2=%d \n", num2);
printf("num3=%d \n", num3);
return 0;
}
```
### 1.7.4 数据类型转换只是临时性的
* 无论是自动类型转换还是强制类型转换,都是为了本次运算而进行的临时性转换,其转换的结果只会保存在临时的内存空间,并不会改变数据原先的类型或值,如下所示:
```c {8}
#include <stdio.h>
int main() {
double total = 100.12; // 总价
int count = 2; // 总数
double price = 0.0; // 单价
int totalInt = (int)total; // 强制类型转换
price = total / count; // 计算单价
printf("total = %.2lf\n", total); // total = 100.12
printf("totalInt = %d\n", totalInt); // totalInt = 100
printf("price = %.2lf\n", price); // price = 50.06
return 0;
}
```
* 虽然 `total` 变量,通过强制类型转换变为了 `int` 类型,才可以赋值给 `totalInt`变量;但是,这种转换并没有影响 `total` 变量本身的`类型`和`值`。
> [!NOTE]
>
> * ① 如果 `total` 变量的`值`或`类型`变化了,那么 `total` 的显示结果,就应该是 `100.00` ,而不是 `100.12`
> * ② 那么,`price` 的结果,显而易见就应该是 `50.00` ,而不是 `50.06` 了。
### 1.7.5 自动类型转换 VS 强制类型转换
* 在 C 语言中,有些数据类型即可以自动类型转换,也可以强制类型转换,如:`int --> double`、`double --> int` 等。但是,有些数据类型只能强制类型转换,不能自动类型转换,如:`void* --> int*` 。
* 可以自动类型转换的类型一定可以强制类型转换;但是,可以强制类型转换的类型却不一定能够自动类型转换。
> [!NOTE]
>
> * ① 目前学习到的数据类型,既可以自动类型转换,也可以强制类型转换。
> * ② 后面,如果学到指针,就会发生指针有的时候,只能强制类型转换却不能自动类型转换;需要说明的是,并非所有的指针都可以强制类型转换,是有条件的,后文讲解。
* 可以自动类型转换的类型,在发生类型转换的时候,一般风险较低,不会给程序带来严重的后果,如:`int --> double` 就没什么毛病,而 `double --> int` 无非丢失精度而已。但是 ,只能强制类型转换的类型,在发生类型转换的时候,通常风险较高,如:`char* --> int*` 就非常奇怪,会导致取得的值也很奇怪,进而导致程序崩溃。
> [!IMPORTANT]
>
> * ① 在实际开发中,如果使用 C 语言进行开发,在进行强制类型转换的时候,需要小心谨慎,防止出现一些奇怪的问题,进而导致程序崩溃!!!
> * ② 现代化的高级编程语言Java 等,直接屏蔽了指针。所以,在使用这些编程语言的时候,无需担心进行强制类型转换时,会出现一些奇怪的问题,进而导致程序崩溃!!!
## 1.8 再谈数据类型
* 通过之前的知识我们知道CPU 是直接和内存打交道的CPU 在处理数据的时候会将数据临时存放到内存中。内存那么大CPU 是怎么找到对应的数据的?
* 首先CPU 会将内存按照字节1 Bytes = 8 bit我们也称为存储单元进行划分如下所示
> [!NOTE]
>
> * ① 操作系统其实并不会直接操作实际的内存而是会通过内存管理单元MMU来操作内存并通过虚拟地址映射Virtual Address Mapping将程序使用的虚拟地址转换为物理地址。虚拟地址映射可以实现内存保护、内存共享和虚拟内存等功能使得程序能够使用比实际物理内存更大的内存空间同时确保程序间不会相互干扰。
> * ② 为了方便初学者学习,后文一律会描述 CPU 直接操作内存(这种说法不严谨,但足够简单和方便理解)。
> * ③ 这些存储单元中,存储的都是 0 和 1 这样的数据,因为计算机只能识别二进制数。
![](./assets/17.svg)
* 并且,为了方便管理,每个独立的小单元格,即:存储单元,都有自己唯一的编号(内存地址),如下所示:
> [!NOTE]
>
> 之所以,要给每个存储单元加上内存地址,就是为了`加快`数据的`存取速度`,可以类比生活中的`字典`以及`快递单号`。
![](./assets/18.svg)
* 我们在定义变量的时候,是这么定义的,如下所示:
```c
int num = 10;
```
> [!NOTE]
>
> 上述的代码其实透露了三个重要的信息:
>
> * ① 数据存储在哪里。
> * ② 数据的长度是多少。
> * ③ 数据的处理方式。
* 其实,在编译器对程序进行编译的时候,是这样做的,如下所示:
> [!NOTE]
>
> * ① 编译器在编译的时候,就将变量替换为内存中存储单元的内存地址(知道了你家的门牌号),这样就可以方便的进行存取数据了(解答了上述的问题 ① )。
> * ② 变量中其实存储的是初始化值 10 在内存中存储单元的首地址,我们也知道,数据类型 int 的存储空间是 4 个字节,那么根据首地址 + 4 个字节就可以完整的将数据从内存空间中取出来或存进去(解答了上述的问题 ② )。
> * ③ 我们知道,数据在计算机底层的存储方式是不一样的,如:整数在计算机底层的存储就是计算机补码的方式,浮点数在计算机底层的存储类似于科学计数法;但是,字符类型在计算机底层的存储和整数以及浮点数完全不同,需要查码表,即:在存储的时候,需要先查询码表,转换为二进制进行存储;在读取的时候,也需要先查询码表,将二进制转换为对应的字符(解答了上述的问题 ③ )。
> [!IMPORTANT]
>
> * ① 数据类型只在定义变量的时候声明,而且必须声明;在使用变量的时候,就无需再声明,因为此时的数据类型已经确定的。
> * ② 在实际开发中,我们通常将普通变量等价于内存中某个区域的值(底层到底是怎么转换的,那是编译器帮我们完成的,我们通常无需关心,也没必要关心)。
> * ③ 某些动态的编程语言JavaScript ,在定义变量的时候,是不需要给出数据类型的,编译器会根据赋值情况自动推断出变量的数据类型,貌似很智能;但是,这无疑增加了编译器的工作,降低了程序的性能(动态一时爽,重构火葬场,说的就是动态编程语言,不适合大型项目的开发;所以,之后微软推出了 TypeScript ,就是为了给 JavaScript 增加强类型系统,以提高开发和运行效率)。
![](./assets/19.svg)
# 第二章:运算符(⭐)
## 2.1 概述
* 运算符是一种特殊的符号,用于数据的运算、赋值和比较等。
* `表达式`指的是一组运算数、运算符的组合,表达式`一定具有值`,一个变量或一个常量可以是表达式,变量、常量和运算符也可以组成表达式,如:
![](./assets/20.svg)
* `操作数`指的是`参与运算`的`值`或者`对象`,如:
![](./assets/21.svg)
* 根据`操作数`的`个数`,可以将运算符分为:
* 一元运算符(一目运算符)。
* 二元运算符(二目运算符)。
* 三元运算符(三目运算符)。
* 根据`功能`,可以将运算符分为:
* 算术运算符。
* 关系运算符(比较运算符)。
* 逻辑运算符。
* 赋值运算符。
* 逻辑运算符。
* 位运算符。
* 三元运算符。
> [!NOTE]
>
> 掌握一个运算符,需要关注以下几个方面:
>
> * ① 运算符的含义。
> * ② 运算符操作数的个数。
> * ③ 运算符所组成的表达式。
> * ④ 运算符有无副作用,即:运算后是否会修改操作数的值。
## 2.2 算术运算符
* 算术运算符是对数值类型的变量进行运算的,如下所示:
| 运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 |
| ------ | ------------ | ---------- | ------------------------ | ------ |
| `+` | 正号 | 1 | 操作数本身 | ❎ |
| `-` | 负号 | 1 | 操作数符号取反 | ❎ |
| `+` | 加号 | 2 | 两个操作数之和 | ❎ |
| `-` | 减号 | 2 | 两个操作数之差 | ❎ |
| `*` | 乘号 | 2 | 两个操作数之积 | ❎ |
| `/` | 除号 | 2 | 两个操作数之商 | ❎ |
| `%` | 取模(取余) | 2 | 两个操作数相除的余数 | ❎ |
| `++` | 自增 | 1 | 操作数自增前或自增后的值 | ✅ |
| `--` | 自减 | 1 | 操作数自减前或自减后的值 | ✅ |
> [!NOTE]
>
> 自增和自减:
>
> * ① 自增、自减运算符可以写在操作数的前面也可以写在操作数后面,不论前面还是后面,对操作数的副作用是一致的。
> * ② 自增、自减运算符在前在后,对于表达式的值是不同的。 如果运算符在前,表达式的值是操作数自增、自减之后的值;如果运算符在后,表达式的值是操作数自增、自减之前的值。
> * ③ `变量前++`:变量先自增 1 ,然后再运算;`变量后++`:变量先运算,然后再自增 1 。
> * ④ `变量前--`:变量先自减 1 ,然后再运算;`变量后--`:变量先运算,然后再自减 1 。
> * ⑤ 对于 `i++``i--` 各种编程语言的用法和支持是不同的例如C/C++、Java 等完全支持Python 压根一点都不支持Go 语言虽然支持 `i++``i--` ,却只支持这些操作符作为独立的语句,并且不能嵌入在其它的表达式中。
* 示例:正号和负号
```c
#include <stdio.h>
int main() {
int x = 12;
int x1 = -x, x2 = +x;
int y = -67;
int y1 = -y, y2 = +y;
printf("x1=%d, x2=%d \n", x1, x2); // x1=-12, x2=12
printf("y1=%d, y2=%d \n", y1, y2); // y1=67, y2=-67
return 0;
}
```
* 示例:加、减、乘、除(整数之间做除法时,结果只保留整数部分而舍弃小数部分)、取模
```c
#include <stdio.h>
int main() {
int a = 5;
int b = 2;
printf("%d + %d = %d\n", a, b, a + b); // 5 + 2 = 7
printf("%d - %d = %d\n", a, b, a - b); // 5 - 2 = 3
printf("%d × %d = %d\n", a, b, a * b); // 5 × 2 = 10
printf("%d / %d = %d\n", a, b, a / b); // 5 / 2 = 2
printf("%d %% %d = %d\n", a, b, a % b); // 5 % 2 = 1
return 0;
}
```
* 示例:取模(运算结果的符号与被模数也就是第一个操作数相同。)
```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
#include <stdio.h>
/*
随意给出一个整数,打印显示它的个位数,十位数,百位数的值。
格式如下:
数字xxx的情况如下
个位数:
十位数:
百位数:
例如:
数字153的情况如下
个位数3
十位数5
百位数1
*/
int main() {
int num = 153;
int bai = num / 100;
int shi = num % 100 / 10;
int ge = num % 10;
printf("百位为:%d \n", bai);
printf("十位为:%d \n", shi);
printf("个位为:%d \n", ge);
return 0;
}
```
## 2.3 关系运算符(比较运算符)
* 常见的关系运算符,如下所示:
| 运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 |
| ------ | -------- | ---------- | ---------------- | ------ |
| `==` | 相等 | 2 | 0 或 1 | ❎ |
| `!=` | 不相等 | 2 | 0 或 1 | ❎ |
| `<` | 小于 | 2 | 0 或 1 | ❎ |
| `>` | 大于 | 2 | 0 或 1 | ❎ |
| `<=` | 小于等于 | 2 | 0 或 1 | ❎ |
| `>=` | 大于等于 | 2 | 0 或 1 | ❎ |
> [!NOTE]
>
> * ① C 语言中,没有严格意义上的布尔类型,可以使用 0 或 1表示布尔类型的值。
> * ② 不要将 `==` 写成 `=``==` 是比较运算符,而 `=` 是赋值运算符。
> * ③ `>=``<=`含义是只需要满足 `大于或等于`、`小于或等于`其中一个条件,结果就返回真。
* 示例:
```c
#include <stdio.h>
int main() {
int a = 8;
int b = 7;
printf("a > b 的结果是:%d \n", a > b); // a > b 的结果是1
printf("a >= b 的结果是:%d \n", a >= b); // a >= b 的结果是1
printf("a < b 的结果是%d \n", a < b); // a < b 的结果是0
printf("a <= b 的结果是:%d \n", a <= b); // a <= b 的结果是0
printf("a == b 的结果是:%d \n", a == b); // a == b 的结果是0
printf("a != b 的结果是:%d \n", a != b); // a != b 的结果是1
return 0;
}
```
## 2.4 逻辑运算符
* 常见的逻辑运算符,如下所示:
| 运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 |
| ------ | ------ | ---------- | ---------------- | ------ |
| `&&` | 逻辑与 | 2 | 0 或 1 | ❎ |
| `\|\|` | 逻辑或 | 2 | 0 或 1 | ❎ |
| `!` | 逻辑非 | 2 | 0 或 1 | ❎ |
* 逻辑运算符提供逻辑判断功能,用于构建更复杂的表达式,如下所示:
| a | b | a && b | a \|\| b | !a |
| ------- | ------- | ------- | -------- | ------- |
| 1 | 1 | 1 | 1 | 0 |
| 1 | 0 | 0 | 1 | 0 |
| 0 | 1 | 0 | 1 | 1 |
| 0 | 0 | 0 | 0 | 1 |
> [!NOTE]
>
> * ① 对于逻辑运算符来说,任何`非零值`都表示`真``零值`表示`假`,如:`5 || 0` 返回 `1` `5 && 0` 返回 `0`
> * ② 逻辑运算符的理解:
> * `&&` 的理解就是:`两边条件,同时满足`。
> * `||`的理解就是:`两边条件,二选一`。
> * `!` 的理解就是:`条件取反`。
> * ③ 短路现象:
> * 对于 `a && b` 操作来说,当 a 为假(或 0 )时,因为 `a && b` 结果必定为 0所以不再执行表达式 b。
> * 对于 `a || b` 操作来说,当 a 为真(或非 0 )时,因为 `a || b` 结果必定为 1所以不再执行表达式 b。
* 示例:
```c
#include <stdio.h>
int main() {
int a = 0;
int b = 0;
printf("请输入整数a的值");
scanf("%d", &a);
printf("请输入整数b的值");
scanf("%d", &b);
if (a > b) {
printf("%d > %d", a, b);
} else if (a < b) {
printf("%d < %d", a, b);
} else {
printf("%d = %d", a, b);
}
return 0;
}
```
* 示例:
```c
#include <stdio.h>
// 短路现象
int main() {
int i = 0;
int j = 10;
if (i && j++ > 0) {
printf("床前明月光\n"); // 这行代码不会执行
} else {
printf("我叫郭德纲\n");
}
printf("%d \n", j); //10
return 0;
}
```
* 示例:
```c
#include <stdio.h>
// 短路现象
int main() {
int i = 1;
int j = 10;
if (i || j++ > 0) {
printf("床前明月光 \n");
} else {
printf("我叫郭德纲 \n"); // 这行代码不会被执行
}
printf("%d\n", j); //10
return 0;
}
```
## 2.5 赋值运算符
* 常见的赋值运算符,如下所示:
| 运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 |
| ------ | ------------ | ---------- | ---------------- | ------ |
| `==` | 赋值 | 2 | 左边操作数的值 | ✅ |
| `+=` | 相加赋值 | 2 | 左边操作数的值 | ✅ |
| `-=` | 相减赋值 | 2 | 左边操作数的值 | ✅ |
| `*=` | 相乘赋值 | 2 | 左边操作数的值 | ✅ |
| `/=` | 相除赋值 | 2 | 左边操作数的值 | ✅ |
| `%=` | 取余赋值 | 2 | 左边操作数的值 | ✅ |
| `<<=` | 左移赋值 | 2 | 左边操作数的值 | ✅ |
| `>>=` | 右移赋值 | 2 | 左边操作数的值 | ✅ |
| `&=` | 按位与赋值 | 2 | 左边操作数的值 | ✅ |
| `^=` | 按位异或赋值 | 2 | 左边操作数的值 | ✅ |
| `\|=` | 按位或赋值 | 2 | 左边操作数的值 | ✅ |
> [!NOTE]
>
> * ① 赋值运算符的第一个操作数(左值)必须是变量的形式,第二个操作数可以是任何形式的表达式。
> * ② 赋值运算符的副作用针对第一个操作数。
* 示例:
```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;
}
```
## 2.6 位运算符(了解)
### 2.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]
>
> 操作数在进行位运算的时候,以它的补码形式计算!!!
### 2.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;
}
```
### 2.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)
### 2.6.4 按位或
* 按位与 `|` 的运算规则是:如果二进制对应的位上只要有 1 就是 1 ,否则为 0 ,即:
* `1 | 1` 的结果是 `1`
* `1 | 0` 的结果是 `1`
* `0 | 1` 的结果是 `1`
* `0 | 0` 的结果是 `0`
* 示例:`9 | 7 = 15`
![](./assets/24.svg)
* 示例:`-9 | 7 = -9`
![](./assets/25.svg)
### 2.6.5 按位异或
* 按位与 `^` 的运算规则是:如果二进制对应的位上一个为 1 一个为 0 就为 1 ,否则为 0 ,即:
* `1 ^ 1` 的结果是 `0`
* `1 ^ 0` 的结果是 `1`
* `0 ^ 1` 的结果是 `1`
* `0 ^ 0` 的结果是 `0`
> [!NOTE]
>
> 按位异或的场景有:
>
> * ① 交换两个数值:异或操作可以在不使用临时变量的情况下交换两个变量的值。
> * ② 加密或解密:异或操作用于简单的加密和解密算法。
> * ③ 错误检测和校正异或操作可以用于奇偶校验位的计算和检测错误RAID-3 以及以上)。
> * ……
* 示例:`9 ^ 7 = 14`
![](./assets/26.svg)
* 示例:`-9 ^ 7 = -16`
![](./assets/27.svg)
### 2.6.6 按位取反
* 运算规则:如果二进制对应的位上是 1则结果为 0如果是 0 ,则结果为 1 。
* `~0` 的结果是 `1`
* `~1` 的结果是 `0`
* 示例:`~9 = -10`
![](./assets/28.svg)
* 示例:`~-9 = 8`
![](./assets/29.svg)
### 2.6.7 二进制左移
* 在一定范围内,数据每向左移动一位,相当于原数据 × 2。正数、负数都适用
* 示例:`3 << 4 = 48` 3 × 2^4
![](./assets/30.svg)
* 示例:`-3 << 4 = -48` -3 × 2 ^4
![](./assets/31.svg)
### 2.6.8 二进制右移
* 在一定范围内,数据每向右移动一位,相当于原数据 ÷ 2。正数、负数都适用
> [!NOTE]
>
> * ① 如果不能整除,则向下取整。
> * ② 右移运算符最好只用于无符号整数,不要用于负数。因为不同系统对于右移后如何处理负数的符号位,有不同的做法,可能会得到不一样的结果。
* 示例:`69 >> 4 = 4` 69 ÷ 2^4
![](./assets/32.svg)
* 示例:`-69 >> 4 = -5` -69 ÷ 2^4
![](./assets/33.svg)
## 2.7 三元运算符
* 语法:
```c
条件表达式 ? 表达式1 : 表达式2 ;
```
> [!NOTE]
>
> * 如果条件表达式为非 0 (真),则整个表达式的值是表达式 1 。
> * 如果条件表达式为 0 (假),则整个表达式的值是表达式 2 。
* 示例:
```c
#include <stdio.h>
int main() {
int m = 110;
int n = 20;
int result = m > n ? m : n;
printf("result = %d\n", result); // result = 110
return 0;
}
```
## 2.8 运算符优先级
* 在数学中,如果一个表达式是 `a + b * c` ,我们知道其运算规则就是:先算乘除再算加减。其实,在 C 语言中也是一样的先算乘法再算加减C 语言中乘除的运算符比加减的运算符的优先级要高。
> [!NOTE]
>
> 所谓`优先级`,就是当多个运算符出现在同一个表达式中时,先执行哪个运算符。
* C 语言中运算符的优先级有几十个,有的运算符优先级不同,有的运算符优先级相同,如下所示:
| **优先级** | **运算符** | **名称或含义** | **结合方向** |
| ---------- | ---------- | ---------------- | ------------- |
| **1** | `[]` | 数组下标 | ➡️(从左到右) |
| | `()` | 圆括号 | |
| | `.` | 成员选择(对象) | |
| | `->` | 成员选择(指针) | |
| **2** | `-` | 负号运算符 | ⬅️(从右到左) |
| | `(类型)` | 强制类型转换 | |
| | `++` | 自增运算符 | |
| | `--` | 自减运算符 | |
| | `*` | 取值运算符 | |
| | `&` | 取地址运算符 | |
| | `!` | 逻辑非运算符 | |
| | `~` | 按位取反运算符 | |
| | `sizeof` | 长度运算符 | |
| **3** | `/` | 除 | ➡️(从左到右) |
| | `*` | 乘 | |
| | `%` | 余数(取模) | |
| **4** | `+` | 加 | ➡️(从左到右) |
| | `-` | 减 | |
| **5** | `<<` | 左移 | ➡️(从左到右) |
| | `>>` | 右移 | |
| **6** | `>` | 大于 | ➡️(从左到右) |
| | `>=` | 大于等于 | |
| | `<` | 小于 | |
| | `<=` | 小于等于 | |
| **7** | `==` | 等于 | ➡️(从左到右) |
| | `!=` | 不等于 | |
| **8** | `&` | 按位与 | ➡️(从左到右) |
| **9** | `^` | 按位异或 | ➡️(从左到右) |
| **10** | `\|` | 按位或 | ➡️(从左到右) |
| **11** | `&&` | 逻辑与 | ➡️(从左到右) |
| **12** | `\|\|` | 逻辑或 | ➡️(从左到右) |
| **13** | `?:` | 条件运算符 | ⬅️(从右到左) |
| **14** | `=` | 赋值运算符 | ⬅️(从右到左) |
| | `/=` | 除后赋值 | |
| | `*=` | 乘后赋值 | |
| | `%=` | 取模后赋值 | |
| | `+=` | 加后赋值 | |
| | `-=` | 减后赋值 | |
| | `<<=` | 左移后赋值 | |
| | `>>=` | 右移后赋值 | |
| | `&=` | 按位与后赋值 | |
| | `^=` | 按位异或后赋值 | |
| | `\|=` | 按位或后赋值 | |
| **15** | `,` | 逗号运算符 | ➡️(从左到右) |
> [!WARNING]
>
> * ① 不要过多的依赖运算符的优先级来控制表达式的执行顺序,这样可读性太差,尽量`使用小括号来控制`表达式的执行顺序。
> * ② 不要把一个表达式写得过于复杂,如果一个表达式过于复杂,则把它`分成几步`来完成。
> * ③ 运算符优先级不用刻意地去记忆,总体上:一元运算符 > 算术运算符 > 关系运算符 > 逻辑运算符 > 三元运算符 > 赋值运算符。
# 第三章:附录
## 3.1 字符集和字符集编码
### 3.3.1 概述
* 字符集和字符集编码(简称编码)计算机系统中处理文本数据的两个基本概念,它们密切相关但又有区别。
* 字符集Character Set是一组字符的集合其中每个字符都被分配了一个`唯一的编号`(通常是数字)。字符可以是字母、数字、符号、控制代码(如换行符)等。`字符集定义了可以表示的字符的范围`,但它并不直接定义如何将这些字符存储在计算机中。
> [!NOTE]
>
> ASCII美国信息交换标准代码是最早期和最简单的字符集之一它只包括了英文字母、数字和一些特殊字符共 128 个字符。每个字符都分配给了一个从 0 到 127 的数字。
* 字符集编码Character Encoding简称编码是一种方案或方法`它定义了如何将字符集中的字符转换为计算机存储和传输的数据(通常是一串二进制数字)`。简而言之,编码是字符到二进制数据之间的映射规则。
> [!NOTE]
>
> ASCII 编码方案定义了如何将 ASCII 字符集中的每个字符表示为 7 位的二进制数字。例如:大写字母`'A'`在 ASCII 编码中表示为二进制的`1000001`,十进制的 `65`
* `字符集`和`字符集编码`之间的关系如下:
![](./assets/35.png)
* Linux 中安装帮助手册:
![](./assets/36.gif)
### 3.3.2 ASCII 编码
* 从`冯·诺依曼`体系结构中,我们知道,计算机中所有的`数据`和`指令`都是以`二进制`的形式表示的;所以,计算机中对于文本数据的数据也是以二进制来存储的,那么对应的流程如下:
![](./assets/37.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/38.gif)
* 其对应的 ASCII 编码表,如下所示:
![](./assets/39.gif)
* 但是,随着计算机的发展,计算机开始了东征之路,由美国传播到东方:
![](./assets/40.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/41.gif)
![](./assets/42.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 编码写的文章,通过邮件发送给韩国人,因为韩国和中国在字符集编码上的高位字节有重叠部分,必然会造成歧义。
### 3.3.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/43.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/44.png)
>[!NOTE]
>
>`宽字符`和`窄字符`是编程和计算机系统中对字符类型的一种分类,主要用于描述字符在内存中的表示形式及其与编码方式的关系。
>
>* ① `窄字符`通常指使用单个字节8 位)来表示的字符。在许多传统的编码系统中,窄字符通常代表 ASCII 字符或其它单字节字符集中的字符。换言之,`窄字符`适合处理简单的单字节字符集ASCII适用于处理西方语言的应用。
>* ② `宽字符`指使用多个字节(通常是两个或更多)来表示的字符。这些字符通常用于表示比 ASCII 范围更广的字符集,如 Unicode 字符。换言之,`宽字符`适合处理多字节字符集UTF-32、UTF-16 等,适用于需要处理多种语言和符号的国际化应用。
>
>在现代编程中,`窄字符`通常与 `UTF-8` 编码关联,特别是在处理文本输入、输出和网络传输时。尽管 `UTF-8` 是变长编码,由于其高效的空间利用和对 `ASCII` 的优化,通常与`窄字符`概念关联。而`宽字符`通常与 `UTF-16` 编码或 `UTF-32`编码关联,这些编码使用更大的固定或半固定长度来表示字符,适合处理更大的字符集。
## 3.2 WSL2 中设置默认编码为中文
### 3.2.1 概述
* 查看 WSL2 的 Linux 发行版的默认编码:
```shell
echo $LANG
```
![](./assets/45.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` 时,系统默认语言环境保持简单和高效,同时支持更广泛的字符集,特别是多语言和非英语字符。这样可以在需要兼容性的同时,提供对全球化字符的支持。
### 3.2.2 AlmaLinux9 设置默认编码
* ① 搜索中文语言包:
```shell
dnf search locale zh
```
![](./assets/46.gif)
* ② 安装中文语言包:
```shell
dnf -y install glibc-langpack-zh
```
![](./assets/47.gif)
* ③ 切换语言环境为中文:
```shell
localectl set-locale LANG=zh_CN.UTF-8
```
![](./assets/48.gif)
* ④ 手动加载配置文件,使其生效:
```shell
source /etc/locale.conf
```
![](./assets/49.gif)
### 3.2.3 Ubuntu 22.04 设置默认编码
* ① 安装中文语言包:
```shell
apt update -y && apt install language-pack-zh-hans -y
```
![](./assets/50.gif)
* ② 切换环境为中文:
```shell
update-locale LANG=zh_CN.UTF-8 LANGUAGE=zh_CN:zh
```
![](./assets/51.gif)
* ③ 手动加载配置文件,使其生效:
```shell
source /etc/default/locale
```
![](./assets/52.gif)
## 3.3 在 C 语言中使用中文字符
### 3.3.1 概述
* 大部分 C 语言文章或教材对中文字符的处理讳莫如深,甚至只字不提,导致很多初学者认为 C 语言只能处理英文,而不支持中文。
* 其实这是不对的。C 语言作为一门系统级别的编程语言,理应支持世界上任何一个国家的文字,如:中文、日文、韩文等。
> [!NOTE]
>
> 如果 C 语言不支持中文,那么简体中文 Windows 操作系统将无从谈起,我们只能被迫使用英文 Windows 操作系统,这对计算机的传播而言将会是一种巨大的阻碍。
### 3.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.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.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;
}
```
## 3.4 C 语言到底使用什么编码?
### 3.4.1 概述
* 在 C 语言中,只有 `char` 类型的`窄字符`才会使用 ASCII 编码。而 `char` 类型的`窄字符串`、`wchar_t` 类型的`宽字符`和`宽字符串`都不使用 ASCII 编码。
* `wchar_t` 类型的`宽字符`和`宽字符串`使用 UTF-16 或者 UTF-32 编码,这个在上文已经讲解了,现在只剩下 `char` 类型的`窄字符串`没有讲解了,这也是下文的重点。
> [!NOTE]
>
> * ① 其实,对于`char` 类型的窄字符串C 语言并没有规定使用哪一种特定的编码,只要选用的编码能够适应当前的环境即可。换言之,`char` 类型的窄字符串的编码与操作系统以及编译器有关。
> * ② 但是,`char` 类型的窄字符串一定不是 ASCII 编码,因为 ASCII 编码只能显示拉丁体系的文字,而不能输出中文、日文、韩文等。
> * ③ 讨论窄字符串的编码要从以下两个方面下手。
### 3.4.2 源文件使用什么编码?
* 源文件用来保存我们编写的代码,它最终会被存储到本地硬盘,或者远程服务器,这个时候就要尽量压缩文件体积,以节省硬盘空间或者网络流量,而代码中大部分的字符都是 ASCII 编码中的字符,用一个字节足以容纳,所以 UTF-8 编码是一个不错的选择。
* UTF-8 兼容 ASCII代码中的大部分字符可以用一个字节保存。另外UTF-8 基于 Unicode支持全世界的字符我们编写的代码可以给全球的程序员使用真正做到技术无国界。
* 常见的 IDE 或者编辑器Sublime Text、Vim 等,在创建源文件的时候一般默认就是 UTF-8 编码。就算不是,我们也会推荐设置为 UTF-8 编码,如下所示:
![](./assets/53.png)
* 对于 C 语言编译器来说,它往往支持多种编码格式的源文件。微软的 MSVC 、GCC 和 LLVM/Clang 都支持 UTF-8 和本地编码的源文件。
### 3.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 编码。
### 3.3.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可以轻松处理中文等多字节字符。
### 3.3.5 编码字符集和运行字符集
* 源文件使用的字符集,通常称为`编码字符集`,即:写代码的时候所使用的字符集。
> [!NOTE]
>
> 源文件需要保存到硬盘,或者在网络上传输,使用的编码要尽量节省存储空间,同时要方便跨国交流,所以一般使用 UTF-8这就是选择编码字符集的标准。
* 程序中的字符或者字符串使用的字符集,通常称为`运行字符集`,即:程序运行时所使用的字符集。
> [!NOTE]
>
> 程序中的字符或者字符串在程序运行后必须被载入到内存才能进行后续的处理对于这些字符来说要尽量选用能够提高处理速度的编码UTF-16 和 UTF-32 编码就能够快速定位(查找)字符。
* `编码字符集`是站在`存储`和`传输`的角度,而`运行字符集`是站在`处理`或者`操作`的角度,所以它们并不一定相同。

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 586 KiB

After

Width:  |  Height:  |  Size: 586 KiB

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,510 +1,1516 @@
# 第一章:内存泄漏Memory Leak
# 第一章:概述
## 1.1 概述
* `流程控制结构`是用来控制程序中`各语句执行顺序`的语句,并且可以将语句组合成能`完成一定功能`的`小逻辑模块`。
* 在程序设计中规定了`三种`流程结构,如下所示:
* `顺序结构`:程序从上到下逐行执行,中间没有任何判断和跳转。
* `分支结构`:根据条件,有选择的执行某段代码。在 C 语言中,有 `if...else``switch...case` 两种分支语句。
* `循环结构`:根据循环条件,重复性的执行某段代码。在 C 语言中,有 `for`、`while`、`do...while` 三种循环结构。
* 有没有过这样的日子,总感觉我们的电脑,不是一个尖端的设备,而像一只疲惫的蜗牛。它在缓慢的爬行,并试图背着重重的楼房去跑马拉松,如下所示:
* 在生活中的`洗衣工厂`,就包含了上述的三种流程结构,如下所示:
![](./assets/1.jpeg)
![](./assets/1.jpg)
> [!NOTE]
>
> 儿歌《蜗牛与黄鹂鸟》的歌词是这样的,如下所示:
>
> * 阿门阿前一棵葡萄树。
> * 阿嫩阿嫩绿地刚发芽。
> * 蜗牛背着那重重的壳呀。
> * 一步一步地往上爬。
> * 阿树阿上两只黄鹂鸟。
> * 阿嘻阿嘻哈哈在笑它。
> * 葡萄成熟还早得很哪。
> * 现在上来干什么。
> * 阿黄阿黄鹂儿不要笑。
> * 等我爬上它就成熟了。
>
> 虽然歌曲的主旨是想通过蜗牛与黄鹂鸟的对话,表达了努力和坚持的重要性,即使速度慢,只要坚定地往前走,总会达到目标。但是,也从侧面说明了蜗牛的速度真的很慢。
* 亦或者,我们的电脑就像一个蹒跚学步的孩子在发脾气,我们多么希望她们耐心点,并配合我们。可是,她们总是拒绝和我们合作,如下所示:
![](./assets/2.jpg)
# 第二章:顺序结构
* 如果这些场景,你都感觉很熟悉,那么你很有可能就是`内存泄漏`的受害者。
## 2.1 概述
> [!NOTE]
>
> * ① `内存泄漏`虽然不可见,但是它会悄悄的蚕食计算机的性能,让曾经快速的系统变成一台陈旧的机器。
> * ② 最为糟糕的时,和留下明显迹象的`漏水`不同,`内存泄漏`是不可见的,这使得它们难以识别,甚至难以修复。也正是因为这个特点,让开发人员和计算机用户都感觉头疼。
* 程序从上到下逐行地执行,表达式语句都是顺序执行的,并且上一行对某个变量的修改对下一行会产生影响。
## 1.2 什么是内存泄漏?
![](./assets/2.png)
* 我们可以将我们的计算机想象成一个繁华的城市,城市的`道路`就代表着计算器的`内存`(计算机的内存是有限的,普遍的家用个人台式机电脑最多只支持 `4` 根内存条。如果是 `DDR4` 的话,最多也就支持 `128` GB。就算是服务器也不是无穷无尽的在其上运行的`程序`就像`车辆`一样,每辆车都执行各自的任务,如下所示:
## 2.2 应用示例
> [!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>
#include <stdio.h>
int main() {
while (true) { // 死循环
malloc(1024); // 分配1024个字节的内存
}
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;
}
```
* 如果我们在 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 概述
* `内存泄漏`是由于未释放不再使用的内存导致内存资源逐渐减少,但不会立即导致程序崩溃,而是长时间运行后可能出现性能问题或最终崩溃。
* `内存溢出`则是由于内存资源耗尽,程序试图分配新内存时失败,通常会导致程序的立即崩溃或异常终止。
* 根据特定条件执行不同的代码块,从而实现灵活的程序控制和更复杂的逻辑。
## 3.2 单分支结构
### 3.2.1 概述
* 语法:
```c
if(条件表达式){
语句;
}
```
> [!NOTE]
>
> * ① `内存泄漏`和`内存溢出`都与内存管理不当有关,但它们发生的机制和直接影响是不同的。
> * ② 避免`内存泄漏`和`内存溢出`都是编写高效、可靠软件的重要方面。
> * ① 在 C 语言中,严格意义上是没有 boolean 类型的,使用`非0` 表示`真true``0` 表示`假false`。
> * ② 当条件表达式为真(`非0` ),就会执行代码块中的语句;否则,就不会执行代码块中的语句。
## 3.2 内存泄漏和内存溢出的联系和区别
* 流程图,如下所示:
> [!IMPORTANT]
>
> `内存泄漏`和`内存溢出`之间并不是必然的因果关系,而是两者可能会相互影响。
* ① `内存泄漏`导致`内存溢出`的可能性:
* 如果一个程序长期运行并且持续发生`内存泄漏`,未被释放的内存会慢慢积累,最终占用系统的大部分内存资源。如果`内存泄漏`严重到占用了所有可用内存,那么程序就会因为无法再分配新的内存,而出现`内存溢出`Out of Memory的情况。
* 因此,`内存泄漏`可以**间接**地导致`内存溢出`,特别是在长时间运行的程序或系统中。
* ② `内存泄漏`和`内存溢出`的区别:
* `内存泄漏`是指程序持续占用内存却不释放,导致可用内存逐渐减少。这种情况可能会在`长时间`内不显现问题,特别是如果程序只泄漏了少量内存。
* `内存溢出`则是一个更`急剧`的问题,它通常在程序尝试分配超过系统可用内存的大块内存时`立刻`发生,导致程序崩溃或异常终止。
* ③ 不必然性:
* 一个程序可能会发生`内存泄漏`,但因为泄漏的内存量很小,系统资源丰富,所以在短时间内不会出现`内存溢出`。
* `内存溢出`也可以在没有`内存泄漏`的情况下发生,如:一个程序需要处理非常大的数据集,直接导致内存不足。
> [!IMPORTANT]
>
> * ① `内存泄漏`有可能会在长时间积累后导致`内存溢出`,但这并不是必然的。
> * ② `内存溢出`可以在多种情况下发生,而`内存泄漏`只是其中可能的一个诱因。
> * ③ 因此,虽然`内存泄漏`可能最终引发`内存溢出`,但两者之间并非每次都是直接关联的。
![](./assets/3.png)
### 3.2.2 应用示例
* 需求:成年人心率的正常范围是每分钟 60~100 次。体检时,如果心率不在此范围内,则提示需要做进一步的检查。
# 第四章:内存泄漏检测和性能分析(⭐)
* 示例:
## 4.1 内存泄漏检测
```c
#include <stdio.h>
### 4.1.1 概述
int main() {
* C 语言中的指针是否使用是个颇具争议的话题现代化的高级编程语言通过各种策略和机制在编译期就能解决指针危险的问题。但是遗憾的是C 语言的指针很大程度上,在运行期才会暴露问题。
* 幸运的是,我们可以使用 `Valgrind` 项目来进行`内存泄漏检测`和`性能分析`,而 `Valgrind` 只支持 Linux 。
int heartBeats = 0;
printf("请输入您的心率:");
scanf("%d", &heartBeats);
### 4.1.2 安装
if (heartBeats < 60 || heartBeats > 100) {
printf("您的心率不在正常范围内,请做进一步的检查。\n");
}
* 在 WSL2 上安装 Valgrind
printf("体检结束!!!");
```shell
dnf -y upgrade && dnf -y install valgrind # AlmaLinux
return 0;
}
```
```shell
apt -y update && apt -y upgrade && apt -y install valgrind # Ubuntu
### 3.2.3 应用示例
* 需求:根据年龄判断,如果是未成年人,则提示 "未成年人请在家长陪同下访问!" 。
* 示例:
```c
#include <stdio.h>
int main() {
int age = 0;
printf("请输入你的年龄:");
scanf("%d", &age);
if (age < 18) {
printf("未成年人请在家长陪同下访问!\n");
}
printf("欢迎继续访问!");
return 0;
}
```
![](./assets/10.gif)
## 3.3 双分支结构
* 查看 valgrind 可执行文件的安装位置:
### 3.3.1 概述
```shell
which valgrind
* 语法:
```c
if(条件表达式) {
语句块1;
}else {
语句块2;
}
```
![](./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 发布的标准内核,所以需要我们手动编译安装。
> * ① 在 C 语言中,严格意义上是没有 boolean 类型的,使用`非0` 表示`真true``0` 表示`假false`。
> * ② 当条件表达式为真(`非0` ),就会执行代码块 1 中的语句;否则,执行代码块 2 中的语句。
* 查看内核版本
* 流程图,如下所示:
```shell
uname -sr
![](./assets/4.png)
### 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;
}
```
![](./assets/19.gif)
### 3.3.2 应用示例
* 设置环境变量,方便后续引用:
* 需求输入年龄如果大于18岁则输出 "你年龄大于18要对自己的行为负责!";否则,输出 "你的年龄不大这次放过你了。"
```shell
export KERNEL_VERSION=$(uname -r | cut -d'-' -f1)
* 示例:
```c
#include <stdio.h>
int main() {
int age = 0;
printf("请输入年龄:");
scanf("%d", &age);
if (age > 18) {
printf("你年龄大于18要对自己的行为负责!\n");
} else {
printf("你的年龄不大,这次放过你了!\n");
}
return 0;
}
```
![](./assets/20.gif)
### 3.3.3 应用示例
* 安装依赖库:
* 需求:判定某个年份是否为闰年?
```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
>[!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;
}
```
![](./assets/21.gif)
## 3.4 多重分支结构
* 下载源码:
### 3.4.1 概述
```shell
git clone \
--depth 1 \
--single-branch --branch=linux-msft-wsl-${KERNEL_VERSION} \
https://github.com/microsoft/WSL2-Linux-Kernel.git
* 语法:
```c
if (条件表达式1) {
语句块1;
} else if (条件表达式2) {
语句块2;
}
...
} else if (条件表达式n) {
语句块n;
} else {
语句块n+1;
}
```
![](./assets/22.gif)
> [!NOTE]
>
> * ① 在 C 语言中,严格意义上是没有 boolean 类型的,使用`非0` 表示`真true``0` 表示`假false`。
> * ② 首先判断关系表达式 1 的结果是真(值为 `非0`)还是假(值为 `0`
> * 如果为真,就执行语句块 1然后结束当前多分支。
> * 如果是假,就继续判断条件表达式 2看其结果是真还是假。
> * 如果是真,就执行语句块 2然后结束当前多分支。
> * 如果是假,就继续判断条件表达式…看其结果是真还是假。
> * ...
> * 如果没有任何关系表达式为真,就执行语句块 n+1然后结束当前多分支。
> * ③ 当条件表达式之间是`互斥`(彼此之间没有交集)关系时,条件判断语句及执行语句间顺序无所谓。
> * ④ 当条件表达式之间是`包含`关系时,必须`小上大下 / 子上父下`,否则范围小的条件表达式将不可能被执行。
> * ⑤ 当 if-else 结构是多选一的时候,最后的 else 是可选的,可以根据需要省略。
> * ⑥ 如果语句块中只有一条执行语句的时候,`{}`是可以省略的;但是,强烈建议保留!!!
* 编译内核代码:
* 流程图,如下所示
```shell
cd WSL2-Linux-Kernel
![image-20240722075241253](./assets/5.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;
}
```
```shell
make -j $(nproc) KCONFIG_CONFIG=Microsoft/config-wsl
### 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;
}
```
![](./assets/23.gif)
## 3.5 多重分支结构 switch
* 编译 perf 工具:
### 3.5.1 概述
```shell
cd tools/perf
* 语法:
```c
switch(表达式){
case 常量值1:
语句块1;
//break;
case 常量值2:
语句块2;
//break;
...
case 常量值n:
语句块n;
//break;
[default:
语句块n+1;
]
}
```
```shell
make clean && make
> [!NOTE]
>
> * ① switch 后面表达式的值必须是一个整型char、short、int、long 等)或枚举类型。
> * ② case 后面的值必须是常量,不能是变量。
> * ③ default 是可选的,当没有匹配的 case 的时候,就执行 default 。
> * ④ break 语句可以使程序跳出 switch 语句块,如果没有 break会执行下一个 case 语句块,直到遇到 break 或者执行到 switch 结尾,这个现象称为穿透。
* 流程图,如下所示:
![](./assets/6.png)
### 3.5.2 应用示例
* 需求编写一个程序该程序可以接收一个字符比如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;
}
```
![](./assets/24.gif)
### 3.5.3 应用示例
* 复制到 PATH 变量所指向的路径中:
* 需求编写程序输入月份输出该月份有多少天。说明1 月、3 月、5 月、7月、8 月、10 月、12 月有 31 天4 月、6 月、9 月、11 月有 30 天2 月有 28 天或 29 天。
```shell
cp perf /usr/bin/
* 示例:
```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;
}
```
![](./assets/25.gif)
### 3.5.4 switch 和 if else if 的比较
### 4.2.3 整合
* ① 如果判断条件是判等,而且符合整型、枚举类型,虽然两个语句都可以使用,建议使用 swtich 语句。
* ② 如果判断条件是区间判断,大小判断等,使用 if...else...if。
* CLion 中配置 perf 的路径:
## 3.6 嵌套分支
![](./assets/26.png)
### 3.6.1 概述
* 在 CLion 中通过 perf 运行代码:
* 嵌套分支是指,在一个分支结构中又嵌套了另一个分支结构,里面的分支的结构称为内层分支,外面的分支结构称为外层分支。
> [!NOTE]
>
> 嵌套分支层数不宜过多,建议最多不要超过 3 层。
### 3.6.2 应用示例
* 需求:根据淡旺季的月份和年龄,打印票价。
> [!NOTE]
>
> * ① 4 -10 是旺季:
> * 成人18-6060 。
> * 儿童(<18半价
> * 老人(>601/3 。
> * ② 其余是淡季:
> * 成人40。
> * 其他20。
* 示例:
```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)
> [!NOTE]
>
> 执行过程是:① --> ② --> ③ --> ④ --> ② --> ③ --> ④ --> ... --> ② 。
### 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 的约数(因数)。
> * 如果一个整数同时是几个整数的约数,则称该整数为这些整数的公约数;其中,数值最大的称为最大公约数。
> * 如果一个整数同时为两个或多个整数的倍数的数,则称该整数为这些整数的公倍数;其中,数值最小的称为最小公倍数。
* 示例:
```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]
>
> 思路:
>
> * ① 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 。
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
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 original = 0;
int rev = 0;
printf("请输入一个整数:");
scanf("%d", &num);
original = num;
// 从右边开始,依次获取每个数字,然后拼接到 rev 中
/**
* 第 1 次123 % 10 = 3rev = 0 * 10 + 3 = 3
* 第 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("%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;
}
```
* 示例:编写程序,要求输入一个数字,判断该数字是否是质数
```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;
}
```
![](./assets/27.gif)

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View File

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

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

Binary file not shown.

Before

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 297 KiB

View File

@ -1,1357 +1,510 @@
# 第一章:数组的概念
# 第一章:内存泄漏Memory Leak
## 1.1 为什么需要数组?
## 1.1 概述
### 1.1.1 需求分析 1
* 有没有过这样的日子,总感觉我们的电脑,不是一个尖端的设备,而像一只疲惫的蜗牛。它在缓慢的爬行,并试图背着重重的楼房去跑马拉松,如下所示:
* 需要统计某公司 50 个员工的工资情况,例如:计算平均工资、最高工资等。如果使用之前的知识,我们需要声明 50 个变量来分别记录每位员工的工资,即:
![](./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 <stdio.h>
#include <stdbool.h>
#include <stdlib.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 大法);此时,我们就可以将所有的`数据`全部存储到一个`容器(数组)`中进行统一管理,并进行其它的操作,如:求最值、求平均值等,如下所示:
```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]);
int main() {
while (true) { // 死循环
malloc(1024); // 分配1024个字节的内存
}
// 其它操作,如:求最值,求平均值等
...
return 0;
}
```
### 1.1.2 需求分析 2
* 如果我们在 Windows 上运行该程序,就可以打开 Windows 的任务管理器(快捷键是`Ctrl + Shift + ESC`),将会发现内存的使用率在飙升。当然,稍等片刻后程序会被终止,是因为 Windows 的内存管理机制,发现我们的程序占用内存太多,会让它崩溃,防止系统卡死(其它的操作系统也有相应的措施)。
* 在现实生活中,我们会使用很多 APP 或微信小程序等,即:
![](./assets/6.gif)
![](./assets/1.png)
## 1.4 内存泄漏会导致什么后果?
* 同样的道理,如果我们使用变量来存储每个商品信息,那么就需要非常多的变量;但是,如果我们将这些`商品信息`都存储到一个`容器(数组)`中,进行统一管理;那么,之后的数据处理将会非常方便。
* ① **内存使用量增加**:随着泄漏和释放的内存越来越多,整体系统内存使用量会增加。这会减少可用于其他进程和应用程序的内存,从而降低系统速度。
* ② **增加分页**:随着`内存泄漏`的累积,系统可能会开始将内存内容交换到磁盘以释放 RAM从而导致更多的磁盘 I/O。这会导致性能降低因为磁盘操作比内存操作慢得多。
* ③ **内存不足错误**:如果`内存泄漏`足够多,系统最终可能会完全耗尽可用内存。这可能会导致崩溃、内存分配失败和程序终止。
* ④ **资源争用**较高的内存使用率还会导致对缓存和资源CPU 时间等)的更多争用,因为系统尝试管理有限的资源。这会进一步降低性能。
* ⑤ **应用程序不稳定**:随着内存使用量随着时间的推移而增长,存在`内存泄漏`的应用程序可能会遇到崩溃、意外行为和间歇性故障。这会导致不稳定和可靠性问题。
* ⑥ **安全风险**`内存泄漏`会使数据在内存中的延迟时间超过预期。此数据可能包含密码、密钥或其他敏感信息,如果恶意软件或攻击者访问这些信息,则会带来安全风险。
### 1.1.3 容器的概念
## 1.5 检测内存泄漏的工具或技术
* `生活中的容器`:水杯(装水、饮料的容器)、衣柜(装衣服等物品的容器)、集装箱(装货物等物品的容器)。
* `程序中的容器`:将多个数据存储到一起,并且每个数据称为该容器中的元素。
* ① **分析工具**
* ① Valgrind用于构建动态分析工具的检测框架最有名的 Memcheck 的套件,可以检测 C 和 C++ 程序中的内存泄漏。
* ② Java VisualVM适用于 Java 应用程序的监控、故障排除和分析工具。
* ③ .NET Memory Profiler用于查找内存泄漏并优化 .NET 应用程序中的内存使用的工具。
* ④ Golang pprof该工具可让您收集 Go 程序的 CPU 配置文件、跟踪和堆配置文件。
## 1.2 什么是数组?
* ② **浏览器开发工具**Chrome、Firefox 和 Edge 等现代 Web 浏览器附带内置的开发人员工具,可帮助识别 Web 应用程序中的内存泄漏,尤其是 JavaScript 中的内存泄漏。
* 数组Array是将多个`相同数据类型`的`数据`按照一定的顺序排序的`集合`,并使用一个`标识符`命名,以及通过`编号(索引,亦称为下标)`的方式对这些数据进行统一管理。
* **静态分析**Lint、SonarQube 或 Clang Static Analyzer 等工具可以扫描代码以识别可能导致内存泄漏的模式
![](./assets/2.png)
* ④ **自动化测试**将内存泄漏检测整合到自动化测试中有助于在开发周期的早期捕获泄漏JUnit适用于 Java或 pytest适用于 Python等工具可以与内存分析工具集成以自动执行此过程。
## 1.3 数组的相关概念
* ⑤ **堆分析**检查应用程序的堆转储可以深入了解正在消耗内存的对象Eclipse MAT内存分析器工具或 Java 堆分析工具 jhat等工具可以协助进行此分析。
* `数组名`:本质上是一个标识符常量,命名需要符合标识符规则和规范。
* `元素`:同一个数组中的元素必须是相同的数据类型。
* `索引(下标)`:从 0 开始的连续数字。
* `数组的长度`:就是元素的个数。
* ⑥ **指标**实施指标来监控一段时间内的内存使用情况有助于识别导致内存消耗增加的模式或特定操作Prometheus 和 Grafana 等。
## 1.4 数组的特点
* ⑦ **第三方库和中间件**:一些第三方解决方案提供内置的内存泄漏检测功能。如果我们怀疑这些组件可能是泄漏源,则必须查看与这些组件相关的文档或论坛。
* ⑧ **手动代码审查**:有时,识别内存泄漏的最佳方法是对代码进行彻底的手动审查,尤其是在分配和释放内存的区域中。
* ⑨ **压力测试**:在高负载或长时间运行应用程序,有助于暴露在正常情况下可能不明显的内存泄漏。
* ① 创建数组的时候,会在内存中开辟一整块`连续的空间`,占据空间的大小,取决于数组的长度和数组中元素的类型。
* ② 数组中的元素在内存中是依次紧密排列且有序的。
* ③ 数组一旦初始化完成,且长度就确定的,并且`数组的长度一旦确定,就不能更改`。
* ④ 我们可以直接通过索引(下标)来获取指定位置的元素,速度很快。
* ⑤ 数组名中引用的是这块连续空间的首地址。
## 1.6 如何避免内存泄漏?
* ① **及时释放内存**:在程序中,确保在不再需要使用内存时及时释放它。
* ② **智能指针**:使用智能指针来帮助在 C++ 等编程语言中进行自动内存管理。
* ③ **将编程语言与垃圾回收器一起使用**:内存分配和释放由 Python 和 Java 等编程语言自动处理,这些语言包含内置的垃圾收集系统。
* ④ **利用内存管理策略:** 有效的内存管理可以防止内存泄漏。这包括始终监控我们的软件使用了多少内存,并了解何时分配和取消分配内存,即:检测内存泄漏的工具或技术。
# 第二章:数组的操作(⭐)
## 1.7 总结
## 2.1 数组的定义
* **内存泄漏**是由于未释放不再使用的内存,导致内存资源逐渐减少,但不会立即导致程序崩溃,而是`长时间`运行后可能出现性能问题或最终崩溃。
### 2.1.1 动态初始化
* 语法:
```c
数据类型 数组名[元素个数|长度];
```
# 第二章内存溢出Out Of MemoryOOM
> [!NOTE]
>
> * ① 数据类型:表示的是数组中每一个元素的数据类型。
> * ② 数组名:必须符合标识符规则和规范。
> * ③ 元素个数或长度:表示的是数组中最多可以容纳多少个元素(不能是负数、也不能是 0 )。
## 2.1 概述
* 示例:
```c
#include <stdio.h>
int main() {
// 先指定元素的个数和类型,再进行初始化
// 定义数组
int arr[3];
// 给数组元素赋值
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
return 0;
}
```
### 2.1.2 静态初始化 1
* 语法:
```c
数据类型 数组名[元素个数|长度] = {元素1,元素2,...}
```
> [!NOTE]
>
> * ① 静态部分初始化:如果数组初始化的元素个数`小于`数组声明的长度,那么就会从数组开始位置依次赋值,不够的就补 0 。
> * ② 静态全部初始化:数组初始化的元素个数`等于`数组的长度。
> [!TIP]
>
> 在 CLion 中开启`嵌入提示(形参名称-->显示数组索引的提示)`功能,即:
>
> ![](./assets/3.png)
>
> 这样,在 CLion 中,将会显示数组初始化时每个元素对应的索引,即:
>
> ![](./assets/4.png)
* 示例:静态部分初始化
```c
#include <stdio.h>
int main() {
// 定义数组和部分初始化:
// 会将给定的值从数组的开始位置一个个的赋值,没有赋值的地方,用 0 填充
int arr[5] = {1, 2};
return 0;
}
```
* 示例:静态全部初始化
```c
#include <stdio.h>
int main() {
// 定义数组和全部初始化:数组初始化的元素个数等于数组的长度。
int arr[5] = {1, 2, 3, 4, 5};
return 0;
}
```
### 2.1.3 静态初始化 2
* 语法:
```c
数据类型 数组名[] = {元素1,元素2,...}
```
> [!NOTE]
>
> 没有给出数组中元素的个数,将由系统根据初始化的元素,自动推断出数组中元素的个数。
* 示例:
```c
#include <stdio.h>
int main() {
// 指定元素的类型,不指定元素个数,同时进行初始化
int arr[] = {1, 2, 3, 4, 5};
return 0;
}
```
### 2.1.4 静态初始化 3
* 在 C 语言中,也可以只给部分元素赋值。当 {} 中的值少于元素的个数的时候,只会给前面的部分元素赋值,至于剩下的元素就会自动初始化为 0 。
```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`
* 示例:
```c
#include <stdio.h>
int main() {
int arr[10] = {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[5] = %d \n", arr[5]); // arr[5] = 0
printf("arr[6] = %d \n", arr[6]); // arr[6] = 0
printf("arr[7] = %d \n", arr[7]); // arr[7] = 0
printf("arr[8] = %d \n", arr[8]); // arr[8] = 0
printf("arr[9] = %d \n", arr[9]); // arr[9] = 0
return 0;
}
```
## 2.2 访问数组元素
* 语法:
```c
数组名[索引|下标];
```
> [!NOTE]
>
> 假设数组 `arr` 有 n 个元素,如果使用的数组的下标 `< 0``> n-1` ,那么将会产生数组越界访问,即超出了数组合法空间的访问;那么,数组的索引范围是 `[0,arr.length - 1]`
* 示例:
```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)
> [!NOTE]
>
> * ① 在很多编程语言中都内置了获取数组的长度的属性或方法Java 中的 arr.length 或 Rust 的 arr.len()。
> * ② 但是C 语言没有内置的获取数组长度的属性或方法,只能通过 sizeof 运算符间接来计算得到。
> * ③ 数组一旦`声明`或`定义`,其`长度`就`固定`了,`不能动态变化`。
* 示例:
```c
#include <stdio.h>
int main() {
// 定义数组和全部初始化:数组初始化的元素个数等于数组的长度。
int arr[] = {1, 2, 3, 4, 5};
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(arr[0]);
// 遍历数组
for (int i = 0; i < length; i++) {
printf("%d \n", arr[i]);
}
return 0;
}
```
## 2.5 遍历数组
* 遍历数组是指按顺序访问数组中的每个元素,以便读取或修改它们,编程中一般使用循环结构对数组进行遍历。
* 示例:声明一个存储有 12、2、31、24、15、36、67、108、29、51 的数组,并遍历数组所有元素
```c
#include <stdio.h>
int main() {
// 定义数组并初始化
int arr[] = {12, 2, 31, 24, 15, 36, 67, 108, 29, 51};
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(int);
// 遍历数组
for (int i = 0; i < length; i++) {
printf("%d\n", arr[i]);
}
return 0;
}
```
* 示例:声明长度为 10 的 int 类型数组,给数组元素依次赋值为 0 ~ 9 ,并遍历数组所有元素
```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;
}
// 遍历数组
for (int i = 0; i < length; i++) {
printf("%d\n", arr[i]);
}
return 0;
}
```
## 2.6 一维数组的内存分析
### 2.6.1 数组内存图
* 假设数组是如下的定义:
```c
int arr[] = {1,2,3,4,5};
```
* 那么,对应的内存结构,如下所示:
* 首先,说明一点,在国内的很多文章中,都将 `Out Of MemoryOOM`翻译为 `内存溢出`,但是本人认为翻译为`内存不足`更为贴切。
* 在生活中,我们在使用计算机的时候,可能会遇到打开视频网站的时候,视频网站崩溃了,并且在浏览器上显示报错信息`Error Code Out Of Memory`,如下所示:
![](./assets/7.png)
> [!NOTE]
>
> * ① 数组名 `arr` 就是记录该数组的首地址,即 `arr[0]` 的地址。
> * ② 数组中的各个元素是连续分布的,假设 `arr[0]` 的地址是 `0xdea7bff880`,则 `arr[1] 的地址 = arr[0] 的地址 + int 字节数4 = 0xdea7bff880 + 4 = 0xdea7bff884` ,依次类推...
* 当然我们在使用微软办公套件Outlook 的时候,可能也会遇到系统提示 `Out Of Memory`,如下所示:
* 在 C 语言中,我们可以通过 `&arr``&arr[0]` 等形式获取数组或数组元素的地址,即:
![](./assets/8.jpg)
```c
#include <stdio.h>
* 亦或者,我们在打游戏的时候,会遇到系统提示 `Out Of Memory`,如下所示:
int main() {
![](./assets/9.png)
// 定义数组
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]
>
> 如果之后试图更改数组名对应的地址,编译器就会报错。
* 示例:错误演示
```c
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 应用示例
* 需求:计算数组中所有元素的和以及平均数。
* 示例:
```c
#include <stdio.h>
int main() {
// 定义数组并初始化
int arr[] = {12, 2, 31, 24, 15, 36, 67, 108, 29, 51};
// 计算数组的长度
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;
}
```
### 2.7.2 应用示例
* 需求:计算数组的最值(最大值和最小值)。
* 上述的种种情景都表明了内存溢出内存不足OOM是`立即显现`的问题,尤其是当系统无法分配足够内存时,会直接导致程序崩溃或异常。
> [!NOTE]
>
> 思路:
> * ① 内存泄漏是一种`逐渐积累`的问题会耗尽系统内存可能最终导致内存不足理解站着茅坑不拉稀最终可能导致可用的茅坑越来越少后面的人就只能等着o(╥﹏╥)o
> * ② 内存溢出(不足)是一种`立即显现`的问题,当系统无法分配足够内存时,会`直接`导致程序崩溃或异常(理解:大象塞进冰箱,冰箱不是无限大,最终可能导致大象身体的一部分露出来,这不就`溢出`吗?换言之,就是冰箱(内存)的容量有限啊,`不`能满`足`实际需要)。
> [!IMPORTANT]
>
> * ① 假设数组中的第一个元素是最大值或最小值,并使用变量 max 或 min 保存。
> * ② 遍历数组中的每个元素:
> * 如果有元素比最大值还要大,就让变量 max 保存最大值。
> * 如果有元素比最小值还要小,就让变量 min 保存最小值。
> 官方定义:当计算机没有足够的内存来执行操作或运行应用程序时,会发生内存不足 OOM 错误。此内存可以是`物理 RAM`(随机存取内存) 或`虚拟内存`,它使用磁盘空间扩展物理内存。当系统耗尽可用内存时,它无法再满足`内存分配`请求,从而导致 OOM 错误。此错误表示除非释放或添加内存,否则系统无法处理进一步的需求。
## 2.2 什么会触发内存溢出?
* 导致`内存溢出`的原因很多,具体取决于编程语言、平台和特定的应用程序场景。以下是一些最常见的原因:
* ① **无限循环或递归**:如果程序中的循环或递归没有正确终止条件,可能会一直运行,消耗掉所有可用内存。
* ② **内存泄漏**:程序不断分配内存而不释放,最终导致可用内存耗尽。这通常是因为程序在使用完某些数据后,没有正确地释放相关的内存。
* ③ **处理大数据集**:如果程序试图一次性加载或处理一个超大的数据集,而该数据集的大小超过了系统的可用内存,这可能会导致内存溢出。
* ④ **资源过度分配**:一些程序在运行时,可能会为某些资源(如缓存、临时数据)分配过多的内存,导致整体系统内存不足。
* ⑤ **错误的内存管理**在手动管理内存的编程语言中C 或 C++),如果程序错误地管理内存(如:重复释放、未释放或非法访问内存),也可能引发内存泄漏,进而导致内存溢出。
* ⑥ **并发操作**:如果多个进程或线程并发地进行大量内存分配操作,且这些操作没有得到有效控制,也可能导致系统内存被耗尽。
* ⑦ **外部库或工具的 Bug**:使用的第三方库或工具中存在内存管理相关的 bug也可能导致内存溢出。
* 示例:
## 2.3 如何避免内存溢出?
```c
#include <stdio.h>
int main() {
// 定义数组并初始化
int arr[] = {12, 2, 31, 24, 15, -36, 67, 108, 29, 51};
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(int);
// 定义最大值
int max = arr[0];
// 定义最小值
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
printf("数组的最小值为:%d\n", min); // 数组的最小值为:-36
return 0;
}
```
### 2.7.3 应用示例
* 需求:统计数组中某个元素出现的次数,要求:使用无限循环,如果输入的数字是 0 ,就退出。
* 示例:
```c
#include <stdio.h>
int main() {
// 定义数组并初始化
int arr[] = {12, 2, 31, 24, 2, -36, 67, 108, 29, 51};
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(int);
// 遍历数组
printf("当前数组中的元素是:");
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// 无限循环
while (true) {
// 统计的数字
int num;
// 统计数字出现的次数
int count = 0;
// 输入数字
printf("请输入要统计的数字:");
scanf("%d", &num);
// 0 作为结束条件
if (num == 0) {
break;
}
// 遍历数组,并计数
for (int i = 0; i < length; i++) {
if (arr[i] == num) {
count++;
}
}
printf("您输入的数字 %d 在数组中出现了 %d 次\n", num, count);
}
return 0;
}
```
### 2.7.4 应用示例
* 需求:将数组 a 中的全部元素复制到数组 b 中。
* 示例:
```c
#include <stdio.h>
#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 应用示例
* 需求:数组对称位置的元素互换。
* ① **优化数据处理**
* 分块处理大数据集:如果需要处理大数据集,可以将数据分块处理,而不是一次性加载整个数据集到内存中。例如:处理大型文件时,可以逐行读取或分批读取。
* 使用流式处理对于需要处理大量数据的操作可以采用流式处理streaming这样只保留当前处理的部分数据在内存中而非全部数据。
* ② **管理对象生命周期**
* 及时释放不再使用的对象在使用动态分配内存的编程语言C++、C#、Java 等确保在对象不再需要时及时释放内存。即使在使用垃圾回收机制的语言Java、Python也要尽量避免保留对不必要对象的引用以便垃圾回收器可以及时清理它们。
* 使用智能指针或自动内存管理在手动管理内存的编程语言中使用智能指针C++中的`std::unique_ptr`或`std::shared_ptr`)来自动管理内存,减少内存泄漏的风险。
* ③ **优化算法**
* 选择更高效的算法对于需要大量计算或数据处理的任务选择内存占用更少的算法。例如尽量使用原地in-place算法它们不需要额外的内存空间。
* 减少冗余数据:避免在内存中存储冗余数据,尽可能在计算过程中利用已有的数据结构,避免重复分配相同的数据。
* ④ **监控和调试**
* 使用内存分析工具在开发过程中使用内存分析工具Valgrind、VisualVM、Py-Spy等来监控程序的内存使用情况查找和修复内存泄漏或不必要的内存分配。
* 设置内存使用限制:在某些环境中,可以设置程序的最大内存使用量,这样当程序达到内存限制时,可以捕捉并处理内存溢出的情况。
* ⑤ **避免无限循环和递归**
- 设置循环或递归的终止条件:确保所有循环和递归都有明确的终止条件,避免因逻辑错误导致无限执行,从而耗尽内存。
- 使用尾递归优化:在支持尾递归优化的语言中,尽量使用尾递归,以减少递归调用带来的内存消耗。
* ⑥ **并发编程中的内存管理**
* 控制并发操作的内存分配:在并发编程中,尽量避免多个线程或进程同时大量分配内存。可以通过任务分配、锁机制等方式合理控制并发操作的内存使用。
* 避免死锁:确保在并发编程中避免死锁情况,因为死锁可能会导致内存资源无法被释放,从而引发内存溢出。
* ⑦ **使用适当的数据结构**
* 选择合适的数据结构:根据需要选择内存效率更高的数据结构。例如,使用数组而不是链表来存储连续的数据,使用哈希表来提高查找效率等。
* 避免不必要的缓存:在程序中使用缓存时,确保缓存的大小是合理的,并且有清理机制,防止缓存占用过多内存。
> [!NOTE]
>
> 思路:假设数组一共有 10 个元素,那么:
>
> * a[0] 和 a[9] 互换。
> * a[1] 和 a[8] 互换。
> * ...
>
> 规律就是 `a[i] <--互换--> arr[arr.length -1 -i]`
> 避免内存溢出通常需要良好的内存管理实践,如:优化数据处理算法、合理控制资源分配、以及定期检查和释放不再使用的内存。
## 2.4 总结
* `内存溢出`则是由于内存资源耗尽,程序试图分配新内存时失败,通常会导致程序的`立即`崩溃或异常终止。
* 示例:
```c
#include <stdio.h>
int main() {
// 原始数组
int arr[] = {12, 2, 31, 24, 15, -36, 67, 108, 29, 51};
// 计算数组的长度
size_t SIZE = sizeof(arr) / sizeof(arr[0]);
// 打印原始数组中的全部元素
printf("原始数组:");
for (int i = 0; i < SIZE; i++) {
printf("%d ", arr[i]);
}
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;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
// 原始数组
int arr[] = {12, 2, 31, 24, 15, -36, 67, 108, 29, 51};
// 计算数组的长度
size_t SIZE = sizeof(arr) / sizeof(arr[0]);
// 打印原始数组中的全部元素
printf("原始数组:");
for (int i = 0; i < SIZE; i++) {
printf("%d ", arr[i]);
}
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;
}
```
### 2.7.6 应用示例
* 需求:将数组中的最大值移动到数组的最末尾。
> [!NOTE]
>
> 思路:从数组的下标 `0` 开始依次遍历到 `length - 1` ,如果 `i` 下标当前的值比 `i+1` 下标的值大,则交换;否则,就不交换。
* 示例:
```c
#include <stdio.h>
int main() {
// 原始数组
int arr[] = {12, 2, 31, -24, 15, -36, 67, 891, 29, 51};
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(arr[0]);
// 打印原始数组中的全部元素
printf("原始数组:");
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
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;
}
```
### 2.7.7 应用示例
* 需求:实现冒泡排序,即将数组的元素从小到大排列。
> [!NOTE]
>
> 思路:一层循环,能实现最大值移动到数组的最后;那么,二层循环(控制内部循环数组的长度)就能实现将数组的元素从小到大排序。
* 示例:
```c
#include <stdio.h>
int main() {
// 原始数组
int arr[] = {12, 2, 31, -24, 15, -36, 67, 891, 29, 51};
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(arr[0]);
// 打印原始数组中的全部元素
printf("原始数组:");
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
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;
}
```
### 2.7.8 应用示例
* 需求:数组中的元素是从小到大排列的,现在要求根据指定的元素获取其在数组中的位置。
> [!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;
}
```
# 第三章:多维数组(⭐)
# 第三章:内存泄漏 VS 内存溢出
## 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、...
* 二维数组要想访问某个元素,先要获取某个一维数组,然后在一维数组中获取对应的数据。
## 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]
>
> * ① C 语言中的一维数组或多维数组都是占用的一整块连续的内存空间其它编程语言可不是这样的Java 等。
> * ② 在实际开发中,最为常用的就是二维数组或三维数组了,以二维数组居多!!!
> 之所以报错的原因,在于 WSL2 中的 Ubuntu 的内核是定制化的(微软自己维护的),并非 Ubuntu 的母公司 Canonical 发布的标准内核,所以需要我们手动编译安装。
## 3.2 二维数组的定义
* 查看内核版本:
### 3.2.1 动态初始化
* 语法:
```c
数据类型 数组名[几个⼀维数组元素][每个⼀维数组中有几个具体的数据元素];
```shell
uname -sr
```
> [!NOTE]
>
> * ① 二维数组在实际开发中,最为常见的应用场景就是表格或矩阵了。
> * ② 几个一维数组元素 = 行数。
> * ③ 每个⼀维数组中有几个具体的数据元素 = 列数。
![](./assets/19.gif)
* 设置环境变量,方便后续引用:
* 示例:
```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;
}
```shell
export KERNEL_VERSION=$(uname -r | cut -d'-' -f1)
```
### 3.2.2 静态初始化 1
![](./assets/20.gif)
* 语法:
* 安装依赖库:
```c
数据类型 数组名[行数][列数] = {{元素1,元素2,...},{元素3,...},...}
```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
```
> [!NOTE]
>
> * ① 行数 = 几个一维数组元素。
> * ② 列数 = 每个⼀维数组中有几个具体的数据元素。
![](./assets/21.gif)
* 下载源码:
* 示例:
```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;
}
```shell
git clone \
--depth 1 \
--single-branch --branch=linux-msft-wsl-${KERNEL_VERSION} \
https://github.com/microsoft/WSL2-Linux-Kernel.git
```
### 3.2.3 静态初始化 2
![](./assets/22.gif)
* 语法:
* 编译内核代码:
```c
数据类型 数组名[][列数] = {{元素1,元素2,...},{元素3,...},...}
```shell
cd WSL2-Linux-Kernel
```
> [!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;
}
```shell
make -j $(nproc) KCONFIG_CONFIG=Microsoft/config-wsl
```
## 3.3 二维数组的理解
![](./assets/23.gif)
* 如果二维数组是这么定义的,即:
* 编译 perf 工具:
```c
int arr[3][4];
```shell
cd tools/perf
```
* 那么,这个二维数组 `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;
}
```shell
make clean && make
```
## 3.5 二维数组的内存分析
![](./assets/24.gif)
* 用`矩阵形式`3 行 4 列形式)表示二维数组,是`逻辑`上的概念,能形象地表示出行列关系。而在`内存`中,各元素是连续存放的,不是二维的,是`线性`的。
* 复制到 PATH 变量所指向的路径中:
* 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;
}
```shell
cp perf /usr/bin/
```
![](./assets/25.gif)
### 4.2.3 整合
* CLion 中配置 perf 的路径:
![](./assets/26.png)
* 在 CLion 中通过 perf 运行代码:
![](./assets/27.gif)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 186 KiB

View File

Before

Width:  |  Height:  |  Size: 628 KiB

After

Width:  |  Height:  |  Size: 628 KiB

View File

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Before

Width:  |  Height:  |  Size: 868 KiB

After

Width:  |  Height:  |  Size: 868 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 315 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 819 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -1,43 +1,104 @@
# 第一章:字符串(⭐)
# 第一章:数组的概念
## 1.1 概述
## 1.1 为什么需要数组?
* 在实际开发中,我们除了经常处理整数、浮点数、字符等,还经常和字符串打交道,如:`"Hello World"`、`"Hi"` 等。
### 1.1.1 需求分析 1
> [!NOTE]
>
> 像这类`"Hello World"`、`"Hi"`等格式 ,使用`双引号`引起来的一串字符称为字符串字面值,简称字符串。
* 对于整数、浮点数和字符C 语言中都提供了对应的数据类型。但是对于字符串C 语言并没有提供对应的数据类型,而是用`字符数组`来存储这类文本类型的数据,即字符串:
* 需要统计某公司 50 个员工的工资情况,例如:计算平均工资、最高工资等。如果使用之前的知识,我们需要声明 50 个变量来分别记录每位员工的工资,即:
```c
char str[32];
#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;
}
```
* 字符串不像整数、浮点数以及字符那样有固定的大小,字符串是不定长的,如:`"Hello World"`、`"Hi"` 等的长度就是不一样的。在 C 语言中,规定了字符串的结尾必须是 `'\0'` ,这种字符串也被称为 `C 风格的字符串`,如:
* 这样会感觉特别机械和麻烦全是复制Ctrl + c和粘贴Ctrl + vCV 大法);此时,我们就可以将所有的`数据`全部存储到一个`容器(数组)`中进行统一管理,并进行其它的操作,如:求最值、求平均值等,如下所示
```c
"Hello World!" // 在 C 语言中,底层存储就是 Hello World!\0
#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)
* `'\0'` 在 ASCII 码表中是第 `0` 个字符,用 `NUL` 表示,称为空字符,该字符既不能显示,也不是控制字符,输出该字符不会有任何效果,它在 C 语言中仅作为字符串的结束标志。
* C 语言在处理字符串时,会从前往后逐个扫描字符,一旦遇到`'\0'`就认为到达了字符串的末尾,就结束处理。`'\0'`至关重要,没有`'\0'`就意味着永远也到达不了字符串的结尾。
* 同样的道理,如果我们使用变量来存储每个商品信息,那么就需要非常多的变量;但是,如果我们将这些`商品信息`都存储到一个`容器(数组)`中,进行统一管理;那么,之后的数据处理将会非常方便。
### 1.1.3 容器的概念
* `生活中的容器`:水杯(装水、饮料的容器)、衣柜(装衣服等物品的容器)、集装箱(装货物等物品的容器)。
* `程序中的容器`:将多个数据存储到一起,并且每个数据称为该容器中的元素。
## 1.2 什么是数组?
* 数组Array是将多个`相同数据类型`的`数据`按照一定的顺序排序的`集合`,并使用一个`标识符`命名,以及通过`编号(索引,亦称为下标)`的方式对这些数据进行统一管理。
![](./assets/2.png)
## 1.3 数组的相关概念
* `数组名`:本质上是一个标识符常量,命名需要符合标识符规则和规范。
* `元素`:同一个数组中的元素必须是相同的数据类型。
* `索引(下标)`:从 0 开始的连续数字。
* `数组的长度`:就是元素的个数。
## 1.4 数组的特点
* ① 创建数组的时候,会在内存中开辟一整块`连续的空间`,占据空间的大小,取决于数组的长度和数组中元素的类型。
* ② 数组中的元素在内存中是依次紧密排列且有序的。
* ③ 数组一旦初始化完成,且长度就确定的,并且`数组的长度一旦确定,就不能更改`。
* ④ 我们可以直接通过索引(下标)来获取指定位置的元素,速度很快。
* ⑤ 数组名中引用的是这块连续空间的首地址。
# 第二章:数组的操作(⭐)
## 2.1 数组的定义
### 2.1.1 动态初始化
* 语法:
```c
数据类型 数组名[元素个数|长度];
```
> [!NOTE]
>
> 在现代化的高级编程语言中都提供了字符串对应的类型Java 中的 StringJDK 11 之前,底层也是通过 `char[]` 数组来实现的) 。
## 1.2 字符数组(字符串)的定义
### 1.2.1 标准写法
* 手动在字符串的结尾添加 `'\0'`作为字符串的结束标识。
> * ① 数据类型:表示的是数组中每一个元素的数据类型。
> * ② 数组名:必须符合标识符规则和规范。
> * ③ 元素个数或长度:表示的是数组中最多可以容纳多少个元素(不能是负数、也不能是 0 )。
@ -47,655 +108,64 @@ char str[32];
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
// 字符数组,不是字符串
char c1[] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'};
// C 风格的字符串
char c2[] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\0'};
// 先指定元素的个数和类型,再进行初始化
// 定义数组
int arr[3];
// 给数组元素赋值
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
return 0;
}
```
### 1.2.2 简化写法(推荐)
### 2.1.2 静态初始化 1
* 字符串写成数组的形式非常麻烦。C 语言中提供了一种简化写法,即:双引号中的字符,会自动视为字符数组。
> [!NOTE]
>
> 简化写法会自动在末尾添加 `'\0'` 字符,强烈推荐使用!!!
* 示例:
* 语法:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
char c1[] = {"Hello World"}; // 注意使用双引号,非单引号
char c2[] = "Hello World"; // //可以省略一对 {} 来初始化数组元素
return 0;
}
数据类型 数组名[元素个数|长度] = {元素1,元素2,...}
```
## 1.3 字符串的输入和输出
### 1.3.1 字符串的输出
* 在 C 语言中,有两个函数可以在控制台上输出字符串,它们分别是:
* ① `puts()`:输出字符串并自动换行,并且该函数只能输出字符串。
* ② `printf()` :通过格式占位符 `%s`,就可以输出字符串,不能自动换行。
> [!NOTE]
>
> * ① `printf()` 函数除了输出字符串之外,还可以输出`其它类型`的数据。
> * ② 在实际开发中,`printf()` 函数用的居多!!!
> * ① 静态部分初始化:如果数组初始化的元素个数`小于`数组声明的长度,那么就会从数组开始位置依次赋值,不够的就补 0 。
> * ② 静态全部初始化:数组初始化的元素个数`等于`数组的长度。
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
char c1[] = {"Hello World"}; // 注意使用双引号,非单引号
char c2[] = "Hello World"; // //可以省略一对 {} 来初始化数组元素
puts(c1); // Hello World
puts(c2); // Hello World
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
char c1[] = {"Hello World"}; // 注意使用双引号,非单引号
char c2[] = "Hello World"; // //可以省略一对 {} 来初始化数组元素
printf("c1 = %s\n", c1); // c1 = Hello World
printf("c2 = %s\n", c2); // c2 = Hello World
return 0;
}
```
### 1.3.2 字符串的输入
* 在 C 语言中,有两个函数可以让用户从键盘输入字符串,它们分别是:
* ① ~~`gets()`:直接输入字符串,并且只能输入字符串~~
* ② `scanf()`:通过格式占位符 `%s`,就可以输入字符串了。
> [!NOTE]
> [!TIP]
>
> * ① `scanf()` 在通过格式占位符 `%s`,读取字符串时以`空格`或 `Enter` 键为分隔,遇到`空格`或 `Enter` 键就认为当前字符串结束了,所以无法读取含有空格的字符串。但是,我们可以将格式占位符,使用 `%[^\n]`来代替 `%s` ,这样就能解决 `scanf()` 函数默认的缺陷。
> * ② `gets()` 认为空格也是字符串的一部分,只有遇到回车键时才认为字符串输入结束。换言之,不管输入了多少个空格,只要不按下回车键,对 `gets()` 来说就是一个完整的字符串。
> * ③ 需要注意的是,`gets()` 函数在 [C11](https://zh.cppreference.com/w/c/11) 标准中,已经被移除了,推荐使用 `fgets` 来代替它,因为有严重的安全漏洞,即:`gets()` 函数读取用户输入直到换行符,但它不会检查缓冲区的大小。这意味着如果用户输入超过了缓冲区的大小,`gets()` 将会导致缓冲区溢出。这种缓冲区溢出很容易被恶意利用,导致程序崩溃或执行恶意代码。
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
char str[32] = {'\0'};
printf("请输入字符串:");
gets(str);
printf("字符串是:%s\n", str);
return 0;
}
```
* 示例:
```c {13}
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
char str[32] = {'\0'};
printf("请输入字符串:");
// scanf() 在读取数据时需要的是数据的地址,这一点是恒定不变的。
// 对于 int、char、float 等类型的变量都要在前边添加 & 以获取它们的地址。
// 而数组或者字符串用于 scanf() 时不用添加 &,它们本身就会转换为地址。
scanf("%[^\n]", str);
printf("字符串是:%s\n", str);
return 0;
}
```
## 1.4 字符串结束不是 `'\0'` 的后果
* 有的时候,程序的逻辑要求我们必须逐个字符为数组赋值,这个时候就很容易遗忘字符串结束标识 `'\0'`,如下所示:
```c {3}
#include <stdio.h>
int main() {
char str[30];
char c;
int i;
for (c = 65, i = 0; c <= 90; c++, i++) {
str[i] = c;
}
printf("%s\n", str);
return 0;
}
```
* 该程序的执行结果,如下所示:
![](./assets/3.png)
* 因为`大写字符`在 `ASCII` 码表是连续的,编码值从 `65` 开始,直到 `90` 结束;并且,为了方便,我们使用了循环。但是,我们却发现结果和我们想要的大不一样,为什么?
> [!NOTE]
> 在 CLion 中开启`嵌入提示(形参名称-->显示数组索引的提示)`功能,即:
>
> * ① 在函数内部定义的变量、数组、结构体、共用体等都称为局部数据。
> ![](./assets/3.png)
>
> * ② 在很多编译器下,局部数据的初始值都是随机的、无意义的,而不是我们通常认为的“零”值。
* 我们在定义 `str` 数组的时候,并没有立即初始化,所以它包含的值都是随机的,只有很小的概率是“零”。循环结束后,`str` 的前 `26` 个元素被赋值了,剩下的 `4` 个元素的值依然是随机的,我们并不清楚到底是什么。
* `printf()` 输出字符串时,会从第 `0` 个元素开始往后检索,直到遇见`'\0'`才停止,然后把`'\0'`前面的字符全部输出,这就是 `printf()` 输出字符串的原理。
* 但是,对于上面的例子,由于我们并没有对最后 `4` 个元素赋值,所以第 `26` 元素可能是 `'\0'`,也有可能第 `27` 个元素是 `'\0'`,也有可能第 `28` 个元素是 `'\0'`;不过,不要问`我``我`也不清楚,可能只有`上帝`才会知道,到底第`几`个元素才是 `'\0'`。而且,我们在定义数组的时候,设置数组的长度是 `30` ,但是貌似输出的字符串的长度是 `32` ,这早已超出了数组的范围,`printf()` 在输出字符串的时候,如果没有遇见 `'\0'` 是不会罢休的,它才不会管`数组访问`是不是`越界`。
> [!NOTE]
> 这样,在 CLion 中,将会显示数组初始化时每个元素对应的索引,即:
>
> * ① 由此可见,不注意`'\0'`的后果有多严重,不但不能正确处理字符串,甚至还会毁坏其它数据!!!
> * ② C 语言为了提高效率保证操作的灵活性并不会对越界行为进行检查即使越界了也能够正常编译只有在运行期间才可能发现问题所以对程序员的要求很高。但是现代化的高级编程语言Java 等,为了降低开发难度以及提高开发效率,像数组这种越界行为,在编译期间就会由编译器提前捕获,并直接报错!!!
> ![](./assets/4.png)
* 如果要避免这些问题也很简单,在字符串后面手动添加 `'\0'` 就可以了,即:
```c {9}
#include <stdio.h>
int main() {
char str[30];
char c;
int i;
for (c = 65, i = 0; c <= 90; c++, i++) {
str[i] = c;
}
str[i] = '\0';
printf("%s\n", str);
return 0;
}
```
* 但是,上述的写法实在麻烦,为什么不在定义数组的时候,给数组中的每个元素都初始化,这样才能从根本上避免上述问题,即:
```c {3}
#include <stdio.h>
int main() {
char str[30] = {'\0'};
char c;
int i;
for (c = 65, i = 0; c <= 90; c++, i++) {
str[i] = c;
}
printf("%s\n", str);
return 0;
}
```
## 1.5 字符串的长度
* 所谓字符串的长度,就是字符串包含了多少个字符(不包括最后的结束符`'\0'`),如:`"abc"` 的长度是 `3` ,而不是 `4`
* 在 C 语言中的 `string.h` 中提供了 `strlen()` 函数,能够帮助我们获取字符串的长度,如下所示:
```c
size_t strlen (const char *__s)
```
* 示例:
```c {13}
#include <stdio.h>
#include <string.h>
int main() {
char str[30] = {'\0'};
char c;
int i;
for (c = 65, i = 0; c <= 90; c++, i++) {
str[i] = c;
}
// ABCDEFGHIJKLMNOPQRSTUVWXYZ
printf("%s\n", str);
// ABCDEFGHIJKLMNOPQRSTUVWXYZ 的长度是 26
printf("%s 的长度是 %zu\n", str, strlen(str));
return 0;
}
```
# 第二章:内存中的变量和数组(⭐)
## 2.1 内存和内存地址
### 2.1.1 内存
* `内存`是一种`计算机硬件`,是`软件`在`运行过程`中,用来`临时存储数据`的。在生活中,最为常见的`内存`就是`随机存取存储器RAM内存条`),其特点如下所示:
* ① 生活中最常见的内存类型,用于存储当前运行的程序和数据。
* ② 内存是易失性存储器,这意味着断电后数据会丢失。
* ③ 它具有高速读写特性,适用于需要快速访问的操作。
* 内存条的外观,如下所示:
![](./assets/4.jpeg)
* 像我们平常使用`记事本`软件一样,当我们输入一些文字的时候,其实是将数据`临时`保存在内存中的,如下所示:
> [!NOTE]
>
> * ① 目前,很多软件都很智能,如果用户没有将数据到保存文件中,将显示红色,以警告用户还没有保存数据,提醒用户需要尽快保存数据!!!
> * ② 但是也有很多软件提供了自动保存数据的功能其原理就是定时1s、3s、5s将内存中的数据刷新到文件中以防止数据丢失
> * ③ 将数据从内存存储到文件中,专业的说法是落盘(落在磁盘上)。
![](./assets/5.gif)
* 此时,如果我们在没有保存的过程下,将`记事本`软件关闭,那么刚才输入的文字将丢失;下次,再打开同样的文件(将数据从磁盘加载进内存,再交给 CPU之前输入的文字将不复存在如下所示
> [!NOTE]
>
> * ① 目前,很多软件都很智能,如果你没有保存,将提醒你是否保存或丢失刚才输入的文字。
> * ② 但是也有很多软件提供了自动保存数据的功能其原理就是定时1s、3s、5s将内存中的数据刷新到文件中以防止数据丢失
> * ③ 将数据从内存存储到文件中,专业的说法是落盘(落在磁盘上)。
![](./assets/6.gif)
> [!IMPORTANT]
>
> 内存就是软件在运行过程中,用来临时存储数据的,最为重要的两个步骤就是:
>
> * ① 将数据`保存`到内存中。
> * ② 从内存中的`对应位置`将数据`取出来`。
### 2.1.2 内存地址
* 在这个计算机的内存条,动不动就 32GB、64GB 、128GB 或更高的年代,如下所示:
![](./assets/7.png)
* 如果有一个 int 4 个字节)类型的数据 `2` ,如何将这个数据保存到内存中?(对应上述的步骤 ①)
![](./assets/8.svg)
* 就算数据 `2` 已经保存到内存中,那么内存中那么多的数据,我们又该如何取出呢?(对应上述的步骤 ②)
![](./assets/9.svg)
> [!IMPORTANT]
>
> 答案就是`内存地址`。
* 操作系统为了更快的去管理内存中的数据,会将`内存条`按照`字节`划分为一个个的`单元格`,如下所示:
![](./assets/10.svg)
> [!NOTE]
>
> 计算机中存储单位的换算,如下所示:
>
> - 1 B = 8 bit。
> - 1 KB = 1024 B。
> - 1 MB = 1024 KB。
> - 1 GB = 1024 MB。
> - 1 TB = 1024 GB 。
> - ……
* 为了方便管理,每个独立的小单元格,都有自己唯一的编号(内存地址),如下所示:
![](./assets/11.svg)
* 之所以,加了`内存地址`,就能`加快`数据的存取速度,可以类比生活中的`字典`
* 如果没有使用`拼音查找法`或`部首查找法`,我们需要一页一页,一行一行的,在整个字典中去搜索我们想要了解的汉字,效率非常低(如果要搜索的汉字在最后一页,可能需要将整个字典从头到尾翻一遍,这辈子真有可能翻得完?)。
![](./assets/12.gif)
* 如果使用`拼音查找法`或`部首查找法`,我们可以很快的定位到所要了解汉字所在的页数,加快了搜索的效率。
![](./assets/13.jpg)
![](./assets/14.jpg)
* 同样的道理,如果`没有`内存地址,我们只能一个个的去寻找想要的数据,效率非常低下,如下所示:
![](./assets/15.gif)
* 如果`使用`内存地址,我们就可以直接定位到指定的数据,效率非常高,如下所示:
![](./assets/16.gif)
> [!IMPORTANT]
>
> * ① 内存地址是计算机中用于标识内存中某个特定位置的数值。
> * ② 每个内存单元都有一个唯一的地址,这些地址可以用于访问和操作存储在内存中的数据。
* 实际中的内存地址,并不是像上面的 `001` 、`002` 、... 之类的数字,而是有自己的规则,即:内存地址规则。
> [!NOTE]
>
> * ① 32 位的操作系统中,内存地址以 32 位的二进制表示。
> * ② 64 位的操作系统中,内存地址以 64 位的二进制表示。
* 在 32 位的操作系统中,内存地址的范围是:`0000 0000 0000 0000 0000 0000 0000 0000` ~ `1111 1111 1111 1111 1111 1111 1111 1111` 2 ^ 32 次方)。
> [!NOTE]
>
> 在 32 位的操作系统中,一共有 4,294,967,296 个内存地址,其最大支持的内存大小是 4,294,967,296 字节,即 4 GB 。
* 在 64 位的操作系统中,内存地址的范围是:`0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000` ~ `1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111` 2 ^ 64 次方)。
> [!NOTE]
>
> * ① 在 64 位的操作系统中,一共有 18,446,744,073,709,551,616 个内存地址,其最大支持的内存大小是 18,446,744,073,709,551,616 字节,即 17,179 TB 。
> * ② 虽然,从理论上 64 位的操作系统支持的内存最大容量是 17,179 TB但是实际操作系统会有所限制win11 的 64 位支持的最大内存是 128 GB ~ 6 TBLinux 64 位支持的最大内存通常是 256 TB。
* 在实际开发中64 位操作系统的内存地址表现形式,如:`0000 0000 0000 0000 0001 0000 1111 1010 0000 0000 0010 0000 0000 0010 0000 1000`,实在是太长了,我们通常转换为十六进制,以方便阅读,如:`0x000010FA00200208` 。
> [!IMPORTANT]
>
> * ① 内存地址是内存中每个单元的编号。
>* ② 内存地址的作用是操作系统用来快速管理内存空间的。
> * ③ 在 32 位操作系统上,内存地址以 32 位的二进制数字表示,最大支持的的内存是 4 GB所以 32 位操作系统已经被淘汰。
> * ④ 在 64 位操作系统上,内存地址以 64 位的二进制数字表示,由于表示形式太长,我们通常会转为十六进制,以方便阅读。
## 2.2 内存中的变量
* 在 C 语言中数据类型的种类很多short、int、long、float、double、char 等。以 int 类型为例,在 32 位或 64 位操作系统中的int 类型的变量都是占 4 个字节,当我们在代码中这么定义变量,如:
```c
#include <stdio.h>
int main(){
// 定义一个变量并初始化
int a = 10;
return 0;
}
```
* 那么,编译器就会这么处理,如下所示:
![](./assets/17.svg)
* 在代码中,我们可以使用 `&变量名` 来获取一个变量的内存首地址,如下所示:
* 示例:静态部分初始化
```c
#include <stdio.h>
int main() {
// 定义一个变量并初始化
int a = 10;
printf("变量 a 的首地址是: %p\n", &a); // 变量 a 的首地址是: 0000002bf1dffd0c
printf("变量 a 的中保存的值是: %d\n", a); // 变量 a 的中保存的值是: 10
// 定义数组和部分初始化:
// 会将给定的值从数组的开始位置一个个的赋值,没有赋值的地方,用 0 填充
int arr[5] = {1, 2};
return 0;
}
```
> [!NOTE]
>
> * ① `变量`是对程序中`数据`在内存中存储空间的抽象,如果不涉及到指针变量,那我们在编码的时候,就只需要将变量等价于内存中存储空间里面存储的数据,而不需要再去考虑编译器底层是如何转换,提高了开发效率(机器语言和汇编可不是这样的,需要关注每个细节)。
> * ② 数据类型只在`定义`变量的时候`指定`,而且必须指定;`使用`变量的时候`无需`再声明,因为此时的数据类型已经确定了。
## 2.3 内存中的数组
* 如果我们在代码中这么定义数组,如下所示:
```c
#include <stdio.h>
int main(){
// 定义一个数组并初始化
int arr[] = {1,2,3};
return 0;
}
```
* 那么,编译器就会这么处理,如下所示:
![](./assets/18.svg)
* 在代码中,我们可以直接打印数组名的内存地址,如下所示:
```c
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3};
printf("arr 的首地址是: %p \n", arr); // arr 的首地址是: 0000003a6f7ffcd4
printf("arr 的首地址是: %p \n", &arr); // &arr 的地址是: 0000003a6f7ffcd4
printf("arr[0] 的地址是: %p \n", &arr[0]); // arr[0] 的地址是: 0000003a6f7ffcd4
printf("arr[1] 的地址是: %p \n", &arr[1]); // arr[1] 的地址是: 0000003a6f7ffcd8
printf("arr[2] 的地址是: %p \n", &arr[2]); // arr[2] 的地址是: 0000003a6f7ffcdc
return 0;
}
```
> [!WARNING]
>
> 在上述示例中,`arr` 和 `&arr` 的值是一样的,但是对应的含义是不同的。
>
> * ① `arr` 是数组名,在大多数情况下会转换为数组第一个元素的地址,即:`arr` 等价于 `&arr[0]`,其数据类型是 `int *`。
> * ② `&arr`是数组名的地址,即整个数组的地址,它指向数组本身,并不是数组第一个元素的地址,`&arr` 的数据类型是 `int(*)[3]`。
# 第三章:数组越界和数组溢出(⭐)
## 3.1 数组越界
* C 语言的数组是静态的,当我们定义的时候,就不能自动扩容。当我们试图访问数组的`负索引`或`超出`数组长度的索引时,就会产生`数组越界`。
> [!NOTE]
>
> * ① C 语言为了提高效率,保证操作的灵活性,并不会对越界行为进行检查,即使越界了,也能够正常编译,只有在运行期间才可能发现问题,所以对程序员的要求很高。
> * ② 但是现代化的高级编程语言Java 等,为了降低开发难度以及提高开发效率,像数组这种越界行为,在编译期间就会由编译器提前捕获,并直接报错!!!
* 请看下面的代码:
```c {9-10,15}
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
int arr[3] = {10, 20, 30};
printf("arr[-1] = %d\n", arr[-1]); // arr[-1] = -23718968
printf("arr[-2] = %d\n", arr[-2]); // arr[-2] = 0
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
printf("arr[3] = %d\n", arr[3]); // arr[3] = -23718976
printf("arr[4] = %d\n", arr[4]); // arr[4] = 605
return 0;
}
```
* 越界访问数组元素的值都是不确定的,没有实际的含义,因为在数组之外的内存,我们并不知道到底是什么,可能是其它变量的值,可能是函数参数,也可能是一个地址,这些都是不可控的。
> [!NOTE]
>
> 由于 C 语言的”放任“,我们访问数组时必须非常小心,要确保不会发生越界。
* 当发生数组越界时,如果我们对该内存有使用权限,那么程序将正常运行,但会出现不可控的结果,即:如果我们对该内存没有使用权限,或者该内存压根就没有就分配,那么程序就会崩溃,如下所示:
```c
#include <stdio.h>
int main() {
int arr[3] = {0};
printf("%d", arr[10000]);
return 0;
}
```
* 其结果,如下所示:
![](./assets/19.png)
> [!NOTE]
>
> * ① 每个程序能使用的内存都是有限的,该程序要访问 `4*10000` 字节处的内存,显然太远了,超出了程序的访问范围。
> * ② 这个地方的内存可能没有被分配,可能是系统本身占用的内存,可能是其它数据的内存,如果放任这种行为,将带来非常危险的后果,操作系统只能让程序停止运行。
* 当然,我们在实际开发中,也不会这么访问,而是会使用 `sizeof` 运算符来获取数组的长度,进而遍历数组中的元素,即:
```c {7}
#include <stdio.h>
int main() {
int arr[3] = {0};
// 获取数组的元素
size_t length = sizeof(arr) / sizeof(int);
for (size_t i = 0; i < length; i++) {
printf("%d\n", arr[i]);
}
return 0;
}
```
## 3.2 数组溢出
* 数组溢出通常是指将数据存储到一个数组中,超出了数组所能容纳的空间,那么多余的元素就会被丢弃。对于一般的数组,貌似没什么问题,如下所示:
```c
#include <stdio.h>
int main() {
int arr[3] = {0, 1, 2, 3, 4};
size_t length = sizeof(arr) / sizeof(int);
for (size_t i = 0; i < length; i++) {
printf("%d\n", arr[i]);
}
return 0;
}
```
* 其结果,如下所示:
![](./assets/20.png)
* 但是,对于字符串而言,就会出现不可控的情况,如下所示:
```c {4}
#include <stdio.h>
int main()
{
char str[10] = "Hello WorldHello WorldHello World";
puts(str);
return 0;
}
```
* 其结果,如下所示:
![](./assets/21.png)
* 因为字符串的长度大于数组的长度,数组只能容纳字符串前面的一部分,即使编译器在字符串最后保存了 `'\0'`,也无济于事,因为超过数组长度的元素都会被丢弃。而 `printf()` 输出字符串时,会从第 `0` 个元素开始往后检索,直到遇见`'\0'`才停止,然后把`'\0'`前面的字符全部输出,至于何时遇到 `'\0'`,就只有上帝才能知道。
> [!NOTE]
>
> * ① 在用字符串给字符数组赋值时,要保证数组长度大于字符串长度,以容纳结束符`'\0'`。
> * ② `数组溢出`通常发生在动态分配内存或者通过不安全的函数(如: `strcpy`)进行字符串操作。
# 第四章C 语言中的数组 VS Java 语言中的数组(⭐)
## 4.1 Linux 下 32 位环境的用户空间内存分布情况
* 对于 32 位的环境而言,理论上程序是可以拥有 4GB 的虚拟地址空间的,在 C 语言中使用到的变量、函数、字符串等都会对应内存中的一块区域。
* 但是,在这 4GB 的地址空间中,要拿出一部分给操作系统内核使用,应用程序无法直接访问这一段内存,这一部分内存地址被称为`内核空间`Kernel Space
> [!NOTE]
>
> - ① Windows 在默认情况下会将高地址的 2GB 空间分配给内核(也可以配置为 1GB
> - ② 而 Linux 默认情况下会将高地址的 1GB 空间分配给内核。
* 也就是说,应用程序只能使用剩下的 2GB 或 3GB 的地址空间,称为`用户空间`User Space
* Linux 下 32 位环境的经典内存模型,如下所示:
![](./assets/22.svg)
* 各个内存分区的说明,如下所示:
| 内存分区 | 说明 |
| :------------------------ | :----------------------------------------------------------- |
| 程序代码区code | 存储程序的执行代码,通常为只读区,包含程序的指令。 程序启动时,这部分内存被加载到内存中,并不会在程序执行期间改变。 |
| 常量区constant | 存放程序中定义的常量值,通常也是只读的,这些常量在程序运行期间不可修改。 |
| 全局数据区global data | 存储程序中定义的全局变量和静态变量。 这些变量在程序的整个生命周期内存在,且可以被修改。 |
| 堆区heap | 用于动态分配内存,例如:通过 `malloc``new` 分配的内存块。 堆区的内存由程序员手动管理,负责分配和释放。 如果程序员不释放,程序运行结束时由操作系统回收。 |
| 动态链接库 | 动态链接库(如: `.dll``.so` 文件)被加载到内存中特定的区域,供程序运行时使用。 |
| 栈区stack | 用于存储函数调用的局部变量、函数参数和返回地址。 栈是自动管理的,随着函数的调用和返回,栈上的内存会自动分配和释放。 |
> [!NOTE]
>
> - ① 程序代码区、常量区、全局数据区在程序加载到内存后就分配好了,并且在程序运行期间一直存在,不能销毁也不能增加(大小已被固定),只能等到程序运行结束后由操作系统收回,所以全局变量、字符串常量等在程序的任何地方都能访问,因为它们的内存一直都在。
> - ② 函数被调用时,会将参数、局部变量、返回地址等与函数相关的信息压入栈中,函数执行结束后,这些信息都将被销毁。所以局部变量、参数只在当前函数中有效,不能传递到函数外部,因为它们的内存不在了。
> - ③ 常量区、全局数据区、栈上的内存由系统自动分配和释放,不能由程序员控制。程序员唯一能控制的内存区域就是`堆`Heap它是一块巨大的内存空间常常占据整个虚拟空间的绝大部分在这片空间中程序可以申请一块内存并自由地使用放入任何数据。堆内存在程序主动释放之前会一直存在不随函数的结束而失效。在函数内部产生的数据只要放到堆中就可以在函数外部使用。
## 4.2 C 语言中的数组
* 之前,我们都是这么使用数组的,如下所示:
* 示例:静态全部初始化
```c
#include <stdio.h>
@ -709,64 +179,1179 @@ int main() {
}
```
* 其实,这样定义的数组是在`栈`中的,而栈的内存空间是有限的,如果数组中的元素过多,将会出现 `Stack Overflow` 的现象,即:栈溢出。
### 2.1.3 静态初始化 2
* 语法:
```c
数据类型 数组名[] = {元素1,元素2,...}
```
> [!NOTE]
>
> * ① 栈内存的大小和编译器有关,编译器会为栈内存制定一个最大值。
> * ② 在 VS 中,默认是 1 MB在 GCC 下,默认是 8 MB。
> * ③ 虽然可以通过参数来修改栈内存的大小;但是,在实际开发中,我们一般也不会这么做。
> 没有给出数组中元素的个数,将由系统根据初始化的元素,自动推断出数组中元素的个数。
* 所以,在实际开发中,如果我们要使用数组,就需要在`堆`中开辟内存空间,因为堆中的内存空间是可以动态扩容和缩容的,只不多在 C 语言中对于堆中申请的内存空间,需要程序员在用完之后,手动释放掉;否则,将会造成内存泄漏现象。
* 示例:
```c
#include <stdio.h>
#include <stdlib.h>
int main() {
int n; // 数组的大小
printf("请输入数组的大小: ");
scanf("%d", &n);
// 使用 malloc 申请内存,申请 n 个 int 类型的空间
int *array = (int *)malloc(n * sizeof(int));
// 检查 malloc 是否成功
if (array == NULL) {
printf("内存分配失败!\n");
return 1; // 程序退出
}
// 初始化数组并输出
for (int i = 0; i < n; i++) {
array[i] = i + 1; // 简单赋值操作
printf("array[%d] = %d\n", i, array[i]);
}
// 使用完毕后,释放内存
free(array);
// 指定元素的类型,不指定元素个数,同时进行初始化
int arr[] = {1, 2, 3, 4, 5};
return 0;
}
```
## 4.3 Java 语言中的数组
### 2.1.4 静态初始化 3
* Java 语言和 C 语言不同Java 语言从语法层面就将数组在内存中的分配放到了`堆`中
* 在 C 语言中,也可以只给部分元素赋值。当 {} 中的值少于元素的个数的时候,只会给前面的部分元素赋值,至于剩下的元素就会自动初始化为 0 。
```c
public class Test {
public static void main(String[] args){
// 在堆内存开辟数组,使用完毕后,不需要手动回收对应的内存空间
int[] arr = new int[4] ;
}
}
int arr[10] = {1,2,3,4,5};
```
> [!NOTE]
>
> * ① 在 Java 语言中,数组的内存分配是由 JVMJava Virtual MachineJava 虚拟机)自动管理的,开发者不需要像在 C 语言中那样手动调用 `malloc` 来申请内存。Java 提供了更加高级的内存管理机制,所有数组在堆中动态分配。
> * ② 在 Java 中声明和初始化数组的过程本质上就是在堆内存中分配数组内存的过程。每个数组在创建时都会被分配到堆中并且由垃圾回收机制Garbage CollectorGC自动负责内存的回收。
> * ③ 我们甚至可以理解为Java 语言是 C 语言在实际开发过程中的最佳实践版本。
> * ① 数组 `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`
* 示例:
```c
#include <stdio.h>
int main() {
int arr[10] = {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[5] = %d \n", arr[5]); // arr[5] = 0
printf("arr[6] = %d \n", arr[6]); // arr[6] = 0
printf("arr[7] = %d \n", arr[7]); // arr[7] = 0
printf("arr[8] = %d \n", arr[8]); // arr[8] = 0
printf("arr[9] = %d \n", arr[9]); // arr[9] = 0
return 0;
}
```
## 2.2 访问数组元素
* 语法:
```c
数组名[索引|下标];
```
> [!NOTE]
>
> 假设数组 `arr` 有 n 个元素,如果使用的数组的下标 `< 0``> n-1` ,那么将会产生数组越界访问,即超出了数组合法空间的访问;那么,数组的索引范围是 `[0,arr.length - 1]`
* 示例:
```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)
> [!NOTE]
>
> * ① 在很多编程语言中都内置了获取数组的长度的属性或方法Java 中的 arr.length 或 Rust 的 arr.len()。
> * ② 但是C 语言没有内置的获取数组长度的属性或方法,只能通过 sizeof 运算符间接来计算得到。
> * ③ 数组一旦`声明`或`定义`,其`长度`就`固定`了,`不能动态变化`。
* 示例:
```c
#include <stdio.h>
int main() {
// 定义数组和全部初始化:数组初始化的元素个数等于数组的长度。
int arr[] = {1, 2, 3, 4, 5};
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(arr[0]);
// 遍历数组
for (int i = 0; i < length; i++) {
printf("%d \n", arr[i]);
}
return 0;
}
```
## 2.5 遍历数组
* 遍历数组是指按顺序访问数组中的每个元素,以便读取或修改它们,编程中一般使用循环结构对数组进行遍历。
* 示例:声明一个存储有 12、2、31、24、15、36、67、108、29、51 的数组,并遍历数组所有元素
```c
#include <stdio.h>
int main() {
// 定义数组并初始化
int arr[] = {12, 2, 31, 24, 15, 36, 67, 108, 29, 51};
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(int);
// 遍历数组
for (int i = 0; i < length; i++) {
printf("%d\n", arr[i]);
}
return 0;
}
```
* 示例:声明长度为 10 的 int 类型数组,给数组元素依次赋值为 0 ~ 9 ,并遍历数组所有元素
```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;
}
// 遍历数组
for (int i = 0; i < length; i++) {
printf("%d\n", arr[i]);
}
return 0;
}
```
## 2.6 一维数组的内存分析
### 2.6.1 数组内存图
* 假设数组是如下的定义:
```c
int arr[] = {1,2,3,4,5};
```
* 那么,对应的内存结构,如下所示:
![](./assets/7.png)
> [!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]
>
> 如果之后试图更改数组名对应的地址,编译器就会报错。
* 示例:错误演示
```c
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 应用示例
* 需求:计算数组中所有元素的和以及平均数。
* 示例:
```c
#include <stdio.h>
int main() {
// 定义数组并初始化
int arr[] = {12, 2, 31, 24, 15, 36, 67, 108, 29, 51};
// 计算数组的长度
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;
}
```
### 2.7.2 应用示例
* 需求:计算数组的最值(最大值和最小值)。
> [!NOTE]
>
> 思路:
>
> * ① 假设数组中的第一个元素是最大值或最小值,并使用变量 max 或 min 保存。
> * ② 遍历数组中的每个元素:
> * 如果有元素比最大值还要大,就让变量 max 保存最大值。
> * 如果有元素比最小值还要小,就让变量 min 保存最小值。
* 示例:
```c
#include <stdio.h>
int main() {
// 定义数组并初始化
int arr[] = {12, 2, 31, 24, 15, -36, 67, 108, 29, 51};
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(int);
// 定义最大值
int max = arr[0];
// 定义最小值
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
printf("数组的最小值为:%d\n", min); // 数组的最小值为:-36
return 0;
}
```
### 2.7.3 应用示例
* 需求:统计数组中某个元素出现的次数,要求:使用无限循环,如果输入的数字是 0 ,就退出。
* 示例:
```c
#include <stdio.h>
int main() {
// 定义数组并初始化
int arr[] = {12, 2, 31, 24, 2, -36, 67, 108, 29, 51};
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(int);
// 遍历数组
printf("当前数组中的元素是:");
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// 无限循环
while (true) {
// 统计的数字
int num;
// 统计数字出现的次数
int count = 0;
// 输入数字
printf("请输入要统计的数字:");
scanf("%d", &num);
// 0 作为结束条件
if (num == 0) {
break;
}
// 遍历数组,并计数
for (int i = 0; i < length; i++) {
if (arr[i] == num) {
count++;
}
}
printf("您输入的数字 %d 在数组中出现了 %d 次\n", num, count);
}
return 0;
}
```
### 2.7.4 应用示例
* 需求:将数组 a 中的全部元素复制到数组 b 中。
* 示例:
```c
#include <stdio.h>
#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 应用示例
* 需求:数组对称位置的元素互换。
> [!NOTE]
>
> 思路:假设数组一共有 10 个元素,那么:
>
> * a[0] 和 a[9] 互换。
> * a[1] 和 a[8] 互换。
> * ...
>
> 规律就是 `a[i] <--互换--> arr[arr.length -1 -i]`
* 示例:
```c
#include <stdio.h>
int main() {
// 原始数组
int arr[] = {12, 2, 31, 24, 15, -36, 67, 108, 29, 51};
// 计算数组的长度
size_t SIZE = sizeof(arr) / sizeof(arr[0]);
// 打印原始数组中的全部元素
printf("原始数组:");
for (int i = 0; i < SIZE; i++) {
printf("%d ", arr[i]);
}
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;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
// 原始数组
int arr[] = {12, 2, 31, 24, 15, -36, 67, 108, 29, 51};
// 计算数组的长度
size_t SIZE = sizeof(arr) / sizeof(arr[0]);
// 打印原始数组中的全部元素
printf("原始数组:");
for (int i = 0; i < SIZE; i++) {
printf("%d ", arr[i]);
}
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;
}
```
### 2.7.6 应用示例
* 需求:将数组中的最大值移动到数组的最末尾。
> [!NOTE]
>
> 思路:从数组的下标 `0` 开始依次遍历到 `length - 1` ,如果 `i` 下标当前的值比 `i+1` 下标的值大,则交换;否则,就不交换。
* 示例:
```c
#include <stdio.h>
int main() {
// 原始数组
int arr[] = {12, 2, 31, -24, 15, -36, 67, 891, 29, 51};
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(arr[0]);
// 打印原始数组中的全部元素
printf("原始数组:");
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
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;
}
```
### 2.7.7 应用示例
* 需求:实现冒泡排序,即将数组的元素从小到大排列。
> [!NOTE]
>
> 思路:一层循环,能实现最大值移动到数组的最后;那么,二层循环(控制内部循环数组的长度)就能实现将数组的元素从小到大排序。
* 示例:
```c
#include <stdio.h>
int main() {
// 原始数组
int arr[] = {12, 2, 31, -24, 15, -36, 67, 891, 29, 51};
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(arr[0]);
// 打印原始数组中的全部元素
printf("原始数组:");
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
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;
}
```
### 2.7.8 应用示例
* 需求:数组中的元素是从小到大排列的,现在要求根据指定的元素获取其在数组中的位置。
> [!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
数据类型 数组名[几个⼀维数组元素][每个⼀维数组中有几个具体的数据元素];
```
> [!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.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;
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 271 KiB

View File

Before

Width:  |  Height:  |  Size: 452 KiB

After

Width:  |  Height:  |  Size: 452 KiB

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

Before

Width:  |  Height:  |  Size: 383 KiB

After

Width:  |  Height:  |  Size: 383 KiB

View File

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 168 KiB

View File

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 239 KiB

View File

Before

Width:  |  Height:  |  Size: 436 KiB

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 143 KiB

View File

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -0,0 +1,772 @@
# 第一章:字符串(⭐)
## 1.1 概述
* 在实际开发中,我们除了经常处理整数、浮点数、字符等,还经常和字符串打交道,如:`"Hello World"`、`"Hi"` 等。
> [!NOTE]
>
> 像这类`"Hello World"`、`"Hi"`等格式 ,使用`双引号`引起来的一串字符称为字符串字面值,简称字符串。
* 对于整数、浮点数和字符C 语言中都提供了对应的数据类型。但是对于字符串C 语言并没有提供对应的数据类型,而是用`字符数组`来存储这类文本类型的数据,即字符串:
```c
char str[32];
```
* 字符串不像整数、浮点数以及字符那样有固定的大小,字符串是不定长的,如:`"Hello World"`、`"Hi"` 等的长度就是不一样的。在 C 语言中,规定了字符串的结尾必须是 `'\0'` ,这种字符串也被称为 `C 风格的字符串`,如:
```c
"Hello World!" // 在 C 语言中,底层存储就是 Hello World!\0
```
* 其对应的图示,如下所示:
![](./assets/1.png)
* `'\0'` 在 ASCII 码表中是第 `0` 个字符,用 `NUL` 表示,称为空字符,该字符既不能显示,也不是控制字符,输出该字符不会有任何效果,它在 C 语言中仅作为字符串的结束标志。
* C 语言在处理字符串时,会从前往后逐个扫描字符,一旦遇到`'\0'`就认为到达了字符串的末尾,就结束处理。`'\0'`至关重要,没有`'\0'`就意味着永远也到达不了字符串的结尾。
![](./assets/2.png)
> [!NOTE]
>
> 在现代化的高级编程语言中都提供了字符串对应的类型Java 中的 StringJDK 11 之前,底层也是通过 `char[]` 数组来实现的) 。
## 1.2 字符数组(字符串)的定义
### 1.2.1 标准写法
* 手动在字符串的结尾添加 `'\0'`作为字符串的结束标识。
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
// 字符数组,不是字符串
char c1[] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'};
// C 风格的字符串
char c2[] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\0'};
return 0;
}
```
### 1.2.2 简化写法(推荐)
* 字符串写成数组的形式非常麻烦。C 语言中提供了一种简化写法,即:双引号中的字符,会自动视为字符数组。
> [!NOTE]
>
> 简化写法会自动在末尾添加 `'\0'` 字符,强烈推荐使用!!!
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
char c1[] = {"Hello World"}; // 注意使用双引号,非单引号
char c2[] = "Hello World"; // //可以省略一对 {} 来初始化数组元素
return 0;
}
```
## 1.3 字符串的输入和输出
### 1.3.1 字符串的输出
* 在 C 语言中,有两个函数可以在控制台上输出字符串,它们分别是:
* ① `puts()`:输出字符串并自动换行,并且该函数只能输出字符串。
* ② `printf()` :通过格式占位符 `%s`,就可以输出字符串,不能自动换行。
> [!NOTE]
>
> * ① `printf()` 函数除了输出字符串之外,还可以输出`其它类型`的数据。
> * ② 在实际开发中,`printf()` 函数用的居多!!!
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
char c1[] = {"Hello World"}; // 注意使用双引号,非单引号
char c2[] = "Hello World"; // //可以省略一对 {} 来初始化数组元素
puts(c1); // Hello World
puts(c2); // Hello World
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
char c1[] = {"Hello World"}; // 注意使用双引号,非单引号
char c2[] = "Hello World"; // //可以省略一对 {} 来初始化数组元素
printf("c1 = %s\n", c1); // c1 = Hello World
printf("c2 = %s\n", c2); // c2 = Hello World
return 0;
}
```
### 1.3.2 字符串的输入
* 在 C 语言中,有两个函数可以让用户从键盘输入字符串,它们分别是:
* ① ~~`gets()`:直接输入字符串,并且只能输入字符串~~
* ② `scanf()`:通过格式占位符 `%s`,就可以输入字符串了。
> [!NOTE]
>
> * ① `scanf()` 在通过格式占位符 `%s`,读取字符串时以`空格`或 `Enter` 键为分隔,遇到`空格`或 `Enter` 键就认为当前字符串结束了,所以无法读取含有空格的字符串。但是,我们可以将格式占位符,使用 `%[^\n]`来代替 `%s` ,这样就能解决 `scanf()` 函数默认的缺陷。
> * ② `gets()` 认为空格也是字符串的一部分,只有遇到回车键时才认为字符串输入结束。换言之,不管输入了多少个空格,只要不按下回车键,对 `gets()` 来说就是一个完整的字符串。
> * ③ 需要注意的是,`gets()` 函数在 [C11](https://zh.cppreference.com/w/c/11) 标准中,已经被移除了,推荐使用 `fgets` 来代替它,因为有严重的安全漏洞,即:`gets()` 函数读取用户输入直到换行符,但它不会检查缓冲区的大小。这意味着如果用户输入超过了缓冲区的大小,`gets()` 将会导致缓冲区溢出。这种缓冲区溢出很容易被恶意利用,导致程序崩溃或执行恶意代码。
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
char str[32] = {'\0'};
printf("请输入字符串:");
gets(str);
printf("字符串是:%s\n", str);
return 0;
}
```
* 示例:
```c {13}
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
char str[32] = {'\0'};
printf("请输入字符串:");
// scanf() 在读取数据时需要的是数据的地址,这一点是恒定不变的。
// 对于 int、char、float 等类型的变量都要在前边添加 & 以获取它们的地址。
// 而数组或者字符串用于 scanf() 时不用添加 &,它们本身就会转换为地址。
scanf("%[^\n]", str);
printf("字符串是:%s\n", str);
return 0;
}
```
## 1.4 字符串结束不是 `'\0'` 的后果
* 有的时候,程序的逻辑要求我们必须逐个字符为数组赋值,这个时候就很容易遗忘字符串结束标识 `'\0'`,如下所示:
```c {3}
#include <stdio.h>
int main() {
char str[30];
char c;
int i;
for (c = 65, i = 0; c <= 90; c++, i++) {
str[i] = c;
}
printf("%s\n", str);
return 0;
}
```
* 该程序的执行结果,如下所示:
![](./assets/3.png)
* 因为`大写字符`在 `ASCII` 码表是连续的,编码值从 `65` 开始,直到 `90` 结束;并且,为了方便,我们使用了循环。但是,我们却发现结果和我们想要的大不一样,为什么?
> [!NOTE]
>
> * ① 在函数内部定义的变量、数组、结构体、共用体等都称为局部数据。
>
> * ② 在很多编译器下,局部数据的初始值都是随机的、无意义的,而不是我们通常认为的“零”值。
* 我们在定义 `str` 数组的时候,并没有立即初始化,所以它包含的值都是随机的,只有很小的概率是“零”。循环结束后,`str` 的前 `26` 个元素被赋值了,剩下的 `4` 个元素的值依然是随机的,我们并不清楚到底是什么。
* `printf()` 输出字符串时,会从第 `0` 个元素开始往后检索,直到遇见`'\0'`才停止,然后把`'\0'`前面的字符全部输出,这就是 `printf()` 输出字符串的原理。
* 但是,对于上面的例子,由于我们并没有对最后 `4` 个元素赋值,所以第 `26` 元素可能是 `'\0'`,也有可能第 `27` 个元素是 `'\0'`,也有可能第 `28` 个元素是 `'\0'`;不过,不要问`我``我`也不清楚,可能只有`上帝`才会知道,到底第`几`个元素才是 `'\0'`。而且,我们在定义数组的时候,设置数组的长度是 `30` ,但是貌似输出的字符串的长度是 `32` ,这早已超出了数组的范围,`printf()` 在输出字符串的时候,如果没有遇见 `'\0'` 是不会罢休的,它才不会管`数组访问`是不是`越界`。
> [!NOTE]
>
> * ① 由此可见,不注意`'\0'`的后果有多严重,不但不能正确处理字符串,甚至还会毁坏其它数据!!!
> * ② C 语言为了提高效率保证操作的灵活性并不会对越界行为进行检查即使越界了也能够正常编译只有在运行期间才可能发现问题所以对程序员的要求很高。但是现代化的高级编程语言Java 等,为了降低开发难度以及提高开发效率,像数组这种越界行为,在编译期间就会由编译器提前捕获,并直接报错!!!
* 如果要避免这些问题也很简单,在字符串后面手动添加 `'\0'` 就可以了,即:
```c {9}
#include <stdio.h>
int main() {
char str[30];
char c;
int i;
for (c = 65, i = 0; c <= 90; c++, i++) {
str[i] = c;
}
str[i] = '\0';
printf("%s\n", str);
return 0;
}
```
* 但是,上述的写法实在麻烦,为什么不在定义数组的时候,给数组中的每个元素都初始化,这样才能从根本上避免上述问题,即:
```c {3}
#include <stdio.h>
int main() {
char str[30] = {'\0'};
char c;
int i;
for (c = 65, i = 0; c <= 90; c++, i++) {
str[i] = c;
}
printf("%s\n", str);
return 0;
}
```
## 1.5 字符串的长度
* 所谓字符串的长度,就是字符串包含了多少个字符(不包括最后的结束符`'\0'`),如:`"abc"` 的长度是 `3` ,而不是 `4`
* 在 C 语言中的 `string.h` 中提供了 `strlen()` 函数,能够帮助我们获取字符串的长度,如下所示:
```c
size_t strlen (const char *__s)
```
* 示例:
```c {13}
#include <stdio.h>
#include <string.h>
int main() {
char str[30] = {'\0'};
char c;
int i;
for (c = 65, i = 0; c <= 90; c++, i++) {
str[i] = c;
}
// ABCDEFGHIJKLMNOPQRSTUVWXYZ
printf("%s\n", str);
// ABCDEFGHIJKLMNOPQRSTUVWXYZ 的长度是 26
printf("%s 的长度是 %zu\n", str, strlen(str));
return 0;
}
```
# 第二章:内存中的变量和数组(⭐)
## 2.1 内存和内存地址
### 2.1.1 内存
* `内存`是一种`计算机硬件`,是`软件`在`运行过程`中,用来`临时存储数据`的。在生活中,最为常见的`内存`就是`随机存取存储器RAM内存条`),其特点如下所示:
* ① 生活中最常见的内存类型,用于存储当前运行的程序和数据。
* ② 内存是易失性存储器,这意味着断电后数据会丢失。
* ③ 它具有高速读写特性,适用于需要快速访问的操作。
* 内存条的外观,如下所示:
![](./assets/4.jpeg)
* 像我们平常使用`记事本`软件一样,当我们输入一些文字的时候,其实是将数据`临时`保存在内存中的,如下所示:
> [!NOTE]
>
> * ① 目前,很多软件都很智能,如果用户没有将数据到保存文件中,将显示红色,以警告用户还没有保存数据,提醒用户需要尽快保存数据!!!
> * ② 但是也有很多软件提供了自动保存数据的功能其原理就是定时1s、3s、5s将内存中的数据刷新到文件中以防止数据丢失
> * ③ 将数据从内存存储到文件中,专业的说法是落盘(落在磁盘上)。
![](./assets/5.gif)
* 此时,如果我们在没有保存的过程下,将`记事本`软件关闭,那么刚才输入的文字将丢失;下次,再打开同样的文件(将数据从磁盘加载进内存,再交给 CPU之前输入的文字将不复存在如下所示
> [!NOTE]
>
> * ① 目前,很多软件都很智能,如果你没有保存,将提醒你是否保存或丢失刚才输入的文字。
> * ② 但是也有很多软件提供了自动保存数据的功能其原理就是定时1s、3s、5s将内存中的数据刷新到文件中以防止数据丢失
> * ③ 将数据从内存存储到文件中,专业的说法是落盘(落在磁盘上)。
![](./assets/6.gif)
> [!IMPORTANT]
>
> 内存就是软件在运行过程中,用来临时存储数据的,最为重要的两个步骤就是:
>
> * ① 将数据`保存`到内存中。
> * ② 从内存中的`对应位置`将数据`取出来`。
### 2.1.2 内存地址
* 在这个计算机的内存条,动不动就 32GB、64GB 、128GB 或更高的年代,如下所示:
![](./assets/7.png)
* 如果有一个 int 4 个字节)类型的数据 `2` ,如何将这个数据保存到内存中?(对应上述的步骤 ①)
![](./assets/8.svg)
* 就算数据 `2` 已经保存到内存中,那么内存中那么多的数据,我们又该如何取出呢?(对应上述的步骤 ②)
![](./assets/9.svg)
> [!IMPORTANT]
>
> 答案就是`内存地址`。
* 操作系统为了更快的去管理内存中的数据,会将`内存条`按照`字节`划分为一个个的`单元格`,如下所示:
![](./assets/10.svg)
> [!NOTE]
>
> 计算机中存储单位的换算,如下所示:
>
> - 1 B = 8 bit。
> - 1 KB = 1024 B。
> - 1 MB = 1024 KB。
> - 1 GB = 1024 MB。
> - 1 TB = 1024 GB 。
> - ……
* 为了方便管理,每个独立的小单元格,都有自己唯一的编号(内存地址),如下所示:
![](./assets/11.svg)
* 之所以,加了`内存地址`,就能`加快`数据的存取速度,可以类比生活中的`字典`
* 如果没有使用`拼音查找法`或`部首查找法`,我们需要一页一页,一行一行的,在整个字典中去搜索我们想要了解的汉字,效率非常低(如果要搜索的汉字在最后一页,可能需要将整个字典从头到尾翻一遍,这辈子真有可能翻得完?)。
![](./assets/12.gif)
* 如果使用`拼音查找法`或`部首查找法`,我们可以很快的定位到所要了解汉字所在的页数,加快了搜索的效率。
![](./assets/13.jpg)
![](./assets/14.jpg)
* 同样的道理,如果`没有`内存地址,我们只能一个个的去寻找想要的数据,效率非常低下,如下所示:
![](./assets/15.gif)
* 如果`使用`内存地址,我们就可以直接定位到指定的数据,效率非常高,如下所示:
![](./assets/16.gif)
> [!IMPORTANT]
>
> * ① 内存地址是计算机中用于标识内存中某个特定位置的数值。
> * ② 每个内存单元都有一个唯一的地址,这些地址可以用于访问和操作存储在内存中的数据。
* 实际中的内存地址,并不是像上面的 `001` 、`002` 、... 之类的数字,而是有自己的规则,即:内存地址规则。
> [!NOTE]
>
> * ① 32 位的操作系统中,内存地址以 32 位的二进制表示。
> * ② 64 位的操作系统中,内存地址以 64 位的二进制表示。
* 在 32 位的操作系统中,内存地址的范围是:`0000 0000 0000 0000 0000 0000 0000 0000` ~ `1111 1111 1111 1111 1111 1111 1111 1111` 2 ^ 32 次方)。
> [!NOTE]
>
> 在 32 位的操作系统中,一共有 4,294,967,296 个内存地址,其最大支持的内存大小是 4,294,967,296 字节,即 4 GB 。
* 在 64 位的操作系统中,内存地址的范围是:`0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000` ~ `1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111` 2 ^ 64 次方)。
> [!NOTE]
>
> * ① 在 64 位的操作系统中,一共有 18,446,744,073,709,551,616 个内存地址,其最大支持的内存大小是 18,446,744,073,709,551,616 字节,即 17,179 TB 。
> * ② 虽然,从理论上 64 位的操作系统支持的内存最大容量是 17,179 TB但是实际操作系统会有所限制win11 的 64 位支持的最大内存是 128 GB ~ 6 TBLinux 64 位支持的最大内存通常是 256 TB。
* 在实际开发中64 位操作系统的内存地址表现形式,如:`0000 0000 0000 0000 0001 0000 1111 1010 0000 0000 0010 0000 0000 0010 0000 1000`,实在是太长了,我们通常转换为十六进制,以方便阅读,如:`0x000010FA00200208` 。
> [!IMPORTANT]
>
> * ① 内存地址是内存中每个单元的编号。
>* ② 内存地址的作用是操作系统用来快速管理内存空间的。
> * ③ 在 32 位操作系统上,内存地址以 32 位的二进制数字表示,最大支持的的内存是 4 GB所以 32 位操作系统已经被淘汰。
> * ④ 在 64 位操作系统上,内存地址以 64 位的二进制数字表示,由于表示形式太长,我们通常会转为十六进制,以方便阅读。
## 2.2 内存中的变量
* 在 C 语言中数据类型的种类很多short、int、long、float、double、char 等。以 int 类型为例,在 32 位或 64 位操作系统中的int 类型的变量都是占 4 个字节,当我们在代码中这么定义变量,如:
```c
#include <stdio.h>
int main(){
// 定义一个变量并初始化
int a = 10;
return 0;
}
```
* 那么,编译器就会这么处理,如下所示:
![](./assets/17.svg)
* 在代码中,我们可以使用 `&变量名` 来获取一个变量的内存首地址,如下所示:
```c
#include <stdio.h>
int main() {
// 定义一个变量并初始化
int a = 10;
printf("变量 a 的首地址是: %p\n", &a); // 变量 a 的首地址是: 0000002bf1dffd0c
printf("变量 a 的中保存的值是: %d\n", a); // 变量 a 的中保存的值是: 10
return 0;
}
```
> [!NOTE]
>
> * ① `变量`是对程序中`数据`在内存中存储空间的抽象,如果不涉及到指针变量,那我们在编码的时候,就只需要将变量等价于内存中存储空间里面存储的数据,而不需要再去考虑编译器底层是如何转换,提高了开发效率(机器语言和汇编可不是这样的,需要关注每个细节)。
> * ② 数据类型只在`定义`变量的时候`指定`,而且必须指定;`使用`变量的时候`无需`再声明,因为此时的数据类型已经确定了。
## 2.3 内存中的数组
* 如果我们在代码中这么定义数组,如下所示:
```c
#include <stdio.h>
int main(){
// 定义一个数组并初始化
int arr[] = {1,2,3};
return 0;
}
```
* 那么,编译器就会这么处理,如下所示:
![](./assets/18.svg)
* 在代码中,我们可以直接打印数组名的内存地址,如下所示:
```c
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3};
printf("arr 的首地址是: %p \n", arr); // arr 的首地址是: 0000003a6f7ffcd4
printf("arr 的首地址是: %p \n", &arr); // &arr 的地址是: 0000003a6f7ffcd4
printf("arr[0] 的地址是: %p \n", &arr[0]); // arr[0] 的地址是: 0000003a6f7ffcd4
printf("arr[1] 的地址是: %p \n", &arr[1]); // arr[1] 的地址是: 0000003a6f7ffcd8
printf("arr[2] 的地址是: %p \n", &arr[2]); // arr[2] 的地址是: 0000003a6f7ffcdc
return 0;
}
```
> [!WARNING]
>
> 在上述示例中,`arr` 和 `&arr` 的值是一样的,但是对应的含义是不同的。
>
> * ① `arr` 是数组名,在大多数情况下会转换为数组第一个元素的地址,即:`arr` 等价于 `&arr[0]`,其数据类型是 `int *`。
> * ② `&arr`是数组名的地址,即整个数组的地址,它指向数组本身,并不是数组第一个元素的地址,`&arr` 的数据类型是 `int(*)[3]`。
# 第三章:数组越界和数组溢出(⭐)
## 3.1 数组越界
* C 语言的数组是静态的,当我们定义的时候,就不能自动扩容。当我们试图访问数组的`负索引`或`超出`数组长度的索引时,就会产生`数组越界`。
> [!NOTE]
>
> * ① C 语言为了提高效率,保证操作的灵活性,并不会对越界行为进行检查,即使越界了,也能够正常编译,只有在运行期间才可能发现问题,所以对程序员的要求很高。
> * ② 但是现代化的高级编程语言Java 等,为了降低开发难度以及提高开发效率,像数组这种越界行为,在编译期间就会由编译器提前捕获,并直接报错!!!
* 请看下面的代码:
```c {9-10,15}
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
int arr[3] = {10, 20, 30};
printf("arr[-1] = %d\n", arr[-1]); // arr[-1] = -23718968
printf("arr[-2] = %d\n", arr[-2]); // arr[-2] = 0
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
printf("arr[3] = %d\n", arr[3]); // arr[3] = -23718976
printf("arr[4] = %d\n", arr[4]); // arr[4] = 605
return 0;
}
```
* 越界访问数组元素的值都是不确定的,没有实际的含义,因为在数组之外的内存,我们并不知道到底是什么,可能是其它变量的值,可能是函数参数,也可能是一个地址,这些都是不可控的。
> [!NOTE]
>
> 由于 C 语言的”放任“,我们访问数组时必须非常小心,要确保不会发生越界。
* 当发生数组越界时,如果我们对该内存有使用权限,那么程序将正常运行,但会出现不可控的结果,即:如果我们对该内存没有使用权限,或者该内存压根就没有就分配,那么程序就会崩溃,如下所示:
```c
#include <stdio.h>
int main() {
int arr[3] = {0};
printf("%d", arr[10000]);
return 0;
}
```
* 其结果,如下所示:
![](./assets/19.png)
> [!NOTE]
>
> * ① 每个程序能使用的内存都是有限的,该程序要访问 `4*10000` 字节处的内存,显然太远了,超出了程序的访问范围。
> * ② 这个地方的内存可能没有被分配,可能是系统本身占用的内存,可能是其它数据的内存,如果放任这种行为,将带来非常危险的后果,操作系统只能让程序停止运行。
* 当然,我们在实际开发中,也不会这么访问,而是会使用 `sizeof` 运算符来获取数组的长度,进而遍历数组中的元素,即:
```c {7}
#include <stdio.h>
int main() {
int arr[3] = {0};
// 获取数组的元素
size_t length = sizeof(arr) / sizeof(int);
for (size_t i = 0; i < length; i++) {
printf("%d\n", arr[i]);
}
return 0;
}
```
## 3.2 数组溢出
* 数组溢出通常是指将数据存储到一个数组中,超出了数组所能容纳的空间,那么多余的元素就会被丢弃。对于一般的数组,貌似没什么问题,如下所示:
```c
#include <stdio.h>
int main() {
int arr[3] = {0, 1, 2, 3, 4};
size_t length = sizeof(arr) / sizeof(int);
for (size_t i = 0; i < length; i++) {
printf("%d\n", arr[i]);
}
return 0;
}
```
* 其结果,如下所示:
![](./assets/20.png)
* 但是,对于字符串而言,就会出现不可控的情况,如下所示:
```c {4}
#include <stdio.h>
int main()
{
char str[10] = "Hello WorldHello WorldHello World";
puts(str);
return 0;
}
```
* 其结果,如下所示:
![](./assets/21.png)
* 因为字符串的长度大于数组的长度,数组只能容纳字符串前面的一部分,即使编译器在字符串最后保存了 `'\0'`,也无济于事,因为超过数组长度的元素都会被丢弃。而 `printf()` 输出字符串时,会从第 `0` 个元素开始往后检索,直到遇见`'\0'`才停止,然后把`'\0'`前面的字符全部输出,至于何时遇到 `'\0'`,就只有上帝才能知道。
> [!NOTE]
>
> * ① 在用字符串给字符数组赋值时,要保证数组长度大于字符串长度,以容纳结束符`'\0'`。
> * ② `数组溢出`通常发生在动态分配内存或者通过不安全的函数(如: `strcpy`)进行字符串操作。
# 第四章C 语言中的数组 VS Java 语言中的数组(⭐)
## 4.1 Linux 下 32 位环境的用户空间内存分布情况
* 对于 32 位的环境而言,理论上程序是可以拥有 4GB 的虚拟地址空间的,在 C 语言中使用到的变量、函数、字符串等都会对应内存中的一块区域。
* 但是,在这 4GB 的地址空间中,要拿出一部分给操作系统内核使用,应用程序无法直接访问这一段内存,这一部分内存地址被称为`内核空间`Kernel Space
> [!NOTE]
>
> - ① Windows 在默认情况下会将高地址的 2GB 空间分配给内核(也可以配置为 1GB
> - ② 而 Linux 默认情况下会将高地址的 1GB 空间分配给内核。
* 也就是说,应用程序只能使用剩下的 2GB 或 3GB 的地址空间,称为`用户空间`User Space
* Linux 下 32 位环境的经典内存模型,如下所示:
![](./assets/22.svg)
* 各个内存分区的说明,如下所示:
| 内存分区 | 说明 |
| :------------------------ | :----------------------------------------------------------- |
| 程序代码区code | 存储程序的执行代码,通常为只读区,包含程序的指令。 程序启动时,这部分内存被加载到内存中,并不会在程序执行期间改变。 |
| 常量区constant | 存放程序中定义的常量值,通常也是只读的,这些常量在程序运行期间不可修改。 |
| 全局数据区global data | 存储程序中定义的全局变量和静态变量。 这些变量在程序的整个生命周期内存在,且可以被修改。 |
| 堆区heap | 用于动态分配内存,例如:通过 `malloc``new` 分配的内存块。 堆区的内存由程序员手动管理,负责分配和释放。 如果程序员不释放,程序运行结束时由操作系统回收。 |
| 动态链接库 | 动态链接库(如: `.dll``.so` 文件)被加载到内存中特定的区域,供程序运行时使用。 |
| 栈区stack | 用于存储函数调用的局部变量、函数参数和返回地址。 栈是自动管理的,随着函数的调用和返回,栈上的内存会自动分配和释放。 |
> [!NOTE]
>
> - ① 程序代码区、常量区、全局数据区在程序加载到内存后就分配好了,并且在程序运行期间一直存在,不能销毁也不能增加(大小已被固定),只能等到程序运行结束后由操作系统收回,所以全局变量、字符串常量等在程序的任何地方都能访问,因为它们的内存一直都在。
> - ② 函数被调用时,会将参数、局部变量、返回地址等与函数相关的信息压入栈中,函数执行结束后,这些信息都将被销毁。所以局部变量、参数只在当前函数中有效,不能传递到函数外部,因为它们的内存不在了。
> - ③ 常量区、全局数据区、栈上的内存由系统自动分配和释放,不能由程序员控制。程序员唯一能控制的内存区域就是`堆`Heap它是一块巨大的内存空间常常占据整个虚拟空间的绝大部分在这片空间中程序可以申请一块内存并自由地使用放入任何数据。堆内存在程序主动释放之前会一直存在不随函数的结束而失效。在函数内部产生的数据只要放到堆中就可以在函数外部使用。
## 4.2 C 语言中的数组
* 之前,我们都是这么使用数组的,如下所示:
```c
#include <stdio.h>
int main() {
// 定义数组和全部初始化:数组初始化的元素个数等于数组的长度。
int arr[5] = {1, 2, 3, 4, 5};
return 0;
}
```
* 其实,这样定义的数组是在`栈`中的,而栈的内存空间是有限的,如果数组中的元素过多,将会出现 `Stack Overflow` 的现象,即:栈溢出。
> [!NOTE]
>
> * ① 栈内存的大小和编译器有关,编译器会为栈内存制定一个最大值。
> * ② 在 VS 中,默认是 1 MB在 GCC 下,默认是 8 MB。
> * ③ 虽然可以通过参数来修改栈内存的大小;但是,在实际开发中,我们一般也不会这么做。
* 所以,在实际开发中,如果我们要使用数组,就需要在`堆`中开辟内存空间,因为堆中的内存空间是可以动态扩容和缩容的,只不多在 C 语言中对于堆中申请的内存空间,需要程序员在用完之后,手动释放掉;否则,将会造成内存泄漏现象。
```c
#include <stdio.h>
#include <stdlib.h>
int main() {
int n; // 数组的大小
printf("请输入数组的大小: ");
scanf("%d", &n);
// 使用 malloc 申请内存,申请 n 个 int 类型的空间
int *array = (int *)malloc(n * sizeof(int));
// 检查 malloc 是否成功
if (array == NULL) {
printf("内存分配失败!\n");
return 1; // 程序退出
}
// 初始化数组并输出
for (int i = 0; i < n; i++) {
array[i] = i + 1; // 简单赋值操作
printf("array[%d] = %d\n", i, array[i]);
}
// 使用完毕后,释放内存
free(array);
return 0;
}
```
## 4.3 Java 语言中的数组
* Java 语言和 C 语言不同Java 语言从语法层面就将数组在内存中的分配放到了`堆`中。
```c
public class Test {
public static void main(String[] args){
// 在堆内存开辟数组,使用完毕后,不需要手动回收对应的内存空间
int[] arr = new int[4] ;
}
}
```
> [!NOTE]
>
> * ① 在 Java 语言中,数组的内存分配是由 JVMJava Virtual MachineJava 虚拟机)自动管理的,开发者不需要像在 C 语言中那样手动调用 `malloc` 来申请内存。Java 提供了更加高级的内存管理机制,所有数组在堆中动态分配。
> * ② 在 Java 中声明和初始化数组的过程本质上就是在堆内存中分配数组内存的过程。每个数组在创建时都会被分配到堆中并且由垃圾回收机制Garbage CollectorGC自动负责内存的回收。
> * ③ 我们甚至可以理解为Java 语言是 C 语言在实际开发过程中的最佳实践版本。