1.3 运算符和表达式
和其他程序设计语言一样,C++记述运算的符号称为“运算符”,运算符的运算对象称为“操作数”。对一个操作数运算的运算符称为“单目运算符”或称为“一元运算符”,如-a、-i;对两个操作数运算的运算符称为“双目(二元)运算符”,如3+5;对三个操作数运算的运算符称为“三目(三元)运算符”,如x?a:b。而表达式是由变量、常量(操作数)通过一个或多个运算符组合而成的,一个合法的C++表达式经过计算应有一个确定的值和类型。不同的运算符构成的是不同的表达式,例如,由关系运算符构成的表达式称为关系表达式,而由逻辑运算符构成的表达式称为逻辑表达式,等等。本节重点介绍C++的常用运算符。
1.3.1 算术运算符
数学中,算术运算包括加、减、乘、除、乘方及开方等。在C++中,算术运算符可以实现这些数学运算。但乘方和开方没有专门的运算符,它们一般通过pow(幂)、sqrt(平方根)等库函数来实现,这些库函数是在头文件math.h中定义的(见附录C)。
由操作数和算术运算符构成的算术表达式常用于数值运算,与数学中的代数表达式相对应。C++算术运算符有双目的加减乘除四则运算符、求余运算符及单目的正负运算符,如下所示:
+ 正号运算符,如+4,+1.22等 - 负号运算符,如-4,-1.22等 * 乘法运算符,如6*8,1.4*3.56等 / 除法运算符,如6/8,1.4/3.56等 % 模运算符或求余运算符,如40%11等 + 加法运算符,如6+8,1.4+3.56等 - 减法运算符,如6-8,1.4-3.56等
C++中算术运算符和数学运算的概念及运算方法是一致的,但要注意以下几点。
(1)除法运算。两个整数相除,结果为整数,如7/5的结果为1,它是将小数部分去掉,而不是四舍五入;若除数和被除数中有一个是实数,则进行实数除法,结果是实型。如7/5.0,7.0/5,7.0/5.0的结果都是1.4。
(2)求余运算。求余运算要求参与运算的两个操作数都是整型,其结果是两个数相除的余数。例如40%5的结果是0,40%11的结果是7。要理解负值的求余运算,例如40%-11的结果是7,-40%11的结果是-7,-40%-11的结果也是-7。
(3)优先级和结合性。在一个包含多种算术运算的混合运算中,先乘、除后加、减的运算规则是由运算符的优先级来保证的。在算术运算符中,单目的正负运算符的优先级最高,其次是乘、除和求余,最后是加、减。优先级相同的运算符,则按它们的结合性进行处理。所谓运算符的结合性是指运算符和操作数的结合方式,它有“从左至右”和“从右至左”两种。“从左至右的结合”是指运算符左边的操作数先与运算符相结合,再与运算符右边的操作数进行运算,如3*5/4的次序是先乘后除;而自右至左的结合次序刚好相反,它是将运算符右边的操作数先与运算符相结合。在算术运算符中,除单目运算符外,其余运算符的结合性都是从左至右。
(4)书写格式。在使用运算符进行数值运算时,若书写时没有在双目运算符两边加上空格,则有时编译系统会做出与自己想象中不同的理解。例如:
结果是不一样的,前者发生编译错误,而后者的结果是37。
为了避免上述情况的发生,在书写时,应有意识地加上一些括号。这样不仅可增强程序的可读性,而且,尤其当对优先关系犹豫时,加上括号是保证正确结果的最好方法。
(5)溢出处理。在C++中,当某数除以0或当其他数值溢出时,编译系统将报告错误并终止程序运行。但对整数超过类型范围溢出时,系统却不认为是一个错误,这在编程时需要特别小心。
【例Ex_OverFlow】 一个整数溢出的例子
#include <iostream.h> int main() { short nTotal,nNum1,nNum2; nNum1 = nNum2 = 1000; nTotal = nNum1*nNum2; cout<<nTotal<<"\n"; return 0; }
程序运行后,显示的结果是16960。 这个结果与想象中的1000000相差太远。这是因为,任何变量的值在计算机内部都是以二进制存储的,nNum1*nNum2的1000000结果很显然超过了短整型数的最大值32767,将1000000放入nTotal中,就必然产生高位溢出,也就是说,1000000的二进制数(11110100001001000000)2中只有后面16位的(0100001001000000)2有效,结果是十进制的16960。这个问题可通过改变变量的类型来解决,如将类型定义成整型(int)或长整型(long)。
1.3.2 赋值运算符
前面已经多次遇到过赋值操作的示例。在C++中,赋值运算是使用赋值符“=”来操作的,它是一个使用最多的双目运算符,结合性从右到左,其作用是将赋值符右边操作数的值存储到左边的操作数所在的内存空间中,显然,左边的操作数应是一个具有存储空间的变量。
需要说明的是,每一个合法的表达式在求值后都有一个确定的值和类型。赋值表达式的值和类型就是赋值符左边操作数的值和类型。例如double变量fTemp的赋值表达式“fTemp = 18”赋值完成后,该赋值表达式的类型是double,表达式的值经类型转换后变成18.0,这种转换是类型自动转换(后面还会讨论到)。这里先来讨论复合赋值与多重赋值的问题。
1.复合赋值
在C++中,规定了下列10种复合赋值运算符:
它们都是在赋值符“=”之前加上其他运算符而构成的,其中的复合赋值运算符的含义如表1.3所示,其他复合赋值运算符的含义均与其相似。
表1.3 复合赋值运算符
尽管复合赋值运算符看起来有些古怪,但却简化了代码,使程序精练,更主要的是在编译时能产生高效的执行代码。需要说明的是:
(1)在复合赋值运算符之间不能有空格,例如 += 不能写成 += ,否则编译时将提示出错信息。
(2)复合运算符的优先级和赋值符的优先级一样,在C++的所有运算符中只高于逗号运算符,而且复合赋值运算符的结合性也和赋值符一样,也是从右至左。因此,在组成复杂的表达式时要特别小心。例如:
a *= b - 4/c + d
等效于
a = a * ( b - 4/c + d)
而不等效于
a = a * b - 4/c + d
2.多重赋值
所谓多重赋值是指在一个赋值表达式中出现两个或更多的赋值符“=”。例如:
nNum1=nNum2=nNum3=100 // 若结尾有分号“;”,则表示是一条语句
由于赋值符的结合性是从右至左的,因此上述程序的赋值过程是这样的:首先对赋值表达式nNum3 = 100求值,即将100赋值给nNum3,同时该赋值表达式的结果是nNum3,值为100;然后将nNum3的值赋给nNum2,这是第二个赋值表达式,该赋值表达式的结果是nNum2,值也为100;最后将nNum2的值赋给nNum1,整个表达式的结果是nNum1。
由于赋值是一个表达式,因而几乎可以出现在程序的任何地方,由于赋值运算符的等级比较低,因此这时的赋值表达式两边应加上圆括号。例如:
a=7+(b=8) // 赋值表达式值为15,a值为15,b值为8 a=(c=7)+(b=8) // 赋值表达式值为15,a值为15,c值为7,b值为8 (a=6)=(c=7)+(b=8) // 赋值表达式值为15,a值为15,c值为7,b值为8
要注意上面最后一个表达式的运算次序:由于圆括号运算符的优先级在该表达式中是最高的,因此先运算( a = 6 )、(c = 7)和( b = 8),究竟这三个表达式谁先运算,取决于编译系统。由于这三个表达式都是赋值表达式,其结果分别为a、c和b,因此整个表达式等效于a = c + b,结果为a=15、b=8、c=7,整个表达式的结果是a。
1.3.3 数据类型转换
在进行运算时,往往要遇到混合数据类型的运算问题。例如,一个整数和一个实数相加就是一个混合数据类型的运算。C++采用两种方法对数据类型进行转换,一种是自动转换,另一种是强制转换。
1.自动转换
自动转换是将数据类型按从低到高的顺序自动进行转换,如图1.6所示,箭头的方向表示转换的方向。由于这种转换不会丢失有效的数据位,因而是安全的。
图1.6 数据类型转换的顺序
例如,10 + 'a' + 2*1.25-5.0/4L的运算次序如下:
(1)进行2*1.25的运算,将2和1.25都转换成double型,结果为double型的2.5。
(2)进行5.0/4L的运算,将长整型4L和5.0都转换成double型,结果值为1.25。
(3)进行10 + 'a' 的运算,先将'a'转换成int型整数97,运算结果为107。
(4)整数107和2.5相加,先将整数107转换成double型,结果为double型,值为109.5。
(5)进行109.5-1.25的运算,结果为double型的108.25。
2.强制转换
强制转换是在程序中通过指定数据类型来改变如图1.6所示的类型转换顺序,将一个变量从其定义的类型改变成为另一种不同的类型。由于这种转换可能会丢失有效的数据位,因而是不安全的。在强制转换操作时,C++有下列两种基本格式:
(<类型名>)<表达式> <类型名>(<表达式>)
这里的类型名是任何合法的C++数据类型,如float、int等。通过类型的强制转换可以将表达式(含常量、变量等)转换成类型名指定的类型,例如:
double f=3.56; int nNum; nNum=(int)f; // 强制double转换成int,小数部分被截去
或者
nNum=int(f); // 或者nNum=(int)(f);
都是将nNum的值变为3。需要注意的是,当对一个表达式进行强制转换时,需将表达式用圆括号括起来。例如,(int)(x+y)是将表达式(x+y)转换为int型,若为(int)x+y,则是将x转换为int型后再与y相加,表达式最后的类型是否是int型还取决于y数据类型。
1.3.4 关系运算符
关系运算是逻辑运算中比较简单的一种。所谓关系运算实际上是比较两个操作数是否符合给定的条件。在C++中,若符合条件,则关系表达式的值为bool型的true或非0(“真”),否则为bool型的false或0(“假”)。由于关系运算需要两个操作数,所以关系运算符都是双目运算符,其结合性是从左至右。C++提供了下列6种关系运算符:
< 小于, 若表达式e1<e2成立,则结果为true,否则为false <= 小于等于, 若表达式e1<=e2成立,则结果为true,否则为false > 大于, 若表达式e1>e2成立,则结果为true,否则为false >= 大于等于, 若表达式e1>=e2成立,则结果为true,否则为false == 相等于, 若表达式e1==e2成立,则结果为true,否则为false != 不等于, 若表达式e1!=e2成立,则结果为true,否则为false
其中,前4种的优先级相同且高于后面的两种。例如,若有表达式:
a == b > c
则等效于a == ( b > c )。若设整型变量a=3、b=4、c=5,则表达式中,先运算b>c,结果该条件不满足,值为false(以0表示),然后再运算a==0,显然也为false,故整个表达式的值是false。
要注意混合表达式的运算次序和结果。由于true或false可以看成0或1的整数,因此关系表达式可以参与算术运算,此时要注意关系运算符的优先级低于算术运算符。例如“2+3<4-1”先计算“2+3”和“4-1”,即为“5<3”,结果为false。若为“2+(3<4)-1”则有“2+0-1”,结果为值1。
1.3.5 逻辑运算符
关系运算符所构成的条件一般比较简单,当需要满足多个条件时,则需使用逻辑运算符。例如,对于数学中的“a<c<b”,则相应的C++表达式可写成“(a<c)&&(c<b)”,其中的“&&”就是一个C++逻辑运算符。逻辑运算符用于将多个关系表达式或逻辑量(“真”或“假”)组成一个逻辑表达式。同样,逻辑表达式的结果也是bool型,要么为true,要么为false。
C++提供了下列3种逻辑运算符:
! 逻辑非(单目) && 逻辑与(双目) || 逻辑或(双目)
逻辑非“!”是指操作数为true结果为false,操作数为false结果为true。
逻辑与“&&”是指当两个操作数都是true时,结果才为true,否则为false。
逻辑或“||”是指当两个操作数中有一个是true时,结果就为true,而只有当它们都为false时,结果才为false。
“逻辑非”、“逻辑与”和“逻辑或”的优先级依次从高到低,且“逻辑非”的优先级还比算术运算符和关系运算符高,而“逻辑与”和“逻辑或”的优先级却比关系运算符要低。
需要说明的是,C++对运算次序进行了许多优化。对于逻辑表达式来说,当有e1&&e2时,若表达式e1为0,则表达式e2不会计算,因为无论e2是何值,整个表达式都为false;类似的,当有e1||e2时,若e1为1,则e2也不会计算,因为无论e2是何值,整个表达式都为true。
例如,若int a, b = 3, c = 0; 则在下面的表达式中
(a=0)&&(c=a+b); // 注意这里的a=0是赋值表达式
因(a = 0)的表达式值为0(false),故(c = a + b)不会被执行。这样,a,b和c的值分别为0,3,0。
若有:
(a=2)||(c=a+b); // 注意这里的a=2是赋值表达式
因(a = 2)的表达式值为2(true),故(c = a+b)也不会被执行(注意此时的逻辑符为“或”)。
1.3.6 位运算符
位运算符对操作数按其在计算机内表示的二制数逐位地进行逻辑运算或移位运算,参与运算的操作数只能是整型常量或整型变量。C++语言提供了6种位运算符:
~ 按位求反(单目) & 按位与(双目) ^ 按位异或(双目) | 按位或(双目) << 左移(双目) >> 右移(双目)符
按位求反“~”是将一个二进制数的每一位求反,即0变成1,1变成0。
按位与“&”是将两个操作数对应的每个二进制位分别进行逻辑与操作。
按位或“|”是将两个操作数对应的每个二进制位分别进行逻辑或操作。
按位异或“^”是将两个操作数对应的每个二进制位分别进行异或操作,相同为0,不同为1。
左移“<<”(两个<符号连写)是将左操作数的二进制值向左移动指定的位数,它具有下列格式:
操作数<<移位的位数
左移后,低位补0,移出的高位舍弃。例如,表达式4<<2的结果是16(二进制为00010000),其中4是操作数,二进制为00000100,2是左移的位数。
右移“>>”(两个>符号连写)是将左操作数的二进制值向右移动指定的位数,它的操作格式与“左移”相似,即具有下列格式:
操作数>>移位的位数
右移后,移出的低位舍弃。如果是无符号数则高位补0;如果是有符号数,则高位补符号位(补1)或补0,不同的编译系统对此有不同的处理方法,Visual C++ 6.0采用的是补符号位(补1)的方法。
需要说明的是,由于左移和右移运算速度比较快,因此在许多场合下用来替代乘和除以2的n次方运算,n为移位的位数。
1.3.7 条件运算符
条件运算符“?:”是C++中唯一的一个三目运算符,它具有下列格式:
<e1>?<e2> :<e3>
表达式e1、表达式e2和表达式e3是条件运算符“?:”的三个操作数。其中,表达式e1是C++中可以产生true和false结果的任何表达式。其功能是:如果表达式e1的结果为true,则执行表达式e2,否则执行表达式e3。例如:
nNum = (a > b) ? 10 : 8;
当(a > b)为true时,则表达式(a > b) ? 10 : 8的结果为10,从而nNum = 10;否则(a > b) ? 10 :8的结果为8,nNum = 8。
需要说明的是,由于条件运算符“?:”的优先级比较低,仅高于赋值运算符,因此“nNum =(a > b) ? 10 : 8”中的条件表达式“(a > b)”两边可以不加圆括号,即可写成:
nNum = a > b ? 10 : 8;
1.3.8 sizeof运算符
sizeof的目的是返回操作数所占的内存空间大小(字节数),它具有下列两种格式:
sizeof(<表达式>) sizeof(<数据类型>)
例如:
sizeof("Hello") // 计算"Hello"所占内存的字节大小,结果为6 sizeof(int) // 计算整型int所占内存的字节数
需要说明的是,由于同一类型的操作数在不同的计算机中占用的存储字节数可能不同,因此sizeof的结果有可能不一样。例如sizeof(int)的值可能是4,也可能是2。
1.3.9 逗号运算符
逗号运算符“,”是优先级最低的运算符,它用于把多个表达式连接起来,构成一个逗号表达式。逗号表达式的一般形式为:
表达式1,表达式2,表达式3,…,表达式n
在计算时,C++将从左至右逐个计算每个表达式,最终整个表达式的结果是最后计算的那个表达式的类型和值,即表达式n的类型和值。例如:
a=1, b=a+2, c=b+3
该表达式依次从左至右计算,最终的类型和值为最后一个表达式“c = b + 3”的类型和值,结果为左值c(c值为6)。
要注意逗号运算符“,”的优先级是最低的,必要时要注意加上圆括号,以使逗号表达式的运算次序先于其他表达式。例如:
j = ( i = 12 , i + 8)
则整个表达式可解释为一个赋值表达式。圆括号中,i = 12 , i + 8是逗号表达式,计算次序是先计算表达式i = 12,然后再计算i + 8。整个表达式的类型和值是j的类型和值(为20)。若不加上圆括号,则含义完全不一样。试比较:
j = i = 12 , i + 8
显然,此时整个表达式可解释为是一个逗号表达式,最终的类型和值取决于i+8的类型和值。
1.3.10 自增和自减
单目运算符自增(++)和自减(--)为变量加1或减1提供了一种非常有效的方法。++和--既可放在变量的左边也可以出现在变量的右边,分别称为前缀运算符和后缀运算符。例如:
int i=5; i++; // 合法:后缀自增,等效于i=i+1; 或i+=1; ++i; // 合法:前缀自增,等效于i=i+1; 或i+=1; i--; // 合法:后缀自减,等效于i=i-1; 或i-=1; --i; // 合法:前缀自减,等效于i=i-1; 或i-=1; (i+1)++; 或++(i+1); // 错误:表达式i+1结果不是一个变量 float f1,f2=3.0f; f1=f2++; // 合法:f1的值为3.0f,f2的值为4.0f (f1=5.0f)++; // 合法:f1=5.0f 表达式的结果仍是f1,是一个变量
若前缀运算符和后缀运算符仅用于某个变量的增1和减1,则这两者是等价的。例如,若a的初值为5,a++和++a都是使a变成6。但如果将这两个运算符和其他运算符组合在一起,在求值次序上就会产生根本的不同。
● 如果用前缀运算符对一个变量增1(减1),则在将该变量增1(减1)后,用新的值在表达式中进行其他的运算。
● 如果用后缀运算符对一个变量增1(减1),则用该变量的原值在表达式中进行其他的运算后,再将该变量增1(减1)。例如:
a=5; b=++a; //A:相当于 a=a+1; b=a;
和
a=5; b=a++; //B:相当于 b=a; a=a+1;
运行后,a值的结果都是6,但b的结果却不一样,前者(A)为6,后者(B)为5。