
2.10 运算符与表达式
2.10.1 运算符分类
C语言提供了13种类型的运算符,如下所示。
(1)算术运算符(+- * / %)。
(2)关系运算符(> <==>=<=!=)。
(3)逻辑运算符(! && ||)。
(4)位运算符(<< >> ~ | ^ &)。
(5)赋值运算符(=及其扩展赋值运算符)。
(6)条件运算符(?:)。
(7)逗号运算符(,)。
(8)指针运算符(*和&)。
(9)求字节数运算符(sizeof)。
(10)强制类型转换运算符((类型))。
(11)分量运算符(.->)。
(12)下标运算符([])。
(13)其他(如函数调用运算符())。
2.10.2 算术运算符及算术表达式
算术运算符包含+、-、*、/和%,当一个表达式中同时出现这5种运算符时,先进行乘(*)、除(/)、取余(%),取余也称取模,后进行加(+)、减(-),也就是乘、除、取余运算符的优先级高于加、减运算符。除%运算符外,其余几种运算符既适用于浮点型数又适用于整型数。当操作符的两个操作数都是整型数时,它执行整除运算,在其他情况下执行浮点型数除法。%为取模运算符,它接收两个整型操作数,将左操作数除以右操作数,但它的返回值是余数而不是商。如图2.10.1所示,除变量s用于帮助读者理解算术运算符的优先级外,其他代码是华为公司的一道面试题,即输入一个整数并将其逆序输出。由算术运算符组成的式子称为算术表达式,表达式一定有一个值。

图2.10.1 算术运算符应用实例
图2.10.1中程序的执行结果如图2.10.2所示。

图2.10.2 图2.10.1中程序的执行结果
思考题:
1.有两个整型变量a与b,假如在不使用第三个变量的情况下,交换变量a和b的值,应如何做?
2.有些CPU的浮点运算能力不强,因此在一些游戏(如王者荣耀)中采用分数来代替小数进行运算。如何比较两个分数的大小(不能用除法转成小数进行比较)?
3.输入一个整数,判断该整数是不是对称数,应如何做?例如12321是对称数,123321也是对称数,但456不是对称数。
2.10.3 关系运算符与关系表达式
关系运算符>、<、==、>=、<=、!=依次为大于、小于、是否等于、大于等于、小于等于和不等于。由关系运算符组成的表达式称为关系表达式。关系表达式的值只有真和假,对应的值为1和0。由于C语言中没有布尔类型,所以在C语言中0值代表假,非0值即为真。例如,关系表达式3>4为假,因此整体值为0,而关系表达式5>2为真,因此整体值为1。关系运算符的优先级低于算术运算符,运算符的优先级的详细情况见附录B。图2.10.3中给出了比较一个浮点数是否等于某个值的方法,因为关系运算符的优先级低,所以f-234.56不用加括号。浮点数中存储的是对应数的近似值,只能保证精度为7位,有兴趣的读者可以注释掉下一行代码而打开上一行代码,看看结果是否相同。

图2.10.3 比较一个浮点数是否等于某个值
图2.10.3中程序的执行结果如图2.10.4所示。

图2.10.4 图2.10.3中程序的执行结果
在工作中,很多程序员容易不小心将两个等号写成一个等号,因此当判断整型变量i是否等于3时,我们可以写为3==i,即把常量写在前面而把变量写在后面。这是因为当不小心将两个等号写为一个等号时,变量在前面就会导致编译不通,从而快速发现错误(这种写法属于华为公司内部的一条编程规范)。
同时,在编写程序时,如果我们需要判断三个数是否相等,那么绝对不可以写为if(5==5==5),这种写法的值无论何时都为假,为什么?因为首先5==5得到的结果为1,然后1==5得到的结果为0。如果要判断三个变量a、b、c是否相等,那么不能写为a==b==c,而应写为a==b && b==c。下面来看一个例子。
【例2.10.1】关系运算符的使用。

例2.10.1中程序的执行结果如图2.10.5所示。

图2.10.5 例2.10.1中程序的执行结果
如例2.10.1所示,如果要判断变量a是否大于3且同时小于10,那么不能写为 3<a<10,这种写法在数学上的确是正确的,但是在程序中是错误的。首先,无论a是大于3还是小于3,对于3<a这个表达式只有1或0两种结果。由于1和0都是小于10的,所以无论a的值为多少,这个表达式的值始终为真,因此在判断变量a是否大于3且同时小于10时,要写成a>3 && a<10,这才是正确的写法。
思考题:有兴趣的读者可以试着修改一下本例的代码,让输入的a为60时打印出“a不在3和10之间”。
2.10.4 逻辑运算符与逻辑表达式
逻辑运算符!、&&、||依次为逻辑非、逻辑与、逻辑或,这和数学上的与、或、非是一致的。逻辑非的优先级高于算术运算符,逻辑与和逻辑或的优先级低于关系运算符。逻辑表达式的值只有真和假,对应的值为1和0。例2.10.2中的代码是计算一年是否为闰年的例子,因为需要重复测试,所以我们用了一个while循环。
在while循环后,我们演示了什么是短路运算。j=1时,判断j==0表达式为假,因为中间为逻辑与,无论后面是真还是假,整体值都为假,所以其后面的printf函数表达式不会再执行,因此看不到打印输出;j=0时,j==0表达式为真,所以其后面的printf函数得不到打印。工作中我们经常用短路运算来避免使用if判断,以降低代码量。执行结果如图2.10.6所示。
针对代码中的逻辑非,首先给变量j赋值10,因为j的值非0,所以!j的值为0;然后,由于逻辑非是单目运算符,结合顺序为从右至左,得到!!j的值为1。也就是说,对0取非,得到的值为1;对非0值取非,得到的值为0。
【例2.10.2】逻辑运算符的使用。


图2.10.6 例2.10.2中程序的执行结果
思考题:如果希望打印逻辑与和逻辑或后面的"system is error",那么应如何修改代码?
2.10.5 位运算符
位运算符<<、>>、~、|、^、&依次是左移、右移、按位取反、按位或、按位异或、按位与。
左移:高位丢弃,低位补0,相当于乘以2。工作中很多时候申请内存时会用左移,例如要申请1GB大小的空间,可以使用malloc(1<<30)。malloc函数的使用将在后面的章节中介绍。
右移:低位丢弃,正数的高位补0,负数的高位补1,相当于除以2。移位比乘法和除法的效率要高,负数右移,对偶数来说是除以2,但对奇数来说是先减1后除以2。例如,-8>>1,得到的是-4,但-7>>1得到的并不是-3而是-4。另外,对于-1来说,无论右移多少位,值永远为-1。
异或:相同的数进行异或时,结果为0,任何数和0异或的结果是其本身。
按位取反:数位上的数是1变为0,0变为1。
按位与和按位或:用两个数的每一位进行与和或。
图2.10.7所给出的例子的执行结果如图2.10.8所示,有兴趣的读者可以自己改动一下。

图2.10.7 位运算符应用实例

图2.10.8 图2.10.7中程序的执行结果
思考题:
1.有两个变量a与b,在不使用第三个变量的情况下,通过异或操作来交换这两个变量的值,这种交换相对于之前的加法交换有何优势?
2.如何用位运算找到一个数的最低位为1的那一位(不可使用循环)?要求复杂度为O(1)。
3.C语言中未提供循环移位的运算符,有兴趣的读者可以自行尝试实现一个能够循环移位的函数。
2.10.6 赋值运算符
为了理解有些操作符存在的限制,必须理解左值(L-value)和右值(R-value)之间的区别。这两个术语多年前由编译器设计者创造并沿用至今,尽管它们的定义与C语言并不严格吻合。
左值是那些能够出现在赋值符号左边的东西,右值是那些可以出现在赋值符号右边的东西。例如,

其中,a是一个左值,因为它标识了一个可以存储结果值的地点;b+25是一个右值,因为它指定了一个值。
它们可以互换吗?比如下面这种写法:

因为每个位置都包含一个值,所以原先用作左值的a此时也可以作为右值;然而,b+25不能作为左值,因为它并未标识一个特定的位置(并不对应特定的内存空间)。因此,上面这条赋值语句是非法的。
查看附录B,可以看到赋值运算符的优先级是非常低的,仅高于逗号运算符。如果我们要用getchar函数循环读取并打印每个字符,那么应如何做呢?我们来看例2.10.3。
【例2.10.3】赋值运算符的使用。

执行结果如图2.10.9所示,输入"hello"时,得到的输出结果为"hello",然后按组合键Ctrl+Z结束输入。

图2.10.9 例2.10.3中程序的执行结果
那么为什么需要将c=getchar()整体括起来再判断是否与EOF相等呢?因为赋值运算符的优先级小于关系运算符,如果不加括号,那么c的值只有0和1两种情况。
思考题:有兴趣的读者可以把括号去掉,单步观察上面程序的打印输出。
接下来介绍复合赋值运算符。
复合赋值运算符操作是一种缩写形式,使用复合赋值运算符能使对变量的赋值操作变得更加简洁。例如,

对变量iNum的赋值进行操作,值为这个变量本身与一个整型常量5相加的结果。使用复合语句可以实现同样的操作。例如,上面的语句可以修改为

赋值运算符与复合赋值运算符的区别如下:
(1)复合赋值运算符简化了程序,可使程序精炼,提升阅读速度。
(2)复合赋值运算符提高了编译效率。
例2.10.4说明了加后赋值与乘后赋值的用法。
【例2.10.4】加后赋值与乘后赋值的用法。

从上面的程序代码可以看到,iNum+=5代表iNum加5后再赋值给iNum,因此iNum的最终值为15,而iResult的值等于其自身乘以iNum的值,所以最终结果为45。程序的执行结果如图2.10.10所示。

图2.10.10 例2.10.4中程序的执行结果
2.10.7 条件运算符与逗号运算符
条件运算符是C语言中唯一的一种三目运算符。三目运算符代表有三个操作数;双目运算符代表有两个操作数,如逻辑与运算符就是双目运算符;单目运算符代表有一个操作数,如逻辑非运算符就是单目运算符。运算符也称操作符。逗号运算符的优先级最低,我们需要掌握的是,逗号表达式的整体值是最后一个表达式的值。下面我们来看一个实例,如图2.10.11所示,通过条件运算符我们可以快速得到3个数中的最大值,避免了很多if判断。通过逗号运算符,我们可以先做一些准备操作,而最终while循环是否结束取决于scanf("%d%d%d",&a,&b,&c)!=EOF这个关系表达式的真假。

图2.10.11 条件运算符应用实例
2.10.8 自增、自减运算符及求字节运算符
自增、自减运算符和其他运算符有很大的区别,因为其他运算符除赋值运算符可以改变变量本身的值外,不会有这种效果。自增、自减就是对变量进行加1、减1操作,那么有了加法和减法运算符为什么还要发明这种运算符呢?原因是自增和自减来源于B语言,当时Ken Thompson 和Dennis M.Ritchie(C语言的发明者)为了不改变程序员的编写习惯,在C语言中保留了B语言中的自增和自减。因为自增、自减会改变变量的值,所以自增和自减不能用于常量!
图2.10.12是自增、自减运算符的应用实例。
图2.10.12中程序的执行结果如图2.10.13所示。
如何才能掌握自增、自减运算符?只要做到了分开两条语句来计算,就肯定不会出错。例如图2.10.12中的j=i++>-1,对于后++或者后--,首先我们需要去掉++或--运算符,也就是首先计算j=i>-1,因为i本身等于-1,所以得到j的值为0,接着单独计算i++,也就是对i加1,所以i从-1加1得到0,因此printf("i=%d,j=%d\n",i,j);语句的执行结果是0和0。

图2.10.12 自增、自减运算符应用实例

图2.10.13 图2.10.12中程序的执行结果