有了变量和I/O语句,我们的编程工具箱中就有了一些非常强大的构建模块。但是,存储和打印数值在编码过程中是非常枯燥的。我们要开始对这些变量的内容进行一些处理。计算新值的能力是提高代码复杂性的首要步骤之一。在C语言(以及许多其他语言)中,你可以借助运算符进行计算,这些符号允许你对两个或多个值进行加、减、乘或比较(即执行“操作”)。
C包含几个预定义的运算符,用于进行基本的数学和逻辑工作。(高级数学和逻辑运算可以通过编写自己的函数来完成,我们将在第5章中介绍。)除了一个特殊的三元运算符(?:将在3.3节中讨论)外,C语言的运算符只能使用一或两个值。图2-3展示了这些一元及二元运算符如何与值和表达式配合使用。
图2-3:二元运算符语法
请注意,你可以在一个序列中对两个以上的值使用运算符,但在C语言的引擎盖下,C会将该序列视为一系列对。一般来说,操作符与表达式一起使用。“表达式”一词的含义很广。表达式可以是简单的字面值或单个变量。表达式也可以非常复杂,需要编写多行代码。在讨论表达式时,要记住的关键一点是表达式具有(或将产生)一个值。
C语言中最直观的运算符可能就是用于数学计算的运算符了。表2-6列出了C语言内置的运算符。
表2-6:算术运算符
你可以用文字、变量或表达式,或者这些东西的组合来做数学运算。让我们试试一个简单的程序,向用户询问两个整数,然后使用这些值进行一些计算。
自己试试这个简短的程序。你可以键入上述代码或打开 ch02/calcs.c 文件。编译并运行后,你应该会得到类似下面的输出结果:
希望大多数答案都是合理的,符合你的期望。一些看似奇怪的结果可能是我们尝试除以两个数字的结果。我们得到的不是8.33333这样的浮点近似值,而是一个整数8。请记住,int类型不支持小数。如果除以两个int,结果总是另一个int,小数部分会被去掉。我指的是去掉,而不是四舍五入。例如,除数为8.995的除法运算结果将返回8,而负数,如-7.89,将返回-7。
但是,如果我们用两个(或更多)运算符做出一个更复杂的表达式呢?我们可以稍微升级一下程序,将3个整数以不同的方式组合起来。查看 ch02/calcs2.c :
如果你愿意,可以调整代码,尝试其他组合。目前,你可以编译并运行该程序,得到以下输出结果:
这些答案与你的预期相符吗?如果不一致,很可能是因为不同运算符的优先级不同。C语言并不是以简单的从左到右的方式处理大型表达式的。有些运算符比其他运算符更重要,它们的优先级高于次要运算符。C会先执行表达式中最重要的操作,然后再执行其余操作。在使用混合运算符对表达式进行运算时,我们经常会看到“运算顺序”这个短语。
乘法、除法和余数(*、/、%)运算都将在加法和减法(+、-)运算之前进行。如果有一系列相同或等价的运算符,那么这些计算将从左到右进行。通常情况下,这样做没有问题,我们只要稍微注意一下表达式各部分的排列,就能得到所需的答案。当我们不能依靠简单的排列方式时,我们可以使用括号来创建特定的自定义运算顺序。请看下面这段代码:
这里有3种排序,但只有最后一种,即average3是正确的。括号表达式14+20首先求值。可以这样理解,括号的优先级高于算术运算。顺便说一句,你可以在任何地方随意使用括号,即使它只是给一个顺序正确的表达式增加了易读性。
“易读性”的概念非常主观。如果括号是计算正确答案所必需的,那么当然需要使用。如果严格来说没有必要,那么只要能帮助你更轻松地阅读表达式,就可以使用它们。如果括号过多,可能会增加阅读代码的难度。最重要的是,使用时要保持一致。
如果表达式特别混乱,类似于这些表达式,括号也可以嵌套:
在这样的表达式中,首先计算最内层的括号表达式(1+2),然后依次计算。
在本章中,我们已经多次讨论过变量类型,但表达式也有类型,有时会给初学者带来惊喜。请看下面的代码段:
如果我们打印出三分之一(one_third)和平均值(average),你猜会出现什么结果?试着创建一个小的C程序来测试你的理论。你的结果应该是这样的:
但是,“三分之一”应该是0.333333,而12和5的平均值应该是8.5。这是怎么回事?编译器看到了一堆整数,并执行了整数数学运算。回想小学时,你可能学过有余数的长除法,即“1除以3整除0次,余数为3”。对于C语言来说,这意味着整数1除以整数3等于整数0(回想一下,如果需要的话,%运算符会给出余数值)。
有没有办法得到我们想要的浮点数答案?有。事实上,有很多方法可以得到正确的答案。在我们编造的示例中,也许最简单的方法就是在初始化表达式中使用浮点字面量:
尝试更改程序,希望能得到新的正确输出:
但是,如果我们不使用字面量呢?如果我们将片段中的平均值计算改为使用第三个int变量呢?
在这种情况下,我们怎样才能正确求出平均值呢?C语言支持类型转换,可以让编译器把一个值当作其他类型的值来处理。在这种情况下非常方便。我们可以像这样将计数变量转换为浮点数:
在要转换的值或表达式前的括号中输入所需的类型。现在我们的计算中有了浮点数值,计算的其余部分将“升级”为浮点表达式,我们将得到正确的答案。升级过程并非偶然。编译器是故意这样做的,这个过程甚至有一个名字——隐式类型转换
。图2-4展示了我们讨论过的许多数值类型的升级路径。
图2-4:隐式类型转换层次结构
在任何涉及不同类型的表达式中,“最大”的类型获胜,其他类型都将晋升为该类型。请注意,在这种转换中偶尔会丢失一些重要信息。如果一个负数被转换为无符号类型,它的符号就会丢失。或者,如果一个长整数被提升为float,甚至是double,那么它的近似值可能会很低。
就像使用括号可以在不改变计算的情况下增加清晰度,如果可以帮助理解表达式的操作,你可以随时使用显式转置。但请注意,运算顺序仍然有效。例如,下面的语句并不相同:
如果将这几行添加到测试程序中,然后打印出3个平均值,你会发现前两个平均值运行正常,第三个平均值却不行。你明白为什么吗?第三个计算中的括号会导致在将错误答案提升为浮点数之前,先执行所有整数类型的原始错误平均值。
我还应该指出的是,任何时候如果你想向下升级到一个“更小”的类型,你都必须使用显式转换。幸运的是,编译器通常可以捕捉到这些情况,并发出警告。