2.5 基本运算

基本运算-1

2.5.1 基本运算-1.mp4 (62.25MB)

基本运算-2

  • 判真、假的标准
    • 知道 C 语言中判断一个数据是真还是假的标准。比如:1 会被判定为真;0 会被判定为假;
  • true、false
    • 知道 C 语言中是不存在这俩玩意儿的
  • 布尔值的格式控制字符
    • 知道布尔值的格式控制字符是 %d
    • 知道该如何打印布尔值
  • 关系表达式
    • 理解关系表达式的概念
    • 知道关系表达式的结果是一个布尔值
  • 逻辑运算
    • 掌握 3 种逻辑运算
  • demo | 输入 2 个整数,求这 2 个数的最大数
  • demo | 输入 1 个代表年份的正整数,判断是否闰年

2.5.2 基本运算-2.mp4 (24.18MB)

基本运算-3

2.5.3 基本运算-3.mp4 (65.83MB)

基本运算-4-扫地机器人

2.5.4 基本运算-4-扫地机器人.mp4 (104.33MB)

notes

简述

这一部分细节很多,但都很重要……

认识 C 语言中的运算符、操作数、表达式
知道 C 语言中都有哪些常见的基本运算
掌握自增、自减运算符的使用,并理解前置和后置之间的差异
理解字符型数据在内存中的存储机制
知道字符数据是可以参与算术运算的
理解 / 和 % 运算的差异,如果要得到浮点数结果应该如何做
知道算术运算符的优先级高于赋值运算符
知道使用 () 来改变运算的优先级
知道除数为 0 会导致什么问题、知道当程序中可能出现除数为 0 的情况时该如何处理
理解未定义的行为指的是什么、认识常见的一些未定义行为、知道未定义行为会导致什么后果

操作数

操作数:运算符所作用的对象,可以是变量、常量或表达式等

操作数可以是:

  • 常量:常量是固定的数值,如整型常量、浮点型常量、字符常量
  • 变量:变量是可变的数值,需要在程序中定义并分配内存空间
  • 表达式:表达式则由运算符和操作数组成,可以根据需要进行计算

表达式

  • 表达式是指 由各种运算符把常量、变量、函数等运算对象连接起来的具有实际意义并符合 C 语法规则的式子
  • 表达式是指 由运算符和操作数组成的序列,用于表示某个值或计算结果
  • 如果一个式子,包含了运算符和操作数,我们就可以认为这是一个表达式
    • 🌰 a + b + c1 + 2 + 3 都是表达式
  • 表达式之间是可以相互嵌套的
    • 🌰 a + b 是表达式,(a + b) * c 是表达式

认识运算符、操作数、表达式

表达式、运算符、操作数在程序中是非常常见的,可以说是到处可见,以下面这个简单的 demo 为例,认识一下它们究竟是什么?

  1. #include <stdio.h>
  2. int main() {
  3. int a = 10; // 定义一个变量 a,并初始化为 10
  4. int b = 5; // 定义一个变量 b,并初始化为 5
  5. int c = a + b; // 定义一个变量 c,其值为变量 a 和 b 的和
  6. int d = (a - b) * c; // 定义一个变量 d,其值为变量 a 和 b 的差乘以变量 c
  7. printf("%d\n", a); // 输出变量 a 的值
  8. printf("%d\n", b); // 输出变量 b 的值
  9. printf("%d\n", c); // 输出变量 c 的值
  10. printf("%d\n", d); // 输出变量 d 的值
  11. return 0;
  12. }
  13. /* 运行结果:
  14. 10
  15. 5
  16. 15
  17. 75
  18. */
  • 表达式 a = 10 中,操作数是 10,运算符是赋值运算符 =
  • 表达式 b = 5 中,操作数是 5,运算符是赋值运算符 =
  • 表达式 a + b 中,操作数是变量 a 和 b,运算符是加号 +
  • 表达式 a - b 中,操作数是变量 a 和 b,运算符是减号 -
  • 表达式 (a - b) * c 中,操作数是变量 a、b 和 c,运算符是括号、减号和乘号

C 语言中的基本运算

image.png

C 语言中的基本运算包括算术运算、关系运算、逻辑运算、地址运算、位运算等。

  • 算术运算:加(+)、减(-)、乘(*)、除(/)和取余(%)五种运算。
  • 关系运算:等于(==)、不等于(!=)、大于(>)、小于(<)、大于等于(>=)和小于等于(<=)六种运算。
  • 逻辑运算:与(&&)、或(||)和非(!)三种运算。
  • 位运算:按位与(&)、按位或(|)、按位异或(^)、取反(~)、左移位(<<)和右移位(>>)六种运算。
  • 赋值运算:简单赋值(=)、加等于(+=)、减等于(-=)、乘等于(*=)、除等于(/=)和取余等于(%=)六种运算。
  • 条件运算:三目运算符(?:),格式为:表达式1 ? 表达式2 : 表达式3。如果表达式1的值为真,则返回表达式2的值;否则返回表达式 3 的值。
  • 逗号运算:由逗号连接的多个表达式的求值,返回的是最后一个表达式的值。
  • 指针运算:地址运算符(&)、取值运算符(*)、指针加法和指针减法等运算。

这些基本运算是程序中非常常见的,了解它们的含义和使用方法对于进行编程是非常重要的。

补充:

  • 上述提到的很多运算,在后续的章节后都会逐一介绍,比如指针运算将在 5. 指针 章节介绍

运算分类 | 根据运算符作用的操作数个数为标准 | 单目运算、双目运算、三目运算

  • 单目运算:指只需要一个操作数的运算符,如取反运算符 !、递增运算符 ++ 等。
  • 双目运算:指需要两个操作数的运算符,如加法运算符 +、乘法运算符 * 等。
  • 三目运算:指需要三个操作数的运算符,只有 C 语言中的三目运算符 ?: 符合这个条件。

这三种运算符都是 C 语言中常见的基本运算符,不同的运算符可以完成不同的计算任务,如加减乘除、比较大小、逻辑运算等。

赋值运算

image.png

image.png

  • 赋值运算是一种二元运算符,用于将右侧表达式的值赋给左侧的变量。
  • 在 C 语言中,赋值运算符使用等号 = 表示。
  • 在赋值运算中,先计算等号右侧的表达式,然后将计算结果赋给等号左侧的变量。
  • 在赋值运算中,等号左侧必须是一个可修改的内容(如变量、数组元素或结构体成员),右侧可以是任何表达式。
    • 🌰 x = 5 表示将整数值 5 赋给变量 x。
  • 除了基本的赋值运算符 = 以外,C 语言还提供了一系列复合赋值运算符,如 +=、-=、*=、/=、%= 等。这些运算符可以将右侧的值和左侧的变量进行某种运算,然后将运算结果赋给左侧的变量。
    • 🌰 x += 5 等价于 x = x + 5,表示将变量 x 的值增加 5,并将结果赋给 x。

算术运算符的优先级高于赋值运算符

  • 如果一个表达式中同时包含算术运算符和赋值运算符,算术运算符会先被计算,然后再将计算结果赋值给变量。
  • 例:表达式 a = b + c * d 中,算术运算符 + 和 的优先级比赋值运算符 = 要高,因此,先计算 c d,然后将其加上 b 的值,最后将结果赋值给变量 a

使用括号改变优先级:

  • 如果想要改变运算符的优先级,可以使用小括号来改变表达式的结合顺序,从而达到预期的计算结果。
  • 例:表达式 (a = b) + c * d 中,赋值运算符 = 的优先级比算术运算符 + 和 都要高,因此,先将 b 的值赋值给变量 a,然后再计算 c d,最后将其结果加上 a 的值
  1. #include <stdio.h>
  2. int main() {
  3. int x = 20;
  4. x *= 10 + 2; // 等效于 x = x * (10 + 2)
  5. printf("x = %d\n", x); // => x = 240
  6. return 0;
  7. }

x *= 10 + 2;

  • 等效于 x = x * (10 + 2)
  • *= 复合赋值运算符
  • + 算术运算符,表示将左右两侧的操作数相加

自增、自减

自增自减运算符是 C 语言中的一种特殊运算符,分为以下两种形式:

  • 前置 自增自减 ++i --i
  • 后置 自增自减 i++ i--

前置:

  • 前置自增自减运算符 ++ 和 — 会 将操作数的值先自增或自减 1,再将新值作为表达式的值返回
  • 例:
    • ++a 表示将变量 a 的值加 1,并将 新值(自增后的值) 作为表达式的值返回;
    • —b 表示将变量 b 的值减 1,并将 新值(自减后的值) 作为表达式的值返回;

后置:

  • 后置自增自减运算符 ++ 和 — 则会先将操作数的值作为表达式的值返回,再将其自增或自减 1。
  • 例:
    • a++ 表示将变量 a 的值(自增前的值)作为表达式的值返回,并将 a 的值加 1;
    • b— 表示将变量 b 的值(自增后的值)作为表达式的值返回,并将 b 的值减 1;

注意:

  • 自增自减运算符只能作用于变量,不能作用于常量或表达式
  • 不能对同一个变量同时使用前置自增自减和后置自增自减运算符,否则会产生未定义行为
  1. #include <stdio.h>
  2. int main(void) {
  3. int x = 0;
  4. int y = ++x + x++;
  5. printf("x:%d\n", x);
  6. printf("y:%d\n", y);
  7. return 0;
  8. }

image.png

++x + x++ 修改一个变量的值并且在同一个表达式中再次使用它的值,这种行为是未定义的,所以应该避免出现类似这样的写法。

  1. #include <stdio.h>
  2. int main() {
  3. int a = 10;
  4. int b, c, d, e;
  5. b = ++a; // 前置自增,a 的值先加 1,然后赋给 b,此时 a=11,b=11
  6. c = a++; // 后置自增,a 的值先赋给 c,然后再加 1,此时 a=12,c=11
  7. d = --a; // 前置自减,a 的值先减 1,然后赋给 d,此时 a=11,d=11
  8. e = a--; // 后置自减,a 的值先赋给 e,然后再减 1,此时 a=10,e=11
  9. printf("a=%d, b=%d, c=%d, d=%d, e=%d\n", a, b, c, d, e);
  10. return 0;
  11. }
  12. /* 运行结果:
  13. a=10, b=11, c=11, d=11, e=11
  14. */

关系运算与逻辑运算

image.png

image.png

  • 关系运算用于比较两个值之间的大小关系,结果只有 true 和 false 两个取值。
  • 常见的关系运算符包括:等于(==)、不等于(!=)、大于(>)、小于(<)、大于等于(>=)、小于等于(<=)等。
  • 逻辑运算用于组合关系运算的结果,结果同样只有 true 和 false 两个取值。
  • 常见的逻辑运算符包括:与(&&)、或(||)、非(!)等。
  • 关系运算的短路现象:C 语言中的逻辑运算是短路运算,即如果某个表达式的结果已经能够决定整个表达式的值,则后面的表达式将不会被计算。

在 C 语言中,判断一个值是“真”值还是“假”值的标准

在 C 语言中,通常把值为 0 视为假,把值非 0 视为真

C 语言中有 true、false 类型嘛

C 语言中没有内置的布尔类型,也没有内置的 true 或 false 值。

关系表达式

  • 关系表达式是指 由关系运算符连接的表达式
  • 通常用于比较两个值的大小关系
  • 结果为一个布尔值,即:真或假

C 语言中只有以下这 6 个关系运算符:

  • 大于

  • < 小于
  • = 大于等于

  • <= 小于等于
  • == 等于
  • != 不等于

关系表达式的结果为真或假,分别用整数 1 和 0 表示,例:

  • 2 > 1 的结果为真,用整数 1 表示;
  • 2 < 1 的结果为假,用整数 0 表示;

注意:

  • 关系表达式也称条件表达式
  • 关系表达式的值为 0 表示条件不成立,值为非 0 (通常是 1)表示条件成立
  • 关系表达式通常用于条件语句和循环语句中,用于控制程序的流程
  • 当关系表达式的值是真时,我们有时也会使用非 0 数值表示,不一定非得是 1

打印关系运算的结果

  1. #include <stdio.h>
  2. int main() {
  3. printf("1 > 2 结果是 %d\n", 1 > 2);
  4. printf("2 > 1 结果是 %d\n", 2 > 1);
  5. return 0;
  6. }
  7. /* 运行结果:
  8. 1 > 2 结果是 0
  9. 2 > 1 结果是 1
  10. */

在 C 语言中,布尔值的格式控制字符是 %d

原因:
在 C 语言中,布尔值实际上是用整数类型表示的,其中 0 表示假,非零值(通常是 1)表示真

打印:
当我们使用 printf 函数输出布尔值时,可以使用 %d 来格式化输出整数类型的值。

  • 如果布尔值为真,则输出 1
  • 如果布尔值为假,则输出 0

逻辑运算

  • 逻辑运算是指 对逻辑值(真和假)进行的运算
  • 逻辑运算通常用于判断语句中的条件表达式
  • 逻辑运算有以下 3 种:
    • 与(AND)
    • 或(OR)
    • 非(NOT)

C 语言中一共有 3 种逻辑运算:

  • &&(逻辑与)
    • 当且仅当两个操作数都为真(非零)时,结果为真
    • 否则,结果为假(零)
  • ||(逻辑或)
    • 当两个操作数中至少有一个为真时,结果为真
    • 否则,结果为假
  • !(逻辑非)
    • 如果操作数为真,则结果为假
    • 如果操作数为假,则结果为真

注意:

  • 逻辑运算常常与关系运算配合使用
  • 例如 if (a > b && c < d) 表示当 a 大于 b 并且 c 小于 d 时,执行 if 语句中的代码。

输入 2 个整数,求这 2 个数的最大数

  1. #include <stdio.h>
  2. int main() {
  3. int a, b, max;
  4. printf("请输入两个整数:");
  5. scanf("%d %d", &a, &b);
  6. max = a > b ? a : b;
  7. printf("最大值为:%d\n", max);
  8. return 0;
  9. }

image.png

解释:
该程序通过 scanf 函数获取用户输入的两个整数,然后使用条件运算符 ? : 比较两个数的大小,找出其中较大的一个,并将其赋值给变量 max。最后,程序输出变量 max 的值作为结果。

max = a > b ? a : b;
三目运算符的语法结构为:(condition) ? expression1 : expression2,其中 condition 是一个条件表达式,expression1 和 expression2 是两个可能的值。如果 condition 满足,则返回 expression1 的值,否则返回 expression2 的值。

输入 1 个代表年份的正整数,判断是否闰年

  1. #include <stdio.h>
  2. int main() {
  3. int year;
  4. printf("请输入一个年份:");
  5. scanf("%d", &year);
  6. if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
  7. printf("%d年是闰年。\n", year);
  8. } else {
  9. printf("%d年不是闰年。\n", year);
  10. }
  11. return 0;
  12. }

image.pngimage.png

解释:
程序先使用 scanf 函数读入用户输入的年份,然后根据判断条件判断该年份是否为闰年,并输出相应的提示信息。

(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
程序中使用了 if 语句来判断年份是否为闰年,具体的判断条件为:

  1. 能被 4 整除但不能被 100 整除的年份是闰年;year % 4 == 0 && year % 100 != 0
  2. 能被 400 整除的年份也是闰年;year % 400 == 0

上述两个条件只要满足一个,那么这一年就是闰年。

位运算

image.png

  • C 语言中的位运算是指 对二进制位进行的运算
  • 位运算在计算机底层的操作中非常常见,例如在嵌入式系统、驱动程序等领域中经常使用位运算来进行高效的操作。
  • 位运算主要应用于需要对二进制数据进行操作的场景,可以对数据进行高效的存储、检索、加密等处理。

位运算逻辑运算符:

  • & 按位与:对两个二进制数的每一位进行与运算,即 当两个对应位都为 1 时,结果为 1,否则为 0
  • | 按位或:对两个二进制数的每一位进行或运算,即 当两个对应位都为 0 时,结果为 0,否则为 1
  • ^ 按位异或:对两个二进制数的每一位进行异或运算,即 当两个对应位相同时,结果为 0,否则为 1
  • ~ 按位取反:对一个二进制数的每一位进行取反操作,即 将每个 0 变为 1,每个 1 变为 0

image.png

image.png

位移运算符:

  • << 左移:将一个二进制数向左移动指定的位数,相当于将该数乘以 2 的 n 次方
  • >> 右移:将一个二进制数向右移动指定的位数,相当于将该数除以 2 的 n 次方(注意,这里是向下取整)

image.png

位运算的常见使用场景:

  • 位掩码(Bitmasking):使用二进制位来标识某些状态或属性,例如将一个整型变量的各个二进制位分别表示为某个开关是否打开等状态,这时候我们可以使用位运算进行标志的设置、获取、清除等操作,常用的位运算符包括按位与(&)、按位或(|)、按位异或(^)等。
  • 位移操作:将某个数值向左或向右移动n个二进制位,相当于将该数值乘以或除以2^n,其中左移运算符(<<)表示将数值向左移动,右移运算符(>>)表示将数值向右移动。
  • 操作系统的内存管理:在操作系统底层中,内存空间通常是以二进制的方式来表示,因此在内存管理中经常使用位运算进行内存的分配、回收等操作。
  • 加密算法:一些加密算法中也会使用到位运算,例如DES加密算法、SHA1等。
  • ……
  1. #include <stdio.h>
  2. int main() {
  3. unsigned int a = 60; /* 60 = 0011 1100 */
  4. unsigned int b = 13; /* 13 = 0000 1101 */
  5. int result = 0;
  6. result = a & b; /* 12 = 0000 1100 */
  7. printf("a & b = %d\n", result);
  8. result = a | b; /* 61 = 0011 1101 */
  9. printf("a | b = %d\n", result);
  10. result = a ^ b; /* 49 = 0011 0001 */
  11. printf("a ^ b = %d\n", result);
  12. result = ~a; /* -61 = 1100 0011 */
  13. printf("~a = %d\n", result);
  14. result = a << 2; /* 240 = 1111 0000 */
  15. printf("a << 2 = %d\n", result);
  16. result = a >> 2; /* 15 = 0000 1111 */
  17. printf("a >> 2 = %d\n", result);
  18. return 0;
  19. }
  20. /* 运行结果:
  21. a & b = 12
  22. a | b = 61
  23. a ^ b = 49
  24. ~a = -61
  25. a << 2 = 240
  26. a >> 2 = 15
  27. */

解释:
以上代码中,使用了位运算的不同操作符来进行位运算,例如按位与运算符 &,按位或运算符 |,按位异或运算符 ^,按位取反运算符 ~,左移运算符 <<,以及右移运算符 >>。这些运算符可以操作整数类型的数据,包括无符号整数和带符号整数。

扫地机器人

image.png

控制变量 x 0110 1111

  • 扫地:第 2 位清零 0110 1101
  • 拖地:第 4 位置一 0110 1111(由于此操作的结果和控制变量的值一致,并没有发生变化,现将要求改为 第 8 位置一 1110 1111
  • 吸尘:第 6 位取反 0101 1111

分析:

  • 置零:**& 0** 和 0 做与运算
  • 置一:**| 1** 和 1 做或运算
  • 取反:**^ 1** 和 1 做异或运算
  1. #include <stdio.h>
  2. void print_bin(unsigned char c) {
  3. printf("c: %d\n", c);
  4. printf("对应的二进制: ");
  5. for (int i = 7; i >= 0; i--) {
  6. printf("%d", (c >> i) & 1);
  7. }
  8. }
  9. int main() {
  10. unsigned char x = 0b01101111; // 控制变量 x
  11. print_bin(x);
  12. printf("\n");
  13. // 第 2 位置 0
  14. x = x & 0b11111101; // x &= ~(1 << 1);
  15. print_bin(x);
  16. printf("\n");
  17. return 0;
  18. }
  19. /* 运行结果:
  20. c: 111
  21. 对应的二进制: 01101111
  22. c: 109
  23. 对应的二进制: 01101101 */

以下写法都是等效的:

  • unsigned char x = 0b01101111;
  • unsigned char x = 111;
  • char x = 0b01101111;
  • char x = 111;
  • int x = 111;
  • ……

表示第 2 位为 0 其它 7 位都是 1 的字符型数据:

  • 0b11111101
  • ~(1 << 1) 这种写法也是蛮常见的,先写一个 1,然后左移,再全部取反
    • 1 等于 0000 0001
    • 1 << 1 等于 0000 0010
    • ~(1 << 1) 等于 1111 1101
  1. #include <stdio.h>
  2. void print_bin(unsigned char c) {
  3. printf("c: %d\n", c);
  4. printf("对应的二进制: ");
  5. for (int i = 7; i >= 0; i--) {
  6. printf("%d", (c >> i) & 1);
  7. }
  8. }
  9. int main() {
  10. unsigned char x = 0b01101111; // 控制变量 x
  11. print_bin(x);
  12. printf("\n");
  13. // 第 8 位置 1
  14. // x = x | 0b10000000;
  15. // x |= 8;
  16. x |= 1 << 7;
  17. print_bin(x);
  18. printf("\n");
  19. return 0;
  20. }
  21. /* 运行结果:
  22. c: 111
  23. 对应的二进制: 01101111
  24. c: 239
  25. 对应的二进制: 11101111 */

高亮的写法都是等效。

  1. #include <stdio.h>
  2. void print_bin(unsigned char c) {
  3. printf("c: %d\n", c);
  4. printf("对应的二进制: ");
  5. for (int i = 7; i >= 0; i--) {
  6. printf("%d", (c >> i) & 1);
  7. }
  8. }
  9. int main() {
  10. unsigned char x = 0b01101111; // 控制变量 x
  11. print_bin(x);
  12. printf("\n");
  13. // 第 6 位取反
  14. // x = x ^ 0b00100000;
  15. // x ^= 0b00100000;
  16. // x ^= 32;
  17. x ^= 1 << 5;
  18. print_bin(x);
  19. printf("\n");
  20. return 0;
  21. }
  22. /* 运行结果:
  23. c: 111
  24. 对应的二进制: 01101111
  25. c: 79
  26. 对应的二进制: 01001111 */

高亮的写法都是等效的。

字符型数据的算数运算

  • 在 C 语言中,字符型数据实际上是整数类型,因此可以进行算数运算
  • 在进行算数运算时,C 语言会将字符型数据看作是其对应的 ASCII 码值。
  • 例如:字符 'A' 的 ASCII 码值为 65,字符 'B' 的 ASCII 码值为 66,因此表达式 'A' + 'B' 的值为 131。
  1. #include <stdio.h>
  2. int main() {
  3. char a = 'A';
  4. char b = 'B';
  5. int c = a + b;
  6. printf("char c:%d\n", c);
  7. printf("char c:%c\n", c);
  8. return 0;
  9. }
  10. /* 运行结果:
  11. char c:131
  12. char c:?
  13. */

这段代码是将两个字符型变量 a 和 b 相加,然后将结果存储到整型变量 c 中,最后输出变量 c 的值。

在 C 语言中,字符型变量可以进行算术运算,其实质是将字符的 ASCII 码值参与计算。在本例中,变量 a 和 b 分别存储字符 ‘A’ 和 ‘B’ 的 ASCII 码值,即 65 和 66,所以变量 c 的值为 131。

在第一个 printf 语句中,使用 %d 格式符输出变量 c 的值,结果为 131。在第二个 printf 语句中,使用 %c 格式符输出变量 c 的值,因为 ASCII 码值为 131 对应的字符为 **'\x83'**,不是可打印字符,所以输出一个不可见的字符

注意:在进行字符型变量的算术运算时,可能会发生溢出现象,因为字符型变量通常只占用一个字节的空间如果结果超出了字符型变量的表示范围,可能会导致结果不正确。因此,在进行字符型变量的算术运算时,需要 注意结果是否会溢出

注意: '\x83' 在 lightly 上打印出来的是一个不可见字符 lightly

'\x83' 在 windows 上的 Visual Studio 中,打印出来的是一个问号 Visual Studio 2022

原因不详……

有符号类型和无符号类型

  1. #include <stdio.h>
  2. int main() {
  3. unsigned char x = 200; // 范围在 0~255 之间
  4. char y = -128; // 范围在 -128~127 之间
  5. printf("unsigned char 类型的 x 的值为:%u\n", x);
  6. printf("char 类型的 y 的值为:%d\n", y);
  7. return 0;
  8. }
  • unsigned char 无符号字符型,范围在 0~255 之间
  • char 如果我们直接写 char 类型,那么默认是 signed char 类型,范围在 -128~127 之间

注意:如果赋的值不在范围内,会报错。

  1. #include <stdio.h>
  2. int main() {
  3. unsigned char uMax = 0b11111111;
  4. unsigned char uMin = 0b00000000;
  5. char cMax = 0b01111111;
  6. char cMin = 0b10000000;
  7. printf("无 符号字符型的最 大 值:%d\n", uMax);
  8. printf("无 符号字符型的最 小 值:%d\n", uMin);
  9. printf("有 符号字符型的最 大 值:%d\n", cMax);
  10. printf("有 符号字符型的最 小 值:%d\n", cMin);
  11. return 0;
  12. }
  13. /* 运行结果:
  14. 无 符号字符型的最 大 值:255
  15. 无 符号字符型的最 小 值:0
  16. 有 符号字符型的最 大 值:127
  17. 有 符号字符型的最 小 值:-128 */

在 C 语言中,char 类型表示一个字符,通常是 8 位二进制数(也可以是其他大小),可以表示 ASCII 码中的一个字符。char 类型有两种类型:signed charunsigned char

  • signed char 范围为 -128 到 127,而 unsigned char 范围为 0 到 255,它们之间的区别在于对于 signed char 类型,最高位表示符号位,0 表示正数,1 表示负数;
  • unsigned char 类型,最高位表示数值位,所有 8 位都用于表示数值,因此 unsigned char 类型的取值范围是 0 到 255。

注意:

  • 由于 char 类型是实现依赖的,因此在某些平台上,char 类型可能是有符号的,也可能是无符号的
  • 在使用 char 类型时,最好显式地指定是 signed char 还是 unsigned char 类型

三目运算

image.png

  • 三目运算是 C 语言中的一种特殊运算,也被称为条件运算符
  • 三目运算可以实现简单的条件判断
  • 语法格式为:表达式1 ? 表达式2 : 表达式3
    • 表达式1 的值为真(非零)时,运算结果为 表达式2 的值
    • 表达式1 的值为假(0)时,运算结果为 表达式3 的值
  1. #include <stdio.h>
  2. int main() {
  3. int a = 10, b = 20, max;
  4. max = (a > b) ? a : b; // 如果 a > b,则 max = a,否则 max = b
  5. printf("max = %d\n", max); // => max = 20
  6. return 0;
  7. }

逗号结合运算

image.png

  1. #include <stdio.h>
  2. int main() {
  3. int a, b, c, x;
  4. a = 2 * 5, a / 10, a - 2; // warning
  5. printf("a = %d\n", a); // => a = 10
  6. x = (a = 10, b = 100, c = 1000);
  7. printf("x = %d\n", x); // => x = 1000
  8. printf("a = %d\n", a); // => a = 10
  9. printf("b = %d\n", b); // => b = 100
  10. printf("c = %d\n", c); // => c = 1000
  11. return 0;
  12. }

image.png

分析警告:warning: left operand of comma operator has no effect [-Wunused-value] a = 2 * 5, a / 10, a - 2;
这个 warning 提示是因为代码中使用了逗号运算符,但是逗号左侧的表达式并没有被使用,是没有实际作用的。在这个例子中,表达式 a = 2 * 5 赋值给了变量 a,但是在其后面的逗号运算中,a / 10 和 a - 2 的结果并没有被使用,而是被直接丢弃了。

a = 2 * 5, a / 10, a - 2; 这条语句,在不同的 C 标准中,打印的结果也许会有些许差异,按照老师课件中的描述,如果后续语句 a / 10a - 2 没有被丢弃的话,那么结果确实是 8

  1. #include <stdio.h>
  2. int main() {
  3. int a = 1, b = 2, c = 3;
  4. int result = (a++, b++, c++, a + b + c);
  5. printf("result = %d\n", result);
  6. return 0;
  7. }
  8. /* 运行结果:
  9. result = 9 */

解释:
上面程序中,定义了三个整型变量 a、b 和 c,并初始化为 1、2 和 3。然后在一个表达式中使用逗号运算符将它们的值依次赋给自身,最后计算 a+b+c 的值,并将结果赋给变量 result。

可以看到,逗号运算符首先计算了 a++b++c++,然后计算了 a+b+c 的值,并将最后一个表达式 a + b + c 的结果赋给了 result 变量。

sizeof

  • sizeof 不是函数,而是 C 语言的一个操作符
  • sizeof 操作符 用于计算一个数据类型、变量、数组等在内存中所占用的字节数,并返回结果

语法:

  1. sizeof(type) // type 表示数据类型,如 int、double、char 等
  2. sizeof(variable) // variable 表示变量名
  3. sizeof(expression) // expression 表示表达式

作用:

  • 动态获取内存空间大小:在 C 语言中,可以使用 malloc() 函数在堆上动态分配内存空间,但在分配内存空间之前需要知道所需内存空间的大小。这时可以使用 sizeof 操作符获取变量或类型所占用的内存空间大小,并在 malloc() 函数中使用相应的字节数进行内存分配。
  • 计算数组长度:在 C 语言中,数组长度通常需要在程序中进行计算或获取。可以使用 sizeof 操作符计算数组的长度,即总字节数除以单个元素占用的字节数。
  • 确定数据类型的大小:在 C 语言中,不同的数据类型在内存中占用的字节数是不同的。可以使用 sizeof 操作符查询数据类型的大小,以便进行类型转换或进行其他操作。
  • 用于指针操作:在 C 语言中,指针可以用于动态分配内存空间、访问数组元素等。可以使用 sizeof 操作符获取指针类型所占用的内存空间大小,并在指针操作中使用相应的字节数进行计算。
  1. #include <stdio.h>
  2. int main() {
  3. int a;
  4. double b;
  5. char c;
  6. int arr[10];
  7. printf("sizeof(int) = %zu\n", sizeof(int));
  8. printf("sizeof(double) = %zu\n", sizeof(double));
  9. printf("sizeof(char) = %zu\n", sizeof(char));
  10. printf("sizeof(arr) = %zu\n", sizeof(arr));
  11. printf("sizeof(a) = %zu\n", sizeof(a));
  12. printf("sizeof(b) = %zu\n", sizeof(b));
  13. printf("sizeof(c) = %zu\n", sizeof(c));
  14. return 0;
  15. }
  16. /* 运行结果:
  17. sizeof(int) = 4
  18. sizeof(double) = 8
  19. sizeof(char) = 1
  20. sizeof(arr) = 40
  21. sizeof(a) = 4
  22. sizeof(b) = 8
  23. sizeof(c) = 1
  24. */

下面是一个使用 sizeof 操作符的 demo,主要展示了以下两种用法:

  1. 计算数据类型的大小。
  2. 计算数组的长度。
  1. #include <stdio.h>
  2. int main() {
  3. // 计算 int 类型在内存中占用的字节数
  4. printf("int类型在内存中占用的字节数:%lu\n", sizeof(int)); // %lu 是 C 语言中用于格式化输出无符号长整型(unsigned long)的格式控制符。
  5. // 定义一个 int 数组
  6. int arr[] = {1, 2, 3, 4, 5};
  7. // 计算数组的长度
  8. int len = sizeof(arr) / sizeof(arr[0]);
  9. printf("数组arr的长度为:%d\n", len);
  10. return 0;
  11. }
  12. /* 运行结果:
  13. int类型在内存中占用的字节数:4
  14. 数组arr的长度为:5 */

注意:

  • sizeof 操作符的参数可以是数据类型、变量或表达式。如果参数是一个变量,那么 sizeof 将返回该变量的数据类型的字节数。
  • sizeof 操作符返回的是一个常量表达式,其值在编译时就已经确定了。因此,sizeof 表达式不会改变运行时程序的状态。
  • sizeof 操作符作用于数组时,返回的是整个数组的字节数,而不是数组的第一个元素的字节数。例如,如果定义一个 int 类型的数组 int a[10],则 sizeof(a) 返回的是 40(即 10 个元素 x 4 个字节/元素)。
  • sizeof 操作符作用于指针时,返回的是指针本身所占用的字节数,而不是指针所指向的内存区域的字节数。例如,对于一个指向 int 类型的指针变量 int *psizeof(p) 返回的是指针类型的字节数,而不是 int 类型的字节数。
  • 对于结构体、联合体等复杂数据类型,sizeof 返回的是其所占用的字节数。例如,如果定义一个结构体 struct point { int x; int y; },则 sizeof(struct point) 返回的是 8(即 2 个 int 类型 x 4 个字节/元素)。
  • 由于 sizeof 操作符返回的是一个 size_t 类型的无符号整数,因此在打印 sizeof 的结果时需要使用 %zu 格式化输出。
  • sizeof 操作符在 C 语言中具有广泛的应用,能够帮助程序员动态地获取内存空间大小、计算数组长度、确定数据类型的大小以及进行指针操作等
  • 在使用 sizeof 操作符时需要了解其返回值类型、参数类型以及其作用于不同类型时的返回值规则。

补充:

  • 上述很多内容都是后续章节才会介绍的点,不理解的先跳过即可。
  • sizeof 算是比较常用,它的应用场景和注意事项也比较多,随着后续的不断学习,逐步完善 sizeof 的相关 demo。

使用 strlen 获取字符串长度

  • 可以使用 C 语言的标准库函数 strlen 来获取一个字符串的长度。strlen 函数的原型定义在 string.h 头文件中,函数返回一个无符号整数,表示字符串中有效字符的个数(不包括字符串结束符 \0)。
  • strlen 函数计算字符串长度时,是从字符串开头一直到 第一个字符串结束符 \0 的位置
  • 字符串中有多个结束符:如果字符串中包含多个字符串结束符,那么 只有第一个字符串结束符之前的字符会被计算在内
  • 字符串中没有结束符:如果字符串中没有字符串结束符,那么 strlen 函数会一直往后读取内存,直到遇到一个空字符为止,这可能会导致不可预期的结果和内存访问错误。因此,在处理字符串时,一定要确保字符串以空字符结尾,否则可能会产生不可预期的错误。

下面是一个使用 strlen 函数获取字符串长度的例子:

  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4. char str1[] = "Hello, world!";
  5. int len = strlen(str1);
  6. printf("字符串 \"%s\" 的长度为 %d\n", str1, len); // => 字符串 "Hello, world!" 的长度为 13
  7. return 0;
  8. }

浮点数在计算机中的存储机制

在计算机中,浮点数通常使用 IEEE 754 标准进行存储和计算。IEEE 754 标准定义了浮点数的二进制表示方法、舍入规则等方面的规范,是广泛采用的浮点数标准。

IEEE 754 标准规定,浮点数由三部分组成:

  • 符号位
  • 指数位
  • 尾数位

float:对于单精度浮点数(float 类型),符号位占 1 位,指数位占 8 位,尾数位占 23 位
double:对于双精度浮点数(double 类型),符号位占 1 位,指数位占 11 位,尾数位占 52 位

浮点数的二进制表示方法如下:

  1. 首位表示 符号位
  2. 接着的若干位表示 指数位
    1. 指数表示实数在数量级上的变化
    2. 指数位使用了偏移表示法,即指数值被减去一个偏移量(单精度浮点数的偏移量为 127,双精度浮点数的偏移量为 1023),以便用无符号数表示正负数。
    3. 偏移的目的是为了方便比较浮点数大小,也为了存储负数的指数。
  3. 最后的若干位表示 尾数位
    1. 尾数则表示实数的精度

浮点数在计算机中的存储方式采用了科学计数法,即将一个实数表示成一个尾数与一个指数的积,指数表示实数在数量级上的变化,尾数则表示实数的精度。

在进行浮点数的运算时,计算机会先将浮点数转换成二进制表示,然后进行运算,最后再将结果转换为十进制浮点数。在这个过程中,会根据 IEEE 754 标准的舍入规则对计算结果进行舍入。由于浮点数在计算机中的存储精度有限,因此在进行高精度计算时需要特别注意精度误差的问题。

浮点运算结果不精确的原因

浮点数在计算机中的表示方法是采用二进制的科学计数法,即:2. 数据类型与表达式(3) - 图24

  • 2. 数据类型与表达式(3) - 图25 表示符号位
  • 2. 数据类型与表达式(3) - 图26 表示有效数字
    • 用二进制数表示
  • 2. 数据类型与表达式(3) - 图27 表示指数
    • 用二进制数表示

浮点数的有效数字和指数都用二进制数表示,因此浮点数在计算机内部的存储方式和十进制表示方式有所不同。

由于计算机内部用二进制来表示浮点数,而 有些十进制数在二进制下是无限循环的,例如 0.1 在二进制下是 0.0001100110011...(循环节为 0011),规范化二进制数,即将小数点左移,直到小数点前只剩下一位非零数字。在这个例子中,规范化后的数为 1.100110011... × 2^(-4)。这种无限循环的二进制数无法精确地表示为有限的二进制数,因此在计算机中计算浮点数时可能会出现一些意料之外的结果,例如:

  1. #include <stdio.h>
  2. int main() {
  3. float a = 0.1;
  4. float b = 0.2;
  5. float c = a + b;
  6. printf("a = %.20f\n", a);
  7. printf("b = %.20f\n", b);
  8. printf("c = %.20f\n", c);
  9. if (a + b == c) {
  10. printf("a + b == c");
  11. } else {
  12. printf("a + b != c");
  13. }
  14. return 0;
  15. }
  16. /* 运行结果:
  17. a = 0.10000000149011611938
  18. b = 0.20000000298023223877
  19. c = 0.30000001192092895508
  20. a + b == c
  21. */

在上面的代码中,虽然我们想要计算的是 0.1 + 0.2,结果却输出了一个近似值 0.30000001192092895508,这是因为在计算机内部,0.1 和 0.2 在二进制下都是无限循环的,而将它们相加后得到的结果同样也是无限循环的,计算机在计算时只能保留一定的精度,因此最终的结果并不是我们期望的精确值。

笔记:由于某些十进制数无法在计算机中使用精确的二进制数来表示,所以会存在浮点数运算结果不精确的情况

  1. 浮点数在计算机中的表示方法是采用二进制的科学计数法
  2. 有些十进制数在二进制下无法精确地表示,例如 0.1 使用二进制表示就是无限循环的

除运算 VS 取余运算

运算符不同:

  • 除运算使用/符号表示
  • 取余运算使用%符号表示

操作数不同:

  • 除运算的操作数可以是整数、浮点数
  • 取余运算的操作数只能是整数

作用不同:

  • 除运算用于计算两个数相除的商
    • 例如:10 / 3 会得到 3,因为 10 除以 3 的商是 3
    • 注意:
      • 如果参与除运算的两个操作数都是整数,则得到的结果也是整数,即商的小数部分被截断
      • 如果要得到小数的结果,可以将其中一个操作数转换为浮点数
  • 取余运算用于计算两个数相除的余数
    • 例如:10 % 3 会得到 1,因为 10 除以 3 的余数是 1
    • 注意:
      • 取余运算的结果与被除数的符号相同
      • 例如:-10 % 3 会得到 -1,因为 -10 除以 3 的余数是 -1
  1. #include <stdio.h>
  2. int main() {
  3. int a = 10 % 3;
  4. int b = -10 % 3;
  5. printf("a: %d\n", a); // => a: 1
  6. printf("b: %d\n", b); // => b: -1
  7. return 0;
  8. }
  1. #include <stdio.h>
  2. int main() {
  3. int a = 10;
  4. int b = 3;
  5. int c = a / b; // 整数除法,结果为 3
  6. int d = a % b; // 取余运算,结果为 1
  7. float e = (float)a / b; // 浮点数除法,结果为 3.3333
  8. printf("%d\n", c);
  9. printf("%d\n", d);
  10. printf("%f\n", e);
  11. return 0;
  12. }
  13. /* 运行结果:
  14. 3
  15. 1
  16. 3.333333
  17. */

字符型数据的算术运算

  • 字符型数据在 C 语言中是用来表示字符的,但是 在计算机中,字符是以数字形式存储的,即字符被转换成了相应的 ASCII 码或 Unicode 码,这些码被存储在计算机内存中。
  • 尽管字符型数据是以字符形式展示,但实际上它们在计算机内部以数字形式进行存储和处理
  • 字符型数据在进行算术运算时,会先被转换为对应的 ASCII 码值,然后再进行运算
  1. #include <stdio.h>
  2. int main() {
  3. char c1 = 'A';
  4. char c2 = 'a';
  5. printf("%d %% %d = %d\n", c1, c2, c1 % c2); // => 65 % 97 = 65
  6. return 0;
  7. }

这段代码定义了两个字符型变量 c1 和 c2 分别赋值为字符 ‘A’ 和字符 ‘a’。在 printf 函数中,使用 %d 格式化字符型数据并进行取余运算,输出结果为 65 % 97 = 65。

字符型数据在计算机内部是以 ASCII 码的形式存储的。字符 ‘A’ 的 ASCII 码是 65,字符 ‘a’ 的 ASCII 码是 97。所以 c1 % c2 实际上是计算了 65 除以 97 的余数,结果是 65。

printf("%d %% %d = %d\n", c1, c2, c1 % c2);
注意:这里使用了 %d 格式化字符型数据,而不是使用 %c 输出字符本身。因此输出结果是 65 而不是 ‘A’。

  1. #include <stdio.h>
  2. int main() {
  3. char c1 = 'A';
  4. char c2 = 'a';
  5. printf("%d %% %d = %c\n", c1, c2, c1 % c2); // => 65 % 97 = A
  6. return 0;
  7. }

除数为 0 会导致什么问题,该如何处理除数可能为 0 的情况

导致的问题:

  • 在 C 语言中,除以零的错误非常常见,除数为 0 会导致运行时错误,称为“除零错误(division by zero)”
  • 当程序执行到除以零的语句时,C 语言编译器会抛出一个异常,导致程序崩溃或异常退出,通常会导致程序的不可预测行为

解决措施:
在编写程序时应该避免除以零的情况,可以使用条件语句或异常处理机制来避免这种错误的发生。例如:

  • 在除数可能为零的情况下,可以先进行判断,如果除数为零则不执行除法运算
  • 在除数为零时抛出异常并进行相应的处理

什么叫“未定义的行为”

  • 未定义行为是指程序中包含一些行为或操作,其具体行为 没有被 C 语言标准所规定
  • 这些行为的结果可能是未定义的、不确定的、甚至是危险的,这取决于编译器和操作系统的具体实现

在 C 语言中,许多行为都是未定义的,例如:

  • 除以零
  • 访问未初始化的变量
  • 越界访问数组
  • 使用 NULL 指针
  • ……

因此,程序员应该避免这些未定义的行为,以免导致程序出现不可预测的结果。