预计阅读本页时间:-
C有大约40个运算符,其中有些运算符比其他运算符要常用得多。我们已经讨论过的那些是最常用的,现在我们将接着介绍4个比较有用的运算符。
5.3.1 sizeof运算符和size_t类型
在第3章“数据和C”中,您看到了sizeof运算符。回顾一下,sizeof运算符以字节为单位返回其操作数的大小(在C中,1个字节被定义为char类型所占用空间的大小。在过去,1个字节通常是8位,但是一些字符集可能使用更大的字节)。操作数可以是一个具体的数据对象(例如一个变量名),或者一个类型。如果它是一个类型(如float),操作数必须被括在圆括号里。程序清单5.8的例子演示了这两种形式的用法。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
程序清单5.8 sizeof.c程序
C规定sizeof返回size_t类型的值。这是一个无符号整数类型,但它不是一个新类型。相反,与可移植类型(如int32_t等)相同,它是根据标准类型定义的。C有一个typedef机制(在14章“结构和其他数据形式”中将对此做进一步讨论),它允许您为一个已有的类型创建一个别名。例如:
使real成为double的别名。现在您可以声明一个real类型的变量:
编译器将会看到单词real,回想起typedef语句把real定义为double的别名,于是它把deal创建为一个double类型的变量。与此相似,C的头文件系统可以使用typedef来使size_t在系统中作为unsigned int或unsigned long的同义词。这样当您使用size_t时,编译器会用适合您的系统的标准类型代替之。
C99更进一步,把%zd作为用来显示size_t类型值的printf( )说明符。如果您的系统没有实现%zd,您可以试着使用%u或%lu代替它。
5.3.2 取模运算符:%
取模运算符(modulus operator)用于整数运算。该运算符计算出用它右边的整数去除它左边的整数得到的余数。例如,13%5(读作“对13除以5取模”)所得值为3,因为13除以5得2并余3。不要对浮点数使用该运算符,那将是无效的。
乍一看,您可能认为这个运算符是数学家使用的深奥工具,但是它实际上相当实用。一个常见的用途是帮助您控制程序的流程。例如,假设您正在编写一个预算账单的程序,该程序被设计为每三个月就加进一笔额外的费用,只需让程序对月份进行除以3的取模操作(即,month%3)并检查结果是否为0。如果是,程序就加进额外的费用。学习了第7章“C控制语句:分支和跳转”中的if语句后,您对此将会有更好的理解。
程序清单5.9是使用%运算符的另外一个例子。同时,它也展示了使用while循环的另一种方法。
程序清单5.9 min_sec.c程序
下面是一个输出示例:
程序清单5.2使用一个计数器来控制while循环。当计数器超出给定的大小,循环终止。而程序清单5.9则使用scanf( )来获得一个新的值赋给变量sec。只要这个变量是正数,循环就会继续。当用户输入0或者一个负值的时候,循环就会停止。两种情况中同样重要的一点在于,每次循环都会修改被测试的变量的值。
负数的取模运算应遵照什么规则?在C99为整数除法规定“趋零截尾”规则之前,该问题的处理方法有很多可能。但有了这条规则之后,如果第一个操作数为负数,那么得到的模也为负数;如果第一个操作数为正数,那么得到的模也为正数:
如果您的系统有不同的行为,那么它还没有遵循C99标准。该标准实际上规定:不管在什么情况下,如果a和b都是整数值,您可以通过从a中减去(a/b)*b来计算a%b。例如,您可以像这样来计算-11%5的值:
5.3.3 增量和减量运算符:++和--
“增量运算符”(increment operator)完成简单的任务,即将其操作数的值增加1。这个运算符以两种方式出现。在第一种方式中,++出现在它作用的变量的前面,这是前缀(prefix)模式。在第二种方式中,++出现在它作用的变量的后面,这是后缀(postfix)模式。这两种模式的区别在于值的增加这一动作发生的准确时间是不同的。我们先解释它们的相似之处,然后再解释其区别。程序清单5.10中的简短例子说明了增量运算符是如何工作的。
程序清单5.10 add_one.c程序
运行add_one.c程序,输出结果如下:
这个程序两次同时计数到5。通过使用如下语句代替两个增量语句,您可以得到相同的结果:
这些是很简单的语句。为什么要不辞辛苦地创建两个缩写形式呢?一个原因是这种精简的形式使您的程序更为整洁,更易于阅读。这些运算符使您的程序看起来很美观,可以赏心悦目。例如,您可以如此重写shoes2.c(程序清单5.2)中的一部分代码:
然而,您仍未充分利用增量运算符的好处。您可以像下面这样缩短这段程序:
这里您已经将增量的过程和while循环的比较合并成一个表达式。这种结构在C中很普遍,所以值得我们进一步观察分析。
首先,这个结构是如何工作的?很简单。shoe的值增加1,然后与18.5进行比较。如果小于18.5,花括号里的语句将被执行一次。然后shoe再次增加1,并且重复这个循环,直到shoe变得太大了为止。将shoe的初始值从3.0改为2.0来补偿在第一个foot计算以前被增加的shoe(请参见图5.4)。
图5.4 通过循环一次
第二,这种方法有什么好处?它更简洁。更重要的是它在一个地方集中了控制循环的两个处理过程。主要处理过程是判断是否继续循环。在本例中,判断是检查看看鞋子的尺码是否小于18.5。附带的处理过程是改变判断的元素:在本例中,是增加鞋子的尺码。
假设您忘了改变鞋子的尺码,于是shoe将总是小于18.5,循环将永不结束。计算机陷于一个无限循环(infinite loop)中,产生一行行相同的东西。最后,您只能无奈地以某种方式强行关闭这个程序。在同一个位置执行循环的判断和循环的改变可以防止您忘记更新循环。
缺点是将两个运算合并到一个单一的表达式里将使代码难于理解,并易于产生计数错误。
增量运算符的另一个优点是它通常产生更高效的机器语言代码,因为它与实际的机器语言指令相似。然而,随着商家推出更好的C编译器,这个好处可能会消失。一个智能编译器能识别出x=x+l,并把它与++x相同对待。
最后,这两个运算符还有另外一个特性,有时在某些微妙的场合这个特性很有用。为了发现这个特性,试着运行在程序清单5.11里的程序。
程序清单5.11 post_pre.c程序
如果您和您的编译器都正确无误地做了每一件事,那么您将得到下列结果:
像我们所能预料到的,a和b都增加了1。然而,aplus具有a改变之前的值,而plusb具有b改变之后的值。这就是前缀形式和后缀形式的不同之处(请参见图5.5)。
图5.5 前缀和后缀
当单独使用这些增量运算符之一时(像在一个独立的语句ego++;中那样),您使用哪种形式无关紧要。然而当运算符及其操作数是一个更大的表达式的一部分时,比方说在您刚才看到的赋值语句中,选择就很重要了。在这种情况下,您必须考虑到您想要的结果。例如,回忆一下我们曾建议使用下面的代码:
这个判断条件提供了一个尺码直到18的表。如果您使用shoe++而不是++shoe,这个表将达到尺码19,因为shoe将在比较之后而不是之前增加。当然,您可以仍然使用下面这种不太精致的形式:
但是没人会相信您是一个真正的C程序员。
当您继续读这本书的时候,您应该更加留意增量运算符的例子。自问一下您是否能互换地使用前缀和后缀形式,或者是否环境决定了必须使用某个特定的选择。
也许一个更为明智的原则是避免那种前缀形式和后缀形式将导致不同效果的代码。例如,不要使用下列语句:
而是使用下列语句来代替它:
然而,有时不那么小心翼翼会更有趣。所以,本书将不总是遵循这个明智的建议。
5.3.4 减量:--
每种形式的增量运算符都有一种形式的减量运算符(decrement operator)与之对应,只须使用--来代替++:
程序清单5.12说明了计算机可以是位熟练的抒情诗人。
清单5.12 bottles.c程序
其输出以如下形式开始:
然后,输出继续进行,最后以如下方式结束:
显然,这位熟练的抒情诗人在复数的表达上有点问题,但是这可以通过使用第7章“C控制语句:分支和跳转”里的条件运算符来解决。
顺便提一下,>运算符代表“大于”。与<(“小于”)相似,它是一个关系运算符(relational operator)。在第6章“C控制语句:循环”中您将会更深入地了解关系运算符。
5.3.5 优先级
增量和减量运算符有很高的结合优先级;只有圆括号比它们的优先级高。所以,x*y++代表(x)*(y++)而不是(x*y) ++。幸亏后者无效,增量运算符和减量运算符只能影响一个变量(或者更一般地讲,一个可修改的左值),而组合x*y本身不是一个变量,尽管它的各个部分是变量。
不要将这两个运算符的优先级和求值的顺序相混淆。假设您有下列代码:
nextnum的值是什么?用值来代替变量可以得到:
只有当使用了n之后,n的值才增加到4。优先级告诉我们++只属于n,而不属于y+n。它也告诉我们什么时候使用n的值计算表达式,而增量运算符的性质决定了什么时候改变n的值。
当n++是表达式的一部分时,您可以认为它表示“先使用n;然后将它的值增加”,另一方面,++n的意思是“先将n值增加,然后再使用它”。
5.3.6 不要太聪明
如果您企图一次使用太多的增量运算符,可能连自己都会被弄糊涂。例如,您可能认为您可以改进squares.c程序(程序清单5.4),方法是使用下面的代码代替while循环来打印整数和它们的平方:
这看起来是合理的。您打印数值num,然后用它本身来乘它以得到平方值,最后将num增加1。事实上,这个程序可能只在某些系统上可以正常工作,但不是所有的系统上都可以。问题是当printf( )获取要打印的值时,它可能先计算最后一个参数的值,从而在计算其他参数之前增加num的值。所以,不是打印成:
而是可能打印成:
它甚至可能从右往左计算,用5替换最右边的一个num,而用6替换另外两个num,从而得出:
在C中,编译器可以选择先计算函数里哪个参数的值。这个自由提高了编译器的效率,但是如果在函数参数里使用了增量运算符就会带来麻烦。
另一个麻烦的可能来源是像这样的语句:
问题依然是编译器可能不以您想象的顺序来操作。您可能认为编译器应该先找到num/2,然后继续进行;但是它可能先做最后的项目,即先增加num的值,然后在num/2中使用新值。这些都是没有保证的。
另一个麻烦的例子如下:
当然在该语句被执行后,n的值比以前大2,但是y的值是不确定的。一个编译器可能在计算y值时使用n的旧值两次,然后将n增加两次。这使y的值为6, n的值为5。或者编译器使用n的旧值一次,然后增加n的值一次,在表达式里再使用第二个n值,最后第二次增加n的值。这种方法使y的值为7, n的值为5。两种选择都是允许的。更准确地说,这个结果是不确定的,这意味着C标准没有定义结果将是什么。
通过如下原则,您可以很容易地避免这些问题:
● 如果一个变量出现在同一个函数的多个参数中时,不要将增量或者减量运算符用于它上面。
● 当一个变量多次出现在一个表达式里时,不要将增量或减量运算符运用到它的上面。
另一方面,关于什么时候执行增量动作,C还是做出了一些保证的。我们在本章稍后的“副作用和顺序点”部分讨论到顺序点时会回到这个主题。