第5章 使用表达式、语句和运算符
从本质上说,程序是一组按顺序执行的命令。这些命令为表达式和语句,使用运算符执行特定的计算或操作。
在本章中,您将学习:
• 什么是语句;
• 什么是语句块(复合语句);
• 什么是运算符;
• 如何执行简单的算术运算和逻辑运算。
5.1 语句
无论是口头语言还是编程语言,它们都是由按顺序执行的语句组成的。下面来分析您学习的第一条重要语句:
这条语句使用cout在屏幕(控制台)上显示文本。在C++中,所有语句都以分号(;)结尾,分号界定了语句的边界。这就像您在书写英语时在句末添加句号(.)一样。可以接着分号开始下一条语句,但出于方便和可读性考虑,通常每条语句放在一行中。换句话说,下面一行代码实际上包含两条语句:
编译器通常不考虑空白,这包括空格、制表符、换行符、回车等,但字符串字面量中的空格将导致输出不同。
下面的代码非法:
这样的代码通常会导致错误——编译器指出第一行缺少引号(”)和结束语句的分号(;)。如果出于某种原因,要将一条语句放到两行中,可在第一行末尾添加反斜杆(\):
对于前面的语句,另一种书写方式是将字符串字面量分成两个:
编译器注意到两个相邻的字符串字面量后,将把它们拼接成一个。
在文本元素很长或表达式由很多变量组成,导致语句很长,大多数显示器无法完全显示时,将语句划分成多行很有帮助。
5.2 复合语句(语句块)
可使用花括号({})将多条语句组合在一起,以创建复合语句(语句块):
语句块通常将众多语句组合在一起,指出它们属于同一条语句。编写if语句或循环时,语句块特别有用,这将在第6章介绍。
5.3 使用运算符
运算符是C++提供的工具,让您能够使用数据:对其进行变换、处理甚至根据数据做决策。
赋值运算符的用法比较直观,本书一直在使用它:
上述语句使用赋值运算符将一个int变量初始化为101。赋值运算符将左边的操作数的值(左值)替换为右边的操作数的值(右值)。
左值通常是内存单元。在前面的示例中,变量MyInteger实际上指向一个内存单元,属于左值。另一方面,右值可以是内存单元的内容。
因此,所有的左值都可用作右值,但并非所有的右值都可用作左值。为更好地理解这一点,请看下面的示例,这行代码不合理,不能通过编译:
5.3.3 加法运算符(+)、减法运算符(−)、乘法运算符(*)、除法运算符(/)和求模运算符(%)
可对两个操作数执行算术运算:使用+相加、使用-相减、使用*相乘、使用/相除、使用%求模:
除法运算符(/)返回两个数相除的结果。然而,如果两个操作数都是整数,结果将不包含小数,因为根据定义,整数不能包含小数。求模运算符(%)返回除法运算的余数,只能用于整数。程序清单5.1是一个简单的应用程序,演示了如何对用户输入的两个数字执行各种算术运算。
程序清单5.1 演示如何对用户输入的整数执行算术运算
输出:
分析:
这个程序的大部分代码的含义都是不言自明的。最有趣的代码可能是使用求模运算符(%)的那行,它返回Num1(365)与Num2(25)相除的余数。
有时需要将变量加 1,尤其是控制循环的变量:每次执行循环时,都需要将这种变量的值递增或递减。
为帮助您完成这种任务,C++提供了递增运算符(++)和递减运算符(—)。
这些运算符的使用语法如下:
从上述示例代码可知,使用递增和递减运算符的方式有两种:放在操作数的前面或放在操作数的后面。放在操作数前面时,称为前缀递增或递减运算符;而放在操作数后面时,称为后缀递增或递减运算符。
首先需要理解前缀和后缀之间的差别,这样才能选择合适的方式。使用后缀运算符时,先将右值赋给左值,再将右值递增或递减。这意味着在上述所有使用后缀运算符的代码中,Num2 都为 Num1的旧值(执行递增或递减前的值)。
前缀运算符的行为完全相反,即先将右值递增或递减,再将结果赋给左值。在所有使用后缀运算符的代码中,Num2的值都与Num1的值相同。程序清单5.2演示了将前缀和后缀递增和递减运算符用于一个int变量的结果。
程序清单5.2 前缀运算符和后缀运算符之间的差别
输出:
分析:
结果表明,后缀运算符和前缀运算符的差别在于:在第 8 和 18 行,被赋值的左值包含对 MyInt执行递增或递减运算前的值。另一方面,在第13和23行,左值包含对MyInt执行递增或递减运算后的值。这是最重要的差别,选择合适的运算符时必须牢记这一点。
在下面的语句中,使用前缀还是后缀运算符对结果没有影响:
这是因为没有将原来的值赋给其他变量,这两种情形的最终结果都是将MyInt递增。
您经常会听到前缀运算符的性能更高还是后缀运算符更高的争论。换句话说,++MyInt优于MyInt++。
至少从理论上说确实如此,因为使用后缀运算符时,编译器需要临时存储初始值,以防需要将其赋给其他变量。就整型变量而言,这对性能的影响几乎可以忽略不计,但对某些类来说,这种争论也许有意义。
明智地选择数据类型,以免溢出
诸如 short、int、long、unsigned short、unsigned int、unsigned long等数据类型的容量有限,如果算术运算的结果超出了选定数据类型的上限,将导致溢出。
就拿unsigned short来说吧,它占用16位内存,因此取值范围为0~65535。usigned short变量的值为65535后,如果再加1,将导致溢出,结果为0。这很像汽车的里程表,如果它只支持5位数字,则里程超过99999公里(或英里)后,里程表将发生机械溢出。
在这种情况下,将计数器的变量类型指定为unsigned short不合适。要支持大于65535的数字,程序员应使用数据类型unsigned int。
数据类型 signed short的取值范围为−32768~32768,如果这种变量的值已经是 32768,则将其加 1的结果为最小的负数——这取决于编译器。
程序清单5.3演示了执行算术运算时可能不小心导致的溢出错误。
程序清单5.3 演示有符号整型变量和无符号整型变量溢出的负面影响
输出:
分析:
正如您看到的,无意的溢出导致应用程序的行为不可预测。如果使用 unsigned short变量来指定要分配的内存量,则在您原本要分配65356字节内存时,实际上请求的却是零字节。
经常需要在执行操作前检查条件是否满足,相等运算符(==,操作数相等)和不等运算符(!=,操作数不相等)可帮助您完成这种检查。
相等性检查的结果为布尔值,即true或false:
除相等性检查外,您可能还想检查变量与特定值之间的大小关系。为帮助您进行这种检查,C++提供了关系运算符,如表5.1所示。
表5.1 系运算符
如表5.1所示,比较运算的结果总是布尔值,即要么为true,要么为false。下面的示例代码演示了如何使用表5.1所示的关系运算符:
程序清单5.4演示了这些运算符的作用——将结果显示在屏幕上。
程序清单5.4 演示相等运算符和关系运算符
输出:
再次运行的输出:
分析:
这个程序显示各种运算的结果。请注意用户提供的两个整数相同时的输出,运算符==、>=和<=的结果相同。
相等运算符和关系运算符只有两种可能的结果,这使得它们非常适合用于做决策,还非常适合用作循环的条件表达式,确保只要条件为 true,就不断执行循环。有条件地执行和循环将在第 6 章更详细地介绍。
逻辑NOT运算用运算符!表示,用于单个操作数。表5.2是逻辑NOT运算的真值表,这种运算将提供的布尔标记反转。
表5.2 辑NOT运算的真值表
AND、OR和XOR等运算需要两个操作数。仅当两个操作数都为true时,逻辑AND运算的结果
才为true。表5.3说明了逻辑AND运算的结果。
表5.3 辑AND运算的真值表
逻辑AND运算用运算符&&表示。
只要有一个操作数为true,逻辑OR运算的结果就为true,如表5.4所示。
表5.4 辑OR运算的真值表
逻辑OR运算用运算符|表示。
逻辑XOR(异或)运算与逻辑OR运算稍有不同,有且只有一个操作数为true时,这种运算的结果才为true,如表5.5所示。
表5.5 辑XOR运算的真值表
C++提供了按位XOR运算,用运算符^表示。这个运算符对操作数相应的各位执行XOR运算。
5.3.9 使用C++逻辑运算NOT(!)、AND(&&)和OR(||)
请看下面的句子:
• 如果明天下雨且没有公交车,我就不能去上班;
• 如果折扣很高或奖金创纪录,我就能买下那辆车。
在编程中,您也需要使用这样的逻辑结构,根据运算的结果决定程序的后续流程。C++提供了逻辑运算符AND和OR,您可在条件语句中使用它们,根据条件改变程序的流程。
程序清单5.5演示了逻辑运算符AND和OR的工作原理。
程序清单5.5 分析C++逻辑运算符&&和||
输出:
再次运行的输出:
分析:
该程序演示了逻辑运算符AND和OR的工作原理,但没有演示如何使用它们来做决策。
程序清单5.6演示了如何使用条件语句和逻辑运算符根据变量的值执行不同的代码行。
程序清单5.6 在if语句中使用逻辑NOT和AND运算符(!和&&)进行条件处理
输出:
再次运行的输出:
最后一次运行的输出:
分析:
程序清单5.6所示的程序使用了还未介绍的if条件语句,请尝试根据输出理解这种语句的行为。第 15行包含条件表达式(Raining && !Buses),可将其读作“下雨且没有公交车”。这个表达式使用了逻辑AND运算符将没有公交车(对有公交车执行逻辑NOT运算)和下雨关联起来。
如果您想更详细地了解if语句,可快速浏览第6章。
程序清单5.7演示了如何将逻辑运算符NOT和OR(!和|)用于条件处理。
程序清单5.7 使用逻辑运算符NOT和OR帮助判断您能否购买那辆梦寐以求的汽车
输出:
再次运行的输出:
最后一次运行的输出:
分析:
第 14行使用了一条 if语句,而第 16行是与之配套的 else语句。在条件(Discount | FantasticBonus)为true时,将执行第15行的语句。这个表达式包含逻辑运算符|,只要您喜欢的汽车的折扣很高,该表达式就为true。当该表达式为false时,将执行else语句后面的语句(第17行)。
5.3.10 按位运算符NOT(~)、AND(&)、OR(|)和XOR(^)
逻辑运算符和按位运算符之前的差别在于,按位运算符返回的并非布尔值,而是对操作数对应位执行指定运算的结果。C++让您能够执行按位 NOT、OR、AND 和 XOR(异或)运算,它们分别使用~将每位取反、使用|对相应位执行OR运算、使用&对相应位执行AND运算、使用^对相应位执行XOR运算。其中后三个运算符对变量与选择的数字(通常是位掩码)执行相应的运算。
在整数的每位都表示特定标记的状态时,有些按位运算很有用。例如,32位的整数可用于表示32个布尔标记。程序清单5.8演示了按位运算符的用法。
程序清单5.8 对整数的各位执行NOT、AND、OR和XOR运算,以演示按位运算符的用法
输出:
分析:
这个程序使用了一种还未介绍过的数据类型——bitset,旨在简化二进制数据的显示。这里使用std::bitset完全是为了方便显示,而没有其他任何目的。第10、13、17和22行将一个整数赋给了一个bitset对象,以便使用它来显示该整数的二进制表示。运算是对整数执行的。首先,请关注输出,它显示了用户输入的整数181的二进制表示,然后依次显示了将按位运算符~、&、|和^用于该整数的结果。第14行使用按位运算NOT对各位取反。这个程序还演示了运算符&、|和^的工作原理,它们对两个操作数的相应位执行相应运算,从而获得最终的结果。只要结合使用这里的结果与前面介绍的真值表,您就能明白其中的工作原理。
要更深入地了解如何在C++中操作位标记,请参阅第25章,它详细介绍了std::bitset。
移位运算符将整个位序列向左或向右移动,其用途之一是将数据乘以或除以2n。
下面的移位运算符使用示例将变量乘以2:
下面的移位运算符使用示例将变量除以2:
程序清单5.9演示了如何使用移位运算符将一个整数乘以或除以2n。
程序清单5.9 使用按位右移运算符(>>)和左移运算符(<<)分别计算整数的1/4和1/2以及2倍和4倍
输出:
分析:
输入的数字为16,其二进制表示为1000。第9行将它向右移1位,结果为0100,即8,这相当于将其减半。第10行向右移两位,从1000变成了00100,即4。第11和12行的左移运算符的效果完全相反。向左移1位时结果为10000,即32,向左移动两位的结果为100000,即64,相当于将数字分别翻了一番和两番!
移位运算符不会旋转值。另外,对有符号数执行按位运算的结果随实现而异,在有些编译器上,向左移位时,并不会导致最低有效位的值变成最高有效位的值,而是将最低有效位设置为零。
复合赋值运算符将运算结果赋给左边的操作数。
请看下面的代码:
其中最后一行代码与下面的代码等效:
因此运算符+=的作用如下:将两个操作数相加,再将结果赋给左边的操作数(Num1)。表 5.6 列出了众多复合赋值运算符,并说明了其工作原理。
表5.6 合赋值运算符
程序清单5.10演示了这些运算符的效果。
程序清单5.10 使用按位右移运算符(>>)和左移运算符(<<)分别计算整数的1/4和1/2以及2倍和4倍
输出:
分析:
在整个程序中,不断使用各种复合赋值运算符修改value的值。每次运算都使用了value,并将结果赋给value。因此,第9行将用户输入的值440加上8,并将结果(448)赋给value。接下来,第11行将448减去2,并将结果(446)赋给value。
这个运算符指出特定类型或变量占用的内存量,单位为字节。sizeof的用法如下:
或
sizeof(…)看起来像函数调用,但它并不是函数,而是运算符。有趣的是,程序员不能定义这个运算符,因此不能重载它。
第12章将更详细地介绍如何定义自己的运算符。
程序清单5.11演示了如何使用sizeof来确定一个数组占用的内存量。另外,您可能想查看程序清单3.4,了解如何使用sizeof来确定常见变量类型占用的内存量。
程序清单5.11 使用sizeof确定包含100个int元素的数组占用的内存量(单位为字节)以及每个元素占用的内存量
输出:
分析:
该程序演示了如何使用sizeof来确定包含100个int元素的数组占用了多少字节的内存,结果为400字节。该程序还表明,每个元素占用的内存为4字节。
在需要动态地给 N 个对象(尤其是您自己创建的类型)分配内存时,sizeof 很有用。您可以使用sizeof确定每个对象占用的内存量,再使用运算符new动态地分配内存。
动态内存分配将在第8章详细介绍。
您可能在学校学过算术运算顺序口诀BODMAS(Brackets Orders Division Multiplication Addition Subtraction,先括号,后乘除,再加减),它指出了复杂算术表达式的计算顺序。
在C++中,假设使用运算符编写了如下表达式:
MyNumber 的值是多少呢?这可没有猜测的空间,C++标准非常严格地指定了各种运算的执行顺序。这种顺序被称为运算符优先级,如表5.7所示。
表5.7 运算符优先级
再来看看前面的复杂表达式:
计算这个表达式的结果时,需要使用表 5.7 所示的运算符优先级。由于乘法和除法的优先级高于加法和减法,而加法和减法的优先级高于移位,因此可将上述表达式简化为如下所示:
由于加法和减法的优先级高于移位,因此可进一步简化为如下所示:
最后,您执行移位运算。由于左移一位翻一番,左移两位翻两番,因此该表达式的结果为295*4,即1180。
使用括号可让代码易于理解。
前述表达式实际上编写得很糟糕。对编译器来说,这个表达式很容易理解,但编写代码时,还应确保它对人来说也容易理解。
因此,将这个表达式写成下面这样要好得多:
5.4 总结
在本章中,您学习了C++语句、表达式和运算符是什么。您学习了如何在C++中执行加、减、乘、除等基本的算术运算,还大致了解了NOT、AND、OR和XOR等逻辑运算。您学习了C++逻辑运算符!、&&和|以及按位运算符~、&、|和^,前者可用于条件语句中,而后者以每次一位的方式操作数据。
您学习了运算符优先级,知道使用括号让代码对其他程序员来说易于理解很重要。您还大致了解了整型溢出,知道避免这种情形发生很重要。
5.5 问与答
问:既然unsigned short占用的内存更少,为何有些程序还使用unsigned int?
答:unsigned short变量的最大取值通常为 65535,如果这种变量的值已经是 65535,再递增将溢出,变成零。为避免这种行为,设计良好的程序在不确定变量的取值将远低于65535时,将其数据类型声明为 unsigned int。
问:我需要将一个数字除以3再翻倍,为此使用了如下代码。这些代码有问题吗?
答:有问题!为何不添加括号,让这行代码对其他程序员来说更容易理解呢?添加注释也没有害处呀。
问:我的应用程序需要将整数值5和2相除,为此我编写了如下代码,但执行后,result的值却为2。请问有问题吗?
答:没有任何问题。int变量不能包含小数,因此这种运算的结果为2,而不是2.5。如果您希望结果为2.5,应将所有变量的数据类型都改为float或double。这些类型的变量用于存储浮点数(小数)。
5.6 作业
作业包括测验和练习,前者帮助读者加深对所学知识的理解,后者提供了使用新学知识的机会。请尽量先完成测验和练习题,然后再对照附录 D 的答案。在继续学习下一章前,请务必弄懂这些答案。
1.编写将两个数相除的应用程序时,将变量声明为哪种数据类型更合适,int还是float?
2.32/7的结果是多少?
3.32.0/7的结果是多少?
4.sizeof(…)是函数吗?
5.我需要将一个数翻倍,再加上5,然后再翻倍,下面的代码是否正确?
6.如果两个操作数的值都为true,对其执行XOR运算的结果是什么?
1.使用括号改善问题5中的代码,使其更清晰。
2.下述代码导致result的值为多少?
3.编写一个程序,让用户输入两个布尔值,并显示对其执行各种按位运算的结果。