This commit is contained in:
许大仙 2024-08-27 11:11:14 +08:00
parent ecdc127e30
commit fda419fbc0
74 changed files with 3279 additions and 3176 deletions

View File

@ -10,16 +10,17 @@ export const sidebar: DefaultTheme.Sidebar = {
{ text: 'C 语言入门', link: `/notes/01_c-basic/01_${commonDirectoryName}/` },
{ text: '变量和进制', 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/09_${commonDirectoryName}/` },
{ text: '内存管理', link: `/notes/01_c-basic/10_${commonDirectoryName}/` },
{ text: '文件操作', link: `/notes/01_c-basic/11_${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}/` },
{ text: '自定义数据类型', link: `/notes/01_c-basic/10_${commonDirectoryName}/` },
{ text: '内存管理', link: `/notes/01_c-basic/11_${commonDirectoryName}/` },
{ text: '文件操作', link: `/notes/01_c-basic/12_${commonDirectoryName}/` },
{ text: '调试工具和调试技巧gdb和make', link: `/notes/02_c-basic/13_${commonDirectoryName}/` },
{ text: '常用库函数', link: `/notes/01_c-basic/13_${commonDirectoryName}/` },
{ text: '常用库函数', link: `/notes/01_c-basic/14_${commonDirectoryName}/` },
]
},
{

View File

@ -1864,9 +1864,8 @@ cp perf /usr/bin/
> [!NOTE]
>
> * ① 默认情况下,在 WSL2 中,只有 Ubuntu 才会将 systemd 作为 pid-1 的守护进程。
> * ② 但是,基于 WSL2 为内核的其余 Linux 发行版本并非将 systemd 作为 pid-1 的守护进程。
> * ③ 本次以 AlmaLinux9 作为演示!!!
> * ① 默认情况下,在 WSL2 中,只有 Ubuntu 才会将 systemd 作为 pid-1 的守护进程(微软维护和定制的 Ubuntu 版本,在 GitHub 的 Codespace 中默认的 Linux 环境就是 Ubuntu。但是基于 WSL2 为内核的其余 Linux 发行版本并非将 systemd 作为 pid-1 的守护进程。
> * ② 本次以 AlmaLinux9 作为演示!!!
* 检查进程树,判断 systemd 是否正在运行:

View File

@ -1029,14 +1029,14 @@ int main() {
### 1.5.1 概述
* 在生活中,我们会经常说:今天天气真 `好`,我的性别是 `女`,我今年 `10`,像这类数据,在 C 语言中就可以用字符char来表示
* 在 C 语言中,变量的`字符类型`可以表示`单`个字符,如:`'1'`、`'A'`、`'&'`。
* 在生活中,我们会经常说:今天天气真 `好`,我的性别是 `女`,我今年 `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 语言中如果想要输出中文、日文等多字节字符就需要使用字符数组char 数组)。
> * ⑤ 在 C++、Java 等高级编程语言中,已经提供了 String (字符串)类型,原生支持 Unicode可以方便地处理多语言和特殊字符。
@ -1164,6 +1164,88 @@ int main() {
}
```
### 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 概述
@ -2348,10 +2430,31 @@ man ascii
- **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 通常是最常用的编码方式,因为它在兼容性和空间效率之间提供了良好的平衡。
> * ④ 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 概述

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,1516 +0,0 @@
# 第一章:概述
* `流程控制结构`是用来控制程序中`各语句执行顺序`的语句,并且可以将语句组合成能`完成一定功能`的`小逻辑模块`。
* 在程序设计中规定了`三种`流程结构,如下所示:
* `顺序结构`:程序从上到下逐行执行,中间没有任何判断和跳转。
* `分支结构`:根据条件,有选择的执行某段代码。在 C 语言中,有 `if...else``switch...case` 两种分支语句。
* `循环结构`:根据循环条件,重复性的执行某段代码。在 C 语言中,有 `for`、`while`、`do...while` 三种循环结构。
* 在生活中的`洗衣工厂`,就包含了上述的三种流程结构,如下所示:
![](./assets/1.jpg)
# 第二章:顺序结构
## 2.1 概述
* 程序从上到下逐行地执行,表达式语句都是顺序执行的,并且上一行对某个变量的修改对下一行会产生影响。
![](./assets/2.png)
## 2.2 应用示例
* 示例:
```c
#include <stdio.h>
int main() {
int x = 1;
int y = 2;
printf("x = %d \n", x); // x = 1
printf("y = %d \n", y); // y = 2
// 对 x 和 y 的值进行修改
x++;
y = 2 * x + y;
x = x * 10;
printf("x = %d \n", x); // x = 20
printf("y = %d \n", y); // y = 6
return 0;
}
```
# 第三章:分支结构(⭐)
## 3.1 概述
* 根据特定条件执行不同的代码块,从而实现灵活的程序控制和更复杂的逻辑。
## 3.2 单分支结构
### 3.2.1 概述
* 语法:
```c
if(条件表达式){
语句;
}
```
> [!NOTE]
>
> * ① 在 C 语言中,严格意义上是没有 boolean 类型的,使用`非0` 表示`真true``0` 表示`假false`。
> * ② 当条件表达式为真(`非0` ),就会执行代码块中的语句;否则,就不会执行代码块中的语句。
* 流程图,如下所示:
![](./assets/3.png)
### 3.2.2 应用示例
* 需求:成年人心率的正常范围是每分钟 60~100 次。体检时,如果心率不在此范围内,则提示需要做进一步的检查。
* 示例:
```c
#include <stdio.h>
int main() {
int heartBeats = 0;
printf("请输入您的心率:");
scanf("%d", &heartBeats);
if (heartBeats < 60 || heartBeats > 100) {
printf("您的心率不在正常范围内,请做进一步的检查。\n");
}
printf("体检结束!!!");
return 0;
}
```
### 3.2.3 应用示例
* 需求:根据年龄判断,如果是未成年人,则提示 "未成年人请在家长陪同下访问!" 。
* 示例:
```c
#include <stdio.h>
int main() {
int age = 0;
printf("请输入你的年龄:");
scanf("%d", &age);
if (age < 18) {
printf("未成年人请在家长陪同下访问!\n");
}
printf("欢迎继续访问!");
return 0;
}
```
## 3.3 双分支结构
### 3.3.1 概述
* 语法:
```c
if(条件表达式) {
语句块1;
}else {
语句块2;
}
```
> [!NOTE]
>
> * ① 在 C 语言中,严格意义上是没有 boolean 类型的,使用`非0` 表示`真true``0` 表示`假false`。
> * ② 当条件表达式为真(`非0` ),就会执行代码块 1 中的语句;否则,执行代码块 2 中的语句。
* 流程图,如下所示:
![](./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;
}
```
### 3.3.2 应用示例
* 需求输入年龄如果大于18岁则输出 "你年龄大于18要对自己的行为负责!";否则,输出 "你的年龄不大这次放过你了。"
* 示例:
```c
#include <stdio.h>
int main() {
int age = 0;
printf("请输入年龄:");
scanf("%d", &age);
if (age > 18) {
printf("你年龄大于18要对自己的行为负责!\n");
} else {
printf("你的年龄不大,这次放过你了!\n");
}
return 0;
}
```
### 3.3.3 应用示例
* 需求:判定某个年份是否为闰年?
>[!NOTE]
>
>* ① year 是 400 的整倍数: year%400==0
>* ② 能被 4 整除,但不能被 100 整除year % 4 == 0 && year % 100 != 0
* 示例:
```c
#include <stdio.h>
int main() {
int year = 0;
printf("请输入年份:");
scanf("%d", &year);
if (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)) {
printf("%d 是闰年\n", year);
} else {
printf("%d 不是闰年\n", year);
}
return 0;
}
```
## 3.4 多重分支结构
### 3.4.1 概述
* 语法:
```c
if (条件表达式1) {
语句块1;
} else if (条件表达式2) {
语句块2;
}
...
} else if (条件表达式n) {
语句块n;
} else {
语句块n+1;
}
```
> [!NOTE]
>
> * ① 在 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 。
* 示例:
```c
#include <stdio.h>
int main() {
int score = 0;
printf("请输入分数:");
scanf("%d", &score);
// 容错:分数不可能小于 0 或大于 100
if (score < 0 || score > 100) {
printf("输入的分数有误!\n");
return 0;
}
if (score >= 90) {
printf("奖励你一部华为 mate60 pro\n");
} else if (score >= 80) {
printf("奖励你一个 ipad\n");
} else if (score >= 60) {
printf("奖励你一个肉夹馍\n");
} else {
printf("你的成绩不及格,没有任何奖励!");
}
return 0;
}
```
### 3.4.2 应用示例
* 需求:判断水的温度,如果大于 95℃则打印 "开水";如果大于 70℃ 且小于等于 95℃则打印 "热水";如果大于 40℃ 且小于等于 70℃则打印 "温水";如果小于等于 40℃则打印 "凉水"。
* 示例:
```c
#include <stdio.h>
int main() {
int temperature = 0;
printf("请输入水的温度:");
scanf("%d", &temperature);
if (temperature > 95) {
printf("开水 \n");
} else if (temperature > 70 && temperature <= 95) {
printf("热水 \n");
} else if (temperature > 40 && temperature <= 70) {
printf("温水 \n");
} else {
printf("凉水 \n");
}
return 0;
}
```
## 3.5 多重分支结构 switch
### 3.5.1 概述
* 语法:
```c
switch(表达式){
case 常量值1:
语句块1;
//break;
case 常量值2:
语句块2;
//break;
...
case 常量值n:
语句块n;
//break;
[default:
语句块n+1;
]
}
```
> [!NOTE]
>
> * ① 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;
}
```
### 3.5.3 应用示例
* 需求编写程序输入月份输出该月份有多少天。说明1 月、3 月、5 月、7月、8 月、10 月、12 月有 31 天4 月、6 月、9 月、11 月有 30 天2 月有 28 天或 29 天。
* 示例:
```c
#include <stdio.h>
int main() {
int month;
printf("请输入月份 (1-12)");
scanf("%d", &month);
switch (month) {
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
printf("%d 月有 31 天\n", month);
break;
case 4:
case 6:
case 9:
case 11:
printf("%d 月有 30 天\n", month);
break;
case 2:
printf("%d 月有 28 天或 29 天\n", month);
break;
default:
printf("输入错误!");
break;
}
return 0;
}
```
### 3.5.4 switch 和 if else if 的比较
* ① 如果判断条件是判等,而且符合整型、枚举类型,虽然两个语句都可以使用,建议使用 swtich 语句。
* ② 如果判断条件是区间判断,大小判断等,使用 if...else...if。
## 3.6 嵌套分支
### 3.6.1 概述
* 嵌套分支是指,在一个分支结构中又嵌套了另一个分支结构,里面的分支的结构称为内层分支,外面的分支结构称为外层分支。
> [!NOTE]
>
> 嵌套分支层数不宜过多,建议最多不要超过 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;
}
```

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

View File

Before

Width:  |  Height:  |  Size: 586 KiB

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,104 +1,83 @@
# 第一章:数组的概念
# 第一章:概述
## 1.1 为什么需要数组?
* `流程控制结构`是用来控制程序中`各语句执行顺序`的语句,并且可以将语句组合成能`完成一定功能`的`小逻辑模块`。
* 在程序设计中规定了`三种`流程结构,如下所示:
* `顺序结构`:程序从上到下逐行执行,中间没有任何判断和跳转。
* `分支结构`:根据条件,有选择的执行某段代码。在 C 语言中,有 `if...else``switch...case` 两种分支语句。
* `循环结构`:根据循环条件,重复性的执行某段代码。在 C 语言中,有 `for`、`while`、`do...while` 三种循环结构。
### 1.1.1 需求分析 1
* 在生活中的`洗衣工厂`,就包含了上述的三种流程结构,如下所示:
* 需要统计某公司 50 个员工的工资情况,例如:计算平均工资、最高工资等。如果使用之前的知识,我们需要声明 50 个变量来分别记录每位员工的工资,即:
![](./assets/1.jpg)
```c
#include <stdio.h>
int main(){
double num1 = 0;
double num2 = 0;
double num3 = 0;
...
printf("请输入第 1 个员工的工资:");
scanf("%lf",&num1);
printf("请输入第 2 个员工的工资:");
scanf("%lf",&num2);
printf("请输入第 3 个员工的工资:");
scanf("%lf",&num3);
...
return 0;
}
```
# 第二章:顺序结构
* 这样会感觉特别机械和麻烦全是复制Ctrl + c和粘贴Ctrl + vCV 大法);此时,我们就可以将所有的`数据`全部存储到一个`容器(数组)`中进行统一管理,并进行其它的操作,如:求最值、求平均值等,如下所示:
## 2.1 概述
```c
#include <stdio.h>
int main(){
// 声明数组
double nums[50];
// 数组的长度
int length = sizeof(nums) / sizeof(double);
// 使用 for 循环向数组中添加值
for(int i = 0;i < length;i++){
printf("请输入第 &d 个员工的工资:",i);
scanf("%lf",&num[i]);
}
// 其它操作,如:求最值,求平均值等
...
return 0;
}
```
### 1.1.2 需求分析 2
* 在现实生活中,我们会使用很多 APP 或微信小程序等,即:
![](./assets/1.png)
* 同样的道理,如果我们使用变量来存储每个商品信息,那么就需要非常多的变量;但是,如果我们将这些`商品信息`都存储到一个`容器(数组)`中,进行统一管理;那么,之后的数据处理将会非常方便。
### 1.1.3 容器的概念
* `生活中的容器`:水杯(装水、饮料的容器)、衣柜(装衣服等物品的容器)、集装箱(装货物等物品的容器)。
* `程序中的容器`:将多个数据存储到一起,并且每个数据称为该容器中的元素。
## 1.2 什么是数组?
* 数组Array是将多个`相同数据类型`的`数据`按照一定的顺序排序的`集合`,并使用一个`标识符`命名,以及通过`编号(索引,亦称为下标)`的方式对这些数据进行统一管理。
* 程序从上到下逐行地执行,表达式语句都是顺序执行的,并且上一行对某个变量的修改对下一行会产生影响。
![](./assets/2.png)
## 1.3 数组的相关概念
## 2.2 应用示例
* `数组名`:本质上是一个标识符常量,命名需要符合标识符规则和规范。
* `元素`:同一个数组中的元素必须是相同的数据类型。
* `索引(下标)`:从 0 开始的连续数字。
* `数组的长度`:就是元素的个数。
* 示例:
## 1.4 数组的特点
```c
#include <stdio.h>
* ① 创建数组的时候,会在内存中开辟一整块`连续的空间`,占据空间的大小,取决于数组的长度和数组中元素的类型。
* ② 数组中的元素在内存中是依次紧密排列且有序的。
* ③ 数组一旦初始化完成,且长度就确定的,并且`数组的长度一旦确定,就不能更改`。
* ④ 我们可以直接通过索引(下标)来获取指定位置的元素,速度很快。
* ⑤ 数组名中引用的是这块连续空间的首地址。
int main() {
int x = 1;
int y = 2;
printf("x = %d \n", x); // x = 1
printf("y = %d \n", y); // y = 2
// 对 x 和 y 的值进行修改
x++;
y = 2 * x + y;
x = x * 10;
printf("x = %d \n", x); // x = 20
printf("y = %d \n", y); // y = 6
return 0;
}
```
# 第二章:数组的操作(⭐)
## 2.1 数组的定义
# 第三章:分支结构(⭐)
### 2.1.1 动态初始化
## 3.1 概述
* 根据特定条件执行不同的代码块,从而实现灵活的程序控制和更复杂的逻辑。
## 3.2 单分支结构
### 3.2.1 概述
* 语法:
```c
数据类型 数组名[元素个数|长度];
if(条件表达式){
语句;
}
```
> [!NOTE]
>
> * ① 数据类型:表示的是数组中每一个元素的数据类型。
> * ② 数组名:必须符合标识符规则和规范。
> * ③ 元素个数或长度:表示的是数组中最多可以容纳多少个元素(不能是负数、也不能是 0 )。
> * ① 在 C 语言中,严格意义上是没有 boolean 类型的,使用`非0` 表示`真true``0` 表示`假false`。
> * ② 当条件表达式为真(`非0` ),就会执行代码块中的语句;否则,就不会执行代码块中的语句。
* 流程图,如下所示:
![](./assets/3.png)
### 3.2.2 应用示例
* 需求:成年人心率的正常范围是每分钟 60~100 次。体检时,如果心率不在此范围内,则提示需要做进一步的检查。
@ -109,87 +88,198 @@ int main(){
int main() {
// 先指定元素的个数和类型,再进行初始化
int heartBeats = 0;
printf("请输入您的心率:");
scanf("%d", &heartBeats);
// 定义数组
int arr[3];
if (heartBeats < 60 || heartBeats > 100) {
printf("您的心率不在正常范围内,请做进一步的检查。\n");
}
// 给数组元素赋值
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
printf("体检结束!!!");
return 0;
}
```
### 2.1.2 静态初始化 1
### 3.2.3 应用示例
* 需求:根据年龄判断,如果是未成年人,则提示 "未成年人请在家长陪同下访问!" 。
* 示例:
```c
#include <stdio.h>
int main() {
int age = 0;
printf("请输入你的年龄:");
scanf("%d", &age);
if (age < 18) {
printf("未成年人请在家长陪同下访问!\n");
}
printf("欢迎继续访问!");
return 0;
}
```
## 3.3 双分支结构
### 3.3.1 概述
* 语法:
```c
数据类型 数组名[元素个数|长度] = {元素1,元素2,...}
if(条件表达式) {
语句块1;
}else {
语句块2;
}
```
> [!NOTE]
>
> * ① 静态部分初始化:如果数组初始化的元素个数`小于`数组声明的长度,那么就会从数组开始位置依次赋值,不够的就补 0 。
> * ② 静态全部初始化:数组初始化的元素个数`等于`数组的长度。
> * ① 在 C 语言中,严格意义上是没有 boolean 类型的,使用`非0` 表示`真true``0` 表示`假false`
> * ② 当条件表达式为真(`非0` ),就会执行代码块 1 中的语句;否则,执行代码块 2 中的语句
* 技巧:
* 流程图,如下所示
* 在 CLion 中可以开启`聚合初始化`功能,即:
![](./assets/4.png)
![](./assets/3.png)
### 3.3.2 应用示例
* 这样,在 CLion 中,将会显示数组初始化中的元素索引,即:
![](./assets/4.png)
* 需求:判断一个整数,是奇数还是偶数。
* 示例:静态部分初识化
* 示例:
```c
#include <stdio.h>
int main() {
// 定义数组和部分初始化:
// 会将给定的值从数组的开始位置一个个的赋值,没有赋值的地方,用 0 填充
int arr[5] = {1, 2};
int num = 0;
printf("请输入一个整数:");
scanf("%d", &num);
if (num % 2 == 0) {
printf("%d 是偶数\n", num);
} else {
printf("%d 是奇数\n", num);
}
return 0;
}
```
### 3.3.2 应用示例
* 需求输入年龄如果大于18岁则输出 "你年龄大于18要对自己的行为负责!";否则,输出 "你的年龄不大这次放过你了。"
* 示例:静态全部初始化
* 示例:
```c
#include <stdio.h>
int main() {
// 定义数组和全部初始化:数组初始化的元素个数等于数组的长度。
int arr[5] = {1, 2, 3, 4, 5};
int age = 0;
printf("请输入年龄:");
scanf("%d", &age);
if (age > 18) {
printf("你年龄大于18要对自己的行为负责!\n");
} else {
printf("你的年龄不大,这次放过你了!\n");
}
return 0;
}
```
### 2.1.3 静态初始化 2
### 3.3.3 应用示例
* 需求:判定某个年份是否为闰年?
>[!NOTE]
>
>* ① year 是 400 的整倍数: year%400==0
>* ② 能被 4 整除,但不能被 100 整除year % 4 == 0 && year % 100 != 0
* 示例:
```c
#include <stdio.h>
int main() {
int year = 0;
printf("请输入年份:");
scanf("%d", &year);
if (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)) {
printf("%d 是闰年\n", year);
} else {
printf("%d 不是闰年\n", year);
}
return 0;
}
```
## 3.4 多重分支结构
### 3.4.1 概述
* 语法:
```c
数据类型 数组名[] = {元素1,元素2,...}
if (条件表达式1) {
语句块1;
} else if (条件表达式2) {
语句块2;
}
...
} else if (条件表达式n) {
语句块n;
} else {
语句块n+1;
}
```
> [!NOTE]
>
> 没有给出数组中元素的个数,将由系统根据初始化的元素,自动推断出数组中元素的个数。
> * ① 在 C 语言中,严格意义上是没有 boolean 类型的,使用`非0` 表示`真true``0` 表示`假false`。
> * ② 首先判断关系表达式 1 的结果是真(值为 `非0`)还是假(值为 `0`
> * 如果为真,就执行语句块 1然后结束当前多分支。
> * 如果是假,就继续判断条件表达式 2看其结果是真还是假。
> * 如果是真,就执行语句块 2然后结束当前多分支。
> * 如果是假,就继续判断条件表达式…看其结果是真还是假。
> * ...
> * 如果没有任何关系表达式为真,就执行语句块 n+1然后结束当前多分支。
> * ③ 当条件表达式之间是`互斥`(彼此之间没有交集)关系时,条件判断语句及执行语句间顺序无所谓。
> * ④ 当条件表达式之间是`包含`关系时,必须`小上大下 / 子上父下`,否则范围小的条件表达式将不可能被执行。
> * ⑤ 当 if-else 结构是多选一的时候,最后的 else 是可选的,可以根据需要省略。
> * ⑥ 如果语句块中只有一条执行语句的时候,`{}`是可以省略的;但是,强烈建议保留!!!
* 流程图,如下所示:
![image-20240722075241253](./assets/5.png)
### 3.4.1 应用示例
* 需求:张三参加考试,他和父亲达成协议,如果成绩不到 60 分没有任何奖励;如果成绩 60分到 80 分,奖励一个肉夹馍;如果成绩 80 分(含)到 90 分,奖励一个 ipad如果成绩 90 分及以上,奖励一部华为 mate60 pro 。
@ -200,173 +290,100 @@ int main() {
int main() {
// 指定元素的类型,不指定元素个数,同时进行初始化
int arr[] = {1, 2, 3, 4, 5};
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;
}
```
## 2.2 访问数组元素
### 3.4.2 应用示例
* 需求:判断水的温度,如果大于 95℃则打印 "开水";如果大于 70℃ 且小于等于 95℃则打印 "热水";如果大于 40℃ 且小于等于 70℃则打印 "温水";如果小于等于 40℃则打印 "凉水"。
* 示例:
```c
#include <stdio.h>
int main() {
int temperature = 0;
printf("请输入水的温度:");
scanf("%d", &temperature);
if (temperature > 95) {
printf("开水 \n");
} else if (temperature > 70 && temperature <= 95) {
printf("热水 \n");
} else if (temperature > 40 && temperature <= 70) {
printf("温水 \n");
} else {
printf("凉水 \n");
}
return 0;
}
```
## 3.5 多重分支结构 switch
### 3.5.1 概述
* 语法:
```c
数组名[索引|下标];
switch(表达式){
case 常量值1:
语句块1;
//break;
case 常量值2:
语句块2;
//break;
...
case 常量值n:
语句块n;
//break;
[default:
语句块n+1;
]
}
```
> [!NOTE]
>
> 假设数组 `arr` 有 n 个元素,如果使用的数组的下标 `< 0``> n-1` ,那么将会产生数组越界访问,即超出了数组合法空间的访问;那么,数组的索引范围是 `[0,arr.length - 1]`
> * ① switch 后面表达式的值必须是一个整型char、short、int、long 等)或枚举类型。
> * ② case 后面的值必须是常量,不能是变量。
> * ③ default 是可选的,当没有匹配的 case 的时候,就执行 default 。
> * ④ break 语句可以使程序跳出 switch 语句块,如果没有 break会执行下一个 case 语句块,直到遇到 break 或者执行到 switch 结尾,这个现象称为穿透。
* 示例:
```c
#include <stdio.h>
int main() {
// 先指定元素的个数和类型,再进行初始化
// 定义数组
int arr[3];
// 给数组元素赋值
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
// 访问数组元素
printf("arr[0] = %d\n", arr[0]); // arr[0] = 10
printf("arr[1] = %d\n", arr[1]); // arr[1] = 20
printf("arr[2] = %d\n", arr[2]); // arr[2] = 30
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
// 定义数组和部分初始化:
// 会将给定的值从数组的开始位置一个个的赋值,没有赋值的地方,用 0 填充
int arr[5] = {1, 2};
// 访问数组元素
printf("arr[0] = %d\n", arr[0]); // arr[0] = 1
printf("arr[1] = %d\n", arr[1]); // arr[1] = 2
printf("arr[2] = %d\n", arr[2]); // arr[2] = 0
printf("arr[3] = %d\n", arr[3]); // arr[3] = 0
printf("arr[4] = %d\n", arr[4]); // arr[4] = 0
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
// 指定元素的类型,不指定元素个数,同时进行初始化
int arr[] = {1, 2, 3, 4, 5};
// 访问数组元素
printf("arr[0] = %d\n", arr[0]); // arr[0] = 1
printf("arr[1] = %d\n", arr[1]); // arr[1] = 2
printf("arr[2] = %d\n", arr[2]); // arr[2] = 3
printf("arr[3] = %d\n", arr[3]); // arr[3] = 4
printf("arr[4] = %d\n", arr[4]); // arr[4] = 5
return 0;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
// 定义数组和全部初始化:数组初始化的元素个数等于数组的长度。
int arr[5] = {1, 2, 3, 4, 5};
// 访问数组元素
printf("arr[0] = %d\n", arr[0]); // arr[0] = 1
printf("arr[1] = %d\n", arr[1]); // arr[1] = 2
printf("arr[2] = %d\n", arr[2]); // arr[2] = 3
printf("arr[3] = %d\n", arr[3]); // arr[3] = 4
printf("arr[4] = %d\n", arr[4]); // arr[4] = 5
return 0;
}
```
## 2.3 数组越界
* 数组下标必须在指定范围内使用,超出范围视为越界。
![](./assets/5.png)
> [!NOTE]
>
> * ① C 语言是不会做数组下标越界的检查,并且编译器也不会报错;但是,编译器不报错,并不意味着程序就是正确!
> * ② 在其它高级编程语言Java、JavaScript、Rust 等中,如果数组越界访问,编译器是会直接报错的!!!
* 示例:
```c
#include <stdio.h>
int main() {
// 定义数组和全部初始化:数组初始化的元素个数等于数组的长度。
int arr[] = {1, 2, 3, 4, 5};
// 访问数组元素
printf("arr[0] = %d\n", arr[0]); // arr[0] = 1
printf("arr[1] = %d\n", arr[1]); // arr[1] = 2
printf("arr[2] = %d\n", arr[2]); // arr[2] = 3
printf("arr[3] = %d\n", arr[3]); // arr[3] = 4
printf("arr[4] = %d\n", arr[4]); // arr[4] = 5
printf("arr[-1] = %d\n", arr[-1]); // 得到的是不确定的结果
printf("arr[5] = %d\n", arr[5]); // 得到的是不确定的结果
return 0;
}
```
## 2.4 计算数组的长度
* 数组长度(元素个数)是在数组定义的时候明确指定且固定的,我们不能在运行的时候直接获取数组长度;但是,我们可以通过 sizeof 运算符间接计算出数组的长度。
* 计算步骤,如下所示:
* ① 使用 sizeof 运算符计算出整个数组的字节长度。
* ② 由于数组成员是同一数据类型;那么,每个元素的字节长度一定相等,那么`数组的长度 = 整个数组的字节长度 ÷ 单个元素的字节长度 `。
* 流程图,如下所示:
![](./assets/6.png)
> [!NOTE]
>
> * ① 在很多编程语言中都内置了获取数组的长度的属性或方法Java 中的 arr.length 或 Rust 的 arr.len()。
> * ② 但是C 语言没有内置的获取数组长度的属性或方法,只能通过 sizeof 运算符间接来计算得到。
> * ③ 数组一旦`声明`或`定义`,其`长度`就`固定`了,`不能动态变化`。
### 3.5.2 应用示例
* 需求编写一个程序该程序可以接收一个字符比如a、b、c、d其中 a 表示星期一b 表示星期二…,根据用户的输入显示相应的信息,要求使用 switch 语句。
@ -377,179 +394,286 @@ int main() {
int main() {
// 定义数组和全部初始化:数组初始化的元素个数等于数组的长度。
int arr[] = {1, 2, 3, 4, 5};
char chs;
printf("请输入一个字符a、b、c、d");
scanf("%c", &chs);
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(arr[0]);
// 遍历数组
for (int i = 0; i < length; i++) {
printf("%d \n", arr[i]);
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;
}
```
## 2.5 遍历数组
### 3.5.3 应用示例
* 遍历数组是指按顺序访问数组中的每个元素,以便读取或修改它们,编程中一般使用循环结构对数组进行遍历。
* 需求编写程序输入月份输出该月份有多少天。说明1 月、3 月、5 月、7月、8 月、10 月、12 月有 31 天4 月、6 月、9 月、11 月有 30 天2 月有 28 天或 29 天
* 示例:声明一个存储有 12、2、31、24、15、36、67、108、29、51 的数组,并遍历数组所有元素
* 示例:
```c
#include <stdio.h>
int main() {
// 定义数组并初始化
int arr[] = {12, 2, 31, 24, 15, 36, 67, 108, 29, 51};
int month;
printf("请输入月份 (1-12)");
scanf("%d", &month);
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(int);
// 遍历数组
for (int i = 0; i < length; i++) {
printf("%d\n", arr[i]);
switch (month) {
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
printf("%d 月有 31 天\n", month);
break;
case 4:
case 6:
case 9:
case 11:
printf("%d 月有 30 天\n", month);
break;
case 2:
printf("%d 月有 28 天或 29 天\n", month);
break;
default:
printf("输入错误!");
break;
}
return 0;
}
```
### 3.5.4 switch 和 if else if 的比较
* ① 如果判断条件是判等,而且符合整型、枚举类型,虽然两个语句都可以使用,建议使用 swtich 语句。
* ② 如果判断条件是区间判断,大小判断等,使用 if...else...if。
## 3.6 嵌套分支
### 3.6.1 概述
* 嵌套分支是指,在一个分支结构中又嵌套了另一个分支结构,里面的分支的结构称为内层分支,外面的分支结构称为外层分支。
> [!NOTE]
>
> 嵌套分支层数不宜过多,建议最多不要超过 3 层。
### 3.6.2 应用示例
* 需求:根据淡旺季的月份和年龄,打印票价。
> [!NOTE]
>
> * ① 4 -10 是旺季:
> * 成人18-6060 。
> * 儿童(<18半价
> * 老人(>601/3 。
> * ② 其余是淡季:
> * 成人40。
> * 其他20。
* 示例:声明长度为 10 的 int 类型数组,给数组元素依次赋值为 0 ~ 9 ,并遍历数组所有元素
* 示例:
```c
#include <stdio.h>
int main() {
// 定义数组
int arr[10];
int month;
int age;
double price = 60;
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(int);
printf("请输入月份 (1-12)");
scanf("%d", &month);
// 给数组的每个元素赋值
for (int i = 0; i < length; i++) {
arr[i] = i;
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;
}
}
// 遍历数组
for (int i = 0; i < length; i++) {
printf("%d\n", arr[i]);
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;
}
```
## 2.6 一维数组的内存分析
### 2.6.1 数组内存图
* 假设数组是如下的定义:
# 第五章:循环结构(⭐)
## 5.1 概述
* 循环结构:在某些条件满足的情况下,反复执行特定代码的功能。
## 5.2 for 循环
### 5.2.1 概述
* 语法:
```c
int arr[] = {1,2,3,4,5};
for(初始化条件①;循环条件表达式②;迭代语句④){
循环体语句③
}
```
* 那么,对应的内存结构,如下所示:
> [!NOTE]
>
> * ① 初始化条件,用于初始化循环变量,只会执行一次,且循环开始前就执行(可以声明多个变量,但是必须是同一类型,用逗号 `,` 隔开)。
> * ② 循环条件表达式每次循环都执行,同 while 循环一样,每次先判断后执行循环体语句。
> * ③ 迭代语句每次循环都执行,在大括号中循环体语句之后执行(如果有多个变量更新,用逗号 `,` 隔开)。
* 流程图,如下所示:
![](./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]
>
> 如果之后试图更改数组名对应的地址,编译器就会报错。
> 执行过程是:① --> ② --> ③ --> ④ --> ② --> ③ --> ④ --> ... --> ② 。
* 示例:错误演示
### 5.2.2 应用示例
```c
int num[5]; // 声明数组
// 使用大括号重新赋值是不允许的,必须在数组声明的时候赋值,否则编译将会报错
num = {1,2,3,4,5} ; // 报错
```
* 示例:错误演示
```c
int num[] = {1,2,3,4,5};
// 使用大括号重新赋值是不允许的,必须在数组声明的时候赋值,否则编译将会报错
num = {2,3,4,5,6}; // 报错
```
* 示例:错误演示
```c
int num[5];
num = NULL; // 报错,需要和 Java 区别一下,在 C 中不可以
```
* 示例:错误演示
```c
int a[] = {1,2,3,4,5}
int b[5] = a ; // 报错,需要和 Java 区别一下,在 C 中不可以
```
## 2.7 数组应用案例
### 2.7.1 应用示例
* 需求:计算数组中所有元素的和以及平均数。
* 需求:输出 5 行 `Hello World!`
@ -560,40 +684,278 @@ int b[5] = a ; // 报错,需要和 Java 区别一下,在 C 中不可以
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];
for (int i = 1; i <= 5; ++i) {
printf("Hello World!\n");
}
double avg = (double)sum / length;
printf("数组的和为:%d\n", sum); // 数组的和为375
printf("数组的平均值为:%.2lf\n", avg); //数组的平均值为37.50
return 0;
}
```
### 2.7.2 应用示例
### 5.2.3 应用示例
* 需求:计算数组的最值(最大值和最小值)。
* 需求:求 1 ~ 100 之内所有偶数的和,以及偶数的个数。
* 示例:
```c
#include <stdio.h>
int main() {
int sum = 0;
int count = 0;
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
sum += i;
count++;
}
}
printf("1 ~ 100 中的所有偶数的和为: %d \n", sum);
printf("1 ~ 100 中的所有偶数的个数为: %d \n", count);
return 0;
}
```
### 5.2.4 应用示例
* 需求:输出所有的水仙花数,所谓水仙花数是指一个 3 位数,其各个位上数字立方和等于其本身,例如:`153 = 1×1×1 + 3×3×3 + 5×5×5`。
* 示例:
```c
#include <stdio.h>
int main() {
int count = 0;
for (int i = 100; i <= 999; i++) {
// 获取三位数
int ge = i % 10;
int shi = i / 10 % 10;
int bai = i / 100;
// 判定是否为水仙花数
if (ge * ge * ge + shi * shi * shi + bai * bai * bai == i) {
printf("水仙花数:%d\n", i);
count++;
}
}
printf("水仙花数总个数:%d\n", count);
return 0;
}
```
### 5.2.5 应用示例
* 需求:将 1 ~ 10 倒序输出10 、9 、8 ...
* 示例:
```c
#include <stdio.h>
int main() {
for (int i = 10; i >= 0; i--) {
printf("%d ", i);
}
return 0;
}
```
### 5.2.6 应用示例
* 需求:输入两个正整数 m 和 n 求其最大公约数和最小公倍数例如12 和 20 的最大公约数是 4 ,最小公倍数是 60 。
> [!NOTE]
>
> * 如果数 a 能被数 b 整除,且结果是整数,那么 a 就叫做 b 的倍数b 就叫做 a 的约数(因数)。
> * 如果一个整数同时是几个整数的约数,则称该整数为这些整数的公约数;其中,数值最大的称为最大公约数。
> * 如果一个整数同时为两个或多个整数的倍数的数,则称该整数为这些整数的公倍数;其中,数值最小的称为最小公倍数。
* 示例:
```c
#include <stdio.h>
int main() {
int m = 12, n = 20;
// 取出两个数中的较小值
int min = (m < n) ? m : n;
for (int i = min; i >= 1; i--) {
if (m % i == 0 && n % i == 0) {
printf("最大公约数是:%d\n", i); // 公约数
break; //跳出当前循环结构
}
}
// 取出两个数中的较大值
int max = (m > n) ? m : n;
for (int i = max; i <= m * n; i++) {
if (i % m == 0 && i % n == 0) {
printf("最小公倍数是:%d\n", i); // 公倍数
break;
}
}
return 0;
}
```
## 5.3 while 循环
### 5.3.1 概述
* 语法:
```c
初始化条件①;
while (循环条件语句②) {
循环体语句③;
迭代语句④;
}
```
> [!NOTE]
>
> * ① `while(循环条件部分)` 中循环条件为`非0`值,表示 `true`、`真`;为`0`值,表示 `false`、`假`。
> * ② 当循环条件表达式成立,就执行循环体语句,直到条件不成立停止循环。
> * ③ 为避免死循环,循环条件表达式不能永远成立,且随着循环次数增加,应该越来越趋向于不成立。
> * ④ for 循环和 while 循环`可以相互转换`,二者没有性能上的差别。
> * ⑤ for 循环与 while 循环的区别:`初始化条件部分的作用域不同`。
* 流程图,如下所示:
![](./assets/8.png)
> [!NOTE]
>
> 执行过程是:① --> ② --> ③ --> ④ --> ② --> ③ --> ④ --> ... --> ② 。
### 5.3.2 应用示例
* 需求:输出 5 行 `Hello World!`
* 示例:
```c
#include <stdio.h>
int main() {
int i = 1;
while (i <= 5) {
printf("Hello World!\n");
i++;
}
return 0;
}
```
### 5.3.3 应用示例
* 需求:求 1 ~ 100 之内所有偶数的和,以及偶数的个数。
* 示例:
```c
#include <stdio.h>
int main() {
int sum = 0;
int count = 0;
int i = 1;
while (i <= 100) {
if (i % 2 == 0) {
sum += i;
count++;
}
i++;
}
printf("1 ~ 100 中的所有偶数的和为: %d \n", sum);
printf("1 ~ 100 中的所有偶数的个数为: %d \n", count);
return 0;
}
```
### 5.3.4 应用示例
* 需求:世界最高山峰是珠穆朗玛峰,它的高度是 8848.86 米,假如我有一张足够大的纸,它的厚度是 0.1 毫米。请问,我折叠多少次,可以折成珠穆朗玛峰的高度?
* 示例:
```c
#include <stdio.h>
int main() {
// 折叠的次数
int count = 0;
// 珠峰的高度
int zfHeight = 8848860;
// 每次折叠的高度
double paperHeight = 0.1;
while (paperHeight <= zfHeight) {
count++;
paperHeight *= 2;
}
printf("需要折叠 %d 次,才能得到珠峰的高度。\n", count);
printf("折纸的高度为 %.2f 米,超过了珠峰的高度", paperHeight / 1000);
return 0;
}
```
### 5.3.5 应用示例
* 需求:给出一个整数 n ,判断该整数是否是 2 的幂次方。如果是,就输出 yes ;否则,输出 no 。
> [!NOTE]
>
> 思路:
>
> * ① 假设数组中的第一个元素是最大值或最小值,并使用变量 max 或 min 保存。
> * ② 遍历数组中的每个元素:
> * 如果有元素比最大值还要大,就让变量 max 保存最大值。
> * 如果有元素比最小值还要小,就让变量 min 保存最小值。
> * ① 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
@ -604,136 +966,34 @@ int main() {
int main() {
// 定义数组并初始化
int arr[] = {12, 2, 31, 24, 15, -36, 67, 108, 29, 51};
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
// 计算数组的长度
size_t length = sizeof(arr) / sizeof(int);
int n = 0;
printf("请输入一个整数:");
scanf("%d", &n);
// 定义最大值
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];
}
while (n > 1 && n % 2 == 0) {
n /= 2;
}
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);
if (n == 1) {
printf("yes");
} else {
printf("no");
}
return 0;
}
```
### 2.7.4 应用示例
### 5.3.6 应用示例
* 需求:将数组 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 应用示例
* 需求:数组对称位置的元素互换。
* 需求整数反转123 --> 321 。
> [!NOTE]
>
> 思路:假设数组一共有 10 个元素,那么:
>
> * a[0] 和 a[9] 互换。
> * a[1] 和 a[8] 互换。
> * ...
>
> 规律就是 `a[i] <--互换--> arr[arr.length -1 -i]`
> 思路:从右边开始,依次获取每一位数字,再拼接起来。
@ -744,282 +1004,97 @@ int main() {
int main() {
// 原始数组
int arr[] = {12, 2, 31, 24, 15, -36, 67, 108, 29, 51};
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
// 计算数组的长度
size_t SIZE = sizeof(arr) / sizeof(arr[0]);
int num = 0;
int original = 0;
int rev = 0;
printf("请输入一个整数:");
scanf("%d", &num);
original = num;
// 打印原始数组中的全部元素
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;
// 从右边开始,依次获取每个数字,然后拼接到 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("交换后数组:");
for (int i = 0; i < SIZE; i++) {
printf("%d ", arr[i]);
}
printf("\n");
printf("%d 的反转是 %d\n", original, rev);
return 0;
}
```
## 5.4 do-while 循环
* 示例:
```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;
}
```
# 第三章:多维数组(⭐)
## 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 动态初始化
### 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 取款机功能。
@ -1029,83 +1104,96 @@ int main() {
#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)");
// 定义二维数组并初始化
int arr[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
scanf("%d", &selection);
// 输出二维数组中的元素
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]);
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;
}
```
### 3.2.2 静态初始化 1
## 5.5 嵌套循环
### 5.5.1 概述
* 所谓的嵌套循环,是指一个循环结构 A 的循环体是另一个循环结构 B 。例如for 循环里面还有一个for 循环,就是嵌套循环。
* 语法:
```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;
for(初始化语句①; 循环条件语句②; 迭代语句⑦) {
for(初始化语句③; 循环条件语句④; 迭代语句⑥) {
循环体语句⑤;
}
}
```
### 3.2.3 静态初始化 2
* 其中for 、while 、do-while 均可以作为外层循环或内层循环。
- 外层循环:循环结构 A
- 内层循环:循环结构 B
* 语法:
```c
数据类型 数组名[][列数] = {{元素1,元素2,...},{元素3,...},...}
```
![](./assets/10.png)
> [!NOTE]
>
> * ① 列数 = 每个⼀维数组中有几个具体的数据元素。
> * ② 可以不指定行数,必须指定列数,编译器会根据元素的个数和列的个数,自动推断出行数!!!
> * ① 实际上,嵌套循环就是将内层循环当成外层循环的循环体。当只有内层循环的循环条件为 false ,才会完全跳出内层循环,才可结束外层的当次循环,开始下一次循环。
> * ② 假设外层循环次数为 m 次,内层循环次数为 n 次,则内层循环体实际上需要执行 m × n 次。
> * ③ 从二维图形的角度看,外层循环控制`行数`,内层循环控制`列数`。
> * ④ 实际开发中,我们最多见到的嵌套循环是两层,一般不会出现超过三层的嵌套循环。如果将要出现,一定要停下来重新梳理业务逻辑,重新思考算法的实现,控制在三层以内;否则,可读性会很差。
### 5.5.2 应用示例
* 需求:打印 5 行 `*` ,要求每行 6 个 `*`
@ -1116,48 +1204,21 @@ int main() {
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]);
for (int i = 1; i <= 5; ++i) {
for (int j = 1; j < 6; ++j) {
printf("* ");
}
printf("\n");
}
return 0;
}
```
## 3.3 二维数组的理解
### 5.5.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]`、... 是一维数组中元素的内存空间。
* 需求:打印 5 行直角三角形。
@ -1168,17 +1229,9 @@ int arr[3][4];
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]);
for (int i = 1; i <= 5; ++i) {
for (int j = 1; j <= i; ++j) {
printf("* ");
}
printf("\n");
}
@ -1187,17 +1240,9 @@ int main() {
}
```
## 3.5 二维数组的内存分析
### 5.5.4 应用示例
* 用`矩阵形式`3 行 4 列形式)表示二维数组,是`逻辑`上的概念,能形象地表示出行列关系。而在`内存`中,各元素是连续存放的,不是二维的,是`线性`的。
* C 语言中,二维数组中元素排列的顺序是`按行存放`的。即:先顺序存放第一行的元素,再存放第二行的元素。例如:数组`a[3][4] `在内存中的存放,如下所示:
![](./assets/21.png)
## 3.6 二维数组的应用案例
* 需求:现在有三个班,每个班五名同学,用二维数组保存他们的成绩,并求出每个班级平均分、以及所有班级平均分,数据要求从控制台输入。
* 需求:打印 5 行倒直角三角形。
@ -1208,37 +1253,130 @@ int main() {
int main() {
// 定义二维数组,用于保存成绩
double arr[3][5];
for (int i = 1; i <= 5; ++i) {
for (int j = 1; j <= 6 - i; ++j) {
printf("* ");
}
printf("\n");
}
// 获取二维数组的行数和列数
int row = sizeof(arr) / sizeof(arr[0]);
int col = sizeof(arr[0]) / sizeof(arr[0][0]);
return 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]);
### 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;
}
}
// 总分
double totalSum = 0;
printf("正数的个数:%d\n", positiveCount);
printf("负数的个数:%d\n", negativeCount);
// 遍历数组,求总分和各个班级的平均分
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];
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个班级的总分为%.2lf\n", i + 1, sum);
printf("第%d个班级的平均分为%.2lf\n", i + 1, sum / col);
printf("%d \n", i);
}
printf("所有班级的总分为:%.2lf\n", totalSum);
printf("所有班级的平均分为:%.2lf\n", totalSum / (row * col));
printf("程序结束!\n");
return 0;
}
@ -1246,45 +1384,113 @@ int main() {
# 第四章:字符串(⭐)
* 示例:编写程序,要求输入一个数字,判断该数字是否是质数
## 4.1 概述
```c
#include <stdio.h>
* 在实际开发中,我们除了经常处理整数、浮点数、字符等,还经常和字符串打交道,如:`"Hello World"`、`"Hi"` 等。
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]
>
> 像这类`"Hello World"`、`"Hi"`等格式 ,使用双引号引起来的一串字符称为字符串字面值,简称字符串。
> continue 一旦执行,就结束(或跳出)当次循环结构;并且,此关键字的后面,不能声明其它语句
* 对于整数、浮点数和字符C 语言中都提供了对应的数据类型。但是对于字符串C 语言并没有提供对应的数据类型,而是用`字符数组`来存储这类文本类型的数据,即字符串:
* 流程图,如下所示:
![](./assets/12.png)
* 示例:打印 0 ~ 10 ,如果遇到 `3` ,就继续下一次打印
```c
char str[32];
#include <stdio.h>
int main() {
for (int i = 0; i < 10; ++i) {
if (i == 3) {
continue;
}
printf("%d \n", i);
}
printf("程序结束!\n");
return 0;
}
```
* 字符串不像整数、浮点数以及字符那样有固定的大小,字符串是不定长的,如:`"Hello World"`、`"Hi"` 等的长度就是不一样的。在 C 语言中,规定了字符串的结尾必须是 `\0` ,这种字符串也被称为 `C 风格的字符串`,如:
* 示例:输出 100 以内(包括 100的数字跳过那些 7 的倍数或包含 7 的数字
```c
"Hello World" // 在 C 语言中,底层存储就是 Hello World\0
#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
![](./assets/22.png)
* `\0` 在 ASCII 码表中是第 0 个字符,用 NUL 表示,称为空字符,该字符既不能显示,也不是控制字符,输出该字符不会有任何效果,它在 C 语言中仅作为字符串的结束标志。
![](./assets/23.png)
* return :并非专门用于结束循环的,它的功能是结束一个方法。当一个方法执行到一个 return 语句的时候,这个方法将被结束。
> [!NOTE]
>
> 在现代化的高级编程语言中都提供了字符串对应的类型Java 中的 StringJDK 11 之前,底层也是通过 `char[]` 数组来实现的) 。
## 4.2 字符数组(字符串)的定义
### 4.2.1 标准写法
* 手动在字符串的结尾添加 `\0`作为字符串的结束标识。
> 和 break 和 continue 不同的是return 直接结束整个方法,不管这个 return 处于多少层循环之内。
@ -1294,320 +1500,17 @@ 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'};
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;
}
```
### 4.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;
}
```
## 4.3 字符串的输入和输出
* 对于字符串的输入和输出,同样可以使用 `scanf``printf` 函数来实现,并且其格式占位符是 `%s`
> [!NOTE]
>
> 之前提到,对于 scanf 函数而言,`%s` 默认是匹配到空格或 Enter 键,如果我们输入的字符串是 `Hello World`,就只能得到 `Hello` ;如果要实现匹配到换行,则可以在输入的时候,将格式占位符 `%s`替换为 `%[^\n]`
* 示例:
```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;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
char str[32];
printf("请输入字符串:");
scanf("%[^\n]", str);
printf("字符串是:%s\n", str);
return 0;
}
```
# 第五章:内存中的变量和数组(⭐)
## 5.1 内存和内存地址
### 5.1.1 内存
* `内存`是一种`计算机硬件`,是`软件`在`运行过程`中,用来`临时存储数据`的。在生活中,最为常见的`内存`就是`随机存取存储器RAM内存条`),其特点如下所示:
* ① 生活中最常见的内存类型,用于存储当前运行的程序和数据。
* ② 内存是易失性存储器,这意味着断电后数据会丢失。
* ③ 它具有高速读写特性,适用于需要快速访问的操作。
* 内存条的外观,如下所示:
![](./assets/24.jpeg)
* 像我们平常使用`记事本`软件一样,当我们输入一些文字的时候,其实是将数据`临时`保存在内存中的,如下所示:
> [!NOTE]
>
> * ① 目前,很多软件都很智能,如果用户没有将数据到保存文件中,将显示红色,以警告用户还没有保存数据,提醒用户需要尽快保存数据!!!
> * ② 但是也有很多软件提供了自动保存数据的功能其原理就是定时1s、3s、5s将内存中的数据刷新到文件中以防止数据丢失
> * ③ 将数据从内存存储到文件中,专业的说法是落盘(落在磁盘上)。
![](./assets/25.gif)
* 此时,如果我们在没有保存的过程下,将`记事本`软件关闭,那么刚才输入的文字将丢失;下次,再打开同样的文件(将数据从磁盘加载进内存,再交给 CPU之前输入的文字将不复存在如下所示
> [!NOTE]
>
> * ① 目前,很多软件都很智能,如果你没有保存,将提醒你是否保存或丢失刚才输入的文字。
> * ② 但是也有很多软件提供了自动保存数据的功能其原理就是定时1s、3s、5s将内存中的数据刷新到文件中以防止数据丢失
> * ③ 将数据从内存存储到文件中,专业的说法是落盘(落在磁盘上)。
![](./assets/26.gif)
> [!IMPORTANT]
>
> 内存就是软件在运行过程中,用来临时存储数据的,最为重要的两个步骤就是:
>
> * ① 将数据`保存`到内存中。
> * ② 从内存中的`对应位置`将数据`取出来`。
### 5.1.2 内存地址
* 在这个计算机的内存条,动不动就 32GB、64GB 、128GB 或更高的年代,如下所示:
![](./assets/27.png)
* 如果有一个 int 4 个字节)类型的数据 `2` ,如何将这个数据保存到内存中?(对应上述的步骤 ①)
![](./assets/28.svg)
* 就算数据 `2` 已经保存到内存中,那么内存中那么多的数据,我们又该如何取出呢?(对应上述的步骤 ②)
![](./assets/29.svg)
> [!IMPORTANT]
>
> 答案就是`内存地址`。
* 操作系统为了更快的去管理内存中的数据,会将`内存条`按照`字节`划分为一个个的`单元格`,如下所示:
![](./assets/30.svg)
> [!NOTE]
>
> 计算机中存储单位的换算,如下所示:
>
> - 1 B = 8 bit。
> - 1 KB = 1024 B。
> - 1 MB = 1024 KB。
> - 1 GB = 1024 MB。
> - 1 TB = 1024 GB 。
> - ……
* 为了方便管理,每个独立的小单元格,都有自己唯一的编号(内存地址),如下所示:
![](./assets/31.svg)
* 之所以,加了`内存地址`,就能`加快`数据的存取速度,可以类比生活中的`字典`
* 如果没有使用`拼音查找法`或`部首查找法`,我们需要一页一页,一行一行的,在整个字典中去搜索我们想要了解的汉字,效率非常低(如果要搜索的汉字在最后一页,可能需要将整个字典从头到尾翻一遍,这辈子真有可能翻得完?)。
![](./assets/32.gif)
* 如果使用`拼音查找法`或`部首查找法`,我们可以很快的定位到所要了解汉字所在的页数,加快了搜索的效率。
![](./assets/33.jpg)
![](./assets/34.jpg)
* 同样的道理,如果`没有`内存地址,我们只能一个个的去寻找想要的数据,效率非常低下,如下所示:
![](./assets/35.gif)
* 如果`使用`内存地址,我们就可以直接定位到指定的数据,效率非常高,如下所示:
![](./assets/36.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 位的二进制数字表示,由于表示形式太长,我们通常会转为十六进制,以方便阅读。
## 5.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/37.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]
>
> * ① `变量`是对程序中`数据`在内存中存储空间的抽象,如果不涉及到指针变量,那我们在编码的时候,就只需要将变量等价于内存中存储空间里面存储的数据,而不需要再去考虑编译器底层是如何转换,提高了开发效率(机器语言和汇编可不是这样的,需要关注每个细节)。
> * ② 数据类型只在`定义`变量的时候`指定`,而且必须指定;`使用`变量的时候`无需`再声明,因为此时的数据类型已经确定了。
## 5.3 内存中的数组
* 如果我们在代码中这么定义数组,如下所示:
```c
#include <stdio.h>
int main(){
// 定义一个数组并初始化
int arr[] = {1,2,3};
return 0;
}
```
* 那么,编译器就会这么处理,如下所示:
![](./assets/38.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]`。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 186 KiB

View File

Before

Width:  |  Height:  |  Size: 628 KiB

After

Width:  |  Height:  |  Size: 628 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 KiB

View File

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 213 KiB

View File

Before

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

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 819 KiB

After

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

View File

Before

Width:  |  Height:  |  Size: 44 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

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

View File

Before

Width:  |  Height:  |  Size: 65 KiB

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: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -1,501 +1,1613 @@
> [!IMPORTANT]
>
> * ① `指针`是 C 语言中`最重要`的概念之一,也是`最难以理解`的概念之一。
> * ② `指针`是 C 语言的`精髓`,要想掌握 C 语言就需要深入地了解指针。
# 第一章:数组的概念
## 1.1 为什么需要数组?
### 1.1.1 需求分析 1
# 第一章:颇具争议的指针
## 1.1 概述
* 目前而言,操作系统几乎都是通过 C 语言来编写和维护的;而 C 语言提供了指针的用法,其能直接操作内存地址,是个非常`强大`和`灵活`的工具;但是,需要开发者`小心谨慎的使用`,以确保程序的稳定性和安全性。
> [!NOTE]
>
> 之所以指针在 C 语言中颇具争议,是因为一方面其功能强大,直接操作内存地址;另一方面,又很危险,不正确的使用指针的方式,非常容易导致程序崩溃。
* 如果没有能很好的使用指针,就会带来一系列的问题,如:
* ① `空指针引用`Null Pointer Dereference当一个指针没有正确初始化或者被赋予了空NULL值时如果程序尝试访问该指针所指向的内存会导致运行时错误甚至导致程序崩溃。
* ② `野指针`Dangling Pointers指针指向的内存地址曾经分配给某个变量或对象但后来该变量或对象被释放或者移动导致指针仍指向已经无效的内存位置。对野指针进行操作可能会导致未定义的行为或程序崩溃。
* ③ `指针算术错误`:在进行指针运算时,如果没有正确管理指针的偏移量或者超出了数组的边界,可能会导致指针指向错误的内存位置,从而影响程序的正确性和安全性。
* ④ `内存泄漏`:如果动态分配的内存通过指针分配,但在不再需要时没有正确释放,会导致内存泄漏,长时间运行的程序可能会耗尽系统资源。
* 为了减少指针带来的风险,开发人员可以采取以下的措施:
* ① `良好的编程实践`:确保指针的初始化和使用是安全的,避免空指针引用和野指针问题。
* ② `边界检查`:在进行指针运算时,始终确保不会超出数组或内存分配的边界。
* ③ `使用指针和引用的适当性`:在可能的情况下,可以考虑使用更安全的语言特性,如:引用(在 C++ 等编程语言中)或者更高级别的数据结构来代替裸指针,从而减少指针使用时的潜在风险。
> [!IMPORTANT]
>
> * ① 既然指针很危险那么通过一系列的手段将指针包装或屏蔽以达到程序安全的目的这是现代化的高级编程语言解决的思路Java、Go、Rust 等)。
> * ② 之所以指针还需要学习是因为在嵌入式等领域其机器的资源CPU、内存等非常有限而现代化的高级编程语言虽然安全但是需要的系统资源也庞大。
> * ③ 我们知道编译型的程序不管编译过程如何复杂至少需要两步编译和运行。通常我们也将这两步称为编译期和运行期。C 语言中的指针之所以危险就在于程序要在运行的时候才会发现问题(后知后觉);而现代化的高级编程语言中的编译器在程序编译的时候就会发现问题(提前发现问题)。
> * ④ C 语言的编译器之所以这么设计的原因,就在于当时的内存和 CPU 是非常有限PDP-7 早期小型计算机CPU18 bit 的电子管逻辑内存4kb 和昂贵72,000 $),如果加入安全限制的功能,会远远超过整个系统的资源。
## 1.2 现代化高级编程语言是如何解决指针危险的?
* `C++`采用了如下的策略和机制,来解决指针危险操作的:
* ① `智能指针` C++ 引入了智能指针(如`std::shared_ptr`、`std::unique_ptr`),这些指针提供了自动资源管理和所有权的语义。`std::unique_ptr`确保只有一个指针可以访问给定的资源,从而避免了传统指针的悬空引用和内存泄漏问题。`std::shared_ptr`允许多个指针共享一个资源,并在所有引用释放后自动释放。
* ② `引用` C++ 中的引用(如:`&`符号)提供了更安全的间接访问方法,与指针相比,引用不能重新绑定到不同的对象,从而减少了意外的指针错误。
* `Go`采用了如下的策略和机制,来解决指针危险操作的:
* ① `内存管理和垃圾回收` Go 语言通过自动垃圾回收器管理内存减少了手动内存管理所带来的指针操作错误。Go 的垃圾回收器定期扫描并释放不再使用的内存,避免了内存泄漏和悬空指针问题。
* ② `指针的安全性` Go 语言的指针是受限的,不支持指针运算,从而减少了指针操作可能带来的风险。
* `Rust`采用了如下的策略和机制,来解决指针危险操作的:
* ① `所有权和借用` Rust 引入了所有权和借用的概念,编译器在编译时静态分析所有权转移和引用的生命周期。这种机制避免了数据竞争和空指针解引用等运行时错误,使得在编译时就能够保证内存安全。
* ② `生命周期` Rust 的生命周期系统确保引用的有效性和安全性,防止了悬空引用和指针乱用。
* `Java`采用了如下的策略和机制,来解决指针危险操作的:
* ① `引用类型和自动内存管理` Java 中所有的对象引用都是通过引用来访问的而不是直接的指针。Java 的自动垃圾回收器负责管理内存,从而避免了手动内存管理可能导致的指针错误,如:内存泄漏和悬空指针。
* ② `强类型系统和异常处理` Java 的强类型系统和异常处理机制减少了指针操作带来的风险空指针解引用异常NullPointerException。编译器在编译时能够捕获许多潜在的类型错误进一步增强了程序的安全性和可靠性。
> [!IMPORTANT]
>
> 总而言之,各种编程语言通过引入不同的策略和机制,如:智能指针、垃圾回收器、所有权和借用,以及强类型系统,有效地减少了指针操作所带来的各种安全性和可靠性问题,提升了程序的稳定性和开发效率。
# 第二章:回顾知识
## 2.1 变量
* 变量就是保存程序运行过程中临时产生的值,其语法如下:
* 需要统计某公司 50 个员工的工资情况,例如:计算平均工资、最高工资等。如果使用之前的知识,我们需要声明 50 个变量来分别记录每位员工的工资,即:
```c
数据类型 变量名 = 值 ;
#include <stdio.h>
int main(){
double num1 = 0;
double num2 = 0;
double num3 = 0;
...
printf("请输入第 1 个员工的工资:");
scanf("%lf",&num1);
printf("请输入第 2 个员工的工资:");
scanf("%lf",&num2);
printf("请输入第 3 个员工的工资:");
scanf("%lf",&num3);
...
return 0;
}
```
> [!IMPORTANT]
>
> 变量名(标识符)需要符合命名规则和命名规范!!!
>
> * 强制规范:
> - ① 只能由`小写`或`大写英文字母``0-9` 或 `_` 组成。
> - ② 不能以`数字`开头。
> - ③ 不可以是`关键字`。
> - ④ 标识符具有`长度`限制,不同编译器和平台会有所不同,一般限制在 63 个字符内。
> - ⑤ 严格`区分大小写字母`Hello、hello 是不同的标识符。
> * 建议规范:
> - ① 为了提高阅读性使用有意义的单词见名知意sumnamemaxyear 等。
> - ② 使用下划线连接多个单词组成的标识符max_classes_per_student 等。
> - ③ 多个单词组成的标识符,除了使用下划线连接,也可以使用小驼峰命名法,除第一个单词外,后续单词的首字母大写,如: studentId、student_name 等。
> - ④ 不要出现仅靠大小写区分不同的标识符name、Name 容易混淆。
> - ⑤ 系统内部使用了一些下划线开头的标识符C99 标准添加的类型 `_Bool`,为防止冲突,建议开发者尽量避免使用下划线开头的标识符。
* 这样会感觉特别机械和麻烦全是复制Ctrl + c和粘贴Ctrl + vCV 大法);此时,我们就可以将所有的`数据`全部存储到一个`容器(数组)`中进行统一管理,并进行其它的操作,如:求最值、求平均值等,如下所示:
* `变量名`的`作用`,如下所示:
* ① 当我们`编写`代码的时候,使用`变量名`来`关联`某块内存的`地址`。
* ② 当 CPU `执行`的时候,会将变量名`替换`为具体的地址,再进行具体的操作。
```c
#include <stdio.h>
## 2.2 普通变量和指针变量的区别
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
![img](./assets/1.png)
* 在现实生活中,我们会使用很多 APP 或微信小程序等,即:
* 普通变量和指针变量的相同点,如下所示:
* ① 普通变量有内存空间,指针变量也有内存空间。
* ② 普通变量有内存地址,指针变量也有内存地址。
* ③ 普通变量所对应的内存空间中有值,指针变量所对应的内存空间中也有值。
* 普通变量和指针变量的不同点:
- ① `普通变量`所对应的内存空间`存储`的是`普通的值`,如:整数、小数、字符等;`指针变量`所对应的内存空间`存储`的是另外一个变量的`地址`。
- ② `普通变量有普通变量的运算方式`,而`指针变量有指针变量的运算方式`(后续讲解)。
![](./assets/1.png)
## 2.3 运算符
* 同样的道理,如果我们使用变量来存储每个商品信息,那么就需要非常多的变量;但是,如果我们将这些`商品信息`都存储到一个`容器(数组)`中,进行统一管理;那么,之后的数据处理将会非常方便。
### 2.3.1 概述
### 1.1.3 容器的概念
* 运算符是一种特殊的符号,用于数据的运算、赋值和比较等
* `表达式`指的是一组运算数、运算符的组合,表达式`一定具有值`,一个变量或一个常量可以是表达式,变量、常量和运算符也可以组成表达式,如:
* `生活中的容器`:水杯(装水、饮料的容器)、衣柜(装衣服等物品的容器)、集装箱(装货物等物品的容器)。
* `程序中的容器`:将多个数据存储到一起,并且每个数据称为该容器中的元素。
![img](./assets/2.png)
## 1.2 什么是数组?
- `操作数`指的是`参与运算`的`值`或者`对象`,如:
* 数组Array是将多个`相同数据类型`的`数据`按照一定的顺序排序的`集合`,并使用一个`标识符`命名,以及通过`编号(索引,亦称为下标)`的方式对这些数据进行统一管理。
![](./assets/3.png)
![](./assets/2.png)
* 根据`操作数`的`个数`,可以将运算符分为:
* 一元运算符(一目运算符)。
* 二元运算符(二目运算符)。
* 三元运算符(三目运算符)。
* 根据`功能`,可以将运算符分为:
* 算术运算符。
* 关系运算符(比较运算符)。
* 逻辑运算符。
* 赋值运算符。
* 逻辑运算符。
* 位运算符。
* 三元运算符。
## 1.3 数组的相关概念
* `数组名`:本质上是一个标识符常量,命名需要符合标识符规则和规范。
* `元素`:同一个数组中的元素必须是相同的数据类型。
* `索引(下标)`:从 0 开始的连续数字。
* `数组的长度`:就是元素的个数。
## 1.4 数组的特点
* ① 创建数组的时候,会在内存中开辟一整块`连续的空间`,占据空间的大小,取决于数组的长度和数组中元素的类型。
* ② 数组中的元素在内存中是依次紧密排列且有序的。
* ③ 数组一旦初始化完成,且长度就确定的,并且`数组的长度一旦确定,就不能更改`。
* ④ 我们可以直接通过索引(下标)来获取指定位置的元素,速度很快。
* ⑤ 数组名中引用的是这块连续空间的首地址。
# 第二章:数组的操作(⭐)
## 2.1 数组的定义
### 2.1.1 动态初始化
* 语法:
```c
数据类型 数组名[元素个数|长度];
```
> [!NOTE]
>
> 掌握一个运算符,需要关注以下几个方面:
>
> - ① 运算符的含义。
> - ② 运算符操作数的个数。
> - ③ 运算符所组成的表达式。
> - ④ 运算符有无副作用,即:运算后是否会修改操作数的值。
> [!IMPORTANT]
>
> 普通变量支持上述的所有运算符;而指针变量并非支持上述的所有运算符,且支持运算符的含义和普通变量相差较大!!!
### 2.3.2 运算符的优先级
* C 语言中运算符的优先级,如下所示:
| **优先级** | **运算符** | **名称或含义** | **结合方向** |
| ---------- | ---------- | ---------------- | ------------- |
| **1** | `[]` | 数组下标 | ➡️(从左到右) |
| | `()` | 圆括号 | |
| | `.` | 成员选择(对象) | |
| | `->` | 成员选择(指针) | |
| **2** | `-` | 负号运算符 | ⬅️(从右到左) |
| | `(类型)` | 强制类型转换 | |
| | `++` | 自增运算符 | |
| | `--` | 自减运算符 | |
| | `*` | 取值运算符 | |
| | `&` | 取地址运算符 | |
| | `!` | 逻辑非运算符 | |
| | `~` | 按位取反运算符 | |
| | `sizeof` | 长度运算符 | |
| **3** | `/` | 除 | ➡️(从左到右) |
| | `*` | 乘 | |
| | `%` | 余数(取模) | |
| **4** | `+` | 加 | ➡️(从左到右) |
| | `-` | 减 | |
| **5** | `<<` | 左移 | ➡️(从左到右) |
| | `>>` | 右移 | |
| **6** | `>` | 大于 | ➡️(从左到右) |
| | `>=` | 大于等于 | |
| | `<` | 小于 | |
| | `<=` | 小于等于 | |
| **7** | `==` | 等于 | ➡️(从左到右) |
| | `!=` | 不等于 | |
| **8** | `&` | 按位与 | ➡️(从左到右) |
| **9** | `^` | 按位异或 | ➡️(从左到右) |
| **10** | `\|` | 按位或 | ➡️(从左到右) |
| **11** | `&&` | 逻辑与 | ➡️(从左到右) |
| **12** | `\|\|` | 逻辑或 | ➡️(从左到右) |
| **13** | `?:` | 条件运算符 | ⬅️(从右到左) |
| **14** | `=` | 赋值运算符 | ⬅️(从右到左) |
| | `/=` | 除后赋值 | |
| | `*=` | 乘后赋值 | |
| | `%=` | 取模后赋值 | |
| | `+=` | 加后赋值 | |
| | `-=` | 减后赋值 | |
| | `<<=` | 左移后赋值 | |
| | `>>=` | 右移后赋值 | |
| | `&=` | 按位与后赋值 | |
| | `^=` | 按位异或后赋值 | |
| | `\|=` | 按位或后赋值 | |
| **15** | `,` | 逗号运算符 | ➡️(从左到右) |
> [!WARNING]
>
> * ① 不要过多的依赖运算符的优先级来控制表达式的执行顺序,这样可读性太差,尽量`使用小括号来控制`表达式的执行顺序。
> * ② 不要把一个表达式写得过于复杂,如果一个表达式过于复杂,则把它`分成几步`来完成。
> * ③ 运算符优先级不用刻意地去记忆,总体上:一元运算符 > 算术运算符 > 关系运算符 > 逻辑运算符 > 三元运算符 > 赋值运算符。
> [!IMPORTANT]
>
> * ① 取值运算符 `*` 和取地址运算符 `&` 的优先级相同,并且运算方向都是从右向左!!!
> * ② 逗号运算符 `,` 的优先级最低,并且运算方向是从左向右!!!
> * ① 数据类型:表示的是数组中每一个元素的数据类型。
> * ② 数组名:必须符合标识符规则和规范。
> * ③ 元素个数或长度:表示的是数组中最多可以容纳多少个元素(不能是负数、也不能是 0 )。
# 第三章:指针的理解和定义(⭐)
## 3.1 变量的访问方式
* 计算机中程序的运行都是在内存中进行的变量也是内存中分配的空间且不同类型的变量占据的内存空间大小不同char 类型的变量是 1 个字节short 类型的变量是 2 个字节int 类型的变量是 4 个字节...
* 之前我们都是通过`变量名(普通变量)`访问内存中存储的数据,如下所示:
* 示例:
```c
#include <stdio.h>
int main() {
// 定义变量,即:开辟一块内存空间,并将初始化值存储进去
int num = 10;
// 先指定元素的个数和类型,再进行初始化
// 访问变量,即:访问变量在内存中对应的数据
printf("num = %d\n", num);
// 定义数组
int arr[3];
// 给变量赋值,即:给变量在内存中占据的内存空间存储数据
num = 100;
// 访问变量,即:访问变量在内存中对应的数据
printf("num = %d\n", num);
// 给数组元素赋值
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
return 0;
}
```
* 上述的这种方式也称为`直接访问`;当然,既然有`直接访问`的方式,必然有`间接访问`的方式,如:`指针`。
### 2.1.2 静态初始化 1
> [!IMPORTANT]
>
> * ① 我们通过`变量名(普通变量)`访问内存中变量存储的数据,之所以称为`直接访问`的方式,是因为对于我们写程序而言,我们无需关心如何根据内存地址去获取内存中对应的数据,也无需关系如何根据内存地址将数据存储到对应的内存空间,这些操作步骤都是`编译器`帮助我们在底层自动完成的(自动化)。
> * ② 但是,我们也可以通过`内存地址`去操作内存中对应的数据(手动化),这种方式就称为`间接访问`的方式了,相对于`直接访问`方式来说,要`理解`的`概念`和`操作`的`步骤`和之前`直接访问`的方式相比,要复杂和麻烦很多,但是效率高。
* 语法:
## 3.2 内存地址和指针
* 其实,在之前《数组》中,我们就已经讲解了`内存地址`的概念了,即:操作系统为了更快的去管理内存中的数据,会将`内存条`按照`字节`划分为一个个的`单元格`,并为每个独立的小的`单元格`,分配`唯一的编号`,即:`内存地址`,如下所示:
![](./assets/4.svg)
```c
数据类型 数组名[元素个数|长度] = {元素1,元素2,...}
```
> [!NOTE]
>
> 有了内存地址,就能加快数据的存取速度,可以类比生活中的`字典`,即:
> * ① 静态部分初始化:如果数组初始化的元素个数`小于`数组声明的长度,那么就会从数组开始位置依次赋值,不够的就补 0 。
> * ② 静态全部初始化:数组初始化的元素个数`等于`数组的长度。
* 技巧:
* 在 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.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} ; // 报错
```
* 示例:错误演示
```c
int num[] = {1,2,3,4,5};
// 使用大括号重新赋值是不允许的,必须在数组声明的时候赋值,否则编译将会报错
num = {2,3,4,5,6}; // 报错
```
* 示例:错误演示
```c
int num[5];
num = NULL; // 报错,需要和 Java 区别一下,在 C 中不可以
```
* 示例:错误演示
```c
int a[] = {1,2,3,4,5}
int b[5] = a ; // 报错,需要和 Java 区别一下,在 C 中不可以
```
## 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;
}
```
# 第三章:多维数组(⭐)
## 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)
## 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;
}
```
# 第四章:字符串(⭐)
## 4.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/22.png)
* `\0` 在 ASCII 码表中是第 0 个字符,用 NUL 表示,称为空字符,该字符既不能显示,也不是控制字符,输出该字符不会有任何效果,它在 C 语言中仅作为字符串的结束标志。
![](./assets/23.png)
> [!NOTE]
>
> 在现代化的高级编程语言中都提供了字符串对应的类型Java 中的 StringJDK 11 之前,底层也是通过 `char[]` 数组来实现的) 。
## 4.2 字符数组(字符串)的定义
### 4.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;
}
```
### 4.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;
}
```
## 4.3 字符串的输入和输出
* 对于字符串的输入和输出,同样可以使用 `scanf``printf` 函数来实现,并且其格式占位符是 `%s`
> [!NOTE]
>
> 之前提到,对于 scanf 函数而言,`%s` 默认是匹配到空格或 Enter 键,如果我们输入的字符串是 `Hello World`,就只能得到 `Hello` ;如果要实现匹配到换行,则可以在输入的时候,将格式占位符 `%s`替换为 `%[^\n]`
* 示例:
```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;
}
```
* 示例:
```c
#include <stdio.h>
int main() {
// 禁用 stdout 缓冲区
setbuf(stdout, NULL);
char str[32];
printf("请输入字符串:");
scanf("%[^\n]", str);
printf("字符串是:%s\n", str);
return 0;
}
```
# 第五章:内存中的变量和数组(⭐)
## 5.1 内存和内存地址
### 5.1.1 内存
* `内存`是一种`计算机硬件`,是`软件`在`运行过程`中,用来`临时存储数据`的。在生活中,最为常见的`内存`就是`随机存取存储器RAM内存条`),其特点如下所示:
* ① 生活中最常见的内存类型,用于存储当前运行的程序和数据。
* ② 内存是易失性存储器,这意味着断电后数据会丢失。
* ③ 它具有高速读写特性,适用于需要快速访问的操作。
* 内存条的外观,如下所示:
![](./assets/24.jpeg)
* 像我们平常使用`记事本`软件一样,当我们输入一些文字的时候,其实是将数据`临时`保存在内存中的,如下所示:
> [!NOTE]
>
> * ① 目前,很多软件都很智能,如果用户没有将数据到保存文件中,将显示红色,以警告用户还没有保存数据,提醒用户需要尽快保存数据!!!
> * ② 但是也有很多软件提供了自动保存数据的功能其原理就是定时1s、3s、5s将内存中的数据刷新到文件中以防止数据丢失
> * ③ 将数据从内存存储到文件中,专业的说法是落盘(落在磁盘上)。
![](./assets/25.gif)
* 此时,如果我们在没有保存的过程下,将`记事本`软件关闭,那么刚才输入的文字将丢失;下次,再打开同样的文件(将数据从磁盘加载进内存,再交给 CPU之前输入的文字将不复存在如下所示
> [!NOTE]
>
> * ① 目前,很多软件都很智能,如果你没有保存,将提醒你是否保存或丢失刚才输入的文字。
> * ② 但是也有很多软件提供了自动保存数据的功能其原理就是定时1s、3s、5s将内存中的数据刷新到文件中以防止数据丢失
> * ③ 将数据从内存存储到文件中,专业的说法是落盘(落在磁盘上)。
![](./assets/26.gif)
> [!IMPORTANT]
>
> 内存就是软件在运行过程中,用来临时存储数据的,最为重要的两个步骤就是:
>
> * ① 将数据`保存`到内存中。
> * ② 从内存中的`对应位置`将数据`取出来`。
### 5.1.2 内存地址
* 在这个计算机的内存条,动不动就 32GB、64GB 、128GB 或更高的年代,如下所示:
![](./assets/27.png)
* 如果有一个 int 4 个字节)类型的数据 `2` ,如何将这个数据保存到内存中?(对应上述的步骤 ①)
![](./assets/28.svg)
* 就算数据 `2` 已经保存到内存中,那么内存中那么多的数据,我们又该如何取出呢?(对应上述的步骤 ②)
![](./assets/29.svg)
> [!IMPORTANT]
>
> 答案就是`内存地址`。
* 操作系统为了更快的去管理内存中的数据,会将`内存条`按照`字节`划分为一个个的`单元格`,如下所示:
![](./assets/30.svg)
> [!NOTE]
>
> 计算机中存储单位的换算,如下所示:
>
> - 1 B = 8 bit。
> - 1 KB = 1024 B。
> - 1 MB = 1024 KB。
> - 1 GB = 1024 MB。
> - 1 TB = 1024 GB 。
> - ……
* 为了方便管理,每个独立的小单元格,都有自己唯一的编号(内存地址),如下所示:
![](./assets/31.svg)
* 之所以,加了`内存地址`,就能`加快`数据的存取速度,可以类比生活中的`字典`
* 如果没有使用`拼音查找法`或`部首查找法`,我们需要一页一页,一行一行的,在整个字典中去搜索我们想要了解的汉字,效率非常低(如果要搜索的汉字在最后一页,可能需要将整个字典从头到尾翻一遍,这辈子真有可能翻得完?)。
![](./assets/32.gif)
* 如果使用`拼音查找法`或`部首查找法`,我们可以很快的定位到所要了解汉字所在的页数,加快了搜索的效率。
![](./assets/33.jpg)
![](./assets/34.jpg)
* 同样的道理,如果`没有`内存地址,我们只能一个个的去寻找想要的数据,效率非常低下,如下所示:
![](./assets/35.gif)
* 如果`使用`内存地址,我们就可以直接定位到指定的数据,效率非常高,如下所示:
![](./assets/36.gif)
> [!IMPORTANT]
>
> * ① 内存地址是计算机中用于标识内存中某个特定位置的数值。
> * ② 每个内存单元都有一个唯一的地址,这些地址可以用于访问和操作存储在内存中的数据。
* 对于之前的代码,如下所示:
```c
#include <stdio.h>
int main() {
// 定义变量,即:开辟一块内存空间,并将初始化值存储进去
int num = 10;
return 0;
}
```
* 虽然,之前我们在程序中都是通过`变量名(普通变量)`直接操作内存中的存储单元;但是,编译器底层还是会通过`内存地址`来找到所需要的存储单元,如下所示:
![](./assets/5.svg)
* 实际中的内存地址,并不是像上面的 `001` 、`002` 、... 之类的数字,而是有自己的规则,即:内存地址规则。
> [!NOTE]
>
> 通过`内存地址`找到所需要的`存储单元`,即:内存地址指向该存储单元。此时,就可以将`内存地址`形象化的描述为`指针👉`,那么:
>
> * ① `变量`:命名的内存空间,用于存放各种类型的数据。
> * ② `变量名`:变量名是给内存空间取一个容易记忆的名字,方便我们编写程序。
> * ③ `变量值`:变量所对应的内存中的存储单元中存放的数据值。
> * ④ `变量的地址`:变量所对应的内存中的存储单元的内存地址(首地址),也可以称为`指针`。
>
> 总结:内存地址 = 指针。
> * ① 32 位的操作系统中,内存地址以 32 位的二进制表示。
> * ② 64 位的操作系统中,内存地址以 64 位的二进制表示。
* `普通变量`所对应的内存空间`存储`的是`普通的值`,如:整数、小数、字符等;`指针变量`所对应的内存空间`存储`的是另外一个变量的`地址(指针)`,如下所示:
![](./assets/6.svg)
* 在 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 位的二进制数字表示,由于表示形式太长,我们通常会转为十六进制,以方便阅读。
## 3.3 指针变量的定义
## 5.2 内存中的变量
* 在 C 语言中数据类型的种类很多short、int、long、float、double、char 等。以 int 类型为例,在 32 位或 64 位操作系统中的int 类型的变量都是占 4 个字节,当我们在代码中这么定义变量,如:
## 3.4 指针的作用
* 查询数据。
* 存储数据。
* 参数传递。
* 内存管理。
在 Java 中引用数据类型的向上类型转换upcasting和向下类型转换downcasting是面向对象编程中常见的操作。这些转换是 Java 继承体系和多态性的重要部分。我们先分别介绍向上类型转换和向下类型转换,然后讨论它们在 C 语言中指针的类似操作。
### 向上类型转换Upcasting
向上类型转换是将一个子类对象引用转换为父类对象引用。由于子类继承了父类的所有方法和属性,子类对象也包含父类对象的所有部分,因此这种转换是安全且隐式的。
**例子:**
```java
class Animal {
void makeSound() {
System.out.println("Animal sound");
}
}
class Dog extends Animal {
void makeSound() {
System.out.println("Bark");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
Animal animal = dog; // Upcasting, 隐式转换
animal.makeSound(); // 输出Bark
}
}
```
在这个例子中,`Dog` 类型的对象 `dog` 被转换为 `Animal` 类型。尽管 `animal` 引用的实际对象是 `Dog`,但在编译时,`animal` 被视为 `Animal` 类型。
### 向下类型转换Downcasting
向下类型转换是将一个父类对象引用转换为子类对象引用。由于父类对象不一定具有子类的所有方法和属性,因此这种转换需要显式进行,并且在运行时进行类型检查(使用 `instanceof` 关键字来确保安全)。
**例子:**
```java
public class Main {
public static void main(String[] args) {
Animal animal = new Dog(); // Upcasting
if (animal instanceof Dog) {
Dog dog = (Dog) animal; // Downcasting, 显式转换
dog.makeSound(); // 输出Bark
}
}
}
```
在这个例子中,`animal` 引用的对象实际是 `Dog` 类型,在向下转换之前使用 `instanceof` 检查以确保安全。
### 区别
- **向上类型转换**:隐式的,安全的,因为子类是父类的扩展。
- **向下类型转换**:显式的,可能不安全,需要运行时检查,因为父类不一定具有子类的特性。
### C 语言中的指针转换
在 C 语言中,指针的转换类似于引用类型的转换,但由于 C 语言没有继承和多态的概念,其转换更多是基于内存布局。
**例子:**
```c
#include <stdio.h>
void printInt(void* ptr) {
printf("%d\n", *(int*)ptr);
int main(){
// 定义一个变量并初始化
int a = 10;
return 0;
}
```
* 那么,编译器就会这么处理,如下所示:
![](./assets/37.svg)
* 在代码中,我们可以使用 `&变量名` 来获取一个变量的内存首地址,如下所示:
```c
#include <stdio.h>
int main() {
int x = 10;
void* voidPtr = &x; // Upcasting, 隐式转换
printInt(voidPtr); // 输出10
int* intPtr = (int*)voidPtr; // Downcasting, 显式转换
printf("%d\n", *intPtr); // 输出10
// 定义一个变量并初始化
int a = 10;
printf("变量 a 的首地址是: %p\n", &a); // 变量 a 的首地址是: 0000002bf1dffd0c
printf("变量 a 的中保存的值是: %d\n", a); // 变量 a 的中保存的值是: 10
return 0;
}
```
在这个例子中,`void*` 是通用指针类型,可以指向任何类型的数据。将 `int*` 转换为 `void*` 是隐式的,而将 `void*` 转换为 `int*` 是显式的。由于 C 没有类型检查,因此这种转换需要程序员自己确保安全。
总结:
- Java 中的向上类型转换和向下类型转换是为了支持多态性和继承,向上转换是安全的,向下转换需要显式进行并且进行运行时检查。
- C 语言中的指针转换没有多态性和继承的概念,但有类似的指针类型转换操作,程序员需要确保转换的安全性。
> [!NOTE]
>
> * ① `变量`是对程序中`数据`在内存中存储空间的抽象,如果不涉及到指针变量,那我们在编码的时候,就只需要将变量等价于内存中存储空间里面存储的数据,而不需要再去考虑编译器底层是如何转换,提高了开发效率(机器语言和汇编可不是这样的,需要关注每个细节)。
> * ② 数据类型只在`定义`变量的时候`指定`,而且必须指定;`使用`变量的时候`无需`再声明,因为此时的数据类型已经确定了。
## 5.3 内存中的数组
* 如果我们在代码中这么定义数组,如下所示:
# 第四章:指针的运算(⭐)
```c
#include <stdio.h>
## 4.1 概述
int main(){
// 定义一个数组并初始化
int arr[] = {1,2,3};
return 0;
}
```
* 那么,编译器就会这么处理,如下所示:
![](./assets/38.svg)
## 4.2 总结
* 在代码中,我们可以直接打印数组名的内存地址,如下所示:
* 在 C 语言中,`普通变量`是直接存储`数据`的`变量`。对于普通变量,支持的操作包括:
* ① **赋值操作**:给变量赋值,如:`int a = 5`。
* ② **算术运算**:可以对数值类型的普通变量进行加、减、乘、除等运算,如:`a + b`、`a - b`、`a * b`、`a / b`
* ③ **关系运算**:可以进行比较运算(大于、小于、等于等),如: `a > b`、`a == b`。
* ④ **逻辑运算**:对布尔类型的值进行与、或、非运算,如: `a && b`、`a || b`、`!a`。
* ⑤ **位运算**:对整数类型的值进行位操作(与、或、异或、取反、左移、右移等),如: `a & b`、`a | b`、`a ^ b`、`~a`、`a << 2`、`a >> 2`。
* ⑥ **自增自减运算**`a++`、`--a` 等。
```c
#include <stdio.h>
* 在 C 语言中,`指针变量`存储的是`另一个变量`的`地址`。对于指针变量,支持的操作包括:
* ① **赋值操作**:可以将一个地址赋值给指针,如: `int *p = &a;`
* ② **解引用操作**:通过指针访问它指向的变量,如: `*p = 10;` 修改指向变量的值。
* ③ **指针运算**
* **指针和整数值的加减运算**:指针可以进行整数的加减运算,用于访问数组或结构体成员,如: `p + 1`
* **指针的自增和自减运算**:指的是内存地址的向前或向后移动,如:`p++`、`p--`。
* **指针间的相减运算**:两个指向同一数组的指针相减可以得到它们之间的元素个数,如: `ptr1 - ptr2`
* **指针间的比较运算**:可以比较两个指针的大小,比较的是各自内存地址的大小,如: `p1 == p2`、`p1 != p2`。
* ④ **数组访问**:指针可以用于访问数组中的元素,通过 `*(p + i)` 访问第 `i` 个元素。
* ⑤ **指向指针的指针**:可以声明指向指针的指针,即多级指针,如:`int **pp = &p;`。
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]`。
在C语言中同类指针相减的结果是一个整数它表示两个指针之间相隔多少个指向的对象单位而不是它们在内存中的字节偏移量。这种对象单位是指针所指向的具体类型的大小。
举个例子来说,如果你有两个指向整数数组元素的指针 `p``q`,那么 `p - q` 的结果将是 `p` 指向的数组元素的索引与 `q` 指向的数组元素索引之间的差值。这个差值代表了在数组中相隔多少个整数元素,而不是它们在内存中的字节偏移量。
这种设计的优势在于,它使得指针运算更加直观和便于理解,特别是在处理数组和其他连续存储的数据结构时。因为指针运算结果的单位是根据指针所指向的具体类型来计算的,这样可以确保不同平台上的程序行为是一致的,不会受到底层硬件架构或者字节对齐规则的影响。
在C语言中数组名和指针有很多相似之处但数组名并不是指针变量。数组名实际是一个常量它指向数组的第一个元素的地址。为了证明这一点可以通过以下几个方面来说明
1. **数组名表示数组首地址**
数组名可以作为一个指针使用,数组名本身表示的是数组首地址。
```c
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // 这句话是合法的ptr现在指向arr[0]
printf("%p\n", arr); // 打印数组名,会打印数组首地址
printf("%p\n", &arr[0]); // 打印第一个元素的地址
```
2. **数组名是常量指针**
数组名是一个常量指针,不能改变它指向的位置,而指针变量可以改变它指向的位置。
```c
int arr[5];
int *ptr = arr; // 合法ptr指向arr[0]
ptr++; // 合法ptr现在指向arr[1]
// arr++; // 非法,编译错误,因为数组名是常量,不能改变
```
3. **sizeof运算符的结果不同**
使用`sizeof`运算符对数组名和指针变量会得到不同的结果,数组名会返回整个数组的大小,而指针变量会返回指针本身的大小。
```c
int arr[5];
int *ptr = arr;
printf("sizeof(arr) = %lu\n", sizeof(arr)); // 返回数组的大小5 * sizeof(int)
printf("sizeof(ptr) = %lu\n", sizeof(ptr)); // 返回指针的大小通常是4或8字节
```
4. **地址运算符的结果不同**
使用地址运算符`&`对数组名和指针变量会得到不同的结果,对数组名使用`&`会返回数组的地址,而对指针变量使用`&`会返回指针变量本身的地址。
```c
int arr[5];
int *ptr = arr;
printf("Address of array: %p\n", &arr); // 返回整个数组的地址
printf("Address of pointer: %p\n", &ptr); // 返回指针变量ptr的地址
```
综上所述,通过这些示例和解释,可以看出数组名虽然在某些场合下可以像指针一样使用,但它并不是一个真正的指针变量,而是一个常量,表示数组的首地址。

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

Before

Width:  |  Height:  |  Size: 452 KiB

After

Width:  |  Height:  |  Size: 452 KiB

View File

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

View File

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 315 KiB

View File

@ -0,0 +1,501 @@
> [!IMPORTANT]
>
> * ① `指针`是 C 语言中`最重要`的概念之一,也是`最难以理解`的概念之一。
> * ② `指针`是 C 语言的`精髓`,要想掌握 C 语言就需要深入地了解指针。
# 第一章:颇具争议的指针
## 1.1 概述
* 目前而言,操作系统几乎都是通过 C 语言来编写和维护的;而 C 语言提供了指针的用法,其能直接操作内存地址,是个非常`强大`和`灵活`的工具;但是,需要开发者`小心谨慎的使用`,以确保程序的稳定性和安全性。
> [!NOTE]
>
> 之所以指针在 C 语言中颇具争议,是因为一方面其功能强大,直接操作内存地址;另一方面,又很危险,不正确的使用指针的方式,非常容易导致程序崩溃。
* 如果没有能很好的使用指针,就会带来一系列的问题,如:
* ① `空指针引用`Null Pointer Dereference当一个指针没有正确初始化或者被赋予了空NULL值时如果程序尝试访问该指针所指向的内存会导致运行时错误甚至导致程序崩溃。
* ② `野指针`Dangling Pointers指针指向的内存地址曾经分配给某个变量或对象但后来该变量或对象被释放或者移动导致指针仍指向已经无效的内存位置。对野指针进行操作可能会导致未定义的行为或程序崩溃。
* ③ `指针算术错误`:在进行指针运算时,如果没有正确管理指针的偏移量或者超出了数组的边界,可能会导致指针指向错误的内存位置,从而影响程序的正确性和安全性。
* ④ `内存泄漏`:如果动态分配的内存通过指针分配,但在不再需要时没有正确释放,会导致内存泄漏,长时间运行的程序可能会耗尽系统资源。
* 为了减少指针带来的风险,开发人员可以采取以下的措施:
* ① `良好的编程实践`:确保指针的初始化和使用是安全的,避免空指针引用和野指针问题。
* ② `边界检查`:在进行指针运算时,始终确保不会超出数组或内存分配的边界。
* ③ `使用指针和引用的适当性`:在可能的情况下,可以考虑使用更安全的语言特性,如:引用(在 C++ 等编程语言中)或者更高级别的数据结构来代替裸指针,从而减少指针使用时的潜在风险。
> [!IMPORTANT]
>
> * ① 既然指针很危险那么通过一系列的手段将指针包装或屏蔽以达到程序安全的目的这是现代化的高级编程语言解决的思路Java、Go、Rust 等)。
> * ② 之所以指针还需要学习是因为在嵌入式等领域其机器的资源CPU、内存等非常有限而现代化的高级编程语言虽然安全但是需要的系统资源也庞大。
> * ③ 我们知道编译型的程序不管编译过程如何复杂至少需要两步编译和运行。通常我们也将这两步称为编译期和运行期。C 语言中的指针之所以危险就在于程序要在运行的时候才会发现问题(后知后觉);而现代化的高级编程语言中的编译器在程序编译的时候就会发现问题(提前发现问题)。
> * ④ C 语言的编译器之所以这么设计的原因,就在于当时的内存和 CPU 是非常有限PDP-7 早期小型计算机CPU18 bit 的电子管逻辑内存4kb 和昂贵72,000 $),如果加入安全限制的功能,会远远超过整个系统的资源。
## 1.2 现代化高级编程语言是如何解决指针危险的?
* `C++`采用了如下的策略和机制,来解决指针危险操作的:
* ① `智能指针` C++ 引入了智能指针(如`std::shared_ptr`、`std::unique_ptr`),这些指针提供了自动资源管理和所有权的语义。`std::unique_ptr`确保只有一个指针可以访问给定的资源,从而避免了传统指针的悬空引用和内存泄漏问题。`std::shared_ptr`允许多个指针共享一个资源,并在所有引用释放后自动释放。
* ② `引用` C++ 中的引用(如:`&`符号)提供了更安全的间接访问方法,与指针相比,引用不能重新绑定到不同的对象,从而减少了意外的指针错误。
* `Go`采用了如下的策略和机制,来解决指针危险操作的:
* ① `内存管理和垃圾回收` Go 语言通过自动垃圾回收器管理内存减少了手动内存管理所带来的指针操作错误。Go 的垃圾回收器定期扫描并释放不再使用的内存,避免了内存泄漏和悬空指针问题。
* ② `指针的安全性` Go 语言的指针是受限的,不支持指针运算,从而减少了指针操作可能带来的风险。
* `Rust`采用了如下的策略和机制,来解决指针危险操作的:
* ① `所有权和借用` Rust 引入了所有权和借用的概念,编译器在编译时静态分析所有权转移和引用的生命周期。这种机制避免了数据竞争和空指针解引用等运行时错误,使得在编译时就能够保证内存安全。
* ② `生命周期` Rust 的生命周期系统确保引用的有效性和安全性,防止了悬空引用和指针乱用。
* `Java`采用了如下的策略和机制,来解决指针危险操作的:
* ① `引用类型和自动内存管理` Java 中所有的对象引用都是通过引用来访问的而不是直接的指针。Java 的自动垃圾回收器负责管理内存,从而避免了手动内存管理可能导致的指针错误,如:内存泄漏和悬空指针。
* ② `强类型系统和异常处理` Java 的强类型系统和异常处理机制减少了指针操作带来的风险空指针解引用异常NullPointerException。编译器在编译时能够捕获许多潜在的类型错误进一步增强了程序的安全性和可靠性。
> [!IMPORTANT]
>
> 总而言之,各种编程语言通过引入不同的策略和机制,如:智能指针、垃圾回收器、所有权和借用,以及强类型系统,有效地减少了指针操作所带来的各种安全性和可靠性问题,提升了程序的稳定性和开发效率。
# 第二章:回顾知识
## 2.1 变量
* 变量就是保存程序运行过程中临时产生的值,其语法如下:
```c
数据类型 变量名 = 值 ;
```
> [!IMPORTANT]
>
> 变量名(标识符)需要符合命名规则和命名规范!!!
>
> * 强制规范:
> - ① 只能由`小写`或`大写英文字母``0-9` 或 `_` 组成。
> - ② 不能以`数字`开头。
> - ③ 不可以是`关键字`。
> - ④ 标识符具有`长度`限制,不同编译器和平台会有所不同,一般限制在 63 个字符内。
> - ⑤ 严格`区分大小写字母`Hello、hello 是不同的标识符。
> * 建议规范:
> - ① 为了提高阅读性使用有意义的单词见名知意sumnamemaxyear 等。
> - ② 使用下划线连接多个单词组成的标识符max_classes_per_student 等。
> - ③ 多个单词组成的标识符,除了使用下划线连接,也可以使用小驼峰命名法,除第一个单词外,后续单词的首字母大写,如: studentId、student_name 等。
> - ④ 不要出现仅靠大小写区分不同的标识符name、Name 容易混淆。
> - ⑤ 系统内部使用了一些下划线开头的标识符C99 标准添加的类型 `_Bool`,为防止冲突,建议开发者尽量避免使用下划线开头的标识符。
* `变量名`的`作用`,如下所示:
* ① 当我们`编写`代码的时候,使用`变量名`来`关联`某块内存的`地址`。
* ② 当 CPU `执行`的时候,会将变量名`替换`为具体的地址,再进行具体的操作。
## 2.2 普通变量和指针变量的区别
* 根据`变量`中`存储`的`值`的`不同`,我们可以将`变量`分为两类:
- `普通变量`:变量所对应的内存中存储的是`普通值`。
- `指针变量`:变量所对应的内存中存储的是`另一个变量的地址`。
* 如下图所示:
![img](./assets/1.png)
* 普通变量和指针变量的相同点,如下所示:
* ① 普通变量有内存空间,指针变量也有内存空间。
* ② 普通变量有内存地址,指针变量也有内存地址。
* ③ 普通变量所对应的内存空间中有值,指针变量所对应的内存空间中也有值。
* 普通变量和指针变量的不同点:
- ① `普通变量`所对应的内存空间`存储`的是`普通的值`,如:整数、小数、字符等;`指针变量`所对应的内存空间`存储`的是另外一个变量的`地址`。
- ② `普通变量有普通变量的运算方式`,而`指针变量有指针变量的运算方式`(后续讲解)。
## 2.3 运算符
### 2.3.1 概述
* 运算符是一种特殊的符号,用于数据的运算、赋值和比较等。
* `表达式`指的是一组运算数、运算符的组合,表达式`一定具有值`,一个变量或一个常量可以是表达式,变量、常量和运算符也可以组成表达式,如:
![img](./assets/2.png)
- `操作数`指的是`参与运算`的`值`或者`对象`,如:
![](./assets/3.png)
* 根据`操作数`的`个数`,可以将运算符分为:
* 一元运算符(一目运算符)。
* 二元运算符(二目运算符)。
* 三元运算符(三目运算符)。
* 根据`功能`,可以将运算符分为:
* 算术运算符。
* 关系运算符(比较运算符)。
* 逻辑运算符。
* 赋值运算符。
* 逻辑运算符。
* 位运算符。
* 三元运算符。
> [!NOTE]
>
> 掌握一个运算符,需要关注以下几个方面:
>
> - ① 运算符的含义。
> - ② 运算符操作数的个数。
> - ③ 运算符所组成的表达式。
> - ④ 运算符有无副作用,即:运算后是否会修改操作数的值。
> [!IMPORTANT]
>
> 普通变量支持上述的所有运算符;而指针变量并非支持上述的所有运算符,且支持运算符的含义和普通变量相差较大!!!
### 2.3.2 运算符的优先级
* C 语言中运算符的优先级,如下所示:
| **优先级** | **运算符** | **名称或含义** | **结合方向** |
| ---------- | ---------- | ---------------- | ------------- |
| **1** | `[]` | 数组下标 | ➡️(从左到右) |
| | `()` | 圆括号 | |
| | `.` | 成员选择(对象) | |
| | `->` | 成员选择(指针) | |
| **2** | `-` | 负号运算符 | ⬅️(从右到左) |
| | `(类型)` | 强制类型转换 | |
| | `++` | 自增运算符 | |
| | `--` | 自减运算符 | |
| | `*` | 取值运算符 | |
| | `&` | 取地址运算符 | |
| | `!` | 逻辑非运算符 | |
| | `~` | 按位取反运算符 | |
| | `sizeof` | 长度运算符 | |
| **3** | `/` | 除 | ➡️(从左到右) |
| | `*` | 乘 | |
| | `%` | 余数(取模) | |
| **4** | `+` | 加 | ➡️(从左到右) |
| | `-` | 减 | |
| **5** | `<<` | 左移 | ➡️(从左到右) |
| | `>>` | 右移 | |
| **6** | `>` | 大于 | ➡️(从左到右) |
| | `>=` | 大于等于 | |
| | `<` | 小于 | |
| | `<=` | 小于等于 | |
| **7** | `==` | 等于 | ➡️(从左到右) |
| | `!=` | 不等于 | |
| **8** | `&` | 按位与 | ➡️(从左到右) |
| **9** | `^` | 按位异或 | ➡️(从左到右) |
| **10** | `\|` | 按位或 | ➡️(从左到右) |
| **11** | `&&` | 逻辑与 | ➡️(从左到右) |
| **12** | `\|\|` | 逻辑或 | ➡️(从左到右) |
| **13** | `?:` | 条件运算符 | ⬅️(从右到左) |
| **14** | `=` | 赋值运算符 | ⬅️(从右到左) |
| | `/=` | 除后赋值 | |
| | `*=` | 乘后赋值 | |
| | `%=` | 取模后赋值 | |
| | `+=` | 加后赋值 | |
| | `-=` | 减后赋值 | |
| | `<<=` | 左移后赋值 | |
| | `>>=` | 右移后赋值 | |
| | `&=` | 按位与后赋值 | |
| | `^=` | 按位异或后赋值 | |
| | `\|=` | 按位或后赋值 | |
| **15** | `,` | 逗号运算符 | ➡️(从左到右) |
> [!WARNING]
>
> * ① 不要过多的依赖运算符的优先级来控制表达式的执行顺序,这样可读性太差,尽量`使用小括号来控制`表达式的执行顺序。
> * ② 不要把一个表达式写得过于复杂,如果一个表达式过于复杂,则把它`分成几步`来完成。
> * ③ 运算符优先级不用刻意地去记忆,总体上:一元运算符 > 算术运算符 > 关系运算符 > 逻辑运算符 > 三元运算符 > 赋值运算符。
> [!IMPORTANT]
>
> * ① 取值运算符 `*` 和取地址运算符 `&` 的优先级相同,并且运算方向都是从右向左!!!
> * ② 逗号运算符 `,` 的优先级最低,并且运算方向是从左向右!!!
# 第三章:指针的理解和定义(⭐)
## 3.1 变量的访问方式
* 计算机中程序的运行都是在内存中进行的变量也是内存中分配的空间且不同类型的变量占据的内存空间大小不同char 类型的变量是 1 个字节short 类型的变量是 2 个字节int 类型的变量是 4 个字节...
* 之前我们都是通过`变量名(普通变量)`访问内存中存储的数据,如下所示:
```c
#include <stdio.h>
int main() {
// 定义变量,即:开辟一块内存空间,并将初始化值存储进去
int num = 10;
// 访问变量,即:访问变量在内存中对应的数据
printf("num = %d\n", num);
// 给变量赋值,即:给变量在内存中占据的内存空间存储数据
num = 100;
// 访问变量,即:访问变量在内存中对应的数据
printf("num = %d\n", num);
return 0;
}
```
* 上述的这种方式也称为`直接访问`;当然,既然有`直接访问`的方式,必然有`间接访问`的方式,如:`指针`。
> [!IMPORTANT]
>
> * ① 我们通过`变量名(普通变量)`访问内存中变量存储的数据,之所以称为`直接访问`的方式,是因为对于我们写程序而言,我们无需关心如何根据内存地址去获取内存中对应的数据,也无需关系如何根据内存地址将数据存储到对应的内存空间,这些操作步骤都是`编译器`帮助我们在底层自动完成的(自动化)。
> * ② 但是,我们也可以通过`内存地址`去操作内存中对应的数据(手动化),这种方式就称为`间接访问`的方式了,相对于`直接访问`方式来说,要`理解`的`概念`和`操作`的`步骤`和之前`直接访问`的方式相比,要复杂和麻烦很多,但是效率高。
## 3.2 内存地址和指针
* 其实,在之前《数组》中,我们就已经讲解了`内存地址`的概念了,即:操作系统为了更快的去管理内存中的数据,会将`内存条`按照`字节`划分为一个个的`单元格`,并为每个独立的小的`单元格`,分配`唯一的编号`,即:`内存地址`,如下所示:
![](./assets/4.svg)
> [!NOTE]
>
> 有了内存地址,就能加快数据的存取速度,可以类比生活中的`字典`,即:
>
> * ① 内存地址是计算机中用于标识内存中某个特定位置的数值。
> * ② 每个内存单元都有一个唯一的地址,这些地址可以用于访问和操作存储在内存中的数据。
* 对于之前的代码,如下所示:
```c
#include <stdio.h>
int main() {
// 定义变量,即:开辟一块内存空间,并将初始化值存储进去
int num = 10;
return 0;
}
```
* 虽然,之前我们在程序中都是通过`变量名(普通变量)`直接操作内存中的存储单元;但是,编译器底层还是会通过`内存地址`来找到所需要的存储单元,如下所示:
![](./assets/5.svg)
> [!NOTE]
>
> 通过`内存地址`找到所需要的`存储单元`,即:内存地址指向该存储单元。此时,就可以将`内存地址`形象化的描述为`指针👉`,那么:
>
> * ① `变量`:命名的内存空间,用于存放各种类型的数据。
> * ② `变量名`:变量名是给内存空间取一个容易记忆的名字,方便我们编写程序。
> * ③ `变量值`:变量所对应的内存中的存储单元中存放的数据值。
> * ④ `变量的地址`:变量所对应的内存中的存储单元的内存地址(首地址),也可以称为`指针`。
>
> 总结:内存地址 = 指针。
* `普通变量`所对应的内存空间`存储`的是`普通的值`,如:整数、小数、字符等;`指针变量`所对应的内存空间`存储`的是另外一个变量的`地址(指针)`,如下所示:
![](./assets/6.svg)
> [!NOTE]
>
> 有的时候,为了方便阐述,我们会将`指针变量`称为`指针`。但是,需要记住的是:
>
> * 指针 = 内存地址。
> * 指针变量 = 变量中保存的是另一个变量的地址。
>
> 下文中提及的`指针`都是`指针变量`,不再阐述!!!
> [!IMPORTANT]
>
> 如果你观察仔细的话,你可能会发现`指针变量`和`普通变量`在内存中占据的存储空间是不一样的,那么到底是什么原因造成这样的结果?
## 3.3 指针变量的定义
## 3.4 指针的作用
* 查询数据。
* 存储数据。
* 参数传递。
* 内存管理。
在 Java 中引用数据类型的向上类型转换upcasting和向下类型转换downcasting是面向对象编程中常见的操作。这些转换是 Java 继承体系和多态性的重要部分。我们先分别介绍向上类型转换和向下类型转换,然后讨论它们在 C 语言中指针的类似操作。
### 向上类型转换Upcasting
向上类型转换是将一个子类对象引用转换为父类对象引用。由于子类继承了父类的所有方法和属性,子类对象也包含父类对象的所有部分,因此这种转换是安全且隐式的。
**例子:**
```java
class Animal {
void makeSound() {
System.out.println("Animal sound");
}
}
class Dog extends Animal {
void makeSound() {
System.out.println("Bark");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
Animal animal = dog; // Upcasting, 隐式转换
animal.makeSound(); // 输出Bark
}
}
```
在这个例子中,`Dog` 类型的对象 `dog` 被转换为 `Animal` 类型。尽管 `animal` 引用的实际对象是 `Dog`,但在编译时,`animal` 被视为 `Animal` 类型。
### 向下类型转换Downcasting
向下类型转换是将一个父类对象引用转换为子类对象引用。由于父类对象不一定具有子类的所有方法和属性,因此这种转换需要显式进行,并且在运行时进行类型检查(使用 `instanceof` 关键字来确保安全)。
**例子:**
```java
public class Main {
public static void main(String[] args) {
Animal animal = new Dog(); // Upcasting
if (animal instanceof Dog) {
Dog dog = (Dog) animal; // Downcasting, 显式转换
dog.makeSound(); // 输出Bark
}
}
}
```
在这个例子中,`animal` 引用的对象实际是 `Dog` 类型,在向下转换之前使用 `instanceof` 检查以确保安全。
### 区别
- **向上类型转换**:隐式的,安全的,因为子类是父类的扩展。
- **向下类型转换**:显式的,可能不安全,需要运行时检查,因为父类不一定具有子类的特性。
### C 语言中的指针转换
在 C 语言中,指针的转换类似于引用类型的转换,但由于 C 语言没有继承和多态的概念,其转换更多是基于内存布局。
**例子:**
```c
#include <stdio.h>
void printInt(void* ptr) {
printf("%d\n", *(int*)ptr);
}
int main() {
int x = 10;
void* voidPtr = &x; // Upcasting, 隐式转换
printInt(voidPtr); // 输出10
int* intPtr = (int*)voidPtr; // Downcasting, 显式转换
printf("%d\n", *intPtr); // 输出10
return 0;
}
```
在这个例子中,`void*` 是通用指针类型,可以指向任何类型的数据。将 `int*` 转换为 `void*` 是隐式的,而将 `void*` 转换为 `int*` 是显式的。由于 C 没有类型检查,因此这种转换需要程序员自己确保安全。
总结:
- Java 中的向上类型转换和向下类型转换是为了支持多态性和继承,向上转换是安全的,向下转换需要显式进行并且进行运行时检查。
- C 语言中的指针转换没有多态性和继承的概念,但有类似的指针类型转换操作,程序员需要确保转换的安全性。
# 第四章:指针的运算(⭐)
## 4.1 概述
## 4.2 总结
* 在 C 语言中,`普通变量`是直接存储`数据`的`变量`。对于普通变量,支持的操作包括:
* ① **赋值操作**:给变量赋值,如:`int a = 5`。
* ② **算术运算**:可以对数值类型的普通变量进行加、减、乘、除等运算,如:`a + b`、`a - b`、`a * b`、`a / b`
* ③ **关系运算**:可以进行比较运算(大于、小于、等于等),如: `a > b`、`a == b`。
* ④ **逻辑运算**:对布尔类型的值进行与、或、非运算,如: `a && b`、`a || b`、`!a`。
* ⑤ **位运算**:对整数类型的值进行位操作(与、或、异或、取反、左移、右移等),如: `a & b`、`a | b`、`a ^ b`、`~a`、`a << 2`、`a >> 2`。
* ⑥ **自增自减运算**`a++`、`--a` 等。
* 在 C 语言中,`指针变量`存储的是`另一个变量`的`地址`。对于指针变量,支持的操作包括:
* ① **赋值操作**:可以将一个地址赋值给指针,如: `int *p = &a;`
* ② **解引用操作**:通过指针访问它指向的变量,如: `*p = 10;` 修改指向变量的值。
* ③ **指针运算**
* **指针和整数值的加减运算**:指针可以进行整数的加减运算,用于访问数组或结构体成员,如: `p + 1`
* **指针的自增和自减运算**:指的是内存地址的向前或向后移动,如:`p++`、`p--`。
* **指针间的相减运算**:两个指向同一数组的指针相减可以得到它们之间的元素个数,如: `ptr1 - ptr2`
* **指针间的比较运算**:可以比较两个指针的大小,比较的是各自内存地址的大小,如: `p1 == p2`、`p1 != p2`。
* ④ **数组访问**:指针可以用于访问数组中的元素,通过 `*(p + i)` 访问第 `i` 个元素。
* ⑤ **指向指针的指针**:可以声明指向指针的指针,即多级指针,如:`int **pp = &p;`。
> [!WARNING]
>
> 在使用指针时,务必小心避免野指针和内存泄漏等问题。
在C语言中同类指针相减的结果是一个整数它表示两个指针之间相隔多少个指向的对象单位而不是它们在内存中的字节偏移量。这种对象单位是指针所指向的具体类型的大小。
举个例子来说,如果你有两个指向整数数组元素的指针 `p``q`,那么 `p - q` 的结果将是 `p` 指向的数组元素的索引与 `q` 指向的数组元素索引之间的差值。这个差值代表了在数组中相隔多少个整数元素,而不是它们在内存中的字节偏移量。
这种设计的优势在于,它使得指针运算更加直观和便于理解,特别是在处理数组和其他连续存储的数据结构时。因为指针运算结果的单位是根据指针所指向的具体类型来计算的,这样可以确保不同平台上的程序行为是一致的,不会受到底层硬件架构或者字节对齐规则的影响。
在C语言中数组名和指针有很多相似之处但数组名并不是指针变量。数组名实际是一个常量它指向数组的第一个元素的地址。为了证明这一点可以通过以下几个方面来说明
1. **数组名表示数组首地址**
数组名可以作为一个指针使用,数组名本身表示的是数组首地址。
```c
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // 这句话是合法的ptr现在指向arr[0]
printf("%p\n", arr); // 打印数组名,会打印数组首地址
printf("%p\n", &arr[0]); // 打印第一个元素的地址
```
2. **数组名是常量指针**
数组名是一个常量指针,不能改变它指向的位置,而指针变量可以改变它指向的位置。
```c
int arr[5];
int *ptr = arr; // 合法ptr指向arr[0]
ptr++; // 合法ptr现在指向arr[1]
// arr++; // 非法,编译错误,因为数组名是常量,不能改变
```
3. **sizeof运算符的结果不同**
使用`sizeof`运算符对数组名和指针变量会得到不同的结果,数组名会返回整个数组的大小,而指针变量会返回指针本身的大小。
```c
int arr[5];
int *ptr = arr;
printf("sizeof(arr) = %lu\n", sizeof(arr)); // 返回数组的大小5 * sizeof(int)
printf("sizeof(ptr) = %lu\n", sizeof(ptr)); // 返回指针的大小通常是4或8字节
```
4. **地址运算符的结果不同**
使用地址运算符`&`对数组名和指针变量会得到不同的结果,对数组名使用`&`会返回数组的地址,而对指针变量使用`&`会返回指针变量本身的地址。
```c
int arr[5];
int *ptr = arr;
printf("Address of array: %p\n", &arr); // 返回整个数组的地址
printf("Address of pointer: %p\n", &ptr); // 返回指针变量ptr的地址
```
综上所述,通过这些示例和解释,可以看出数组名虽然在某些场合下可以像指针一样使用,但它并不是一个真正的指针变量,而是一个常量,表示数组的首地址。