目录
1. 操作符分类
算术操作符
移位操作符
位操作符
赋值操作符
单目操作符
关系操作符
逻辑操作符
条件操作符
逗号表达式
下标引用、函数调用和结构成员
2. 算术操作符
+ - * / %
加 减 乘 除 取模
我们详细来看一下除和取模。
注意:
1.除了% 操作符之外,其他的几个操作符可以作用于整数和浮点数。
2. 对于/ 操作符如果两个操作数都为整数,执行整数除法。而只要除号两端的操作数里面至少有一个是浮点数,执行的就是浮点数除法。
3. % 操作符的两个操作数必须为整数。返回的是整除之后的余数。
3. 移位操作符
移位操作符移动的是二进制位。
<< 左移操作符
>> 右移操作符
注意:移位操作符(左移、右移)的操作数都只能是整数。
移位操作符移动的是二进制补码。
所以在了解移位操作符之前,我们需先了解整数的二进制表达形式。
整数的二进制表示其实有3种: 原码、反码、补码。
按照一个数的正负,直接写出它的二进制表示形式得到的就是原码,整型占4个字节(32bit)。
1、对于正整数:原码、反码、补码相同。
2、对于负整数: 原码:直接按照数字的正负写出的二进制序列。
反码:原码的符号位不变,其他位按位取反(1 --> 0 , 0 -->1)。
补码:反码+1。
第一位是符号位:为0,则为正数;
为1,则为负数。
整数在内存中存储的是二进制的补码,所以在参与移位的时候,移动的都是补码。
3.1 左移操作符
移位规则:
左边丢弃、右边补0
例如(正整数左移):
例如(负整数左移):
注:我们打印的时候是打印的原码,我们内存存储的补码,所以我们求出来补码后,先去看是正数还是负数,正数补码原码相同直接打印,负数的补码化为原码再打印。
由上两个演示结果来看,<< 操作符还有乘2的效果。
3.2 右移操作符
移位规则:
首先右移运算分两种:
- 算术移位
右边丢弃,左边补该值的原符号位(原来是负数则补1,是正数则补0)- 逻辑移位
右边丢弃,左边补0
例如 正整数右移(算数右移和逻辑右移结果相同):
例如(负整数算术右移):
例如(负整数逻辑右移):
大多数编译器采用的是算术移位。
警告⚠ :
对于移位运算符,不要移动负数位,这个是标准未定义的。
例如:
这种写法标准未定义。
4. 位操作符
& 按位与
| 按位或
^ 按位异或
注意:他们的操作数必须是整数。
& - 按位与
对应的两个二进制位只要有0,按位与的结果就为0,
对应的两个二进制位同时为1,按位与的结果才为1。
| - 按位或
对应的两个二进制位只要有1,按位或的结果就为1,
对应的两个二进制位同时为0,按位或的结果才为0。
^ - 按位异或
对应的两个二进制位相同为0,
对应的两个二进制位相异为1。
练习1:
不能创建临时变量(第三个变量),实现两个整数的交换。
(1)正常创建变量的交换(不符合题意)
(2)算数逻辑交换(符合题意,但存在溢出的问题)
这种方法的缺陷在于:假设我们的a和b的值都接近int类型范围的最大值,这两者相加,就会超出int类型的范围最大值(溢出),这样可能会使得我们想得到的结果发生改变。
(3)位操作符交换
练习2:
编写代码实现:求一个整数存储在内存中的二进制中1的个数。
思路:将该整数 &(按位与) 1,判断二进制最低位是否为1,然后再将该整数二进制位向右移动一位,再次 &(按位与) 1,这样循环32次(整数是4个字节,32个比特位)就可以判断该整数的二进制位的每一位是否为1。
5. 赋值操作符
1、简单赋值操作符:=
注:一个等号 “ = ” 表示赋值,两个等号“ == ” 表示等于。
通过赋值符我们可以给初始变量进行初始化赋值,也可以将先前赋的值进行调整重新赋值。
例如:
赋值操作符可以连续使用,比如:
虽然这种方法在语法上支持,一般我们不建议使用连续赋值的方法,它不易调试,可读性不好,容易让人误解。
2、符合赋值操作符:
+= 加等
-= 减等
*= 乘等
/= 除等
%= 取模等
>>= 左移等
<<= 右移等
&= 按位与等
|= 按位或等
^= 按位异或等
这些运算符都可以写成复合的效果。
例如:
其他运算符一样的道理。其实用法都是一样的,就是简化我们的代码,这样写更加简洁。
6. 单目操作符
举例:加号操作符就是双目操作符,a+b,因为加号操作符有两个操作数。
但是
单目操作符:只有一个操作数的操作符。
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
-- 前置-- 、后置--
++ 前置++ 、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
(1)逻辑反操作符:!
C语言中,0表示假,非0表示真。
逻辑反操作符把真变为假,假变为真。
比如:
(2)正值操作符:+ 负值操作符: -
正值
负值
(3)取地址操作符:&
取出一个变量在内存中的起始地址。
(4)sizeof操作符
计算变量所占内存空间的大小,单位是字节。
计算类型所创建的变量占据空间的大小,单位是字节。
计算整个数组的大小,单位是字节。
注:sizeof是操作符,不是函数。
(5)~ 操作符
对一个数的二进制按位取反
练习:
把a的二进制中的第五位改为1
把之前改好的第五位再改回0
注: ~ 操作符对二进制位的正负数都可以进行按位取反!
(6) ++ 操作符:前置++ 、后置++
- - 操作符:前置 - - 、后置 - -
前置++
后置++
前置 - -
后置 - -
还有别的用法,用后置 - - 举例:
(7) 间接访问操作符(解引用操作符):*
(8) 强制类型转换符:(类型)
如图编译器会发出警告,因为a是int类型,3.14是double类型。
7. 关系操作符
> 大于
>= 大于等于
< 小于
<= 小于等于
!= 不等于
== 等于
这些关系运算符比较简单,但是我们要注意一些运算符使用时候的陷阱。
警告:
在编程的过程中注意区分 == 和 = ,不要写错。
8. 逻辑操作符
&& 逻辑与
|| 逻辑或
注意区分逻辑与和按位与
注意区分逻辑或和按位或
1&2----->0
1&&2---->1
1|2----->3
1||2---->1
逻辑与和逻辑或只关注真假,不关注二进制位。
&&:逻辑与(并且的意思)
左右两边同时为真,整体为真,
左右两边有一边为假,整体为假
|| :逻辑或(或者的意思)
左右两边有一边为真,整体为真,
左右两边同时为假,整体为假
举例:判断闰年时用到的逻辑与和逻辑或
特点:
逻辑与( && ) : 左边有一个判断为假后,右边的表达式不再计算
逻辑或( | | ) : 左边有一个判断为真后,右边的表达式不再计算
练习:
9. 条件操作符
条件操作符(三目操作符)
表达式1 ? 表达式2 : 表达式3
计算方法:
表达式1 ? 表达式2 : 表达式3
真 √ X
假 X √
1为真,2计算,3不计算,2是整个表达式的结果
1为真,2不计算,3计算,3是整个表达式的结果
练习:
10. 逗号表达式
表达式1, 表达式2, 表达式3, ......表达式N
逗号表达式,就是用逗号隔开的多个表达式。
逗号表达式,从左向右依次执行,整个表达式的结果是最后一个表达式的结果。
注:必须从左向右依次计算,前面的值是不可忽略的,因为最后一个表达式的值,可能会受到前面表达式值的影响而发生改变。
11. 下标引用、函数调用和结构成员
1、 [ ] 下标引用操作符
操作数:一个数组名 + 一个索引值
2、 ( ) 函数调用操作符
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
3、结构成员
. 结构体对象 . 成员名
-> 结构体指针->成员名
(*ps) . age
等价于
ps->age
12. 表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
12.1 隐式类型转换
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
例:
b和c的值被提升为普通整型,然后再执行加法运算。
加法运算完成之后,结果将被截断,然后再存储于a中。
如何进行整体提升呢?
整形提升是按照变量的数据类型的符号位来提升的
正数和负数整型提升时,高位补充符号位,无符号整型提升时,最高位补0。
整形提升的例子:
例1:
例2:
例2中的a,b要进行整形提升,但是c不需要整形提升
a,b整形提升之后,变成了负数,所以表达式a == 0xb6,b == 0xb600 的结果为假,但是c不发生整形提升,则表达式c==0xb6000000 的结果为真。所以程序输出的结果是c。
例3:
例3中的,c只要参与表达式运算,就会发生整形提升,表达式+c ,就会发生提升,所以sizeof(+c) 是4个字节。
表达式-c 也会发生整形提升,所以sizeof(-c) 是4个字节,但是sizeof© ,就是1个字节。
12.2 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
>= int 的时候,算术转换
<= int 的时候,整型转换
警告:
但是算术转换要合理,要不然会有一些潜在的问题。
12.3 操作符的属性
复杂表达式的求值有三个影响的因素。
- 操作符的优先
- 操作符的结合性
- 是否控制求值顺序。
两个相邻的操作符先执行哪个?
取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
C语言操作符优先级、结合性参考表:
优先级从上往下依次递减
首先确定优先级,相邻操作符按照优先级高低计算。优先级相同的情况下,结合性才起作用。拿不准的就加上括号,或者拆开去写。
我们掌握了每个操作符的优先级与结合性,但不代表所有表达式都可以确定唯一计算路径,也有很多问题表达式,存在潜在问题。
例1:
注释:代码1在计算的时候,由于 * 比 + 的优先级高,只能保证,* 的计算是比 + 早,但是优先级并不能决定第三个 * 比第一个 + 早执行。
所以表达式的计算机顺序就可能是:
或者:
例2:
注释:同上,操作符的优先级只能决定自减–的运算在+的运算前面,但是我们并没有办法得
知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义
的。
例3:
表达式3在不同编译器中测试结果:非法表达式程序的结果
例4:
这个代码有没有实际的问题?
有问题!
虽然在大多数的编译器上求得结果都是相同的。
但是上述代码answer = fun() - fun() * fun(); 中我们只能通过操作符的优先级得知:先算乘法,
再算减法。
函数的调用先后顺序无法通过操作符的优先级确定。
例5:
Linux环境的结果:
VS2013环境的结果:
看看同样的代码产生了不同的结果,这是为什么?
简单看一下汇编代码,就可以分析清楚。
这段代码中的第一个 + 在执行的时候,第三个 ++ 是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个 + 和第三个前置++ 的先后顺序。
结论:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
总结
感谢观看,欢迎三连,如有错误,欢迎指正。