目录
1 运算符基础
1.1 什么是运算符
1.2 什么是表达式
1.3 左操作数和右操作数
1.4 运算符分类
1.4.1 按照操作数个数分类
1.4.2 按照功能分类
1.5 如何掌握运算符
2 算术运算符
2.1 正号和负号
2.2 加、减、乘、除
2.3 取模(取余)
2.4 自增和自减
3 关系运算符(比较运算符)
4 逻辑运算符
4.1 逻辑与 &&
4.2 逻辑或 ||
4.3 逻辑非 !
5 赋值运算符
5.1 左值和右值
5.2 注意事项
5.3 综合案例
6 位运算符
6.1 按位与 &
6.2 按位或 |
6.3 按位异或 ^
6.4 按位取反 ~
6.5 按位左移 <<
6.6 按位右移 >>
7 三元运算符
7.1 基本语法
7.2 表达式最终取值
7.3 案例演示
7.4 案例:计算两个数的最大值
7.5 案例:计算三个数的最大值
8 运算符优先级
9 复杂表达式的计算过程
9.1 练习 1
9.2 练习 2
9.3 练习 3
10 测试题
1 运算符基础
1.1 什么是运算符
运算符是一种具有特殊含义的符号,它们在编程和数学中用于执行各种操作,包括但不限于数据的运算、赋值、比较以及逻辑处理等。这些操作能够改变、结合或比较一个或多个操作数(运算数)的值,从而生成新的值或执行特定的动作。
1.2 什么是表达式
表达式是编程和数学中用于表示计算或操作的结构,它由一组运算数(操作对象,如变量、常量等)和运算符(如加、减、乘、除等)按照一定规则组合而成。表达式的主要特点是它一定能够计算出或代表一个值。
表达式可以非常简单,如一个单独的变量或常量,也可以非常复杂,包含多个运算符和运算数,甚至可能嵌套函数调用、条件表达式等。
1.3 左操作数和右操作数
在 C 语言的表达式中,通常有两个主要元素参与操作:左操作数和右操作数。这些术语主要用于二元运算符(如加法 +、减法 -、乘法 *、除法 / 等)的上下文中,但也适用于其他需要两个操作数的场合,如赋值操作。
左操作数:位于运算符左侧的操作数。在赋值操作中,左操作数通常是一个变量,用于存储操作的结果。
右操作数:位于运算符右侧的操作数。在大多数情况下,右操作数可以是另一个变量、一个常量或一个表达式的结果。
例如,在表达式 a = b + 5; 中,a 是左操作数,而 b + 5 是右操作数(其中 b 和 5 分别作为加法运算符 + 的左操作数和右操作数)。
1.4 运算符分类
1.4.1 按照操作数个数分类
- 元运算符(一目运算符)
- 二元运算符(二目运算符)
- 三元运算符(三目运算符)
1.4.2 按照功能分类
- 算术运算符
- 赋值运算符
- 关系运算符
- 逻辑运算符
- 位运算符
1.5 如何掌握运算符
掌握一个运算符,关注以下几个方面:
- 运算符的含义。
- 运算符操作数的个数。
- 运算符所组成的表达式值。
- 运算符有无副作用,副作用指运算后是否会修改操作数的值。
2 算术运算符
算术运算符是对数值类型的变量进行运算的,在 C 程序中使用的非常多。进行算术运算时,操作数的类型会影响结果的类型和精度。
运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 |
---|---|---|---|---|
+ | 正号 | 1 | 操作数本身 | 无 |
- | 负号 | 1 | 操作数符号取反 | 无 |
+ | 加号 | 2 | 两个操作数之和 | 无 |
- | 减号 | 2 | 两个操作数之差 | 无 |
* | 乘号 | 2 | 两个操作数之积 | 无 |
/ | 除号 | 2 | 两个操作数之商 | 无 |
% | 取模(取余) | 2 | 两个整型操作数相除的余数 | 无 |
++ | 自增 | 1 | 操作数自增前或自增后的值(取决于使用的前缀或后缀形式) | 有 |
-- | 自减 | 1 | 操作数自减前或自减后的值(取决于使用的前缀或后缀形式) | 有 |
2.1 正号和负号
#include <stdio.h>int main()
{int x = 12;// 定义两个整型变量 x1 和 x2,分别赋值为 x 的负值和正值(注意:在 C 语言中,+x 实际上就是x本身,不改变值)int x1 = -x, x2 = +x;printf("x1=%d, x2=%d\n", x1, x2); // 预期输出:x1=-12, x2=12int y = -67;// 定义两个整型变量 y1 和 y2,分别赋值为 y 的负值和正值(同理,+y 也是 y 本身,不改变值)int y1 = -y, y2 = +y;printf("y1=%d, y2=%d\n", y1, y2); // 预期输出:y1=67, y2=-67return 0;
}
2.2 加、减、乘、除
#include <stdio.h>int main()
{// 尝试将浮点数 2.5 加到整数 5 上。由于赋值给 int 类型的变量 a,结果会被隐式转换为整数。// 因此,2.5 的小数部分被舍去,a 的值为 7。int a = 5 + 2.5;printf("%d * %d = %d\n", a, a, a * a); // 输出 a 的平方,即 7*7=49// 尝试从整数 a 中减去浮点数 2.5。由于 a 是 int 类型,结果会再次被隐式转换为整数。// 因此,7 - 2.5 变为 4.5,然后 4.5 被截断为整数 4,赋值给 a。a = a - 2.5; // 7 - 2.5 = 4.5,但 a 的类型是 int,所以 a 的值是 4printf("%d * %d = %d\n", a, a, a * a); // 输出 a 的平方,即 4*4=16// 尝试进行整数除法。6 除以 4 的结果原本应该是 1.5,但由于 b 是 double 类型,// 而参与除法的两个操作数都是整数,整数除法结果也是整数,即 1。// 然后,这个整数结果被隐式转换为 double 类型,赋给 b,所以 b 的值是 1.000000。double b = 6 / 4;printf("%f\n", b); // 输出 1.000000// 这里 6.0 是 double 类型,与整数 4 进行除法运算时,// 由于至少有一个操作数是浮点数,所以结果也是浮点数,即 1.5。// 结果直接赋给 double 类型的变量 c。double c = 6.0 / 4;printf("%f\n", c); // 输出 1.500000// 使用强制类型转换将整数 6 转换为 double 类型,然后与整数 4 进行除法运算。// 由于至少有一个操作数是浮点数,结果也是浮点数。double d = (double)6 / 4;printf("%f\n", d); // 输出 1.500000// 总结:在 C 语言中,进行算术运算时,操作数的类型会影响结果的类型和精度。
}
注意:进行算术运算时,操作数的类型会影响结果的类型和精度。如:整数之间做除法时,结果只保留整数部分而舍弃小数部分。
#include <stdio.h>int main()
{// 易错点,请注意// %f 是专门用于浮点数的格式说明符,当传入一个整数时,会发生未定义行为printf("10 / 3 = %f\n", 10 / 3); // 10 / 3 = 0.000000// 这里的强转只是针对 10 / 3 的整型结果 3 -> 3.000000printf("10 / 3 = %f\n", (double)(10 / 3)); // 10 / 3 = 3.000000// 正确写法printf("10 / 3 = %f\n", 10.0 / 3); // 10 / 3 = 3.333333printf("10 / 3 = %f\n", 10 / 3.0); // 10 / 3 = 3.333333printf("10 / 3 = %f\n", (double)10 / 3); // 10 / 3 = 3.333333printf("10 / 3 = %f\n", 10 / (double)3); // 10 / 3 = 3.333333return 0;}
注意:在使用格式占位符输出时,输出的类型一定要和格式占位符相匹配,不然会发生未定义行为,输出的数据值是不可靠的!
2.3 取模(取余)
% 操作符是取模(或称为求余)操作符,其操作数必须是整数类型(包括有符号整数和无符号整数)。这是因为取模操作本质上是在进行整数除法后的余数计算,而浮点数除法并不直接产生 “余数” 的概念,因为浮点数除法的结果是一个新的浮点数,它表示了两个数相除的确切结果(或尽可能精确的结果),而不是一个整数商和一个余数。
如果尝试对浮点数使用 % 取模操作符,编译器会报错,因为它不是一个有效的操作。
当使用负数进行取模运算时,结果的符号与被除数(即 % 运算符左边的数)的符号相同。
#include <stdio.h>int main()
{// 计算 10 除以 3 的余数int res1 = 10 % 3;printf("%d\n", res1); // 输出: 1,因为 10 除以 3 商 3 余 1// 计算 -10 除以 3 的余数// 注意,C 语言中的取模运算结果的符号与被除数相同int res2 = -10 % 3;printf("%d\n", res2); // 输出: -1,因为 -10 除以 3 商 -3 余 -1// 计算 10 除以 -3 的余数// 同样,结果的符号与被除数相同int res3 = 10 % -3;printf("%d\n", res3); // 输出: 1,因为 10 除以 -3 商 -3 余 1// 计算 -10 除以 -3 的余数// 结果的符号与被除数相同int res4 = -10 % -3;printf("%d\n", res4); // 输出: -1,因为 -10 除以 -3 商 3 余 -1return 0;
}
2.4 自增和自减
自增、自减运算符可以写在操作数的前面也可以写在操作数后面,不论前面还是后面,对操作数的副作用是一致的。
自增、自减运算符在前在后,对于表达式的值是不同的。
- 如果运算符在前,表达式的值是操作数自增、自减之后的值,前缀运算符(++i 或 --i)会先改变变量的值,然后再返回这个新值。
- 如果运算符在后,表达式的值是操作数自增、自减之前的值,后缀运算符(i++ 或 i--)会先返回变量的当前值,然后再改变这个值。
#include <stdio.h>int main()
{/* 自增、自减运算符不论前面还是后面,对操作数的副作用是一致的 */int a = 8;a++;printf("a = %d\n", a); // a = 9++a;printf("a = %d\n", a); // a = 10/* 自增、自减运算符在前在后,对于表达式的值是不同的。*/int i1 = 10, i2 = 20;// 使用后缀递增运算符(i1++),先赋值后递增int i = i1++; // 将 i1 的当前值(10)赋给 i,然后 i1 自增为11printf("%d\n", i); // 输出 i 的值,为 10printf("%d\n", i1); // 输出 i1 的值,为 11// 使用前缀递增运算符(++i1),先递增后赋值i = ++i1; // i1 自增为 12,然后将新的值(12)赋给 iprintf("%d\n", i); // 输出 i 的值,为 12printf("%d\n", i1); // 输出 i1 的值,也为 12// 使用后缀递减运算符(i2--),先赋值后递减i = i2--; // 将 i2 的当前值(初始值20)赋给 i,然后 i2 自减为 19printf("%d\n", i); // 输出 i 的值,为 20printf("%d\n", i2); // 输出 i2 的值,为 19// 使用前缀递减运算符(--i2),先递减后赋值i = --i2; // i2 自减为18(因为上一步 i2 为 19),然后将新的值(18)赋给 iprintf("%d\n", i); // 输出 i 的值,为 18printf("%d\n", i2); // 输出 i2 的值,也为 18return 0;
}
提示:在实际项目中不建议使用自增自减运算符来完成一个复杂的计算。 面试时可能会出一些复杂的自增自减计算。
3 关系运算符(比较运算符)
运算符 | 描述 | 操作数个数 | 表达式的值 | 副作用 |
---|---|---|---|---|
== | 相等 | 2 | 0 或 1 | 无 |
!= | 不等 | 2 | 0 或 1 | 无 |
< | 小于 | 2 | 0 或 1 | 无 |
> | 大于 | 2 | 0 或 1 | 无 |
<= | 小于等于 | 2 | 0 或 1 | 无 |
>= | 大于等于 | 2 | 0 或 1 | 无 |
在 C 语言中,0 表示假,非 0 表示真。
#include <stdio.h>int main()
{int a = 8;int b = 7;// 打印 a>b 的比较结果,输出为 1(真)printf("a>b的值:%d\n", a > b);// 打印 a>=b 的比较结果,输出为 1(真),因为 a 确实大于等于 bprintf("a>=b的值:%d\n", a >= b);// 打印 a<b 的比较结果,输出为 0(假),因为 a 不小于 bprintf("a<b的值:%d\n", a < b);// 打印 a<=b 的比较结果,输出为0(假)printf("a<=b的值:%d\n", a <= b);// 打印 a==b 的比较结果,输出为0(假),因为 a 不等于 bprintf("a==b的值:%d\n", a == b);// 打印 a!=b 的比较结果,输出为1(真),因为 a 确实不等于 bprintf("a!=b的值:%d\n", a != b);return 0;
}
4 逻辑运算符
运算符 | 描述 | 操作数个数 | 表达式的值 | 副作用 |
---|---|---|---|---|
&& | 逻辑与 | 2 | 0 或 1 | 无 |
|| | 逻辑或 | 2 | 0 或 1 | 无 |
! | 逻辑非 | 1 | 0 或 1 | 无 |
4.1 逻辑与 &&
【一假则假】如果两个操作数都为真(非零),那么表达式的值为真,否则为假。
如果第一个操作数为假,第二个操作数没有计算的必要了,这种现象称为短路现象。即一旦第一个条件确定整个表达式的结果,就不会再评估后续的条件。
#include <stdio.h>int main()
{double score = 70;// 使用逻辑与(&&)运算符检查 score 是否在 60 到 80(包括 60 和 80)之间if (score >= 60 && score <= 80){printf("ok1\n"); // 如果 score 在 60 到 80 之间,打印 "ok1",这个分支会执行}else{printf("ok2\n"); // 如果 score 不在 60 到 80 之间,打印 "ok2",这个分支不会执行}int a = 10, b = 99;// 展示逻辑运算符的短路现象// 在这个例子中,由于 a 小于 2 为假(逻辑上),因此不会检查 ++b > 99,因为逻辑与(&&)运算符的短路性质// 这称为短路现象,即一旦第一个条件确定整个表达式的结果,就不会再评估后续的条件if (a < 2 && ++b > 99){printf("ok100"); // 由于 a 不小于 2,这个分支不会执行}printf("b=%d\n", b); // 打印 b 的值,由于短路现象,b 的值没有变化,为 99return 0;
}
4.2 逻辑或 ||
【一真则真】只要有一个操作数为真,表达式的值就为真;两个操作数都为假,表达式的值为假。
如果第一个操作数为真,第二个操作数没有计算的必要了,这种现象称为短路现象。即一旦第一个条件确定整个表达式的结果,就不会再评估后续的条件。
#include <stdio.h>int main()
{double score = 70;// 使用逻辑或(||)运算符检查 score 是否大于等于 70 或小于等于 80// 注意:这里的条件为真,因为 score 等于 70,既满足大于等于 70 也满足小于等于 80if (score >= 70 || score <= 80){printf("ok1\n"); // 由于条件总是为真,所以这里总是打印 "ok1"}else{printf("ok2\n"); // 这个分支永远不会执行}int a = 10, b = 99;// 展示逻辑运算符的短路现象// 在这个例子中,由于 a 大于 5 为真(逻辑上),因此不会检查 b++ > 100,因为逻辑或(||)运算符只要有一边为真,整个表达式就为真// 这称为短路现象,即一旦第一个条件确定整个表达式的结果,就不会再评估后续的条件if (a > 5 || b++ > 100){printf("ok100\n"); // 由于 a 大于 5 为真,所以这里会打印 "ok100"}printf("b=%d\n", b); // 打印 b 的值,由于短路现象,b 的值没有变化,为 99return 0;
}
4.3 逻辑非 !
操作数状态取反作为表达式的值。
#include <stdio.h>int main()
{int score = 100; int res = score > 99;printf("%d\n", res); // 100 > 99,为真,输出:1printf("%d\n", !res); // 取反,输出:0// 检查 res 的值,如果 res 为真(即 score > 99),则执行以下语句if (res){printf("hello, tom\n"); // 因为 score 确实大于 99,所以这里会打印 "hello, tom"}// 紧接着检查 !res 的值,即 res 的否定。由于 res 为真(1),!res 为假(0)// 由于条件不满足(!res为假),所以不会执行块内的语句if (!res){printf("hello,jack \n"); // 这个语句不会执行,因为 !res为假}return 0;
}
5 赋值运算符
运算符 | 描述 | 操作数个数 | 表达式的值 | 副作用 |
---|---|---|---|---|
= | 赋值 | 2 | 左边操作数的值(赋值后的值) | 有,左边操作数的值被更新 |
+= | 相加赋值 | 2 | 左边操作数的值(相加后的值) | 有,左边操作数的值被更新 |
-= | 相减赋值 | 2 | 左边操作数的值(相减后的值) | 有,左边操作数的值被更新 |
*= | 相乘赋值 | 2 | 左边操作数的值(相乘后的值) | 有,左边操作数的值被更新 |
/= | 相除赋值 | 2 | 左边操作数的值(相除后的值) | 有,左边操作数的值被更新 |
%= | 取余赋值 | 2 | 左边操作数的值(取余后的值) | 有,左边操作数的值被更新 |
<<= | 左移赋值 | 2 | 左边操作数的值(左移后的值) | 有,左边操作数的值被更新 |
>>= | 右移赋值 | 2 | 左边操作数的值(右移后的值) | 有,左边操作数的值被更新 |
&= | 按位与赋值 | 2 | 左边操作数的值(按位与后的值) | 有,左边操作数的值被更新 |
^= | 按位异或赋值 | 2 | 左边操作数的值(按位异或后的值) | 有,左边操作数的值被更新 |
|= | 按位或赋值 | 2 | 左边操作数的值(按位或后的值) | 有,左边操作数的值被更新 |
5.1 左值和右值
左值和右值是 C 语言中更一般、更底层的概念,它们与操作数的内存位置(或存储持续性)有关。
左值(Lvalue):左值是一个具有确定内存位置的表达式,即它可以出现在赋值操作的左侧。左值可以是变量名、数组元素、结构体成员等。左值表达式代表了一个对象的身份(即其内存位置),而不是一个单纯的值。
右值(Rvalue):右值是一个表示值的表达式,但没有明确的内存位置。右值通常是一个常量、算术表达式的结果、函数调用返回的结果等。右值只能出现在赋值操作的右侧,或作为参数传递给函数,而不能被赋值(即不能出现在赋值操作的左侧)。
重要的是要注意,左值和右值的区分并不仅限于它们在表达式中的位置(尽管这通常是一个很好的指标)。真正决定一个表达式是左值还是右值的是其是否能代表一个具体的内存位置。
例如,在 a = 5; 中,a 是一个左值(因为它代表了一个内存位置),而 5 是一个右值(因为它仅仅是一个值,没有内存位置)。但在某些情况下,左值表达式可以通过特定的操作(如取地址运算符 &)转换为右值(虽然这种情况比较特殊且不常见)。
5.2 注意事项
赋值运算符的第一个操作数(左值)必须是变量的形式,第二个操作数可以是任何形式的表达式。
左值必须可修改:赋值操作要求左侧必须是一个左值,即一个可以存储新值的内存位置。如果尝试将一个右值(如表达式的结果)放在赋值语句的左侧,编译器会报错,因为右值没有内存位置来存储新值。
右值提供值:赋值操作的右侧可以是一个右值,它提供了要赋给左值的具体值。右值可以是字面量、表达式的结果、函数调用返回的值等。
示例:
- 正确用法:a = b + 25;(a 是左值,b + 25 是右值)
- 错误用法:b + 25 = a;(尝试将右值 b + 25 用作左值,这是不允许的)
- 编译错误结果,如下图所示:
赋值运算符的副作用针对第一个操作数。
在复合赋值运算符(如 +=、-=、*=、/=、%= 等)中,等号(=)后面的表达式首先被计算为一个整体的值,然后将这个值与等号左边的变量进行相应的运算,并将结果赋值回该变量。
以 a += b + 2; 为例,他表示的是 a = a + (b + 2); 详细的计算过程可以分为以下两步:首先计算等号右边的表达式 b + 2。假设 b 的值是已知的(若 b = 3),则 b + 2 的结果是 5。然后,将这个结果 5 与 a 的当前值进行加法运算,即 a 的当前值(假设为 10)加上 5,得到 15。最后,将计算结果 15 赋值回 a,更新 a 的值为 15。
5.3 综合案例
#include <stdio.h>int main()
{int a = 10, b = 20, c = 30, d = 4;// 简单的赋值a = 5; // a 被重新赋值为 5printf("%d\n", a); // 5// 加法赋值c += 3; // 等价于 c = c + 3; c 的值变为 33printf("%d\n", c); // 33// 减法赋值c -= b; // 等价于 c = c - b; c 的值变为 13printf("%d\n", c); // 13// 乘法赋值a *= 2; // 等价于 a = a * 2; a 的值变为 10(但实际上是 5*2=10,因为上面 a 被重新赋值为 5)printf("%d\n", a); // 10// 除法赋值b /= 2; // 等价于 b = b / 2; b 的值变为 10printf("%d\n", b); // 10// 取模赋值c %= 3; // 等价于 c = c % 3; c 的值变为 1(因为 13 除以 3 余 1)printf("%d\n", c); // 1// 连等写法int e = 12, f;f = e *= a; // 从右往左,e = e*a = 12*10 = 120; 然后 f=e,即把 e 的值赋值给 fprintf("%d\n", e); // 120printf("%d\n", f); // 120/* 可先学习本节内容后续的位运算后再回来观看这里的代码 */// 左移赋值d <<= 2; // 等价于 d = d << 2; d 的值变为 16(4 左移 2 位,4 乘以 2 的 2 次方)printf("%d\n", d); // 16// 右移赋值d >>= 1; // 等价于 d = d >> 1; d 的值变为 8(16 右移 1 位,16 除以 2 的 1 次方)printf("%d\n", d); // 8// 按位与赋值a &= 1; // 等价于 a = a & 1; a 的值变为 0(因为 10(二进制 1010)与 1(二进制 0001)的结果是 0000)printf("%d\n", a); // 0// 按位异或赋值b ^= 3; // 等价于 b = b ^ 3; b 的值变为 9(因为 10(二进制 1010)异或 3(二进制 0011)的结果是 1001)printf("%d\n", b); // 9// 按位或赋值b |= 4; // 等价于 b = b | 4; b 的值变为 13(因为 9(二进制 1001)或 4(二进制 0100)的结果是 1101)printf("%d\n", b); // 13return 0;
}
6 位运算符
运算符 | 描述 | 操作数个数 | 副作用 |
---|---|---|---|
& | 按位与 | 2 | 无 |
| | 按位或 | 2 | 无 |
^ | 按位异或 | 2 | 无 |
~ | 按位取反 | 1 | 无 |
<< | 按位左移 | 2 | 无 |
>> | 按位右移 | 2 | 无 |
按位与、按位或、按位异或、按位取反运算符是对整数的二进制表示进行操作。这些操作是逐位进行的,即它们比较两个数的每一位,并根据比较结果生成一个新的数。
按位左移和按位右移操作直接对整数的二进制表示进行操作,通过移动其位来产生新的值。
操作数进行位运算的时候,是以它的补码形式进行运算。
6.1 按位与 &
计算规则:【有 0 为 0】对于两个数的每一位,如果两个相应的位都为 1,则结果的该位为 1;否则,结果的该位为 0。
示例:5 & 3
- 5 的二进制表示为 0101
- 3 的二进制表示为 0011
- 按位与的结果为 0001,即十进制的 1
6.2 按位或 |
计算规则:【有 1 为 1】对于两个数的每一位,如果两个相应的位中至少有一个为 1,则结果的该位为 1;如果两个相应的位都为 0,则结果的该位为 0。
示例:5 | 3
- 5 的二进制表示为 0101
- 3 的二进制表示为 0011
- 按位或的结果为 0111,即十进制的 7
6.3 按位异或 ^
计算规则:【相同为 0,不同为 1】对于两个数的每一位,如果两个相应的位不同(一个为 1,另一个为 0),则结果的该位为 1;如果两个相应的位相同(都为 0 或都为 1),则结果的该位为 0。
示例:5 ^ 3
- 5 的二进制表示为 0101
- 3 的二进制表示为 0011
- 按位异或的结果为 0110,即十进制的 6
#include <stdio.h>int main()
{int a = 17; // 二进制表示为 0000 0000 0000 0000 0000 0000 0001 0001int b = -12; // 在大多数计算机中,这将是补码形式,即 1111 1111 1111 1111 1111 1111 1111 0100/* 可以通过下面的方式求得 -12 的补码 */// 12 的二进制原码表示为: 0000 0000 0000 0000 0000 0000 0000 1100// -12 的二进制原码表示为:1000 0000 0000 0000 0000 0000 0000 1100// -12 的二进制反码表示为:1111 1111 1111 1111 1111 1111 1111 0011// -12 的二进制补码表示为:1111 1111 1111 1111 1111 1111 1111 0100/* 也可以通过十六进制形式打印,然后再转换成二进制 */printf("a: %#x;b: %#x\n", a, b); // a: 0x11;b: 0xfffffff4// 按位与操作 &// 计算规则:【有 0 为 0】// 0000 0000 0000 0000 0000 0000 0001 0001 -> 17// &// 1111 1111 1111 1111 1111 1111 1111 0100 -> -12// 0000 0000 0000 0000 0000 0000 0001 0000 -> 16printf("a&b=%d\n", a & b); // 输出为 16// 按位或操作 |// 计算规则:【有 1 为 1】// 0000 0000 0000 0000 0000 0000 0001 0001 -> 17// |// 1111 1111 1111 1111 1111 1111 1111 0100 -> -12// 1111 1111 1111 1111 1111 1111 1111 0101 -> -11(1111 0101 数值位取反 1000 1010 再加一 1000 1011)printf("a|b=%d\n", a | b); // 输出为 -11// 按位异或操作 ^// 计算规则:【相同为 0,不同为 1】// 0000 0000 0000 0000 0000 0000 0001 0001 -> 17// ^// 1111 1111 1111 1111 1111 1111 1111 0100 -> -12// 1111 1111 1111 1111 1111 1111 1110 0101 -> -27(1110 0101 数值位取反 1001 1010 再加一 1001 1011)printf("a^b=%d\n", a ^ b); // 输出为 -27return 0;
}
计算过程分析:
提示:上图后面两个式子通过补码求原码的方法是:先补码末尾减一(得到反码)再数值位取反。也可以通过:先补码数值位取反(注意这不是反码,只能算草稿内容)再末尾加一 的方法得到原码。
6.4 按位取反 ~
计算规则:是对一个数的二进制表示中的每一位(包括符号位)进行取反操作,即将所有的 0 变为 1,所有的 1 变为 0。
#include <stdio.h>int main()
{int a = 17;int b = -12;// 按位非操作 ~a// 0000 0000 0000 0000 0000 0000 0001 0001 -> 17// ~// 1111 1111 1111 1111 1111 1111 1110 1110 -> -18(1110 1110 数值位取反 1001 0001 再加一 1001 0010)printf("~a=%d\n", ~a); // 输出为 -18// 按位非操作 ~b// 1111 1111 1111 1111 1111 1111 1111 0100 -> -12// ~// 0000 0000 0000 0000 0000 0000 0000 1011 -> 11printf("~b=%d\n", ~b); // 输出为 11return 0;
}
计算过程分析:
6.5 按位左移 <<
按位左移操作将一个数的二进制表示向左移动指定的位数。左移时,左侧边缘超出的位将被丢弃,而在右侧边缘新增的位将用 0 填充。
a << b
这里,a 是要被左移的数,b 是指定左移的位数。
计算规则:
- 将 a 的二进制表示向左移动 b 位。
- 左侧边缘超出的位将被丢弃。
- 在右侧边缘新增的位用 0 填充。
注意事项:
- 如果 b 是负数,则行为是未定义的(Undefined Behavior, UB)。
- 整数溢出是可能的,特别是当左移后的值超出了该整数类型的表示范围时。
- 左移移操作通常用于将数【乘以 2 的幂次方】。
- 对于无符号整数,左移后的结果将保持为无符号数。
- 对于有符号整数,左移可能导致符号位的变化,进而影响整数的正负。然而,C 标准对左移有符号整数的具体行为(特别是当移动导致符号位改变时)没有明确定义,这取决于编译器的实现。
6.6 按位右移 >>
按位右移操作将一个数的二进制表示向右移动指定的位数。右移时,右侧边缘超出的位将被丢弃,而左侧边缘新增的位根据整数的类型(有符号还是无符号)有不同的填充规则。
a >> b
这里,a 是要被右移的数,b 是指定右移的位数。
计算规则:
- 将 a 的二进制表示向右移动 b 位。
- 右侧边缘超出的位将被丢弃。
- 对于无符号整数,在左侧边缘新增的位用 0 填充。
- 对于有符号整数,在左侧边缘新增的位的填充规则依赖于编译器的实现(通常是算术右移,即用符号位填充,但这不是 C 标准要求的,也就是说,如果符号位为 0(表示正数或零),则在最左侧插入 0;如果符号位为 1(表示负数),则在最左侧插入 1)。
注意事项:
- 如果 b 是负数,则行为是未定义的(Undefined Behavior, UB)。
- 右移操作通常用于将数【除以 2 的幂次方】。
- 对于有符号整数,C 标准没有规定必须使用算术右移还是逻辑右移(即是否用符号位填充),这取决于编译器的具体实现。
算术右移是一种针对二进制数(特别是带符号的二进制数)进行的移位操作。在算术右移中,二进制数的所有位向右移动指定的位数,丢弃最右侧的位(即最低位),并在最左侧插入与符号位相同的值。也就是说,如果符号位为 0(表示正数或零),则在最左侧插入 0;如果符号位为 1(表示负数),则在最左侧插入 1。
逻辑右移是一种不考虑符号位的右移操作。在逻辑右移中,二进制数的所有位向右移动指定的位数,丢弃最右侧的位,并在最左侧用 0 填充。这种操作不关注数的符号,只是简单地将每一位向右移动。
#include <stdio.h>int main()
{int a = 17; // 二进制表示为 0000 0000 0000 0000 0000 0000 0001 0001int b = -12; // 在大多数计算机中,这将是补码形式,即 1111 1111 1111 1111 1111 1111 1111 0100// 按位左移// a 左移 2 位,相当于乘以 2 的 2 次方// 0001 0001 -> 0100 0100,即十进制中的 68printf("a<<2=%d\n", a << 2); // 输出 a<<2=68// b 左移 2 位,负数左移时,左侧超出的位被丢弃,右侧新增的位用 0 填充// 但注意,结果是按照补码来解释的// 1111 0100 -> 1101 0000,-48(1101 0000 数值位取反 1010 1111 再加一 1011 0000,即 -48)printf("b<<2=%d\n", b << 2); // 输出 b<<2=-48// 按位右移// a 右移 3 位,相当于除以 2 的 3 次方并向下取整// 0001 0001 -> 0000 0010,即十进制中的 2printf("a>>3=%d\n", a >> 3); // 输出 a>>3=2// b 右移 3 位,对于有符号整数,右移时通常使用算术右移(即左侧新增的位用符号位填充)// 但这取决于编译器的具体实现// 1111 0100 -> 1111 1110,-2(1111 1110 数值位取反 1000 0001 再加一 1000 0010,即-2)printf("b>>3=%d\n", b >> 3); // 输出 b>>3=-2return 0;
}
计算过程分析:
7 三元运算符
7.1 基本语法
条件表达式?表达式1:表达式2;
7.2 表达式最终取值
如果条件表达式为非 0(真),整个表达式的值是表达式 1;
如果条件表达式为 0(假),整个表达式的值是表达式 2;
7.3 案例演示
#include <stdio.h>int main()
{int a = 10;int b = 99;// 使用条件运算符(三元运算符)来决定 res 的值// 因为 a 不大于 b,所以执行 b--,并将结果赋值给 res// 注意:b-- 是后缀自减,意味着先返回 b 的当前值(99),然后再将 b 减 1// 这里 res 被赋值为 99,然后 b 变为 98int res = a > b ? a++ : b--; // 条件表达式为 0(假),整个表达式的值是表达式 2:b--,即等价于 int res = b--; 先赋值再自减float n1 = a > b ? 1.1 : 1.2; // 条件表达式为 0(假),整个表达式的值是表达式 2:1.2// 注意:由于 b 在前面的条件运算符中已经被自减,所以这里 b 的值是 98printf("a=%d\n", a); // 输出 a=10,因为 a 没有被改变printf("b=%d\n", b); // 输出 b=98,因为 b 在前面的条件运算符中自减了printf("res=%d\n", res); // 输出 res=99,因为 res 被赋值为 b 自减之前的值printf("n1=%f\n", n1); // 输出 n1=1.200000return 0;
}
7.4 案例:计算两个数的最大值
#include <stdio.h>int main()
{int a = 10;int b = 100;int max = a > b ? a : b;printf("a和b中最大的数字:%d", max); // a 和 b 中最大的数字:100return 0;
}
7.5 案例:计算三个数的最大值
#include <stdio.h>int main()
{int a = 10;int b = 100;int c = 199;/* 分步判断 */// int max1 = a>b ? a : b;// int max2 = max1>c ? max1:c;// 使用嵌套的三元运算符来找出 a、b、c 中的最大值// 首先比较 a 和 b,然后将较大的值与 c 比较// (a > b ? a : b) 这部分先比较 a 和 b,如果 a 大于 b,则返回 a,否则返回 b// 然后将这个结果与 c 比较:(a > b ? a : b) > c ? (a > b ? a : b) : c// 如果 (a > b ? a : b) 的结果大于 c,则返回 (a > b ? a : b) 的结果,否则返回 cint max = (a > b ? a : b) > c ? (a > b ? a : b) : c;// 打印 a、b、c 中最大的数字printf("a、b、c中最大的数字:%d", max); // 输出将会是 "a、b、c中最大的数字:199"return 0;
}
8 运算符优先级
优先级 | 运算符 | 名称或含义 | 结合方向 |
---|---|---|---|
1 | [] | 数组下标 | 左到右 |
() | 圆括号 | ||
. | 成员选择(对象) | ||
-> | 成员选择(指针) | ||
2 | - | 负号运算符 | 右到左 |
(类型) | 强制类型转换 | ||
++ | 自增运算符 | ||
-- | 自减运算符 | ||
* | 取值运算符 | ||
& | 取地址运算符 | ||
! | 逻辑非运算符 | ||
~ | 按位取反运算符 | ||
sizeof | 长度运算符 | ||
3 | / | 除 | 左到右 |
* | 乘 | ||
% | 余数(取模) | ||
4 | + | 加 | 左到右 |
- | 减 | ||
5 | << | 左移 | 左到右 |
>> | 右移 | ||
6 | > | 大于 | 左到右 |
>= | 大于等于 | ||
< | 小于 | ||
<= | 小于等于 | ||
7 | == | 等于 | 左到右 |
!= | 不等于 | ||
8 | & | 按位与 | 左到右 |
9 | ^ | 按位异或 | 左到右 |
10 | | | 按位或 | 左到右 |
11 | && | 逻辑与 | 左到右 |
12 | || | 逻辑或 | 左到右 |
13 | ?: | 条件运算符 | 右到左 |
14 | = | 赋值运算符 | 右到左 |
/= | 除后赋值 | ||
*= | 乘后赋值 | ||
%= | 取模后赋值 | ||
+= | 加后赋值 | ||
-= | 减后赋值 | ||
<<= | 左移后赋值 | ||
>>= | 右移后赋值 | ||
&= | 按位与后赋值 | ||
^= | 按位异或后赋值 | ||
|= | 按位或后赋值 | ||
15 | , | 逗号运算符 | 左到右 |
总结:
运算符优先级不用刻意地去记忆,总体上:一元运算符 > 算术运算符 > 关系运算符 > 逻辑运算符 > 三元运算符 > 赋值运算符。
常用的优先级关系: ! > 算术运算符 > 关系运算符 > && > || > 三元运算符 > 赋值运算符
提示:如果实在是拿不准优先级,直接无脑加括号()。
9 复杂表达式的计算过程
9.1 练习 1
对于表达式 5>3&&8<4-!0 的最终值是多少?计算过程是怎样的?
正确的计算过程如下图所示:
由于 && 的短路性质 ,所以先计算 && 左边的表达式:5>3 逻辑值为 1,然后计算 && 右边的表达式:8<4-!0 。在表达式 8<4-!0 中,先进行非运算, !0 逻辑值为 1,然后进行算术运算, 4-1 值为 3,然后进行关系运算, 8 < 3 逻辑值为 0,最后进行逻辑运算,1 && 0 逻辑值为 0。
9.2 练习 2
再来看这么一个例子:若 a= 2,b=3,c=4,则表达式 a+b<c&&b==c&&a||b+c&&b+c 的计算过程是怎样的?值为多少?
正确的计算过程如下所示:
由于 && 的短路性质 ,所以先计算 && 左边的表达式 a+b<c,即 2+3<4 逻辑值为 0,所以 && 右边的表达式 b==c&&a 不会执行,即 || 左边的值为 0;然后计算 || 右边的表达式,对于表达式 b+c&&b+c 先进行从左到右的算符运算,然后进行逻辑与运算,即 7&&7,逻辑值为 1;最后进行逻辑或运算,即 0 || 1 ,逻辑值为 1。
9.3 练习 3
最后再来看这样一个例子:设有 int a=1,b=2,c=3,d=4,m=2,n=2; 执行 (m=a>b) && (n=c>d) 后 m 和 n 的值是多少?(m=a>b) && (n=c>d) 的结果是多少?计算过程是怎样的?
正确的计算过程如下所示:
由于 && 的短路性质 ,所以先计算 && 左边的表达式 (m=a>b) ,对于表达式 (m=a>b),先进行关系比较运算,a>b 逻辑值为 0,然后进行赋值运算,即 m=0; 由于 && 左边的表达式为 0,所以右侧表达式不执行。最终,m 的值为 0,n 的值不变还是为 2,整体表达式的结果为0。
提示:通过上面几个例子,我们也可以看出来复杂表达式的计算相当不易,所以我们在编写代码时,尽量不要编写这么复杂的代码,能简化最好。
- 不要过多的依赖运算的优先级来控制表达式的执行顺序,这样可读性太差,尽量使用小括号来控制表达式的执行顺序。
- 不要把一个表达式写得过于复杂,如果一个表达式过于复杂,则把它分成几步来完成。
10 测试题
1. 写出至少 5 个二元运算符。
【答案】如:+、-、*、/、%、<、<=、>、>=、=、!=、&&、||、&、|、<<、>>、=、+=、-= 等。
2. 写出下面程序的执行结果。
int a = 5, b = 10, result;
result = (a > b) ? a : b;
printf("%d", result);
【答案】10
【解析】使用条件运算符 (a > b) ? a : b 来比较 a 和 b 的大小。如果 a 大于 b,则结果为 a,否则结果为 b。在这种情况下,因为 a 的值为 5,而 b 的值为 10,所以条件为假,结果为 b。
3. 写出下面程序的执行结果。
int num = 8; // 0000 1000
num = num << 2; // 0010 0000,也就相当于 8 乘 2 的 2 次方
printf("%d", num);
【答案】32
【解析】8 转换为二进制 0000 1000,然后左移两位变为 0010 0000,对应的十进制值为 32。
4. 请写出下列代码的运算结果。
int num = 10;
if (num++ || num--)
{num++;
}
printf("%d", ++num);
【答案】13
【解析】
- if (num++ || num--) { num++; }: 这是一个 if 语句它使用逻辑或运算符连接两个条件,num++ 表示使用当前值,然后将 num 的值递增,在这里,它增加了 num 到 11。
- 由于逻辑或运算符的短路特性,第一个条件为真(非零),则不会执行第二个条件,num -- 没有执行。
- if 的条件表达式最终是是成立的,代码块中的 num++ 会被执行,将 num 增加到12。
- printf("%d", ++num); ,++num 表示先将 num 的值递增,然后使用递增后的值。所以输出的结果是 13。