5.2 基本运算符

C使用运算符(operator)来代表算术运算。例如,+运算符使在它两侧的值加在一起。如果术语“运算符”对您来说很奇怪,那么请您记住那些东西总得有个名称。与被称之为“那些东西”或“算术处理器”相比较,被称为“运算符”看起来的确是一个更好的选择。现在我们看一下用于基本算术运算的运算符:=、+、-、*,以及/(C没有指数运算符。然而,标准C的数学库为此提供了一个pow( )函数。例如,pow(3.5, 2.2)返回3.5的2.2次幂)。

5.2.1 赋值运算符:=

在C里,符号=不表示“相等”,而是一个赋值运算符。下面的语句将值2002赋给名字为bmw的变量:

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

阅读 ‧ 电子书库

也就是说,符号=的左边是一个变量名,右边是赋给该变量的值。符号=被称为赋值运算符(assignment operator)。再次强调不要把这行读为“bmw等于2002”,而应该读为“将值2002赋给变量bmw”。赋值运算符的动作是从右到左。

或许变量的名字和变量值之间的区别看起来微乎其微,但是请考虑下面的常用计算机语句:

阅读 ‧ 电子书库

在数学上,该语句没有任何意义。如果您给一个有限的数加1,结果不会“等于”开始的那个数,但是作为计算机赋值语句,它却是很合理的。它意味着“找到名字为i的变量的值;对那个值加1,然后将这个新值赋给名字为i的变量”(请参见图5.1)。

阅读 ‧ 电子书库

图5.1 语句i=i+l;

像下面这样的语句:

阅读 ‧ 电子书库

在C中是没有意义的(确切地说是无效的),原因是2002只是一个常量。您不能将一个值赋给一个常量;那个常量本身就是它的值了。所以,当您准备键入代码时请记住在符号=左边的项目必须是一个变量的名字。实际上,赋值运算符左边必须指向一个存储位置。最简单的方法是使用变量的名字,但是以后您会看到,“指针”也可以用于指向一个存储位置。更普遍地,C使用术语“可修改的左值”(modifiable lvalue)来标志那些我们可以为之赋值的实体。“可修改的左值”或许不是那么直观易懂,所以我们先看看一些定义。

几个术语:数据对象、左值、右值和操作数

“数据对象”(data object)是泛指数据存储区的术语,数据存储区能用于保存值。例如,用于保存变量或数组的数据存储区是一个数据对象。C的术语左值(lvalue)指用于标识一个特定的数据对象的名字或表达式。例如,变量的名字是一个左值。所以对象指的是实际的数据存储,但是左值是用于识别或定位那个存储的标识符。

因为不是所有的对象都是可更改值的,所以C使用术语“可修改的左值”来表示那些值可以被更改的对象。所以,赋值运算符的左边应该是一个可修改的左值。lvalue中的1确实来自于left,因为可修改的左值可以用在赋值运算符的左边。

术语“右值”(rvalue)指的是能赋给可修改的左值的量。例如,考虑下面的语句:

阅读 ‧ 电子书库

这里bmw是一个可修改的左值,2002是一个右值。您可能猜到rvalue里的r来自于right。右值可以是常量、变量或者任何可以产生一个值的表达式。

在您学习事物的名称时,我们称之为“项目”的东西(比如在“符号=左边的项目”中的“项目”)的正确术语是“操作数”(operand)。操作数是运算符操作的对象。例如,您可以把吃一个汉堡描述为用“吃”运算符操作“汉堡”这个操作数。与之相似,您可以说=运算符的左操作数是可修改的左值。

C的基本赋值运算符有点与众不同。试一下程序清单5.3里的短程序。

程序清单5.3 golf.c程序

阅读 ‧ 电子书库

许多程序语言将在本程序里的三重赋值处卡壳,但是C可以顺利接受它。赋值是从右到左进行的。首先jane得到值68,然后tarzan得到值68,最后cheeta得到值68。所以输出结果如下:

阅读 ‧ 电子书库

5.2.2 加法运算符:+

“加法运算符”(addition operator)使得在它两侧的值被加到一起。例如,语句:

阅读 ‧ 电子书库

将打印数24而不是打印表达式:

阅读 ‧ 电子书库

被加的值(操作数)可以是变量也可以是常量。所以语句:

阅读 ‧ 电子书库

使计算机先查找右边的两个变量的值,将它们加起来,最后将这个和赋给变量income。

5.2.3 减法运算符:-

“减法运算符”(subtraction operator)从它前面的数中减去它后面的数。例如,下面的语句将值200.0赋给takehome:

阅读 ‧ 电子书库

+和-运算符被称为二元(binary)或双值(dyadic)运算符,这表示它们需要两个操作数。

5.2.4 符号运算符:-和+

负号可以用于指示或改变一个值的代数符号。例如,下面的语句序列:

阅读 ‧ 电子书库

把值12赋给smokey。

当这样使用负号时,称它为一元运算符(unary operator),表示它只需要一个操作数(请参见图5.2)。

阅读 ‧ 电子书库

图5.2 一元和二元运算符

C90标准将一元+运算符加进了C中。这个运算符不改变它的操作数的值或符号;它只是使您能使用像下面这样的语句:

阅读 ‧ 电子书库

而不会得到编译器的报错信息。这种结构在以前是不允许的。

5.2.5 乘法运算符:*

乘法由符号*表示。语句:

阅读 ‧ 电子书库

用2.54乘以变量inch,然后将结果赋给cm。

有时候,您可能会需要一个平方表。C没有计算平方的函数,但是正如程序清单5.4所示,您可以使用乘法来计算平方。

程序清单5.4 squares.c程序

阅读 ‧ 电子书库

正如您自己验证的那样,该程序打印了前20个整数和它们的平方。我们看一个更为有趣的例子。

指数增长

您可能听说过这个故事,有一位强大的统治者想奖励一位对他做出突出贡献的学者。他问这位学者他想要什么,这位学者指着棋盘说,在第1个方格里放1粒小麦,在第2个方格里放2粒,在第3个方格里放8粒,依次类推。由于缺乏丰富的数学知识,统治者惊讶于此请求的谦逊,因为他原本准备奖励很大一笔财产。如程序清单5.5的运行结果所示,这显然是跟统治者开了一个玩笑。它计算了每个方格里应放多少粒小麦,并计算了总数。您可能对小麦的产量不是很熟悉,所以这个程序将这个总数与美国小麦年产量的粗略估计进行了比较。

程序清单5.5 wheat.c程序

阅读 ‧ 电子书库

阅读 ‧ 电子书库

开始的输出结果倒是中规中矩:

阅读 ‧ 电子书库

10个方格以后,该学者只得到略超过1000粒的小麦,但是看看到50个方格时发生了什么!

阅读 ‧ 电子书库

总量已经超过了整个美国每年的总产量!如果您想看看到第64个方格时发生了什么,不妨自己运行一下这个程序。

这个例子演示了指数增长的现象。世界人口增长和我们对能源的使用都遵循同样的模式。

5.2.6 除法运算符:/

C使用符号/来表示除法。/左边的值被它右边的值除。例如,下面的语句把值4.0赋给four:

阅读 ‧ 电子书库

整型数的除法运算和浮点型数的除法运算有很大的不同。浮点类型的除法运算得出一个浮点数结果,而整数除法运算则产生一个整数结果。整数不能有小数部分,这使得用3去除5很让人头痛,因为结果有小数部分。在C中,整数除法结果的小数部分都被丢弃。这个过程被称为截尾(truncation)。

试一下程序清单5.6中的程序,看看截尾如何进行,以及整数除法与浮点数除法有什么不同。

程序清单5.6 divide.c程序

阅读 ‧ 电子书库

阅读 ‧ 电子书库

程序清单5.6包括了一个“混合类型”的实例:用一个整数值去除一个浮点类型的值。C是比较宽容的语言,它允许您这样做,但是正常情况下您应该避免混合类型。输出结果如下:

阅读 ‧ 电子书库

注意,没有把整数除法运算的结果四舍五入到最近的整数,而是进行截尾,即舍弃整个小数部分。当您对整数与浮点数进行混合运算时,结果是浮点数。实际上,计算机不能真正用整数去除浮点数,所以编译器将两个操作数转变成一致的类型。在这种情况下,做除法运算之前将整数转化为浮点数。

C99标准之前的C语言给了实现者一些空间,让他们来决定对于负数整数除法如何工作。可以使用这样的方法,即舍入过程采用小于或等于该浮点数的最大整数。当然,3相对于3.8而言是符合上面描述的。那么-3.8呢?最大整数方法会建议将其四舍五入为-4,因为-4小于-3.8。但是另外一种舍入方法是简单地丢弃小数部分,这种称为“趋零截尾”的解释方法建议将-3.8转换成-3。在C99以前,不同实现采用不同的方法。但是C99要求使用“趋零截尾”,所以应把-3.8转换成-3。

整数除法的属性在处理某些问题时是很方便的。很快您就会看到一个例子。首先,还有另一个重要的事情:当您在一个语句中进行多种运算时将发生什么?这就是下面要讨论的问题。

5.2.7 运算符的优先级

考虑下面的代码行:

阅读 ‧ 电子书库

此语句有一个加法运算,一个乘法运算和一个除法运算。哪个运算会先发生?是25.0先加到60.0,然后用n乘以前面的结果85.0,最后将得到的结果用SCALE来除吗?还是先用n乘以60.0,得到的结果加上25.0,最后再用SCALE去除前面得到的结果?还是其他顺序?我们取n为6.0, SCALE为2.0。如果您使用这些值对这个语句进行计算,您将发现第一个方法得到255,第二个方法得到192.5。C程序必定采用了某种其他顺序,因为该程序给出butter的值为205.0。

显然,执行各种操作的顺序很重要,所以C需要关于执行顺序的明确规则。C通过建立一个运算符的优先顺序来满足上述需求。将一个优先级赋予每个运算符。正像在普通的算术运算中那样,乘法和除法具有比加法和减法更高的优先级,所以先执行乘法和除法运算。如果两个运算符有相同的优先级将会发生什么?如果它们共享一个操作数,会根据它们在语句里出现的顺序执行它们。对于大多数的运算符,该顺序是从左到右的(=运算符是这个规则的例外)。所以,在下面的语句中:

阅读 ‧ 电子书库

运算顺序如表5.1所示:

表5.1 运算顺序

 

 

60.0*n 表达式里的第一个*或/(假设n的值为6,所以60.0*n的结果为 360.0)
360.0/SCALE 然后,表达式里的第二个*或/
25.0+180 最后(因为SCALE的值是2.0),表达式里的第一个+或-的结果为205.0

许多人喜欢用一种称为“表达式树”(expression tree)的图表来表示赋值的顺序。图5.3是这种图表的例子。此图表显示了最初的表达式是如何逐步简化为一个值的。

阅读 ‧ 电子书库

图5.3 图示运算符、操作数和求值顺序的表达式树

如何让加法在除法之前执行?您可以像下面这样:

阅读 ‧ 电子书库

最先被执行的是圆括号中包含的部分。在圆括号内部,运算按正常的规则进行。在本例中,先执行乘法运算,然后是加法。圆括号内的表达式就是如此完成的。现在可以用SCALE去除这个结果了。

表5.2总结了迄今为止用到的运算符的规则(本书的封三包括了涉及到的所有运算符的表)。

表5.2 按优先级递减顺序排列的运算符

 

 

运 算 符 结 合 性
( ) 从左到右
+-(一元运算符) 从右到左
*/ 从左到右
+-(二元运算符) 从左到右
= 从右到左

注意减号的两种用法具有不同的优先级,加号的两种用法也是如此。结合性那一列指出运算符如何与其操作数相结合。例如,一元减号与它右边的量相结合,在除法中用右边的操作数去除左边的操作数。

5.2.8 优先级和求值顺序

运算符的优先级为决定表达式里求值的顺序提供了重要的规则,但是它并不决定所有的顺序。C留下了一些由实现者自己来决定的选择。考虑下面的语句:

阅读 ‧ 电子书库

当两个运算符共享一个操作数时,优先级规定了求值的顺序。例如,12既是*运算符的操作数又是+运算符的操作数,根据优先级的规定乘法运算先进行。与之相似,优先级规定了对5进行乘法操作而不是加法操作。总之,两个乘法运算6*12和5*20在加法运算之前进行。优先级没有确定的是这两个乘法运算中到底哪个先进行。C将这个选择权留给实现者,这是因为可能一种选择在一种硬件上效率更高,而另一种选择在另一种硬件上效率更高。不管先执行哪个乘法运算,表达式都会简化为72+100,所以对于这个具体的例子,这个选择不影响最终的结果。您说“但是乘法的结合是从左到右的顺序。难道这不意味着先执行最左边的乘法吗?”(可能您没有那样说,但是有人可能会这么说)。结合规则适用于共享同一操作数的运算符。例如,在表达式12/3*2里,有相同优先级的/和*运算符共享操作数3;所以,从左到右的原则适用于这个例子。这个表达式将被简化为4*2,即8(如果从右到左计算,结果将是12/6,即2,这种选择对该例产生了影响)。在前面的例子中,两个*运算符不共享一个操作数,所以,从左到右的规则对它并不适用。

试验一下规则

我们用更复杂的例子来试一下这些规则。请看程序清单5.7。

程序清单5.7 rules.c程序

阅读 ‧ 电子书库

这个程序将打印什么值?猜测一下,然后运行该程序或阅读下面的描述来检查您的答案。

首先,圆括号有最高的优先级。先计算-(2+5)*6里的圆括号,还是先计算(4+3* (2+3))里的圆括号?像我们刚刚讨论过的那样,这有赖于不同的实现。在本例中每个选择都将导致同样的结果,所以我们先执行左面的计算。圆括号的高优先级意味着在子表达式-(2+5) *6中,您先计算(2+5)并得到7。下一步,您对7应用一元负运算符得到-7。现在这个表达式变为:

阅读 ‧ 电子书库

下一步是计算2+3的值。表达式变为:

阅读 ‧ 电子书库

下一步,因为在圆括号里*又高于+的优先级,所以表达式变成:

阅读 ‧ 电子书库

然后:

阅读 ‧ 电子书库

用6乘以-7得到下面的表达式:

阅读 ‧ 电子书库

然后进行加法运算,得到:

阅读 ‧ 电子书库

现在score赋值为-23。最后,top得到值-23。记住=运算符的结合是从右到左的。