2.3 C语言运算符
什么是运算符呢?当然是能进行相关运算的一些符号啦!就像小学数学里所学到的“+、-、×、÷”四则运算符。刚才已经学到赋值运算符了,当然C语言中还有大量的运算符,这些运算符若从所需要的操作数个数上看,可分为一目、二目和三目运算符。例如赋值运算符,它需要左右两个操作数,所以它就是二目运算符;对于用作说明一个数是正数还是负数的正号运算符“+”和负号运算符“–”,由于它只需要一个操作数,所以它就是一目运算符!至于三目运算符,就是同时需要三个操作数了。其实C语言中只有一个三目运算符,物以稀为贵,好东西我们放到后面再讲,不过先提醒一句,C语言中的所有运算符都需要使用英文字符,千万不要使用中文的标点符号了(初学者常犯的错误)。
2.3.1 算术运算符
算术运算符应该是我们最为熟悉的运算符,加、减、乘、除,从小学就认识它,不过C语言里还多了个取模运算符,用于求两个整数相除后所得的余数,因此也被叫作“求余运算符”。这5个算术运算符都是二目运算符,使用案例见表2.9。
表2.9 算术运算符
从表里可以看出,与我们从小所熟知的运算相比,有两点不同之处:①乘法运算符是一个星号“*”,而不是传统的“×”;除法运算符是一个斜杠“/”,而不是传统的“÷”。②“7 / 4”的结果是整数值“1”,而不是小数值“1.75”。这是因为C语言规定,进行算术运算时,在左右两个操作数中,将以较大的那个数据类型为标准进行运算,也就是会先把较小的那个数据类型转换成较大的数据类型,然后再进行运算。这种把小类型自动转为大类型的过程,我们把它称为“隐式类型转换”。C语言的基本数据类型从小到大排列如下(在本书中long与int具有同等大小,所以没有列出long):
char < unsigned char < short < unsigned short < int < unsigned < float < long long < double < long double
在本例中,由于两个操作数都是整型的,不需要进行隐式类型转换,所以结果仍为整型。若是把其中的一个操作数变成double类型的,那么就会发生隐式类型转换,先把int类型转换为double类型,然后再进行运算。假若现在的式子为“7.0 / 4”,就需要先把整型的“4”转换成double类型的“4.0”,然后再运算,得出一个double类型的结果“1.75”;同理,若式子为“7 / 4.0”,就会把整型“7”转换为double类型的“7.0”,再运算并得出结果“1.75”。
下面再讲一下在使用算术运算符时的一些注意点。
小学数学老师告诉过我们:“先乘除、后加减”,这在C语言中仍然有效,也就是算术运算符中的乘法、除法以及取模运算符的优先级要比加法、减法的优先级高。
在除法运算中,除数不能为0,否则就会得到一个错误的结果(例如得到一个表示无穷大或无穷小的值),并且容易使程序出现异常。
取模运算时两边的操作数都应是整型,并且只有左边操作数才会影响到结果的正负关系,即左操作数若为正值,则取模结果也为正值。反之,若左操作数为负值,取模结果也为负值。例如:“7 % –4”的结果仍为3,但“–7 % 4”的结果为–3。
2.3.2 关系运算符
关系运算符和算术运算符一样,也都是二目运算符,算术运算符的作用是为了求值,而关系运算符的作用是用于比较左右两个操作数的大小关系。因此,笔者更喜欢把“关系运算符”称为“比较运算符”。当然比较的结果无非就是“是”与“否”两个,但C语言把“是”用“真(1)”来表示,把“否”用“假(0)”来表示。也就是通过关系运算符运算的结果非“1”即“0”。C语言中关系运算符见表2.10。
表2.10 关系运算符
对于每一种关系运算,我们可以把它想象成是老师在向我们提问。例如老师问:“5和8是否相等?”同学们回答:“否”,否的话就表示假,那么结果就是0。反之,如果老师问:“5和8是否不相等?”同学们回答“是”,是的话就表示真,结果就是1。
不过不得不提的是,能够表示真的值不仅仅只是1,而是任何的非零值。也就是说在C语言中,只有值为0表示假,其他的都表示真,只不过通常都用1来表示真罢了。
最后还有一点,对于由两个字符组成的运算符,书写的时候,字符的顺序不可颠倒,除非构成运算符的两个字符是相同的。例如“!=”不可写成“=!”,“>=”不可写成“=>”。
2.3.3 逻辑运算符
C语言中有3个逻辑运算符,分别是逻辑非(!)、逻辑与(&&)、逻辑或(||),通过逻辑运算的结果和关系运算符一样,都是真(1)或假(0),所以也常把这种值称为逻辑值。逻辑运算符是把操作数当成逻辑值来看待,并进行相关运算。
具体的逻辑运算符使用方式如表2.11所示。
表2.11 逻辑运算符
逻辑非运算符的作用是得到一个反转操作数的逻辑值,即操作数若为真(非零值),则得到的结果为假,反之,若操作数为假(零值),得到的结果就为真(值为1)。
使用逻辑与运算符和逻辑或运算符的时候也要注意,这两个运算符有“短路”效果,不过不要害怕机器会爆炸,它不是电路的短路,而是运算的短路。当使用逻辑与运算符时,若左操作数的结果为假,则直接返回结果为假,而不会去检查右操作数;同样地,使用逻辑或运算符时,若左操作数的结果为真,则直接返回结果为真,也不会再去检查右操作数了。这种只要通过左操作数就能得知结果,而不用去检查右操作数的行为就称为逻辑运算的“短路”现象。
2.3.4 位运算符
前面讲过,二进制码中最小的单位是位(bit),8位构成1字节。但我们前面所讲的算术运算符、关系运算符和逻辑运算符都不能对位直接进行操作,如果想要对位进行相应的操作就需要用到位运算符,如表2.12所示。
表2.12 位运算符
为了更好地理解位运算符,下面举例说明整数23,其8位的二进制码为“0001 0111”。
23 << 1:进行按位左移1位的操作,则会把这8位都向左移动1位,原来的最高位0被移出抛弃,最低位补0,最终得到“0010 1110”这样一个8位的二进制码,对应的整数值为46,正好是23的2倍。
23>>1:进行按位右移1位的操作,则会把这8位都向右移动1位,原来的最低位1被移出抛弃,最高位补符号位0,最终得到“0000 1011”这样一个8位的二进制码,对应的整数值为11,正好是23被2整除的结果。
~23:对8位二进制码进行按位取反,得到结果“1110 1000”,最高位由0变成了1,所以成了一个负数,对应的整数值为–24。
现在再来一个整数50,其8位二进制码为“00110010”。
23 & 50:将两个整数的二进制码的每一位进行按位与的操作,即对应的两位都为1时,结果为1,否则为0,得到结果码为“0001 0010”,对应的整数值为18。
23 | 50:将两个整数的二进制码的每一位进行按位或的操作,即对应的两位都为0时,结果为0,否则为1,得到结果码为“0011 0111”,对应的整数值为55。
23 ^ 50:将两个整数的二进制码的每一位进行按位异或的操作,即对应的两位不同(一个为1,一个为0)时,结果为1,两位相同(同为1或同为0)时为0,得到结果码为“0010 0101”,对应的整数值为37。
这里的按位与运算符与逻辑与运算符、按位或运算符与逻辑或运算符、按位取反运算符与逻辑非运算符看起来是有些类似,但有着本质的不同:逻辑运算符都是对操作数进行运算的,而位运算符是对操作数的二进制位进行运算的,这一点要谨记。
2.3.5 复合赋值运算符
把前面所学的赋值运算符与算术运算符或部分位运算符结合就会构成复合赋值运算符,使用复合赋值运算符可以起到简化代码、提高编译效果的作用。不过这些运算符只能对可修改的变量使用,不可用于常量。假设有整型变量a,可通过复合赋值运算符对它进行操作,具体如表2.13所示。
表2.13 复合赋值运算符
2.3.6 带副作用的运算符
算术运算符、关系运算符、逻辑运算符和位运算符,不管是单目还是双目,都有一个共同之处:这些运算符不会修改操作数,只会通过运算产生一个新值作为结果返回。例如“!0”,表示对操作数0(假)进行逻辑非运算,这会产生一个新的值1(真)作为结果返回,而不是把0(假)修改成1(真);再例如“23 << 1”,表示将左操作数23按位左移1(右操作数)位后,产生一个新值46作为结果返回,它并不会修改左操作数23和右操作数1的值。
那么有没有可以修改操作数的运算符呢?答案是肯定的,例如之前学过的赋值运算符和复合赋值运算符,这些运算符都会把产生的结果赋值给左操作数,也就是它修改了左操作数的值。我们通常把这些能够改变操作数的行为称为“副作用”,把拥有这类行为的运算符称为“带副作用的运算符”。赋值运算符和复合赋值运算符就是属于这种带副作用的运算符。这时,肯定有读者会好奇,想刨根问底,C语言中还有其他带副作用的运算符吗?哈哈,你猜!
2.3.7 自增、自减运算符
这两个运算符的名字挺有趣,不过先在这儿告诉大家,这两个运算符是最简单的,同时也是最让人头疼的两个运算符,很容易让人疑惑。
先说它简单的原因吧,它们都是一目运算符,作用就是对操作数进行加1或减1的操作,自增运算符就是让操作数加1,自减运算符就是让操作数减1,是不是很简单?当然看到这也该知道它们都是带副作用的运算符了吧。
那又为什么会说它们是容易让人疑惑的运算符呢?因为它们会“变身”。不可思议吧,它们“摇身一变”就会各自多出个孪生的兄弟出来,让人不小心就被迷惑,分不清谁是谁了。也就是说自增、自减运算符不是两个,而是四个运算符。为了分清它们,把它们中的一个称为“前缀的”,另一个称为“后缀的”,所以就有了两个前缀的自增、自减运算符和两个后缀的自增、自减运算符。
还是通过一个例子让它们露出“庐山真面目”吧,例如现在有一个整型变量a,它的初始值为1,现通过自增、自减运算符来对它进行操作,看看是何结果,具体见表2.14。
表2.14 自增、自减运算符
先来看自增运算符,所谓前缀就是运算符在操作数的前面,后缀就是运算符在操作数的后面,不管是使用前缀或是后缀,通过运算都会让操作数加1,也就是变量a的值都会被修改(运算符的副作用产生的效果)为2。也许有读者注意到表里的后缀自增所对应的“结果”栏里明显是1!注意!“结果”栏里显示的不是变量a的最终值,而是通过这个自增运算符运算后产生的新值。如果使用前缀自增运算符,新值就是操作数加1之后的值,如果使用的是后缀自增运算符,则新值就是操作数加1之前的值。
重点就在这里:如果我们只是单纯地希望操作数加1,而不会去使用这个新值,则不管使用前缀的或后缀的自增运算符都可以;反之,如果需要使用这个新值,则前缀的与后缀的就有区别了,下面再用代码片段来说明一下:
最终4个变量中,由于经过自增运算,变量a和b的值都被修改为11,变量m得到的新值为变量a修改(加1)之后的值,所以也是11,而变量n得到的新值为变量b修改之前的值,所以是10。
自增运算符如果搞懂了,那么自减运算符也就自然懂了,此处不再赘述。
2.3.8 其他运算符
一下学了这么多的运算符,是不是C语言的运算符都学完了呢?没有!不过剩下的也不太多了,而且部分运算符会留到后面的章节中再讲。下面再来讲几个比较常用的运算符。
1.类型转换运算符“( )”
在讲算术运算符的时候说过,如果左右两个操作数类型不同,那么相对较小的数据类型会自动地转换成较大的数据类型,然后再进行运算,这种自动将小类型转换为大类型的行为就属于隐式类型转换。那么现在要讲的这个类型转换运算符就属于显式的类型转换,它不但可以像隐式类型转换一样将一个小类型转换为大类型,而且也可以将一个大类型转换为一个小类型,这是隐式类型转换做不了的,所以它的功能更强大。类型转换运算符的使用方式如下:
( 数据类型 ) 操作数
其中“( )”为类型转换运算符,小括号内的数据类型表示要转换的目的数据类型,即在操作数的基础上,得到一个目的数据类型的值作为结果返回。例如:
double d = 3.14; //定义一个双精度浮点数类型变量d,其初始值为3.14 int a = (int)d; //对变量d进行类型转换,得到整型值3作为整型变量a的初始值
这个例子中,变量d是double类型的,其值为3.14,通过类型转换运算符将其进行转换,得到一个整型新值3(抛弃了小数部分),并把它赋给整型变量a。需要注意的是,类型转换运算符不是带副作用的运算符,所以它的操作数(变量d)并不会被修改,它仍然是double类型的,值也依然是3.14。
此外,在C语言中小括号“( )”并非都是作为类型转换运算符来使用的,例如下面的逗号运算符例子中,会将小括号用于一个赋值表达式中,从而起到提升优先级的作用。
2.逗号运算符
逗号也是个运算符?没错!但不是C语言中所有的逗号都是运算符,例如前面在定义变量的时候可以使用逗号:
int a, b, c;
这里定义了三个整型变量,每个变量名之间用逗号隔开。这儿的逗号就不是运算符,它只是个分隔符。其实不只是这里,在很多时候逗号也不算为运算符。例如在函数的参数列表中也会用逗号来分隔各个参数;在为数组初始化的时候,也会在初始值列表里用逗号来分隔各个值。关于函数与数组我们会在后面讲到。
那什么情况下逗号才是运算符呢?我们先来看看逗号运算符的使用方式:
操作数1, 操作数2, 操作数3, …
像这样位于多个操作数间的逗号,就是逗号运算符,是不是很简单?那逗号运算符有什么作用呢?其实也很简单,既然是运算符,就会有运算的结果,逗号运算符的运算结果就是最后一个操作数的值。例如:
int a; a = (3, 4, 5);
上面的小括号内共有3个操作数,操作数之间的逗号就是逗号运算符。由于最后一个操作数的值是5,所以逗号运算符的运算结果就是5,也就是最终会把5赋值给整型变量a。需要注意的是,这儿的小括号不是类型转换的意思,而是为了提升小括号内逗号表达式的优先级,因为逗号运算符的优先级比赋值运算符的优先级低(逗号运算符是C语言所有运算符中优先级最低的),所以若没有小括号,就会把“a = 3”作为第一个操作数来使用了,而逗号运算符的运算结果5被抛弃,最终变量a的值为3。
3.条件运算符
C语言中唯一的三目运算符出场啦!它就是条件运算符,符号为“?:”,一个英文的问号和一个英文的冒号。它的使用方式如下:
操作数1 ? 操作数2 : 操作数3
三个操作数被问号和冒号所分隔,那这个条件运算符的运算结果是什么呢?有两种情况:①若操作数1为真(非零值),则将操作数2的值作为运算结果;②若操作数1为假(零值),则将操作数3的值作为运算结果。也就是根据操作数1是真是假这个条件,来决定结果是操作数2还是操作数3,二者中必选其一。例如:
int a, b; a = 1 ? 10 : 100; //条件运算符的结果为操作数2的值 b = 0 ? 10 : 100; //条件运算符的结果为操作数3的值
由于1为真,所以变量a的值被赋为操作数2的值10;而0表示假,所以变量b的值为操作数3的值100。
4.sizeof运算符
前面所学的运算符都是由符号构成的,而sizeof运算符是C语言中唯一一个由字母构成的运算符。它的作用是获取操作数的大小。这个操作数可以是一种数据类型,也可以是某种数据类型的常量或变量。它的使用方式是:
sizeof (操作数);
在小括号中放入一个操作数,sizeof运算符就可以返回它的大小,以字节为单位。通过sizeof运算符就可以很方便地获知某种数据类型在内存中所占用的空间大小。例如:
sizeof运算符是不是很厉害啊?另外,还有一个小窍门:若操作数是一种数据类型,那么必须使用小括号,如果操作数并非是数据类型的话,就可以省略小括号,像下面这样来使用: