# 第一章:变量(⭐) ## 1.1 程序中变化的数据 - 在生活中,我们使用最多的不是固定的数据,而是会变化的数据: - ① 购物车商品的`数量`、`价格`等。 - ② 一首歌`播放的时间`、`进度条`、`歌词的展示`等。 - ③ 微信聊天中`消息条数`、`时间`、`语音的长度`、`头像`、`名称`等。 - ④ 游戏中技能的`冷却时间`、`血量`、`蓝量`、`buff 时间`、`金币的数量`等。 - …… * 下图是一个`购物车`中`变化`的`数据`,即: ![](./assets/1.png) * 那么,在实际开发中,我们就会使用`变量`来`保存`和`操作`这些`变化`的`数据`。 ## 1.2 变量 * 变量的定义:变量是程序中不可或缺的组成单位,最基本的存储单元。其实,变量就是一个存储数据的临时空间,可以向其中存储不同类型的数据,如:整数、小数、字符、字符串等,并且变量中的数据在程序运行的时候可以动态改变。 > [!NOTE] > > * `变量`:用来`存储数据`的`容器`。 > * `数据`:可以是一个用来计算的`数字`,如:上文购物车中的`价格`等;也可以是一句话中的`关键词`或`其它任意格式的数据`。 > * 变量的`特别`之处就在于`它存放的数据是可以改变`的。 * 我们可以将`变量`想象为一个`容器`,盒子中`装的`就是我们想要的`数据`,并且我们需要`给`盒子`取`一个`特别的名称`;通过这个`特别的名称`,我们可以`给`盒子`添加数据`或`移除数据`,这个`特别的名称`就是`变量名`。 ![](./assets/2.png) > [!NOTE] > > * ① `变量`是内存中的一个`存储区域`,该区域的数据可以在`同一类型`范围内`不断变化`。 > * ② 通过`变量名`,可以`操作`这块内存区域,向其中`存储数据`或`获取数据`以及`移除数据`。 > * ③ 变量的构成包含三个要素:`数据类型`、`变量名`、`需要存储的数据`。 > * ④ 在生活中,我们会经常说:这件衣服的价格是 `100(整型)` 元,这双鞋子的价格是 `250.5(小数,浮点类型)` 元,`今天天气真好(字符串类型)`之类的话;在计算机科学中,这些都是数据,并且它们是有类型,即:数据类型。(数据类型用于定义变量所能存储的数据的种类以及可以对这些数据进行的操作的一种分类,每种数据类型都有特定的属性和用途,它们决定了变量在内存中如何表示和存储,以及变量可以执行哪些操作) ## 1.3 变量的声明和使用 * ① 变量必须先声明,后使用。 * ② 可以先声明变量再赋值,也可以在声明变量的同时进行赋值。 * ③ 变量的值可以在同一类型范围内不断变化。 >[!IMPORTANT] > >* ① 在实际开发中,我们通常都会在声明变量的同时,给其赋值,这被称为初始化。 >* ② 如果不在声明变量的同时,进行初始化,默认情况下,系统会赋予的随机值,我们也称为垃圾值。 * 示例:先声明,再使用 ```c #include int main() { // 声明一个整型变量,取名为 a int a; // 给变量赋值 a = 10; printf("a = %d\n", a); return 0; } ``` * 示例:初始化(声明变量的同时给其赋值) ```c #include int main() { // 声明一个整型变量,取名为 b ,并直接赋值(初始化,实际开发中最为常用) int b = 200; // 修改变量 b 的值,将变量 a 的值赋值给变量 b b = 300; printf("b= %d\n", b); return 0; } ``` * 示例:同时声明多个变量并赋值 ```c #include int main() { // 同时声明多个整型的变量并赋值 int c1 = 10, c2 = 20, c3 = 30; printf("c1 = %d\n", c1); printf("c2 = %d\n", c2); printf("c3 = %d\n", c3); return 0; } ``` ## 1.4 从计算机底层看变量 ### 1.4.1 内存条的内部结构 * 如果只看内存条的外观,无非就是一些集成电路和颗粒而已,如下所示: ![](./assets/3.jpeg) * 并且,我们只需要将内存条插入到计算机主板对应的内存条插槽上,就可以正常工作,如下所示: ![](./assets/4.jpg) * 在家用的台式机主板上,通常有 4 个插槽或 2 个插槽,例如:本人的计算机就支持 4 个插槽,如下所示: ![](./assets/5.png) >[!NOTE] > >* ① 上图中的外形规格是 DIMM,所以我们通常也以 DIMM 也表示内存条。 >* ② DIMM 是内存条的物理形式,安装在主板的内存插槽中。 >* ③ 常见的 DIMM 类型包括 UDIMM(非缓冲 DIMM)、RDIMM(缓冲 DIMM)和 LRDIMM(负载减少DIMM)。 * 我们可以通过 [CPU-Z](https://www.cpuid.com/) 这个软件,查看 CPU 的一些指标信息,如下所示: ![](./assets/6.png) > [!NOTE] > > * ① 通过 CPU-Z 表明本人的台式机是支持双通道的,`channel` 在`计算机`中可以`翻译`为`信道`或`通道`。 > * ② 通道是内存控制器与内存模块之间的通信路径。 > * ③ 多通道内存可以提高数据传输带宽。例如:双通道内存系统同时使用两个通道来传输数据,从而提高性能。 > * ④ 现代主板通常支持双通道(Dual Channel)、四通道(Quad Channel)甚至八通道(Octa Channel)。 * 对于家用台式机而言,如果将内存条的插槽从左到右依次编号,如下所示: ![](./assets/7.png) * 其中,`槽1` 和 `槽2` 是一个通道,`槽3` 和 `槽4` 是一个通道;所以,通常是这么建议的: * 如果只有 1 根内存条,就插到 `槽2` 中。 * 如果有 2 根内存条,就分别插入到 `槽2` 和 `槽4` 中。 * 如果有 4 根内存条,就全插满即可。 > [!NOTE] > > 组成双通道配置的内存条需要遵循一些基本要求来确保它们能够正常以双通道模式运行: > > - ① **相同容量**:理想情况下,组成双通道的内存条应该具有相同的容量。这样可以确保它们在处理数据时的一致性和兼容性。 > - ② **匹配的速度规格**:内存条应该具有相同的速度规格,即它们的频率(如:DDR4-2400、DDR4-3200等)应该相同。不同速度的内存条可以一起工作,但系统会以所有内存条中最慢的那个的速度运行。 > - ③ **相同的时序**:内存条的时序(如:CL16-18-18-38)应该匹配。时序参数影响内存的响应速度和稳定性,不匹配的时序可能会降低性能或导致系统不稳定。 > - ④ **相同的制造商和型号**(推荐):虽然不是强制性要求,但选择相同制造商和型号的内存条可以最大限度地减少兼容性问题。不同制造商的内存条可能在微小的规格和性能上有差异,这有可能影响双通道配置的效能。 * 内存条表面会有内存颗粒,如下所示: ![](./assets/8.png) > [!NOTE] > > 上图中的内存条有 8 个内存颗粒;但是,高端服务器上的内存条通常会存在 9 个内存颗粒,最后 1 个内存颗粒专门用来做 ECC 校验。 * 一个内存条有两面,高端的内存条两面都有内存颗粒,我们将每个面称为 Rank 。那么,如果内存条有两个面,就是存在 Rank0 和 Rank1 ,即: ![](./assets/9.png) * 内存条表面的黑色颗粒,我们称为 chip(芯片) ,即: ![](./assets/10.png) > [!NOTE] > > * ① 内存颗粒是内存条上的 DRAM 芯片,每个芯片包含多个存储单元。 > * ② 内存颗粒存储数据并与内存控制器进行数据交换。 * 在 chip 中还有 8 个 bank,每个 bank 就是数据存储的实体,这些 bank 组成了一个二维矩阵,只要声明了 column 和 row 就可以从每个 bank 中取出 8bit (1 Bytes)的数据,如下所示: ![img](./assets/11.png) * 综上所示,内存条的分层结构就是 `Channel > DIMM > Rank -> Chip -> Bank -> Row/Column`。 ### 1.4.2 变量的作用 * 如果我们希望计算 10 和 20 的和;那么,在计算机中需要怎么做? * ① 首先,计算 10 和 20 的运算,一定在 CPU 中进行,因为在计算机中的各个部件中,只有 CPU 有运算器(ALU)。 * ② 其次,我们需要将 10 和 20 交给 CPU ;由于 CPU 只能和内存进行交互,那么我们必须将 10 和 20 存储到内存中。 > [!NOTE] > > 即使 10 和 20 是存储在文件中的,也需要先加载进内存,然后再交给 CPU 进行运算。 * ③ 最后,只需要告诉 CPU 做何种运算,如:加、减、乘、除等。 * 其中,最为重要的问题就是如何将数据存储到内存中?答案就是通过`变量`。 ![](./assets/12.png) * 我们知道,计算机底层是使用二进制来表示指令和数据的;但是,如果我们的代码都是这样的,即: ```txt 0000,0000,000000010000 代表 LOAD A, 16 0000,0001,000000000001 代表 LOAD B, 1 0001,0001,000000010000 代表 STORE B, 16 ``` * 这样,直接使用`内存地址`来编写代码(机器语言)实现是太难阅读、修改和维护了;于是,我们就使用了汇编语言来编写代码,并通过编译器来将汇编语言翻译为机器语言,即: ```txt LOAD A, 16 -- 编译 --> 0000,0000,000000010000 LOAD B, 1 -- 编译 --> 0000,0001,000000000001 STORE B, 16 -- 编译 --> 0001,0001,000000010000 ``` * 但是,这样的汇编语言还是面向机器的,编程时仍然需要记住和管理大量内存地址,不具备程序的移植性;于是,我们就是使用了高级语言来编写代码,并引入了变量的概念,即: ```c int num = 10; ``` * 我们使用`变量名`来`关联`内存`地址`,这样我们在编写代码的时候,就可以不用直接操作内存地址,极大地提高了代码的可读性和开发效率。并且,当程序运行完毕之后,程序所占用的内存还会交还给操作系统,以便其它程序使用。 * 综上所述,高级语言编译器的作用就是: * ① 编写源代码时使用变量名。 * ② 程序在经过编译器的编译之后,所有变量名被替换为具体地址。 * ③ …… * 此时,我们就可以知道,`变量`就是内存中用于`存储数据`的`临时空间`,并且变量中的值是可以变化的。 * `内存`中空间的`最小单位`是`字节`(Bytes),即 8 个 0 或 1 ,如下所示: ```txt 00011001 00100110 00100110 00100110 00100110 ... ``` > [!NOTE] > > 计算机中存储单位的换算,如下所示: > > * 1 B = 8 bit。 > * 1 KB = 1024 B。 > * 1 MB = 1024 KB。 > * 1 GB = 1024 MB。 > * 1 TB = 1024 GB 。 > * …… * 在内存中,每一个字节都有一个编号,这个编号我们称之为地址。一个变量至少占用 1 个字节(1 个或多个字节),我们将变量的第一个字节所占用的地址(变量的首地址),就称之为该变量的地址。CPU 就可以通过变量地址找到某个变量的值,然后拿到具体的数据进行计算了。 > [!NOTE] > > 变量就是保存程序运行过程中临时产生的值。 * 其实,到这里还是有疑惑的?我们说过,一个变量至少会占用 1 个字节,如果一个变量占用了 4 个字节,而 CPU 只会通过变量的地址(首地址)获取数据,那么 CPU 是如何获取完整的数据的?答案就是通过`数据类型`,数据类型除了限制数据的种类,还限制了数据在内存中所占空间的大小,如上图所示: * ① 假设变量 `a` 的首地址是 `01` ,变量的数据类型是 `4` 个字节。 * ② 那么,CPU 就会依次,从 `01 ~ 04` 中获取数据。 * 再次,剖析下变量的语法格式: ```txt 数据类型 变量名 = 值; ``` * `变量名`的`作用`,如下所示: * ① 当我们`编写`代码的时候,使用`变量名`来`关联`某块内存的`地址`。 * ② 当 CPU `执行`的时候,会将变量名`替换`为具体的地址,再进行具体的操作。 > [!CAUTION] > > 变量名(标识符)需要符合命名规则和命名规范!!! * `数据类型`的`作用`,如下所示: * ① 变量的数据类型`决定了`变量所占空间的大小。当我们在声明变量的时候写了数据数据类型,CPU 就知道从变量的首地址位置开始取多少字节。 * ② 变量的数据类型`决定了`两个变量是否能够运行,以及能够做何种运算。例如:JavaScript 就没有 char 类型的变量,都是 string 类型,可以和任意数据类型的数据拼接,并转换为 string 类型;Java 中有 char 类型的变量,底层都会转换 unicode 编码,然后再计算。 * `值`的`作用`,如下所示: * ① `值`就是`内存`中`实际存储`的`数据`。 * ② `=` 是赋值操作符,就是将等号右侧的数据存储到等号左侧的变量名所代表的内存空间。 * 那么,如下代码的含义就是: ```c // int 数据类型,4 个字节 // num 变量名 -- 关联内存中的一块存储空间 // = 10 将 10 存储到 num 所代表的 4 个字节的存储空间中 int num = 10; ``` ## 1.6 变量的重要操作 ### 1.6.1 变量的输出 * 在计算机中,所谓的`输入`和`输出`都是以计算机(CPU 和内存)为主体而言的,即: >[!NOTE] > >输入:从输入设备(键盘、鼠标、扫描仪)向计算机输入数据。 > >输出:从计算机向外部输出设备(显示器、打印机)输出数据。 ![](./assets/13.png) * 在 C 语言中,提供了 `printf()` 函数用于输出信息,其函数声明是: ```c int printf (const char *__format, ...) { ... } ``` * `printf` 的标准含义是格式化输出文本,来源于 `print formatted(格式化打印)`的缩写,其语法规则,如下所示: ![](./assets/14.png) > [!NOTE] > > * 格式化字符串:是使用双引号括起来的字符串,里面包含了普通的字符串和格式占位符。 > * 格式占位符(格式声明符):由 `%` 和`格式字符`组成,作用是将输出的数据转换为指定的格式后输出,这里的 `%d` 表示整数。 > * 输出列表:是程序要输出的一些数据,可以是常量、变量或表达式,需要和格式占位符一一对应。 * 在计算机中,二进制、八进制、十进制以及十六进制的英文名称和缩写,如下所示: * 二进制(binary),缩写是 bin。 * 八进制(octal),缩写是 oct。 * 十进制(decimal),缩写是 dec。 * 十六进制(Hexadecimal),缩写是 hex。 * 其实,我们也可以在 Windows 系统中的计算器中来看到,即: ![](./assets/15.png) > [!IMPORTANT] > > 在生活中的 decimal 是小数的意思;但是,在计算机中,decimal 的完整含义是 decimal integer ,即十进制整数。 * 示例: ```c #include int main() { // 声明变量并赋值 int num = 18; // 使用输出语句,将变量 num 的值输出,其中 %d 表示输出的是整数 printf("我今年%d岁\n", num); return 0; } ``` ### 1.6.2 计算变量的大小 * 我们可以使用 `sizeof`关键字(运算符)来计算变量或类型所占内存空间的大小。 * 示例: ```c #include int main() { int num = 10; printf("变量所占内存空间的大小:%zd字节\n", sizeof(num)); // 数据类型所占内存空间的大小 printf("数据类型所占内存空间的大小:%zd字节\n", sizeof(int)); return 0; } ``` ### 1.6.3 获取变量的地址 * 在 C 语言中,我们可以使用`取地址运算符 &` 来获取变量的地址。 * 示例: ```c #include int main() { int num = 10; printf("变量 num 的值是:%d\n", num); printf("变量 num 的地址(指针)是:%#p\n", &num); return 0; } ``` ### 1.6.4 变量的输入 * 在 C 语言中,提供了 `scanf()` 函数用于从标准输入(通常是键盘)中读取数据并根据变量的地址赋值给变量(变量需要提前声明),其函数声明是: ```c int scanf(const char *__format, ...) { ... } ``` * 其语法规则,如下所示: ![](./assets/16.png) > [!NOTE] > > `&age`、`&num` 中的 `&`是寻址操作符,`&age` 表示变量 `age` 在内存中的地址。 > [!CAUTION] > > * ① scanf() 函数中的 `%d`,如果是连着写,即:`%d%d`,那么在输入数据的时候,数据之间不可以使用逗号`,`分隔,只能使用空白字符(空格、tab 键或回车键),即:`2空格3tab`或`2tab3回车`等。 > > * ② 如果是 `%d,%d`,则输入的时候需要加上逗号`,`,即:`2,3`。 > * ③ 如果是 `%d %d`,则输入的时候需要加上空格,即:`2空格3`。 * 示例:计算圆的面积,半径由用户指定 ```c #include int main() { // 禁用 stdout 缓冲区 // CLion debug 独有,后文不再提及,如果 debug 有问题,就添加如下代码 setbuf(stdout, NULL); float radius; printf("请输入一个半径:"); scanf("%f", &radius); double area = 3.1415926 * radius * radius; printf("半径是%f的圆的面积是%.2lf", radius, area); return 0; } ``` * 示例:输入一个整数值,求其绝对值 ```c #include int main() { int num; printf("请输入一个整数:"); scanf("%d", &num); int absNum; if (num < 0) { absNum = -num; } else { absNum = num; } printf("%d的绝对值是:%d", num, absNum); return 0; } ``` * 示例:输入多个变量的值,求其乘积 ```c #include int main() { int a, b, c; printf("请输入整数 a 、b 和 c 的值:"); scanf("%d %d %d", &a, &b, &c); int result = a * b * c; printf("%d × %d × %d = %d", a, b, c, result); return 0; } ``` ## 1.7 标识符 ### 1.7.1 概述 * 在 C 语言中,变量、函数、数组名、结构体等要素命名的时候使用的字符序列,称为标识符。 > [!NOTE] > > 在上世纪 60 - 70 年代的时候,因为国家贫穷,人民生活不富裕等原因,家长虽然会给孩子取名为:`张建国`、`李华强`等;但是,也会取小名为`二狗子`、`狗剩`等,目的是希望孩子能健康成长(养活),像 `张建国`、`李华强`、`二狗子`、`狗剩`都是名字(标识符),伴随人的一生。 ### 1.7.2 标识符的命名规范 * 强制规范: * ① 只能由`小写`或`大写英文字母`,`0-9` 或 `_` 组成。 * ② 不能以`数字`开头。 * ③ 不可以是`关键字`。 * ④ 标识符具有`长度`限制,不同编译器和平台会有所不同,一般限制在 63 个字符内。 * ⑤ 严格`区分大小写字母`,如:Hello、hello 是不同的标识符。 * 建议规范: * ① 为了提高阅读性,使用有意义的单词,见名知意,如:sum,name,max,year 等。 * ② 使用下划线连接多个单词组成的标识符,如:max_classes_per_student 等。 * ③ 多个单词组成的标识符,除了使用下划线连接,也可以使用小驼峰命名法,除第一个单词外,后续单词的首字母大写,如: studentId、student_name 等。 * ④ 不要出现仅靠大小写区分不同的标识符,如:name、Name 容易混淆。 * ⑤ 系统内部使用了一些下划线开头的标识符,如:C99 标准添加的类型 `_Bool`,为防止冲突,建议开发者尽量避免使用下划线开头的标识符。 * 示例:合法(不一定建议)的标识符 ```txt a、BOOK_sun、MAX_SIZE、Mouse、student23、Football、FOOTBALL、max、_add、num_1、sum_of_numbers ``` * 示例:非法的标识符 ```txt $zj、3sum、ab#cd、23student、Foot-baii、s.com、b&c、j**p、book-1、tax rate、don't ``` ### 1.7.3 关键字 * C 语言中的关键字是编译器`预定义`的`保留字`,它们有`特定`的`含义`和`用途`,用于控制程序的结构和执行。 * C80 和 C90 (ANSI C)定义的关键字,如下所示: | 类型(功能) | 具体关键字 | | -------------------------- | ------------------------------------------------------------ | | 数据类型关键字 | `char`、`double`、`float`、`int`、`long`、`short`、`signed`、`unsigned`、`void` | | 存储类说明符关键字 | `auto`、`extern`、`register`、`static`、`typedef`、`volatile`、`const` | | 控制语句关键字 | `break`、`case`、`continue`、`default`、`do`、`else`、`for`、`goto`、`if`、`return`、`switch`、`while` | | 结构体、联合体和枚举关键字 | `enum`、`struct`、`union` | | 其他关键字 | `sizeof` | * C99 新增的关键字,如下所示: | 类型(功能) | 具体关键字 | | ------------------ | --------------------------------- | | 数据类型关键字 | `_Bool`、`_Complex`、`_Imaginary` | | 存储类说明符关键字 | `inline`、`restrict` | | 其他关键字 | `_Complex`、 `_Imaginary` | * C11 新增的关键字,如下所示: | 类型(功能) | 具体关键字 | | ------------------ | ------------------------------------------------------------ | | 存储类说明符关键字 | `_Atomic` | | 其他关键字 | `_Alignas`、 `_Alignof`、 `_Generic`、 `_Noreturn`、 `_Static_assert`、 `_Thread_local` | > [!IMPORTANT] > > * ① 关键字不能用作标识符(如:变量名、函数名等)。 > * ② 不要死记硬背这些关键字,在实际开发中,并不一定全部使用到;而且,在学到后面的时候,会自动记住这些关键字以及对应的含义。 # 第二章:常量(⭐) ## 2.1 概述 * 在程序运行过程中,不能改变的量就是常量。 >[!NOTE] > >* ① 在数学中的 `π`,就是一个常量,其值为 3.1415926 。 >* ② 在生活中,人类的性别只有`男`和`女`;其中,`男`和`女`也是常量。 >* ③ ... ## 2.2 常量的分类 * 在 C 语言中的变量的分类,如下所示: * ① 字面量常量。 * ② 标识符常量: * `#define` 宏定义的标识符常量。 * `const` 关键字修饰的标识符常量。 * 枚举常量。 >[!NOTE] > >* 所谓的`字面量常量`,就是可以直接使用的常量,不需要声明或定义,包括:整数常量、浮点数常量以及字符常量。 >* 所谓的`标识符常量`,就是使用标识符来作为常量名,包括: `#define` 宏定义的标识符常量、`const` 关键字修饰的标识符常量、枚举常量。 * 示例:字面量常量 ```c #include int main() { 1; 'A'; 12.3; "你好"; return 0; } ``` * 示例:字面量常量 ```c #include int main() { printf("整数常量 =》%d\n", 1); printf("字符常量 =》%c\n", 'A'); printf("浮点数常量 =》%f\n", 12.3); printf("字符串常量 =》%s\n", "你好"); return 0; } ``` ## 2.3 使用 #define 定义常量 * `#define` 来定义常量,也叫作宏定义,就是用一个标识符来表示一个常量值,如果在后面的代码中出现了该标识符,那么编译时就全部替换成指定的常量值,即用宏体替换所有宏名,简称`宏替换`。 * 格式是: ```c #define 常量名 常量值 ``` > [!CAUTION] > > * ① 其实`宏定义`的常量的`执行时机`是在`预处理`阶段,将所有`宏常量`替换完毕,才会继续编译代码。 > * ② 不要以 `;` 结尾,如果有 `;` ,分号也会成为常量值的一部分。 > * ③ `# define` 必须写在 `main` 函数的外面!!! > * ④ `常量名`习惯用`大写字母`表示,如果多个单词,使用 `_` 来分隔,以便和变量区分。 * 示例: ```c #include #define PI 3.1415926 int main() { double radius = 2.5; double area = PI * radius * radius; printf("半径为%lf的圆的面积是%.2lf", radius, area); return 0; } ``` ## 2.4 const 关键字 * C99 标准新增,这种方式跟定义一个变量是类似的;只不过,需要在变量的数据类型前加上 `const` 关键字。 * 和使用 `#define定义宏常量`相比,const 定义的常量有详细的数据类型,而且会在编译阶段进行安全检查,在运行时才完成替换,所以会更加安全和方便。 * 格式是: ```c const 数据类型 常量名 = 常量值; ``` * 示例: ```c #include const int PI = 3.1415926; int main() { double radius = 2.5; double area = PI * radius * radius; printf("半径为%lf的圆的面积是%.2lf", radius, area); return 0; } ``` ## 2.5 枚举常量 * 格式: ```c enum 枚举常量 { xxx = 1; yyy; ... } ``` > [!NOTE] > > * ① 默认情况下,枚举常量是从 0 开始递增的。 > * ② 也可以在定义枚举常量的时候,自定义它们的值。 * 示例: ```c #include enum sex { MALE = 1, FEMALE = 2, }; int main() { printf("%d\n", MALE); printf("%d\n", FEMALE); return 0; } ``` * 示例: ```c #include enum Sex { MALE = 1, FEMALE = 2, }; int main() { enum Sex sex; printf("请输入性别(1 表示男性, 2 表示女性):"); scanf("%d", &sex); printf("您的性别是:%d\n", sex); return 0; } ``` ## 2.6 #defind 定义常量 VS const 定义常量 * ① 执行时机:`#define` 是预处理指令,在编译`之前`执行;`const` 是关键字,在编译`过程`中执行。 * ② 类型检查:`#define` 定义常量`不用指定类型`,`不进行类型检查`,只是简单地文本替换;`const` 定义常量`需要指定数据类型`,`会进行类型检查`,类型安全性更强。 # 第三章:进制 ## 3.1 概述 * 计算机的底层只有`二进制`,即计算机中`运算`和`存储`的`所有数据`都需要转换为`二进制`,包括:数字、字符、图片、视频等。 ![](./assets/17.jpg) * 之前,我们也提到现代的计算机(量子计算机除外)几乎都遵循`冯·诺依曼`体系结构,其理论要点如下: * ① **存储程序**:`程序指令`和`数据`都存储在计算机的内存中,这使得程序可以在运行时修改。 * ② **二进制逻辑**:所有数据和指令都以`二进制`形式表示。 * ③ **顺序执行**:指令按照它们在内存中的顺序执行,但可以有条件地改变执行顺序。 * ④ **五大部件**:计算机由`运算器`、`控制器`、`存储器`、`输入设备`和`输出设备`组成。 * ⑤ **指令结构**:指令由操作码和地址码组成,操作码指示要执行的操作,地址码指示操作数的位置。 * ⑥ **中心化控制**:计算机的控制单元(CPU)负责解释和执行指令,控制数据流。 * 所以,再次论证了为什么计算机只能识别二进制。 ## 3.2 进制 ### 3.2.1 常见的进制 * 在生活中,我们最为常用的进制就是`十进制`,其规则是`满 10 进 1` ,即: ![](./assets/18.jpeg) * 在计算机中,常见的进制有`二进制`、`八进制`和`十六进制`,即: * 二进制:只能 0 和 1 ,满 2 进 1 。 * 八进制:0 ~ 7 ,满 8 进 1 。 * 十六进制:0 ~ 9 以及 A ~ F ,满 16 进 1 。 > [!NOTE] > > 在十六进制中,除了 0 到 9 这十个数字之外,还引入了字母,以便表示超过 9 的值。字母 A 对应十进制的 10 ,字母 B 对应十进制的 11 ,以此类推,字母 F 对应十进制的 15。 * 进制的换算举例,如下所示: | 十进制 | 二进制 | 八进制 | 十六进制 | | ------ | ------ | ------ | -------- | | 0 | 0 | 0 | 0 | | 1 | 1 | 1 | 1 | | 2 | 10 | 2 | 2 | | 3 | 11 | 3 | 3 | | 4 | 100 | 4 | 4 | | 5 | 101 | 5 | 5 | | 6 | 110 | 6 | 6 | | 7 | 111 | 7 | 7 | | 8 | 1000 | 10 | 8 | | 9 | 1001 | 11 | 9 | | 10 | 1010 | 12 | a 或 A | | 11 | 1011 | 13 | b 或 B | | 12 | 1100 | 14 | c 或 C | | 13 | 1101 | 15 | d 或 D | | 14 | 1110 | 16 | e 或 E | | 15 | 1111 | 17 | f 或 F | | 16 | 10000 | 20 | 10 | | ... | ... | ... | ... | * 二进制和十六进制的关系:十六进制是以 16 为基数的进制系统,16 在二进制中表示为 ( 2^4 ),即:一个十六进制可以表示 4 位二进制。 > [!NOTE] > > 十六进制的范围是:0 ~ F (0 ~ 15)对应的二进制数的范围是:0000 ~ 1111 (0 ~ 15)。 * 每个十六进制数都可以映射到一个唯一的 4 位二进制数,即: | 十六进制 | 二进制 | | -------- | ------ | | 0 | 0000 | | 1 | 0001 | | 2 | 0010 | | 3 | 0011 | | 4 | 0100 | | 5 | 0101 | | 6 | 0110 | | 7 | 0111 | | 8 | 1000 | | 9 | 1001 | | A | 1010 | | B | 1011 | | C | 1100 | | D | 1101 | | E | 1110 | | F | 1111 | >[!NOTE] > >由此可见,每个十六进制数字确实由 4 位二进制数表示。 * 二进制和八进制的关系:八进制是以 8 为基数的进制系统,8 在二进制中表示为 ( 2^3 );即:一个八进制位可以表示 3 个二进制位。 > [!NOTE] > > 八进制的范围是:0 ~ 7 对应的二进制数的范围是:000 ~ 111。 * 每个八进制数位都可以映射到一个唯一的 3 位二进制数,即: | 八进制 | 二进制 | | ------ | ------ | | 0 | 000 | | 1 | 001 | | 2 | 010 | | 3 | 011 | | 4 | 100 | | 5 | 101 | | 6 | 110 | | 7 | 111 | > [!NOTE] > > 由此可见,每个八进制数字确实由 3 位二进制数表示。 ### 3.2.2 C 语言中如何表示不同进制的整数? * 规则如下: * 在 C 语言中,如果是`二进制`(字面常量),则需要在二进制整数前加上 `0b` 或 `0B` 。 * 在 C 语言中,如果是`八进制`(字面常量),则需要在八进制整数前加上 `0` 。 * 在 C 语言中,如果是`十进制`(字面常量),正常数字表示即可。 * 在 C 语言中,如果是`十六进制`(字面常量),则需要在十六进制整数前加上 `0x`或`0X` 。 * 示例: ```c #include int main() { int num1 = 0b10100110; // 二进制 int num2 = 0717563; // 八进制 int num3 = 1000; // 十进制 int num4 = 0xaf72; // 十六进制 printf("num1 = %d\n", num1); // num1 = 166 printf("num2 = %d\n", num2); // num2 = 237427 printf("num3 = %d\n", num3); // num3 = 1000 printf("num4 = %d\n", num4); // num4 = 44914 return 0; } ``` ### 3.2.3 输出格式 * 在 C 语言中,可以使用不同的`格式占位符`来`输出`不同`进制`的整数,如下所示: * `%d`:十进制整数。 * `%o` :八进制整数。 * `%x`:十六进制整数。 * `%#o` :显示前缀 `0` 的八进制整数。 * `%#x` :显示前缀 `0x` 的十六进制整数。 * `%#X` :显示前缀 `0X` 的十六进制整数。 > [!CAUTION] > > C 语言中没有输出二进制数的格式占位符!!! * 示例: ```c #include int main() { int num = 100; printf("%d 的十进制整数: %d\n", num, num); // 100 的十进制整数: 100 printf("%d 的八进制整数: %o\n", num, num); // 100 的八进制整数: 144 printf("%d 的十六进制整数: %x\n", num, num); // 100 的十六进制整数: 64 printf("%d 的八进制(前缀)整数: %#o\n", num, num); // 100 的八进制(前缀)整数: 0144 printf("%d 的十六进制(前缀)整数: %#x\n", num, num); // 100 的十六进制(前缀)整数: 0x64 printf("%d 的十六进制(前缀)整数: %#X\n", num, num); // 100 的十六进制(前缀)整数: 0X64 return 0; } ``` ## 3.3 进制的运算规则 * `十进制`的运算规则,如下所示: * 逢`十`进`一`(针对加法而言)。 * 借`一`当`十`(针对减法而言)。 * `二进制`的运算规则,如下所示: * 逢`二`进`一`(针对加法而言)。 * 借`一`当`二`(针对减法而言)。 * `八进制`的运算规则,如下所示: * 逢`八`进`一`(针对加法而言)。 * 借`一`当`八`(针对减法而言)。 * `十六进制`的运算规则,如下所示: * 逢`十六`进`一`(针对加法而言)。 * 借`一`当`十六`(针对减法而言)。 ## 3.4 进制的转换 ### 3.4.1 概述 * 不同进制的转换,如下所示: ![](./assets/19.png) * 在计算机中,数据是从右往左的方式排列的;其中,最右边的是低位,最左边的是高位,即: ![](./assets/20.png) ### 3.4.2 二进制和十进制的转换 #### 3.4.2.1 二进制转换为十进制 * 规则:从最低位开始,将每个位上的数提取出来,乘以 2 的 (位数 - 1 )次方,然后求和。 > [!NOTE] > > * ① 在学术界,将这种计算规则,称为`位权相加法`。 > * ② `八进制转换为十进制`、`十六进制转换为十进制`和`二进制转换为十进制`的算法相同!!! * 示例:十进制转十进制 ![](./assets/21.png) * 示例:二进制转十进制 ![](./assets/22.png) #### 3.4.2.2 十进制转换二进制 * 规则:将该数不断除以 2 ,直到商为 0 为止,然后将每步得到的余数倒过来,就是对应的二进制。 > [!NOTE] > > * ① 在学术界,将这种计算规则,称为`短除法`或`连续除2取余法`。 > * ② 很好理解,只有不断地除以 2 ,就能保证最大的数字不超过 2 ,这不就是二进制(只能有 0 或 1)吗? > * ③ `八进制转换为二进制`、`十六进制转换为二进制`和`十进制转换为二进制`的算法相同!!! * 示例:十进制转十进制 ![](./assets/23.png) * 示例:十进制转二进制 ![](./assets/24.png) ### 3.4.3 二进制转八进制 * 规则:每 3 位二进制就是一个八进制。 * 示例:011 101 001 -> 351 ![](./assets/25.png) ### 3.4.4 二进制转十六进制 * 规则:每 4 位二进制就是一个十六进制。 * 示例:1110 1001 -> 0xE9 ![](./assets/26.png) ## 3.5 原码、反码和补码 ### 3.5.1 概述 * 机器数:一个数在计算机的存储形式是二进制,我们称这些二进制数为机器数。机器数可以是有符号的,用机器数的最高位来存放符号位,0 表示正数,1 表示负数。 ![](./assets/27.png) * 真值:因为机器数带有符号位,所以机器数的形式值不等于其真实表示的值(真值),以机器数 1000 0001 为例,其真正表示的值(首位是符号位)为 -1,而形式值却是 129 ,因此将带有符号位的机器数的真正表示的值称为机器数的真值。 ![](./assets/28.png) ### 3.5.2 原码 * 原码的表示与机器数真值表示的一样,即用第一位表示符号,其余位表示数值。 * 规则: * 正数的`原码`是它本身对应的二进制数,符号位是 0 。 * 负数的`原码`是它本身绝对值对应的二进制数,但是符号位是 1 。 * +1 的原码,使用 8 位二进数来表示,就是: | 十进制数 | 原码(8位二进制数) | | -------- | ------------------- | | +1 | `0`000 0001 | * -1 的原码,使用 8 位二进数来表示,就是: | 十进制数 | 原码(8位二进制数) | | -------- | ------------------- | | -1 | `1`000 0001 | > [!IMPORTANT] > > 按照原码的规则,会出现 `+0` 和 `-0` 的情况,即:`0`000 0000(+0)、`1`000 0000(-0),显然不符合实际情况;所以,计算机底层虽然存储和计算的都是二进数,但显然不是原码。 ### 3.5.3 反码 * 规则: * 正数的反码和它的原码相同。 * 负数的反码是在其原码的基础上,符号位不变,其余各位取反。 * +1 的反码,使用 8 位二进数来表示,就是: | 十进制数 | 原码(8位二进制数) | 反码(8位二进制数) | | -------- | ------------------- | ------------------- | | +1 | `0`000 0001 | `0`000 0001 | * -1 的反码,使用 8 位二进数来表示,就是: | 十进制数 | 原码(8位二进制数) | 反码(8位二进制数) | | -------- | ------------------- | ------------------- | | -1 | `1`000 0001 | `1`111 1110 | > [!IMPORTANT] > > 按照反码的规则,如果是 `+0`,对应的原码是 `0`000 0000;那么,其反码还是 `0`000 0000 ;如果是 `-0`,对应的原码是 `1`000 0000,其反码是 `1`111 1111,显然不符合实际情况;所以,计算机底层虽然存储和计算的都是二进数,但显然不是反码。 ### 3.5.4 补码 * 规则: * 正数的补码和它的原码相同。 * 负数的补码是在其反码的基础上 + 1 。 * +1 的补码,使用 8 位二进数来表示,就是: | 十进制数 | 原码(8位二进制数) | 反码(8位二进制数) | 补码(8位二进制数) | | -------- | ------------------- | ------------------- | ------------------- | | +1 | `0`000 0001 | `0`000 0001 | `0`000 0001 | * -1 的补码,使用 8 位二进数来表示,就是: | 十进制数 | 原码(8位二进制数) | 反码(8位二进制数) | 补码(8位二进制数) | | -------- | ------------------- | ------------------- | ------------------- | | -1 | `1`000 0001 | `1`111 1110 | `1`111 1111 | * 如果 0 ,按照 `+0` 的情况进行处理,即: ![](./assets/29.png) * 如果 0 ,按照 `-0` 的情况进行处理,即: ![](./assets/30.png) > [!IMPORTANT] > > * ① 补码表示法解决了`原码`和`反码`存在的`两种`零(`+0` 和 `-0`)的问题,即:在补码表示法中,只有`一个`零,即 0000 0000。 > * ②补码使得`加法运算`和`减法运算`可以统一处理,通过将减法运算`转换`为加法运算,可以简化硬件设计,提高了运算效率。 > * ③ 计算机底层`存储`和`计算`的都是`二进数的补码`。 ### 3.5.5 总结 * ① 正数的原码、反码和补码都是一样的,三码合一。 * ② 负数的反码是在其原码的基础上,按位取反(0 变 1 ,1 变 0 ),符号位不变;负数的补码是其反码 + 1 。 * ③ 0 的补码是 0 。 ## 3.6 计算机底层为什么使用补码? * 如果计算是 `2 - 2` ,那么可以转换为 `2 + (-2)`,这样计算机内部在处理`减法计算`的时候,就会将其转换为`加法计算`的形式,以简化硬件设计和提高计算效率。 * `最高位`表示`符号位`,由于符号位的存在,如果使用`原码`来计算,就会导致`计算结果不正确`,即: ![](./assets/31.png) * `补码`的设计可以巧妙的让`符号位`也参与计算,并且可以得到`正确的计算结果`,即: ![](./assets/32.png)