2.5 算术运算
数值的算术运算是程序中最常见的,也是C语言最主要的应用之一。数学中,算术运算包括加、减、乘、除、乘方及开方等。但在C语言中,算术运算符只能实现四则运算的最基本功能,对于乘方和开方却没有专门的运算符,它们一般是通过头文件math.h中定义的pow(求幂)、sqrt(求平方根)等库函数来实现(见附录C)。
2.5.1 算术运算符
由操作数和算术运算符构成的算术表达式常用于数值运算,与数学中的代数表达式相对应。在C语言中,最常用的算术运算符是加、减、乘、除和求余运算符,它们都是双目运算符,所谓双目运算符,或称二元运算符,是指这样的运算符需要两个操作数。例如,6-8,6和8都是“-”运算符的操作数,由于是两个操作数,因而“-”运算符是双目运算符。
除此之外,算术运算符还有单目的正、负运算符,所谓单目运算符,或称一元运算符,是指这样的运算符仅需要1个操作数。例如,-4中的负运算符“-”就是一个单目运算符, 4是它的操作数。类似地,在C语言中,算术运算符有:
+ 正号运算符,如+4,+1.23等 – 负号运算符,如–4,–1.23等 ∗ 乘法运算符,如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.正号运算符(+)
由于正号运算符并不改变操作数的符号,因而正号运算符是没有什么实际意义的,仅为语法而设定。例如,+ -8就是-8;+a就是a,但+a是一个表达式,而a却是一个变量。
2.除法运算符(/)
两个整数相除,结果为整数,如7/5的结果为1,它会将小数部分去掉,而不是四舍五入;若除数和被除数中有一个是实数,则进行实数除法,结果是实型。如7/5.0,7.0/5, 7.0/5.0的结果都是1.4。之所以是这样的结果是因为C语言有类型自动转换机制(后面会讨论)。
3.求余运算符(%)
求余运算(%)是指求左操作数(运算符左边的操作数)被右操作数(运算符右边的操作数)整除后的余数,或指求被除数整除后的余数。例如,7%4是求7被4整除后的余数,结果是3;40%5是求40被5整除后的余数,结果是0。
需要说明:
(1)求余结果的符号与被除数(左操作数)的符号相同,而不论除数(右操作数)是正还是负。例如,-7%4、-7%-4的结果都是-3;7%-4的结果是3;特殊地,-7%7或-7%-7或7/-7的结果都是0(因为0没有正、负之分,因此-0和+0的补码都是一样的)。
(2)在算术运算符中,只有求余运算的两个操作数要求都是整型值,含有实数的求余运算在C语言中是无效的。
(3)合理利用“/”和“%”运算符可以得到一个数的位数值。例如,下面的程序是将一个正的十进制的3位数变成逆序的3位数,例如将123变成321。
【例Ex_Rev.c】 正3位数的逆序转换
#include <stdio.h> #include <conio.h> int main() { unsigned short a,b1,b2,b3,b; scanf( "%d", &a ); b1=a/100; /* 求百位上的数值 */ b2=(a % 100)/10; /* 求十位上的数值 */ b3=a % 10; /* 求个位上的数值 */ b =100*b3+10*b2+b1; printf( "the reverse of %d is %d\n", a, b); return 0; }
代码中,b1是取百位上的数值,例如,若a = 123,则123/100 = 1;b2是取十位上的数值,若a = 123,则a%100的结果便是23,23/10=2,即得到十位上的数为2;同样,通过a%10可以得到个位上的数。程序的运行结果如下:
123↵ the reverse of 123 is 321
事实上,除了上述运算外,C语言的其他算术运算符和数学运算的概念及运算方法也都是一致的,但需要注意算术运算中的数值类型、运算次序和表达式的值和类型等一些问题。
2.5.2 数值类型转换
在由双目运算符构成或由多个运算符构造的混合表达式中,由于存在两个或两个以上的操作数,当这些操作数的数据类型不一致时,就会出现这样的问题:表达式最终是什么样的结果?其结果值究竟是什么类型?例如,对于除法运算符,若有7/5,因7和5都是整数(整型),因而最后运算的结果应为整数1;类似地,若有5/7,则结果应为整数0。若有7.0/5.0,因7.0和5.0都是double型实数,因而最后运算的结果也应为double型实数1.4;但若有7.0/5或7/5.0,即其中一个操作数是实数,另一个是整数,则表达式结果的类型是什么呢?
为了保证数值运算结果的准确性,当运算符的多个操作数的数据类型不一致时,C语言编译器会自动将低类型的操作数向高类型进行转换,称为类型的自动转换。这里,类型的高与低,是指类型所能表示的最大数值的大小程度。例如,char型允许数据的最大值为127,而short型允许数据的最大值为32767。这样,对于char和short来说,short就是高类型,而char就是低类型。
一般来说,由低类型向高类型转换是不会丢失有效的数据位的,可见这种类型转换是安全的转换。如图2.8所示箭头方向表示转换方向。
图2.8 类型转换的顺序
显然,按图2.8中的自动转换顺序,则7.0/5或7/5.0相当于7.0/5.0,其结果是double实数1.4。需要强调:
(1)同类型的有符号自动转换为无符号。由于这种转换的结果与实际运算结果不一致,所以有的编译器(如Visual C++ 6.0)就会出现相应的警告错误。警告错误虽不影响编译的顺利通过,但对于编程者来说,仍应引起足够重视。特别要注意,当负数自动转换为无符号时,由于负数补码的最高位(符号位)是1,符号位被视为数据位,从而使负数变成一个很大的正数。例如,-6+1u的结果却是65531(ANSI C结果),这是因为-6的补码(16位)是(1111 1111 1111 1010)2,转换成无符号时,-6就变成了65 530,从而导致最后的结果是65 531。
(2)当字符型自动转换成其他类型时,实质上是将其编码值进行转换。换一句话说,当字符参与算术运算时,实质上就是其编码值在参与运算。例如,‘a’+ 20,则其结果为117,即用于运算的是字符‘a’的编码值97。
可见,自动转换是C语言编译器对多个类型不一致的操作数进行的默认处理。这种默认处理方式有时并不是程序所指定的结果,这时就指定在程序中对操作数(或表达式的值)进行强制转换,即在操作数或表达式的左边加上要转换成的合法的类型名,且类型名两边加上圆括号“()”,格式如下:
(<类型名>)<操作数> (<类型名>)(<表达式>)
例如,在8/(int)3.1中,(int)3.1是将double型实数3.1强制转换成整数3,这样原来的表达式就变成了8/3,结果为整数2。
再比如,若有8 / (int)( 3.0 + 2.1 ),则(int)( 3.0 + 2.1 )是将表达式3.0 + 2.1的结果(double型实数5.1)强制转换成整型,即为整数5,这样,原来的表达式就变成了8/5,结果为整数1。
注意:
(1)使用类型的强制转换时,类型名两边必须加上左括号和右括号,当被强制转换的操作数是表达式时,表达式的两边也要加上圆括号,如(int)( 3.0 + 2.1 )。
(2)若强制转换的类型比操作数或表达式的类型要高,则这种强制转换是安全的,否则是不安全的,因为会丢失数据。如(int)3.8,结果为3,丢失0.8。
(3)在程序中要合理地使用强制转换。例如,7/5,两个操作数的类型都是整数,结果为1。若要想得到的结果为1.4,那么,(double)7 / 5、7 /(double)5和(double)7 /(double)5都是可以的,但(double)(7/5)是不行的,因为它对7/5这个表达式的值(为1)进行(double)类型的强制转换,结果是1.0。
2.5.3 优先级和结合性
在由多个运算符构造的混合表达式中,表达式的运算次序显得格外重要,因为这将影响其最后的运算结果。例如,2+3*4,究竟是先运算2+3,还是先运算3*4呢?
为了解决这个问题,C语言首先规定了各个运算符的优先等级,并用相对的数值来反映优先等级的高低,数值越小,优先级越高,数值相同,优先级相同,如附录A所示。这里只列出算术运算符的优先级,如表2.3所示。
表2.3 算术运算符的优先级和结合性
从表中可以看出,在算术运算符中,单目运算符的优先级最高,其次是乘、除和求余,最后是加、减。这就是说,对于2+3*4来说,由于“*”的优先级比“+”高,故先运算3*4,结果为12,再运算2+12,结果为14。所以,在一个包含多种算术运算的混合运算中,先乘除后加减的运算规则是由运算符的优先级来保证的。
但当有:4%3*2,由于运算符“%”和“*”的优先等级数值都是3,即优先级相同。那么此时究竟是先运算4%3,还是先运算3*2呢?
C语言规定,优先级相同的运算符按它们的结合性来进行处理。所谓运算符的结合性是指运算符和操作数的结合方式,它有从左至右和从右至左两种。从左至右的结合,简称左结合,是指操作数先与其左边的运算符相结合,再与右边的运算符结合;而自右至左的结合,简称右结合,次序刚好相反,它将操作数先与其右侧的运算符相结合。
需要说明:
(1)上述左结合和右结合的定义仅仅是从其字面的含义而给出的。实际上,运算符的结合性的使用必须满足这样的条件:两个相同优先等级的运算符共用一个操作数。
例如,在算术运算符中,除单目运算符外,其余算术运算符的结合性都是从左至右(参见表2.3)。这样,由于4%3*2表达式中,运算符“%”和“*”共用一个操作数3,因它们的结合性是左结合,因此3先与左边的运算符“%”相结合,亦即先运算4%3,结果为1,然后再与右边的运算符“*”相结合,即1*2,结果为2。
(2)若不满足结合性的使用条件,则具体的运算次序由编译器来决定。例如, 2*3+4*5,究竟是先运算2*3还是先运算4*5,由编译器来决定,即不同的编译器有不同的处理方式,但运算的结果通常都是相同的。
(3)若表达式由优先等级相同的运算符构成,例如,2 * 3 * 4 * 5 * 6,则运算的次序由编译器来决定。
在程序设计中,不同编译器对表达式处理方式的不一致是一种常见现象,一般情况下,这种不一致的现象不会改变它们的最终结果。若最终结果会改变,则这种情况必须在程序中通过修改代码来回避。
2.5.4 算术表达式的值和类型
由不同运算符构成的表达式的值和类型是不同的。对于算术表达式(由算术运算符构成的表达式)来说,最后表达式的结果总是表现为是优先级最低的那个运算符的表达式,其值的类型是该运算符可使用的操作数中最高的类型。
例如,10 + 'a' + 2*1.25 - 5.0/4L,则根据优先级和结合性,表达式的运算次序应为
(1)进行2*1.25的运算,因1.25是double型,故将2转换成double型,结果为double型的2.5。这时表达式变为:10 + 'a' + 2.5 - 5.0/4L。
(2)进行5.0/4L的运算,因5.0是double型,故将长整型4L转换成double型,结果值为1.25。这时表达式变为:10 + 'a' + 2.5 - 1.25。
(3)进行10 + 'a' 的运算,因10是int型,故将'a'转换成int整数97,运算结果为107。这时表达式变为:107 + 2.5 - 1.25。
(4)整数107和2.5相加,因2.5是double型,故将整数107转换成double型,结果为double型,值为109.5。这时表达式变为:109.5 - 1.25。
(5)进行109.5 - 1.25的运算,结果为double型的108.25。
可见,上述算术表达式的最终结果表现为由优先级最低的“-”运算符构成的表达式“109.5 - 1.25”,最后结果是它们的最高数据类型——double型,值为108.25。实际上,由于不同的编译器对表达式的优化有所不同,因此上述运算次序可能不一样,但结果一般应是相同的。
下面来看一个程序,试分析printf函数中的实参表达式“x + a % 3 * (int)(x+y) % 2 / 4”的值和类型。
【例Ex_Express.c】 分析表达式的值和类型
#include <stdio.h> #include <conio.h> int main() { int a=7; float x=2.5,y=4.7; printf( "%f\n", x + a % 3 * (int)(x+y) % 2 / 4 ); return 0; }
分析:
(1)上述代码中,变量x和y的初值分别设定为double型的2.5和4.7,但由于变量x和y定义时指定的类型是float,因此x和y的实际初值分别为2.5f和4.7f。这种将高类型的数据用于低数据类型的变量初始化或赋值时,由于在转换过程中存在数据丢失的危险,因而较好的编译器在编译时都会给出相应的警告错误。
(2)有了a,x和y的初值后,表达式x + a % 3 * (int)(x+y) % 2/4就变成2.5f + 7 % 3 *(int)( 2.5f +4.7f) % 2/4,由于圆括号的运算优先等级最高,故先运算( 2.5f +4.7f),结果为float型数值7.2f,然后运算(int) 7.2f,因为强制类型转换运算符的优先等级仅次于圆括号,结果为int型数值7,这样表达式就变成2.5f + 7 % 3 * 7 % 2/4。
()在表达式 * 中,由于运算符“”的优先等级是最低的,所以应先运算7 % 3 * 7 % 2/4,而这个式子中,运算符“*”、“/”和“%”的优先等级都是一样的,因而应按其“从左至右”的结合性来运算。但要注意,编译对程序代码中的语义、句义和词义的验证和识别一般总是按“自上而下,从左至右”的顺序来进行的。这就是说,表达式7 % 3 * 7 % 2/4首先被提取的应是“7 % 3 * 7”,由于结合性是“从左至右”的,故先运算7 % 3,结果为1,表达式变成1* 7 % 2 / 4,然后再提取、再运算,结果为0。这样表达式2.5f + 7 % 3 * 7 % 2 / 4就成为2.5f + 0。
程序的运行结果如下:
2.500000
2.5.5 代数式和表达式
数值计算是所有高级语言的最典型的应用之一。为了能让C程序进行数值计算,还必须将代数式写成C语言合法的表达式。例如,若有代数式:
则相应的C语言的表达式应为
1.0 / 2.0 * ( a *x + ( a + x )/( 4.0 * a ) )
需要强调:
(1)注意书写规范。在使用运算符进行数值运算时,在双目运算符的两边与操作数之间一定要添加空格。若缺少空格,有时编译器会得出与我们的设想不同的结果。例如:
–5*–6––7 /* 不合法的表达式 */
和
结果是不一样的,前者发生编译错误,而后者的结果是37。但对于单目运算符来说,虽然也可以使其与操作数之间存在一个或多个空格,但最好与操作数写在一起。
(2)注意加上圆括号。在书写C语言的表达式时,应尽可能地、有意识地加上一些圆括号。这不仅能增强程序的可读性,而且,当对优先关系不确定时,加上圆括号是保证得到正确结果的最好方法,可见括号运算符“( )”的优先级几乎是最高的。例如,若有代数式:
在代数式中,3ae和bc隐含了乘运算,这是代数式的一种约定。但在C语言中,这种约定是不允许的,相应的C语言的表达式应写成:
(3.0 * a * e )/( b * c)
要注意圆括号“( )”中的左括号“(”和右括号“)”是成对出现的。
(3)注意数据类型。尽管在混合数据类型的运算中,C编译器会将数据类型向表达式最高类型自动转换,但这种转换是有条件的。例如:
1/2 * ( 3.0 + 4 )
其结果为0.0。这是因为编译器首先运算圆括号里的3.0 + 4,由于3.0是double型实型,故结果也为double型,值为3.4。此时表达式变为1/2*3.4,按结合性应先运算1/2,由于“/”运算符两侧的操作数都是整型,类型不会自动转换成double,故其计算结果是整数0,最后运算0*3.4,由于3.4是double型,所以整个表达式的结果是double实数0.0。
可见,只有当双目运算符两侧的操作数的类型不一致时,编译器才会对其进行类型的自动转换。为了保证计算结果的正确性,应尽可能地将操作数的类型写成表达式中的最高类型。即上述表达式应写成:
1.0/2.0 * ( 3.0 + 4.0 )
(4)注意符号^。数学中的符号^表示幂运算,而在C语言中,该符号表示位运算的异或操作,要注意它们的区别。例如,若有代数式:
x^2 – e^5
则相应的C语言的表达式可写成:
x * x – pow(2.718281828, 5.0)
或写成:
x * x – exp( 5.0)
pow和exp都是在头文件math.h中定义的库函数。其中,库函数pow有两个参数x, y,用来求其幂,即等于数学式x^y。exp函数用来进行以e为底的幂运算ex,x是该库函数要指定的参数。
同时,作为技巧,对于数学式x^2或x^3等,应尽量使用连乘的形式(如x*x或x*x*x),而不要调用库函数pow,因为每次函数调用都要额外占用内存,且计算速度也比较慢。