5.2 基本上操作符
C用操作符(operator)则表示微积分演算。比如,+操作符使在它两边的值总和。假如你真的名词“操作符”很怪异,所以请读懂小东西总要有位中文名称。与叫“那些小东西”或“演算处置符”,还不如叫“操作符”。那时,他们如是说呵呵用作基本上微积分演算的操作符:=、+、-、*和/(C没成分股操作符。但,C的国际标准微积分库提供更多了两个pow()表达式用作成分股演算。比如,pow(3.5, 2.2)回到3.5的2.2次幂)。
5.2.1 表达式操作符:=
在C词汇中,=并不意味著“成正比”,而要两个表达式操作符。下面的表达式表达式句子:
bmw = 2002;把值2002赋给表达式bmw。换句话说,=号右侧是两个cp,右侧是赋给该表达式的值。记号=被称作表达式操作符。除此之外,下面的句子不读成“bmw等同于2002”,而读成“把值2002赋给表达式bmw”。表达式犯罪行为从右往左展开。
或许cp和表达式值的差别看起来非常非常有限,但,考量下面这条常见的句子:
i = i + 1;对微积分来说,这全然没错。假如给两个非常有限的数加之1,它不可能“等同于”原来的数。但,在计算机表达式表达式句子中,这很合理。该句子的意思是:找出表达式i的值,把该值加1,然后把新值表达式表达式i(见图5.1)。
图5.1 句子i = i + 1;
在C词汇中,类似这样的句子没意义(实际上是无效的):
2002 = bmw;因为在这种情况下,2002被称作右值(rvalue),只能是字面常量,不能给常量表达式,常量本身就是它的值。因此,在编写代码时要读懂,=号右侧的项必须是两个cp。实际上,表达式操作符右侧必须引用两个存储位置。最简单的方法就是使用cp。但,后面章节还会如是说“指针”,可用作指向两个存储位置。概括地说,C使用可修改的左值(modifiable lvalue)标记那些可表达式的实体。或许“可修改的左值”不太好懂,他们再来看一些定义。
几个名词:数据对象、左值、右值和操作符
表达式表达式句子的目的是把值存储到内存位置上。用作存储值的数据存储区域统称作数据对象(data object)。C国际标准只有在提到这个概念时才会用到对象这个名词。使用cp是标识对象的一种方法。除此之外,还有其他方法,但要在后面的章节中才学到。比如,可以指定数组的元素、结构的成员,或者使用指针表达式(指针中存储的是它所指向对象的地址)。左值(lvalue)是C词汇的名词,用作标识特定数据对象的中文名称或表达式。因此,对象指的是实际的数据存储,而左值是用作标识或定位存储位置的标签。
对于早期的C词汇,提到左值意味著:
1.它指定两个对象,可以引用内存中的地址;
2.它可用在表达式操作符的右侧,左值(lvalue)中的l源自left。
但后来,国际标准中新增了const限定符。用const创建的表达式不可修改。因此,const标识符满足下面的第1项,但不满足第2项。一方面C继续把标识对象的表达式定义为左值,一方面某些左值却不能放在表达式操作符的右侧。
为此,C国际标准新增了两个名词:可修改的左值(modifiable lvalue),用作标识可修改的对象。所以,表达式操作符的右侧应该是可修改的左值。当前国际标准建议,使用名词对象定位值(object locator value)更好。
右值(rvalue)指的是能表达式给可修改左值的量,且本身不是左值。比如,考量下面的句子:
bmw = 2002;这里,bmw是可修改的左值,2002是右值。读者或许猜到了,右值中的r源自right。右值可以是常量、表达式或其他可求值的表达式(如,表达式调用)。实际上,当前国际标准在描述这一概念时使用的是表达式的值(value of an expression),而不是右值。
他们看几个简单的示例:
int ex; int why; int zee; const int TWO = 2; why = 42; zee = why; ex= TWO * (why + zee);这里,ex、why和zee都是可修改的左值(或对象定位值),它们可用作表达式操作符的右侧和右侧。TWO是不可改变的左值,它只能用作表达式操作符的右侧(在该例中,TWO被初始化为2,这里的=操作符则表示初始化而不是表达式,因此并未违反规则)。同时,42是右值,它不能引用某指定内存位置。除此之外,why和zee是可修改的左值,表达式(why + zee)是右值,该表达式不能则表示特定内存位置,而且也不能给它表达式。它只是程序计算的两个临时值,在计算完毕后便会被丢弃。
在学习中文名称时,被称作“项”(如,表达式操作符右侧的项)的就是演算对象(operand)。演算对象是操作符操作的对象。比如,可以把“吃汉堡”描述为:“吃”(操作符)操作“汉堡”(演算对象)。类似地可以说,=操作符的右侧演算对象应该是可修改的左值。
C的基本上表达式操作符有些与众不同,请看程序清单5.3。
程序清单5.3 golf.c程序
/* golf.c — 高尔夫锦标赛记分卡 */ #include <stdio.h> int main(void) { int jane, tarzan, cheeta; cheeta = tarzan = jane = 68; printf(” cheeta tarzan jane\n”); printf(“First round score %4d %8d %8d\n”, cheeta, tarzan, jane); return 0; }许多其他词汇都会回避该程序中的三重表达式,但C全然没问题。表达式的顺序是从右往左:首先把68赋给jane,然后再赋给tarzan,最后赋给cheeta。因此,程序的输出如下:
cheeta tarzan jane First round score 68 68 685.2.2 加法操作符:+
加法操作符(addition operator)用作加法演算,使其两边的值相加。比如,句子:
printf(“%d”, 4 + 20);打印的是24,而不是表达式
4 + 20
相加的值(演算对象)可以是表达式,也可以是常量。因此,执行下面的句子:
income = salary + bribes;计算机会查看加法操作符右侧的两个表达式,把它们相加,然后把和赋给表达式income。
在此提醒读者注意,income、salary和bribes都是可修改的左值。因为每个表达式都标识了两个可被表达式的数据对象。但,表达式salary + bribes是两个右值。
5.2.3 减法操作符:-
减法操作符(subtraction operator)用作减法演算,使其右侧的数减去右侧的数。比如,下面的句子把200.0赋给takehome:
takehome = 224.00 – 24.00;+和–操作符都被称作二元操作符(binary operator),即那些操作符需要两个演算对象才能完成操作。
5.2.4 记号操作符:-和+
减号还可用作标明或改变两个值的代数记号。比如,执行下面的句子后,smokey的值为12:
rocky = –12; smokey = –rocky;以这种方式使用的负号被称作一元操作符(unary operator)。一元演算符只需要两个演算对象(见图5.2)。
图5.2 一元和二元操作符
C90国际标准新增了一元+操作符,它不会改变演算对象的值或记号,只能这样使用:
dozen = +12;编译器不会报错。但在以前,这样做是不允许的。
5.2.5 乘法操作符:*
记号*则表示乘法。下面的句子用2.54乘以inch,并将结果赋给cm:
cm = 2.54 * inch;C没平方表达式,假如要打印两个平方表,怎么办?如程序清单5.4所示,可以使用乘法来计算平方。
程序清单5.4 squares.c程序
/* squares.c — 计算1~20的平方 */ #include <stdio.h> int main(void) { int num = 1; while (num < 21) { printf(“%4d %6d\n”, num, num * num); num = num + 1; } return 0; }该程序打印数字1~20及其平方。接下来,他们再看两个更有趣的例子。
1.成分股增长
读者可能听过这样两个故事,一位强大的统治者想奖励做出突出贡献的学者。他问这位学者想要什么,学者指着棋盘说,在第1个方格里放1粒小麦、第2个方格里放2粒小麦、第3个方格里放4粒小麦,第4个方格里放8粒小麦,以此类推。这位统治者不熟悉微积分,很惊讶学者竟然提出如此谦虚的要求。因为他原本准备奖励给学者一大笔财产。假如程序清单5.5运行的结果正确,这显然是跟统治者开了两个玩笑。程序计算出每个方格应放多少小麦,并计算了总数。可能大多数人对小麦的产量不熟悉,该程序以谷粒数为单位,把计算的小麦总数与粗略估计的世界小麦年产量展开了比较。
程序清单5.5 wheat.c程序
/* wheat.c — 成分股增长 */ #include <stdio.h> #define SQUARES 64 // 棋盘中的方格数 int main(void) { const doubleCROP =2E16; // 世界小麦年产谷粒数 double current, total; int count = 1; printf(“square grains total “); printf(“fraction of \n”); printf(” added grains “); printf(“world total\n”); total = current = 1.0; /* 从1颗谷粒开始 */ printf(“%4d %13.2e %12.2e %12.2e\n”, count, current, total, total / CROP); while(count < SQUARES) { count = count +1; current = 2.0 * current; /* 下两个方格谷粒翻倍 */ total = total + current; /* 更新总数 */ printf(“%4d %13.2e %12.2e %12.2e\n”, count, current, total, total / CROP); } printf(“Thats all.\n”); return 0; }程序的输出结果如下:
square grains total fraction of added grains world total 1 1.00e+00 1.00e+00 5.00e-17 2 2.00e+00 3.00e+00 1.50e-16 3 4.00e+00 7.00e+00 3.50e-16 4 8.00e+00 1.50e+01 7.50e-16 5 1.60e+01 3.10e+01 1.55e-15 6 3.20e+01 6.30e+01 3.15e-15 7 6.40e+01 1.27e+02 6.35e-15 8 1.28e+02 2.55e+02 1.27e-14 9 2.56e+02 5.11e+02 2.55e-14 10 5.12e+02 1.02e+03 5.12e-1410个方格以后,该学者得到的小麦仅超过了1000粒。但,看看55个方格的小麦数是多少:
55 1.80e+16 3.60e+16 1.80e+00总量已超过了世界年产量!不妨自己动手运行该程序,看看第64个方格有多少小麦。
这个程序示例演示了成分股增长的现象。世界人口增长和他们使用的能源都遵循相同的模式。
5.2.6 除法操作符:/
C使用记号/来表示除法。/右侧的值是被除数,右侧的值是除数。比如,下面four的值是4.0:
four = 12.0/3.0;整数除法和浮点数除法不同。浮点数除法的结果是浮点数,而整数除法的结果是整数。整数是没小数部分的数。这使得5除以3很让人头痛,因为实际结果有小数部分。在C词汇中,整数除法结果的小数部分被丢弃,这一过程被称作截断(truncation)。
运行程序清单5.6中的程序,看看截断的情况,体会整数除法和浮点数除法的差别。
程序清单5.6 divide.c程序
/* divide.c — 演示除法 */ #include <stdio.h> int main(void) { printf(“integer division: 5/4 is %d \n”, 5 / 4); printf(“integer division: 6/3 is %d \n”, 6 / 3); printf(“integer division: 7/4 is %d \n”, 7 / 4); printf(“floating division: 7./4. is %1.2f \n”, 7. / 4.); printf(“mixed division: 7./4 is %1.2f \n”, 7. / 4); return 0; }程序清单5.6中包含两个“混合类型”的示例,即浮点值除以整型值。C相对其他一些词汇来说,在类型管理上比较宽容。尽管如此,一般情况下还是要避免使用混合类型。该程序的输出如下:
integer division: 5/4 is 1 integer division: 6/3 is 2 integer division: 7/4 is 1 floating division: 7./4. is 1.75 mixed division: 7./4 is 1.75注意,整数除法会截断计算结果的小数部分(丢弃整个小数部分),不会四舍五入结果。混合整数和浮点数计算的结果是浮点数。实际上,计算机不能真正用浮点数除以整数,编译器会把两个演算对象转换成相同的类型。本例中,在展开除法演算前,整数会被转换成浮点数。
C99国际标准以前,C词汇给词汇的实现者留有一些空间,让他们来决定如何展开负数的整数除法。一种方法是,舍入过程采用小于或等同于浮点数的最大整数。当然,对于3.8来说,处置后的3符合这一描述。但-3.8会怎样?该方法建议四舍五入为-4,因为-4小于-3.8。但,另一种舍入方法是直接丢弃小数部分。这种方法被称作“趋零截断”,即把-3.8转换成-3。在C99以前,不同的实现采用不同的方法。但C99规定使用趋零截断。所以,应把-3.8转换成-3。
5.2.7 操作符优先级
考量下面的代码:
butter = 25.0 + 60.0 * n / SCALE;这条句子中有加法、乘法和除法演算。先算哪两个?是25.0加之60.0,然后把计算的和85.0乘以n,再把结果除以SCALE?还是60.0乘以n,然后把计算的结果加之25.0,最后再把结果除以SCALE?还是其他运算顺序?假设n是6.0,SCALE是2.0,带入句子中计算会发现,第1种顺序得到的结果是255,第2种顺序得到的结果是192.5。C程序一定是采用了其他的演算顺序,因为程序运行该句子后,butter的值是205.0。
显然,执行各种操作的顺序很重要。C词汇对此有明确的规定,通过操作符优先级来解决操作顺序的问题。每个操作符都有自己的优先级。正如普通的微积分演算那样,乘法和除法的优先级比加法和减法高,所以先执行乘法和除法。假如两个操作符的优先级相同怎么办?假如它们处置同两个演算对象,则根据它们在句子中出现的顺序来执行。对大多数操作符来说,这种情况都是按从左到右的顺序展开(=操作符除外)。因此,句子:
butter = 25.0 + 60.0 * n / SCALE;的演算顺序是:
60.0 * n 首先计算表达式中的*或/(假设n的值是6,所以60.0*n得360.0) 360.0 / SCALE 然后计算表达式中第2个*或/ 25.0 + 180 最后计算表达式里第1个+或-,结果为205.0(假设SCALE的值是2.0)许多人喜欢用表达式树(expression tree)来则表示求值的顺序,如图5.3所示。该图演示了如何从最初的表达式逐步简化为两个值。
图5.3 用表达式树演示操作符、演算对象和求值顺序
如何让加法演算在除法演算之前执行?可以这样做:
flour = (25.0 + 60.0 * n) / SCALE;最先执行圆括号中的部分。圆括号内部按正常的规则执行。该例中,先执行乘法演算,再执行加法演算。执行完圆括号内的表达式后,用演算结果除以SCALE。
表5.1总结了到目前为止学过的操作符优先级。
表5.1 操作符优先级(从高至低)
注意正号(加号)和负号(减号)的两种不同用法。结合律栏列出了操作符如何与演算对象结合。比如,一元负号与它右侧的量相结合,在除法中用除号右侧的演算对象除以右侧的演算对象。
5.2.8 优先级和求值顺序
操作符优先级为表达式中的求值顺序提供更多重要的依据,但并没规定所有的顺序。C给词汇的实现者留出选择的余地。考量下面的句子:
y = 6 * 12 + 5 * 20;当操作符共享两个演算对象时,优先级决定了求值顺序。例如下面的句子中,12是*和+操作符的演算对象。根据操作符的优先级,乘法的优先级比加法高,所以先展开乘法演算。类似地,先对5展开乘法演算而不是加法演算。简来说之,先展开两个乘法演算6 * 12和5 * 20,再展开加法演算。但,优先级并未规定到底先展开哪两个乘法。C词汇把主动权留给词汇的实现者,根据不同的硬件来决定先计算前者还是后者。可能在一种硬件上采用某种方案效率更高,而在另一种硬件上采用另一种方案效率更高。无论采用哪种方案,表达式都会简化为72 + 100,所以这并不影响最终的结果。但,读者可能会根据乘法从左往右的结合律,认为应该先执行+操作符左边的乘法。结合律只适用作共享同一演算对象的操作符。比如,在表达式12 / 3 * 2中,/和*操作符的优先级相同,共享演算对象3。因此,从左往右的结合律在这种情况起作用。表达式简化为4 * 2,即8(假如从右往左计算,会得到12/6,即2,这种情况下计算的先后顺序会影响最终的计算结果)。在该例中,两个*操作符并没共享同两个演算对象,因此从左往右的结合律不适用作这种情况。
学以致用
接下来,他们在更复杂的示例中使用以上规则,请看程序清单5.7。
程序清单5.7rules.c程序
/* rules.c — 优先级测试 */ #include <stdio.h> int main(void) { int top, score; top = score = -(2+5) * 6 + (4 + 3 * (2 + 3)); printf(“top = %d, score = %d\n”, top, score); return 0; }该程序会打印什么值?先根据代码推测呵呵,再运行程序或阅读下面的分析来检查你的答案。
首先,圆括号的优先级最高。先计算-(2 + 5) * 6中的圆括号部分,还是先计算(4 + 3 * (2 + 3))中的圆括号部分取决于具体的实现。圆括号的最高优先级意味着,在子表达式-(2 + 5) * 6中,先计算(2 + 5)的值,得7。然后,把一元负号应用在7上,得-7。那时,表达式是:
top = score = -7 * 6 + (4 + 3 * (2 + 3))下一步,计算2 + 3的值。表达式变成:
top = score = -7 * 6 + (4 + 3 * 5)接下来,因为圆括号中的*比+优先级高,所以表达式变成:
top = score = -7 * 6 + (4 + 15)然后,表达式为:
top = score = -7 * 6 + 19-7乘以6后,得到下面的表达式:
top = score = –42 + 19然后展开加法演算,得到:
top = score = –23那时,-23被表达式给score,最终top的值也是-23。读懂,=运算符的结合律是从右往左。
本文摘自《C Primer Plus(第6版)中文版》
[美] 史蒂芬·普拉达(Stephen Prata) 著,姜佑 译
《C Primer Plus(第6版)中文版》是一本经过仔细测试、精心设计的完整C词汇教程,它涵盖了C词汇编程中的核心内容。《C Primer Plus(第6版)中文版》作为计算机科学的经典著作,讲解了包含结构化代码和自顶向下设计在内的程序设计原则。
与以前的版本一样,作者的目标仍旧是为读者提供更多一本入门型、条理清晰、见解深刻的C词汇教程。作者把基础的编程概念与C词汇的细节很好地融合在一起,并通过大量短小精悍的示例同时演示一两个概念,通过学以致用的方式鼓励读者掌握新的主题。
每章末尾的复习题和编程练习题进一步强化了*重要的信息,有助于读者理解和消化那些难以理解的概念。本书采用了友好、易于使用的编排方式,不仅适合打算认真学习C词汇编程的学生阅读,也适合那些精通其他编程词汇,但希望更好地掌握C词汇这门核心词汇的开发人员阅读。
《C Primer Plus(第6版)中文版》在之前版本的基础之上展开了全新升级,它涵盖了C词汇*新的进展以及C11国际标准的详细内容。本书还提供更多了大量深度与广度齐备的教学技术和工具,来提高你的学习。