你真的会使用 C 语言中的 “ 操作符 ” 吗?
🌟 前言
本期的主要内容是 C 语言中的操作符
重点讲解 各种操作符的介绍 和 表达式求值
操作符分类
C 语言中操作符总共有 10 种,分别是:
算术操作符 移位操作符 位操作符 赋值操作符 单目操作符 关系操作符 逻辑操作符 条件操作符 逗号表达式 下标引用、函数调用和结构成员
1. 算术操作符
分为:
加:
+
减:-
乘:*
除:/
取模(余):%
这里的话,重点讲一下:/
和 %
📄代码示例一
取模:%
运行结果:
%
:取模,得到的是相除之后的余数;
其实还有可能碰到这种情况:n % 3
,那么余数是多少呢?
注意: n % 3
的余数一定为:0
或者1
或者2
,永远不可能大于等于 3;
那如果这样呢?
%
操作符的两个操作数必须为整数,返回的是整除之后的余数。
📄代码示例二
除:/
运行结果:
那如果这样呢?
对于除号/
,想打印浮点数,分子分母至少一个是浮点数!
🍅 总结
除了
%
操作符之外,其他的几个操作符可以用于整数和浮点数。对于
/
操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。%
操作符的两个操作数必须为整数。返回的是整除之后的余数。
2. 移位操作符
分为:
左移操作符::
<<
右移操作符:>>
其实讲移位操作符之前,先来了解一下计算机中的原码、反码和补码
🌳 原码 反码 补码
一个数在计算机内部如果是有符号数,则其最高位作为符号位; 如果符号位为 0,表示该数为正数;如果符号位为 1,表示该数为负数。(0 正 1 负) 如何求原码、反码和补码呢? 原码:最高位作为符号位,其余各位为数值为(0 正 1 负) 反码:正数的反码和原码相同,负数的反码是在原码的基础上:符号位不变,其余各位按位取反 补码:正数的补码与原码相同,负数的补码是在反码的基础上加
1
以下求原反补的过程:
例:求+25
和-25
的原码、反码和补码
①不考虑正负,将 25 转换成二进制 25D=11001B ② +25 -25 原: 00011001 10011001
反: 00011001 11100110
补: 00011001 11100111
再来看一个
例:求+30
和-30
的原码、反码和补码
①不考虑正负,将 30 转换成二进制 30D=11110B ② +30 -30
原: 00011110 10011110
反: 00011110 11100001
补: 00011110 11100010
计算机中使用的是补码,什么是补码,怎么去理解补码?
补码可以理解成一个循环;
这里不过多阐述了,如果还有不懂的可以去百度一下!
🌳 左移操作符
移位规则:左边抛弃、右边补 0
🔵 正数左移
代码示例:
运行结果:
那么这个结果是怎么来的呢?
1、首先把十进制的 5 转换成二进制
十进制:5 二进制:00000101 写出原码反码补码: 原码:00000101 反码:00000101 补码:00000101
所以5
的补码为:00000101
2、再把补码向左移动 2 位
为什么向左移动 2 位? 因为代码是
a<<2
!然后:
于是我们就得到了一个新的补码:
00010100
3、转换
再把新的补码转换为十进制的数 也就是把
00010100
转换成十进制,得到了20
明白了吗?
🔵 负数左移
代码示例:
运行结果:
那么这个-20
是怎么得来的呢?
1、首先把十进制的-5
转换成二进制
但是我们得先求出5
的原码
十进制:5 二进制:0000101 所以:-5 的原码、反码、补码为: 原码:10000101 反码:11111010 补码:11111011
所以-5
的补码为:11111011
2、再把补码向左移动 2 位
然后:
最后:
于是我们就得到了一个新的补码:11101100
3、回推
这里就不能直接把
11101100
转换成二进制了,因为这是-5
所以我们得由:补码 ---> 反码 ---> 原码
,这样逆序的过程,推算出原码
所以我们得到了新的原码:10010111
4、转换
10010100
换算成十进制就是:20
但是因为符号位为:1
,0 正 1 负
所以结果为:-20
这就是左移操作符,懂了吗?
🌳 右移操作符
首先右移操作符分为两种:
算术右移
逻辑右移
移位规则:
算术右移:左边用原该值的符号位填充,右边丢弃
逻辑右移:左边用 0 填充,右边丢弃
那么到底是用算术右移还是逻辑右移呢?
主要是取决于编译器的!
我们常见的编译器都是算术右移
🔵算术右移
这里还是拿数字 5 来举例
🟣 正数算术右移
代码示例:
运行结果:
1、移动
上面我们已经求出了5
的补码:00000101
看代码给的是向右移动一位
然后:
所以得到新的补码:00000010
2、转换
因为是正数,所以我们直接把00000010
转换成十进制:2
🟣 负数算术右移
代码示例:
运行结果:
1、移动
上面我们已经求出了-5
的补码:11111011
看代码给的是向右移动一位
然后:
所以得到新的补码:11111101
2、回推
我们得以:
补码 ---> 反码 ---> 原码
,这样逆序的过程,推算出原码
所以我们得到了新的原码:10000011
3、转换
10000011
换算成十进制就是:3
但是因为符号位为:1
,0 正 1 负
所以结果为:-3
这就是算术右移的方法,学废了吗?
🔵逻辑右移
逻辑右移的方法和左移操作符有点类似 就是:右边丢弃,左边空的补
0
这里就不演示啦!
3. 位操作符
分为:
按位与:
&
,按二进制位与 按位或:|
,按二进制位或 按位异或:^
,按二进制位异或
注:他们的操作数必须是整数
🌳 按位与
代码示例:
运行结果:
为什么会得到这个结果呢?
按位与的规则: 两个都是 1 才是 1,否则 0
1、首先求出 3 和-5 的补码 3 的补码:0000 0011 -5 的补码:1111 1011 a & b 的计算方式是:a 和 b 存在内存中的二进制的补码进行计算的
所以相与的结果为:
3 的补码:00000011 -5 的补码:11111011 相与结果:00000011
但是记住:计算中存储的是补码
所以我们得到的是相与过后的补码:00000011
再转换成原码:
补码:00000011 反码:00000011 原码:00000011
再把原码换算成十进制:00000011
=3
这就是按位与
的规则
🌳 按位或
代码示例:
运行结果:
为什么会得到这个结果呢?
按位与的规则: 只要有 1 就是 1,两个同时为 0 才为 0
同样还是先拿出3
和-5
的补码
3 的补码:00000011 -5 的补码:11111011 相或结果:11111011
所以我们得到的是相或过后的补码:11111011
再转换成原码:
补码:11111011 反码:11111010 原码:10000101
再把原码换算成十进制:10000101
=-5
(符号位=1,所以要加负号)
🌳 按位异或
代码示例:
运行结果:
为什么会得到这个结果呢?
按位异或的规则: 相同为 0,相异为 1
同样还是先拿出3
和-5
的补码
3 的补码:00000011 -5 的补码:11111011 相或结果:11111000
所以我们得到的是异或过后的补码:11111000
再转换成原码:
补码:11111000 反码:11110111 原码:10001000
再把原码换算成十进制:10001000
=-8
(符号位=1,所以要加负号)
4. 赋值操作符
赋值操作符是一个很棒的操作符,它可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。
赋值操作符可以连续使用,比如:
🌳 复合赋值符
加等:
+=
减等:-=
乘等:*=
除等:/=
模等:%=
右移等:>>=
左移等:<<=
按位与等:&=
按位或等:|=
按位异或等:^=
这些运算符都可以写成复合的效果。
代码示例:
是不是很简单?
5. 单目操作符
逻辑反操作:
!
负值:-
正值:+
取地址:&
操作符的类型长度:sizeof
对一个数的二进制按位取反:~
前置、后置--
:--
前置后置++
:++
间接访问操作符(解引用操作符):*
强制类型转换:(类型)
以上这些操作符只需要一个操作数
讲几个重点
🌳 sizeof
代码示例
运行结果:
sizeof(sh = i + 5)
以sh
的类型为准
sh 为 short 型,所以结果为 2;
sizeof 中的表达式sh = i + 5
并没有真实运算,因此 sh 的值仍然为 0
所以得出结论:sizeof
内部的表达式不去真实计算
🌳 ~
按位取反:~
问题:
假设我想把 00001011 的倒数第三个 0 改为 1 怎么用代码弄?
很简单,我们只有把它和 00000100 相 |
一下就行;
(相或规则:只要有 1 就是 1,两个同时为 0 才为 0)
那么 00000100 怎么来的?? ?数字 1 向左移两位 1<<2
11:00001011 1:00000001 1<<2:00000100 11:00001011
代码示例:
运行结果:
那如果我想把 1111 改回去呢???
15:00000000 00000000 0000000 00001111 和这个数相 &:11111111 11111111 11111111 111110111 那 11111111 11111111 11111111 111110111 怎么得来的呢??? 首先就是:00000000 00000000 00000000 00000100 取反得来 00000000 00000000 00000000 00000100 而这个又是 1 向左移两位的结果 00000000 00000000 00000000 00000001(数字 1)
所以逻辑是 首先 1<<2
然后取反
最后相与
(相与规则:两个都是 1 才是 1,否则 0)
代码示例:
运行结果:
🌳 ++和--
++
分为:
1、前置++:先使用,再++;
2、后置++:先++,再使用;
先看前置++
:
运行结果:
先让a
自己+1
,再把a+1
的结果赋值给b
;
所以:a=11, b=11
;
再看后置++
:
运行结果:
先把a
的值赋给b
,再让a
自己+1
;
所以:a=11, b=10
;
--
的使用和++
一样,这里就补演示了
6. 关系操作符
分为:
大于:
>
大于等于:>=
小于:<
小于等于:<=
不相等:!=
相等:==
这些关系运算符比较简单,没什么可讲的,但是我们要注意一些运算符使用时候的陷阱
在编程的过程中要注意:==
和=
,如果不小心写错,会导致错误。
7. 逻辑操作符
分为:
逻辑与:
&&
逻辑或:||
但是这里我们要区分上面的位操作符:
按位与:
&
按位或:|
位操作符是计算数字的二进制位,而逻辑操作符是计算的整个表达式的真假;
🌳 &&
逻辑与&&
:从左向右所有表达式都为真(非 0),那整体就为真(1),否则为假(0)
代码示例:
运行结果:
那么这段代码怎么计算的呢?
1、首先在
i = a++ && ++b && d++;
这段表达式中;从=
的右边开始,从左向右依次执行 2、a++
先使用a
的值,然后再进行+1
,所以此时a
的值就为 0; 3、a=0
表示为假(非 0 为真),所以这个逻辑表达式就为假,后面的++b && d++
不再执行; 4、所以打印结果:a=1, b=2, c=3, d=4
;
🌳 ||
逻辑或||
:从左向右所有表达式有一个为真(非 0),那么整体就为真(1),只有所有表达式都为假时整体才为假(0);
代码示例:
运行结果:
那么这段代码怎么计算的呢?
1、首先还是从左往右依次执行:
a++ || ++b || d++
2、a++
先使用a
的值,然后再进行+1
,所以此时a
的值就为 0; 3、a=0
表示为假(非 0 为真),因此继续执行; 4、++b
先让b
自己+1
,再使用b
;此时b=3
表示为真; 5、因此整个表达式的结果就为真,后面的d++
的操作将不再执行;
🍅 总结
逻辑与 &&
:左操作数为假,右边不计算;
逻辑或 ||
:左操作数为真,右边不计算;
8. 条件操作符
也叫做 三目操作符
exp1 ? exp2 : exp3
表达式 exp1 如果成立,则返回表达式 2 的值;否则返回表达式 3 的值
三目运算符和if-else
语句十分类似;
代码示例:
运行结果:
用if-else
语句的话,代码就比较啰嗦;
那可以换成条件表达式;
代码示例:
9. 逗号表达式
exp1, exp2, exp3, …expN
逗号表达式,就是用逗号隔开的多个表达式
计算方法:从左向右依次执行,整个表达式的结果是最后一个表达式的结果
代码示例:
运行结果:
那么这段代码怎么计算的呢?
1、
c = (a > b, a = b + 10, a, b = a + 1)
从左向右依次计算 2、a>b
结果为假,这个表达式的结果为 0; 3、a=b+10
的结果为 12,此时 a 的值为 12; 4、这个a
单独放在这里,继续执行后面的表达式; 5、b=a+1
的值为 13, 因此整个表达式的结果就是最后一个表达式b = a + 1
的结果 12
但其实这段代码还有可以改进的地方!
逗号表达式可以和 while 循环结合,使语句更简洁
代码示例:
写成这样的话,是不是就重复了
改写代码:
10. 下标引用、函数调用和结构成员
分为:
下标引用操作符:
[ ]
函数调用操作符:( )
访问结构体成员:.
结构体指针访问:->
🌳 [ ]下标引用操作符
用来访问和使用数组的;
操作数:一个数组名+
一个索引值
🌳 ( )函数调用操作符
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数
运行结果:
🌳 访问一个结构的成员
分为:
结构体.成员名:
.
结构体指针 -> 成员名:->
.
代码示例:
运行结果:
->
代码示例:
运行结果:
两种访问方法都可以!
11. 表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
🌳 隐式类型转换
C 的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在 CPU 的相应运算器件内执行,CPU 内整型运算器(ALU)的操作数的字节长度 一般就是 int 的字节长度,同时也是 CPU 的通用寄存器的长度。 因此,即使两个 char 类型的相加,在 CPU 执行时实际上也要先转换为 CPU 内整型操作数的标准长度。 通用 CPU(general-purpose CPU)是难以直接实现两个 8 比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。 所以,表达式中各种长度可能小于 int 长度的整型值,都必须先转换为 int 或 unsigned int,然后才能送入 CPU 去执行运算。
📄代码示例一:
运行结果:
x
和y
的值被提升为普通整型,然后再执行加法运算。
加法运算完成之后,结果将被截断,然后再存储于 z 中,因此结果并不是 130
如何进行整体提升呢?
整形提升是按照变量的数据类型的符号位来提升的 char x = 3 首先 3 是一个整数,它的大小是 4 个字节也就是 32 位,00000000000000000000000000000011 但是 x 是 char 类型,这个类型的变量 x 只能存放 1 个字节也就是 8 位:00000011
同理:
char y = 127; 127 写成 32 位是 00000000000000000000000001111111 char 类型的 b 只能存放 8 位:01111111
最后
x 和 y 在相加时,由于它们的大小只有 1 字节,并不满足普通整形类型 4 字节的大小; 因此为了提升计算精度要把它们进行整形提升
整形提升是按照变量的数据类型的符号位来提升的
也就是最左边是 1 提升的位就补 1,是 0 就补 0 因此 x 和 y 在相加时,要先整形提升为 32 位,即: 3 的整型提升:00000000000000000000000000000011 127 的整型提升:00000000000000000000000001111111 相加:00000000000000000000000010000010
注意:
但是 z 是 char 类型,只能存放 8 位 因此 z 里面放的是:10000010
在打印 z 的时候,z 也要进行整形提升:
z 最左边的符号位是 1,所以提升的位都补 1,整形提升以后的 32 位结果是: 11111111111111111111111110000010
整形提升以后得到的是补码,我们再把补码转换成原码,就是打印的结果:
补码 :11111111111111111111111110000010
反码 :11111111111111111111111110000001
原码:10000000000000000000000001111110
这个原码就是我们打印的结果-126
📄代码示例二:
运行结果:
分析:
a
,b
要进行整形提升,但是 c 不需要整形提升;
a
,b
整形提升之后,变成了负数,所以表达式 a==0xb6
, b==0xb600
的结果是假;
但是 c 不发生整形提升,则表 达式 c==0xb6000000
的结果是真;
所以结果为:c
我们来看一下它们各自的值:
可以看到,只有 c 的值是相等的,c 本身就是个int
型,它并不用整形提升;
a 和 b 整形提升以后,它们的值发生了改变。
至于如何把 16 进制转换成 2 进制,然后整形提升,这些以及在前面说过很多遍了,在这里就不具体说明了;
强调一点: 只要参与到表达式运算,就会发生整形提升
📄代码示例三:
运行结果:
分析:
c
只要参与表达式运算,就会发生整形提升,表达式+c
,就会发生提升; 所以sizeof(+c)
和sizeof(-c)
是 4 个字节; 而sizeof( c )
并不会发生整形提升,所以是一个字节 ;
🌳 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。
下面的层次体系称为寻常算术转换。
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
警告: 但是算术转换要合理,要不然会有一些潜在的问题。 比如一个
int
类型和float
类型相加,那int
类型首先就要转化成float
类型,然后再相加; 这不难理解,在此不作过多说明; 但是算术转换要合理,要不然会丢失精度。
比如:
🌳 操作符属性
复杂表达式的求值有三个影响的因素:
操作符的优先级
操作符的结合性
是否控制求值顺序
两个相邻的操作符先执行哪个?
取决于他们的优先级;如果两者的优先级相同,取决于他们的结合性。
操作符优先级:
再来一张比较易懂的图:
由于操作符具有优先级和结合性,因此非常容易写出很多有问题的代码,比如:
操作符的优先级只能决定自减–
的运算在+
的运算的前面,但是我们无法知道最左边的 a 是已经自减以后的 a 还是没自减之前的 a;
再来看一个:
上面代码在计算的时候,由于*
比+
的优先级高,只能保证,*
的计算是比+
早;
但是优先级并不能决定第三个*
比第一个+
早执行。
代码示例:
在算answer = fun() - fun() * fun();
的时候,虽然根据优先级知道先算乘再算减,
但是哪个 fun()先调用呢?这个问题其实是未知的,函数的调用顺序不一样,其运算的结果也是不一样的。
还有下面这种:
在不同编译器中测试结果:非法表达式程序的结果
以下是在不同编译器当中测得的结果:
最后一个代码:
这段代码中的第一个 ++ i
在执行的时候,第三个++
i 是否执行,这个是不确定的;
因为依靠操作符的优先级和结合性是无法决定第一个++
和第三个前置++
的先后顺序。
以上这些代码在不同的编译器下运行的结果都是不同的,因为不同的编译器其运算和函数调用的顺序都是不同的。
🍅 总结
我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
结语
以上就是 C 语言中所有的操作符详解和使用方法,如有错误欢迎指正!
学会了吗?
那么来做一些练习题吧!
链接:操作符重点难题
🌟你知道的越多,你不知道越多,我们下期见!
版权声明: 本文为 InfoQ 作者【Albert Edison】的原创文章。
原文链接:【http://xie.infoq.cn/article/c8fa13f1a1fc01c1ed144aabb】。文章转载请联系作者。
评论