第3 章 表达式 【学习内容】 本章介绍C++对数据进行处理的最基本的手段———运算操作和表达式。内容包括: ◆ C++表达式的基本概念。 ◆ 各种运算符与表达式。 ◆ 运算优先级和结合律。 ◆ 表达式语句。 ◆ C++的类型转换。 【学习目标】 ◆ 熟练运用各种运算符与表达式。 ◆ 理解运算优先级和结合律。 ◆ 掌握类型转换的方法。 在第2章中,我们了解了如何在C++程序中表示整数、浮点数等基本数据,以及程序输入 输出方式等,但还不清楚如何对数据做进一步的操作。例如,要编写程序求解一个一元二次方 程的实数根,如何对系数做运算,这是本章需要解决的问题。 3.1 表达式基础 3.1.1 基本概念 表达式(expression)由一个或多个操作数(operand)及运算符(operator)组成,对表达式求 值将得到一个结果。常量和变量是最简单的表达式,其结果就是常量和变量的值。通过运算 符把一个或多个操作数组合起来可以构造各种表达式。程序通过计算表达式完成对数据的 处理。 C++语言提供了丰富的运算符(又称操作符)以实现各种运算功能,主要包括算术运算 符、关系运算符、逻辑运算符、位运算符、赋值运算符等。除此之外,还有一些用于完成特殊任 务的运算符。在第5章中,还会看到表达式可以包括函数调用,即把函数调用的返回值作为操 作数参与运算。 按照操作数的数量,可以把运算符分为一元运算符、二元运算符、多元运算符。例如,对于 取地址符号(&)有一个操作数,所以是一元运算符;小于符号(<)有两个操作数,所以是二元 表达式 第3章 3 5 运算符。函数调用这种特殊的运算符对操作数的数量没有限制。 同一运算符在不同的场景下有不同的含义。例如,对于星号(*),通常表示乘法运算(二 元运算符),当用于指令运算时又表示解引用运算(一元运算符)。其实,对于这两种情况,可以 看作两个完全不同的运算符。 3.1.2 优先级和结合律 表达式与操作数之间可以通过运算符进一步组合,构成更为复杂的表达式,称为复合表达 式(compoundexpression)。复合表达式是指含有两个和多个运算符的表达式。例如: 16+9.8*15 计算复合表达式时,运算优先级和结合律共同决定了运算对象的组合方式,运算符的优先级 规定了不同运算符出现在同一表达式中优先运算的级别,而结合性质则规定了同等优先级的运 算符出现在同一表达式中运算的顺序。高优先级运算符的运算对象要比低优先级运算符的运算 对象更加紧密地组合在一起。如果优先级相同,则组合规则由结合律确定。其中,右结合是指对 于同等优先级的操作符从右至左依次计算,而左结合则表示从左至右依次计算。 附录A 的“C++运算符的优先级和结合性”给出了各种运算符的计算优先级和结合性质, 计算机计算表达式时总是按照该表的规定进行的。例如,乘除法优先级高于加减法,则意味着 乘除法的运算对象会比加减法先进行运算。加、减、乘、除等算术运算符满足左结合律,这意味 着如果运算符的优先级相同,将按照从左到右的顺序组合运算对象。例如: 根据运算符的优先级,表达式1+2*3的值是7,而不是9; 根据运算符的结合律,表达式10-5-3的值是2,而不是8。 尤其注意,括号无视优先级和结合律,表达式中括号括起来的部分被当成一个整体先求 值,然后再与其他部分按优先级和结合律进行组合。例如: cout << (6+3) * (4/2) << endl; //输出为18 cout << 6+3 * (4/2) << endl; //输出为12 编程风格建议:当表达式较为复杂,或者对于运算优先级和结合律不太确定时,应该考虑 多使用括号,一方面保证表达式的正确性,另一方面提高程序的可读性,方便他人理解程序的 功能。 3.2 运算符和表达式 下面介绍具体的运算符和表达式。根据组成表达式的运算符的不同,表达式可以分为算 术表达式、关系表达式、逻辑表达式、位运算表达式、逗号表达式和赋值表达式等。 3.2.1 算术运算 1.基本的算术运算 基本的算术运算符主要有以下5种。 ● +:加法,运算结果为两个操作数的和。 ● -:减法,运算结果为左操作数减去右操作数的差。 C++程序设计(第4 版) 3 6 ● *:乘法,运算结果为两个操作数的乘积。 ● /:除法,运算结果为左操作数除以右操作数的商。 ● %:求余,运算结果为左操作数除以右操作数的余数。 这5种运算符都是二元运算符,要求有两个操作数,即左操作数和右操作数。单独的常量 和变量是表达式,任何表达式又可以成为操作数。表达式和运算符可以构成更复杂的表达式。 在表达式中,运算符之间的优先关系与数学上的规定类似。 (1)括号中的表达式优先级最高,有括号嵌套时,内层括号优先于外层括号,由内层向外 层求值。 (2)乘法、除法和求余运算优先级次之,三者的优先级相同。如果表达式中有连续的乘 法、除法和求余运算,则遵循左结合原则,从左算到右。 (3)加法和减法优先级较低,二者优先级相同。如果表达式中有连续的加法和减法运算, 也遵循左结合原则,从左算到右。 数据类型包括两方面的含义:值的集合和数据上的运算。上述运算符中,+、-、*和/可 以施加于整数和浮点类型上,分别代表整数和实数上的加、减、乘、除运算,其结果类型与操作 数的类型相同。求余运算%只用于整型数据,其结果也是整型。 注意,运算符/用于整数和实数的含义是不一样的(即整数和实数的除法运算都用同一个 运算符号/来表示,这种现象称为重载)。两个实数做/运算,其结果为实数;两个整数做/运算, 其结果为整数,其具体结果取决于C++在具体机器上的实现:一般来说,大多数实现都采取 “向零取整”,即直接截去商的小数部分。 例如: 5/3 结果为1 -5/3 结果为-1 5/2 结果为2 -5/2 结果为-2 整型数据的求余运算%需注意,其运算结果在不同C++的实现中也不一样。C++规定, 如果两个操作数都是非负的,那么结果一定是非负的;否则,要看C++的具体实现,C++标准 对这种情况未进行明确规定,例如在MicrosoftC++实现中,求余运算%的结果总是与左操作 数的符号一致。这样,整除运算和求余运算的关系是: e1 % e2 = e1 - (e1/e2) * e2 下面是一些例子。 11/3 结果为3 11%3 结果为2 -11/3 结果为-3 -11%3 结果为-2 11/-3 结果为-3 11% -3 结果为2 -11/-3 结果为3 -11% -3 结果为-2 对于+、-、*和/来说,如果两个操作数的类型不同,C++会对操作数做隐式类型转换, 使得两个操作数具有相同的数据类型,详见后面的3.3节。具体做法是,总是将类型较低的操 作数转换成较高的数据类型,运算结果也具有较高的数据类型。例如: 5/2 结果为整型的2 5/2.0 先将5 转换成double 类型,结果为double 类型的2.5 表达式 第3章 3 7 【例3-1】 温标转换。摄氏温标和华氏温标是目前国际使用最为广泛的两种计量温度的 标准。摄氏温标是1740年瑞典人摄尔修斯(Celsius)提出的,在标准大气压下以水作为测温物 质,以C为表示符号。华氏温标是1714年德国人华伦海特(Fahrenheit)创立的温标,以水银 作为测温物质,用F为表示符号。摄氏温标转华氏温标的计算方法是: F=9÷5×C+32 由此,如下编写温标转换程序。 //ex3_1.cpp:把摄氏温标转换成对应华氏温标 #include using namespace std; int main() { float C, F; cout << "Input the temperature with Centigrade:"; cin >> C; F = 9.0 / 5 * C + 32; //温标转换 cout << "The Fahrenheit value is:" << F << endl; return 0; } 程序运行结果: Input the temperature with Centigrade:37.2 The Fahrenheit value is:98.96 本程序的实现思路较为直接,C和F是两个浮点类型的变量,分别表示对应的摄氏温度和 华氏温度,读入要转换摄氏温度后,参照计算公式通过一个复合表达式实现求解,然后输出 结果。但 在这个程序中,需要特别注意的是,如果表达式跟计算公式一致(即F=9/5*C+32), 由于除法和乘法优先级相同,并且是左结合的,所以会先执行除法。对于表达式“9/5”,由 于被除数和除数都是整数,所以执行的是整数除法,即结果为整数1,而不是意料之中的1.8, 所以这种写法是错误的。为了解决这个问题,上述代码使用的是“F=9.0/5*C+32”,由于被 除数是浮点数,按照类型转换的原则和结果(见3.3节),此时会执行浮点除法,得到期望 值1.8。 2.其他算术运算 除了上面介绍的二元运算符,C++ 还有一元运算符,这些运算符只有一个操作数。如取 正运算+和取负运算-,一元取正运算+和二元加法运算的符号是相同的,取正运算的结果就 是操作数本身,而一元取负运算的结果则是操作数的负数。 C++还有两个使用起来非常方便、灵活的算术运算符:自增运算符++和自减运算符--。 这两个运算符都是一元运算符,只有一个操作数,而且该操作数必须具有左值性质(即该操作 数具有对应的内存地址,其值可以被修改)。它们的功能相对复杂一些,而且++和--出现 在操作数之前和之后具有不同的功能,具体见表3-1。 C++程序设计(第4 版) 3 8 表3-1 算术运算符++和--的功能运算符名称示例 运算符名 称示 例说 明 ++ 前自增++a 将a加1,a增加后的新值为运算的结果 ++ 后自增a++ 将a加1,a增加前的旧值为运算的结果 -- 前自减--a 将a减1,a减少后的新值为运算的结果 -- 后自减a-- 将a减1,a减少前的旧值为运算的结果 自增运算和自减运算的优先级相同,它们都比括号的优先级低,但比加法、减法、乘法、除 法和求余运算要高。下面给出几个示例,假设变量a的当前值为5,那么有 ++a + 12 结果为18(运算后a 为6) a++ + 12 结果为17(运算后a 为6) --a * 12 结果为48(运算后a 为4) a-- * 12 结果为60(运算后a 为4) 以上代码确实令人困惑,实际程序中很少出现这类代码。自增、自减运算一般单独使用, 不用在复合表达式中。 自增、自减运算符提示:虽然传统的C语言程序员经常使用后自增、后自减,但除非必要, 一般情况下优先考虑使用前置版本,即前自增和前自减。首先是消除不必要的歧义,通常使用 自增、自减运算的初衷是把操作数加1或减1,不会使用自增、自减运算之前的结果。其次,编 译器为了保存自增、自减运算之前的结果,需要付出一定的开销,对于整数等简单类型来说代 价不大,但对于后面介绍的基于面向对象方法实现的迭代器等类型来说,这个额外的消耗成本 是很大的。 3.2.2 关系运算 现实世界中通常需要对数据进行比较,为此C++也提供了一组关系运算符,以实现对数 据进行关系比较,包括<、>、<=、>=、==和!=等,它们分别计算数据的小于、大于、小于 或等于、大于或等于、相等和不相等关系。关系运算的结果反映了操作数的大小关系,为布尔 类型,即true和false。下面是一些关系运算表达式示例。 1 <= 0 结果为false x != x+1 结果总为true x > y 结果表示x 是否大于y 需要注意,“相等比较”运算符是连续两个等号(即==),对于<=、>=、==和!=,这些 运算符都是由两个字符组成,书写时两个字符间不能加空格。 3.2.3 逻辑运算 前面的关系运算能够得到布尔型的结果,即true和false。但是,仅有这些简单的条件测 试不能满足现实世界中复杂的逻辑计算问题。例如,当需要判断各种复杂的组合条件时,还必 须提供逻辑运算的能力。 C++提供了3种逻辑运算符:二元逻辑与运算符(&&)、二元逻辑或运算符(||)和一元 逻辑非运算符(!)。表3-2是表示3种逻辑运算的真值表。 表达式 第3章 3 9 表3-2 逻辑运算的真值表 a b a&& b a||b !a true true true true false true false false true false false true false true true false false false false true 通过这3种逻辑运算符的组合,可以构造出非常复杂的条件,以实现强大的逻辑测试功 能。3种逻辑运算中,逻辑非(!)的优先级最高,逻辑与(&&)次之,逻辑或(||)最低。在结合 性质上,逻辑非(!)是右结合,而逻辑与(&&)和逻辑或(||)为左结合。关系表达式是一种基 本的逻辑表达式。下面是一组逻辑表达式的例子。 (grade >= 60) && (grade <= 70) 表示测试grade是否在60和70之间(包括60和70)。 在计算逻辑表达式时,C++语言遵循一种所谓的“短路计算法”,即如果按照逻辑运算符 的优先顺序和结合性质,只要计算部分表达式就可以确定整个表达式的结果,就不再计算表达 式的剩余部分。以上面的表达式为例,计算(grade>= 60)&& (grade<= 70)时,如果 grade是小于60的,&& 左边的子表达式(grade>= 60)的结果为false,此时无须计算右边 的子表达式(grade<=70),就可以知道整个表达式为false。对于a&&b来说,只有当计算 了a得到的结果为true时,才需要计算b的值,以确定整个表达式的值。与此类似,对于逻辑 或运算(||),只有当左操作数的结果为false时,才需要计算右操作数的值,该值就是整个表达 式的结果。 【例3-2】 判断闰年:用户输入一个年份,编程判断该年份是否为闰年。 满足闰年的条件是:年份是4的倍数,但不是100的倍数,或者是400的倍数。判断年份 是否是某数倍数(即整除),则可以用年份对该数做求余运算,如果余数为0,则是倍数关系。 然后再通过逻辑运算符把这些条件组合起来进行判断。 由此,如下编写判断闰年程序。 //ex3_2.cpp:判断是否为闰年 #include using namespace std; int main() { int year; cout << "请输入一个年份:"; cin >> year; if (((year % 4) == 0) && ((year % 100) != 0) || ((year % 400) == 0)) cout << "该年份是闰年." << endl; else cout << "该年份不是闰年." << endl; return 0; } C++程序设计(第4 版) 4 0 下面是两种可能的运行结果: 请输入一个年份:2020 该年份是闰年. 请输入一个年份:2022 该年份不是闰年. 测试用户输入的年份保存在变量year,然后再通过下面的表达式判断是否是闰年。 ((year % 4) == 0) && ((year % 100) != 0) || ((year % 400) == 0) 若表达式结果为true,则满足闰年条件。其中出现了求余运算、关系运算和逻辑运算等多 种运算符。初学C++时,如果不确定运算的优先级,可以参照示例代码,多使用括号以确保正 确性。比较熟练之后,有些括号可以省略。本例还使用了if-else选择结构,关于选择结构详见 4.3节。此外,从完整性角度出发,本例应该要判断用户输入年份取值的合法性。 需要说明的是,C 和C++ 程序在做逻辑判断时,可以把任意非零值当作true(全零为 false)。所以,如果在C和C++程序中看到类似下面的代码,不要奇怪。 double x; …i f (x) //x≠0 cout << 1 / x << endl; 上述程序的判断条件是,如果x≠0,则条件为“真”,计算并输出x的倒数。 从C++11标准开始,C++新引入了关键字and、or、not,也可以实现逻辑与、或、非运算, 即其作用等同于运算符&&、||、!。例如: (grade >= 60) and (grade <= 70) 也是判断grade是否在60和70之间(包括60和70)。 3.2.4 位运算 位运算是C语言的重要特色之一,C++保留了这一特色。位运算允许在二进制位级别上 对数据进行处理。各种位运算符说明如表3-3所示。 表3-3 各种位运算符说明 运 算 符名 称示 例说 明 & 按位与 a&b a和b的每一位做与运算 | 按位或 a|b a和b的每一位做或运算 ^ 按位异或a^b a和b的每一位做异或运算 ~ 按位取反~a 将a的每一位取反 << 向左移位a<< b 将a的每一位向左移b位 >> 向右移位a>> b 将a的每一位向右移b位 这些运算符中,除了~是一元运算符以外,其余的都是二元运算符,操作数都只能是整型 表达式 第3章 4 1 或字符型数据,不能为浮点型数据,结果也为整型或字符型。 1.按位与(&) & 运算结果的每一位是两个操作数二进制表示的对应位进行与运算的结果,即如果两个 操作数的对应位都为1,则结果的对应位也为1,否则为0。 例如,3&14的结果为2。具体计算过程如下。 3 的二进制表示: 00000011 14 的二进制表示: 00001110 3 & 14 结果的二进制表示: 00000010 2.按位或(|) |运算结果的每一位是两个操作数二进制表示的对应位进行或运算的结果,即如果两个操 作数的对应位都为0,则结果的对应位也为0,否则为1。 例如,3|14的结果为15。具体计算过程如下。 3 的二进制表示: 00000011 14 的二进制表示: 00001110 3 | 14 结果的二进制表示: 00001111 3.按位异或(^) ^运算结果的每一位是两个操作数二进制表示的对应位进行异或运算的结果,即如果两个 操作数的对应位不相同,则结果的对应位为1,否则为0。 例如,3^14的结果为13。具体计算过程如下。 3 的二进制表示: 00000011 14 的二进制表示: 00001110 3 ^ 14 结果的二进制表示: 00001101 4.按位取反(~) ~运算结果的每一位是操作数二进制表示的对应位进行取反运算的结果,即如果操作数 的对应位为0,则结果的对应位为1,否则为0。 例如,14 的二进制表示: 00001110 ~14 结果的二进制表示: 11110001 5.向左移位(<<) <<运算将左操作数的二进制表示向左移位,移动的位数就是右操作数的值,右端移出的 空位填充0,移位后的左操作数的值即为运算的结果。 例如,3<<5的具体计算过程如下。 3 的二进制表示: 00000011 3 << 5 的结果: 01100000 6.向右移位(>>) >>运算将左操作数的二进制表示向右移位,移动的位数就是右操作数的值,移位后的左 操作数的值即为运算的结果。左端移出的空位填充方式取决于左操作数的类型和具体的值: 如果左操作数是无符号类型,或者是有符号类型但其值非负(最高位为0),那么高位填充0;如 C++程序设计(第4 版) 4 2 果左操作数是有符号类型,并且为负数(最高位为1),那么高位填充的值取决于所用的计算机 系统,有的C++实现填充0,有的填充1。 例如,-7>>5的具体计算过程如下。 -7 的二进制补码表示: 11111001 -7 >> 5 的结果: 00000111 (填充0) -7 >> 5 的结果: 11111111 (填充1) 对于&、|和^等二元位运算,如果参与位运算的操作数类型不同,系统将二者按右端对齐,并 根据操作数的类型和值进行填充,使得两个操作数位数完全一样。例如,如果a是short,占2字 节,b是int,占4字节,则系统将按照a的符号位扩展成4字节,即如果a非负,则用0扩展;如果 a为负数,则用1扩展。如果需扩展的操作数是无符号类型,则总是用0扩展。 位运算符的优先顺序为:按位取反~的优先级最高,向左移位<<和向右移位>>次之, 然后依次是&、^和|。 位运算为程序员提供了非常灵活的、直接对二进制位进行处理的手段。通过各种位运算 的合理组合,可以实现很多有趣的功能,位运算的常见组合如表3-4所示。 表3-4 位运算的常见组合 功 能表 达 式说 明 逐位清零a&0 将a的每一位清零 取指定的二进制位a&0X00FF 取a的低位字节 设置指定位a|0X00FF 将a的低位字节的每一位设置为1 指定位翻转a^0X00FF 将a的低位字节的每一位置翻转,0变1,1变0 乘以2的n次幂a<< n a乘以2n 除以2的n次幂a>> n a除以2n 需要注意,一种常见错误是将位运算和逻辑运算搞混。例如,将位与(&)和逻辑与 (&&)、位或(|)和逻辑或(||)、位求反(~)和逻辑非(!)搞混。 此外,C++11标准新引入了关键字bitand、bitor、xor、compl,也可以分别实现按位与、按 位或、按位异或、按位取反运算。 下面通过解决一个现实问题来演示C++位运算的应用。常见的数字图像有彩色图像和 灰度图像两种。彩色图像一般使用RGB颜色模型,其中R(红色)、G(绿色)、B(蓝色)3个分量 的取值是[0,255]内的一个整数。如果用一个32位的无符号整数来表示一个像素的颜色,其 二进制格式是00000000BBBBBBBBGGGGGGGGRRRRRRRR,即31~24位取0,23~16位 为蓝色分量,15~8位为绿色分量,7~0为红色分量。例如,RGB格式的颜色值4292863,对 应的二进制格式是00000000010000011000000011111111,所以该颜色的B分量是65,G 分 量是128,R分量是255。而灰度图像是把白色与黑色之间按对数关系分为若干等级,灰度取 值是[0,255]内的一个整数。 对于把RGB格式的彩色转灰度,有一个著名的心理学公式: Gray=R×0.299+G×0.587+B×0.114 现在编程实现把RGB彩色图像转换成灰度图像功能的程序,用户输入一个由RGB颜色 表达式 第3章 4 3 模型的无符号整数,程序输出对应的灰度值(也是无符号整数)。 问题分析:在该问题中,用户输入的颜色值是一个32位的无符号整数,关键是如何按照 RGB颜色模型的格式分别提取出红色分量、绿色分量和蓝色分量。根据二进制位数分配,最 直接的方法可通过二进制位运算实现提取3个分量值,例如把颜色值与255(换算成二进制, 低8位全为1,其余位为0)进行位与运算,即可提取出红色分量。对于绿色分量,可以把颜色 值向右移动8位,即把15~8位移动到7~0位,再与255进行位与运算,得到结果。蓝色分量 类似处理。然后,剩下的转换按照公式直接计算即可。 【例3-3】 将彩色颜色值转换成灰度值。 //ex3_3.cpp:把按RGB 格式输入彩色颜色值转换成灰度值 #include using namespace std; int main() { unsigned int color; unsigned short R, G, B, gray; cout << "Input the color using RGB pattern:"; cin >> color; R = color & 255; //提取红色分量 G = (color >> 8) & 255; //提取绿色分量 B = (color >> 16) & 255; //提取蓝色分量 gray = (unsigned short)(R * 0.299 + G * 0.587 + B * 0.114); cout << "The corresponding gray value is " << gray << endl; return 0; } 程序运行结果: Input the color using RGB pattern:4292863 The corresponding gray value is 158 由于红、绿、蓝3个分量和灰度值都小于256,所以第8行声明为unsignedshort类型的变 量,第11~13行是按上述分析分别计算提取3个分量值,第14行按心理学公式计算得到的是 一个浮点数,所以还需要把结果强制转换为unsignedshort类型。 需要说明的是,提取红、绿、蓝3个分量的方法还有其他做法。例如,以256为进制单位来 看待RGB颜色值,把颜色值对256求余可以得到红色分量(color% 256),把颜色值除以256 后再对256求余可以得到绿色分量(color/256%256),蓝色分量类似可得。 3.2.5 赋值运算 1.赋值运算 赋值运算实现了对变量的赋值,即为已声明的变量赋给一个特定值。其功能是:先将赋 值运算符右边表达式的值计算出来,将该值赋给赋值运算符左边的变量,并将该值作为赋值运 算的结果。因此,赋值表达式a=3+5表示将3加5的和8赋给变量a。 需要特别指出的是,C++的赋值运算符除了对变量进行赋值以外,作为一种运算符,还具 有运算的结果,这是C++与很多其他程序设计语言不同的地方。对于赋值表达式a=3+5,整个 表达式的计算结果也是8。所以,可以连续使用赋值运算符。例如: C++程序设计(第4 版) 4 4 a = b = c = 33 + 67; 赋值运算符比大多数运算符的优先级都要低,且具有右结合性质,即相邻的赋值运算符从 右算到左。所以,上述示例代码执行结束后,变量a、b和c的值都是100。 赋值运算符“=”是二元运算符,也就是说有两个操作数。赋值运算符和这两个操作数一 起构成了赋值表达式,其中左边的操作数必须具有左值的表达式。 左值(lvalue)和右值(rvalue)是来源于C语言的概念,简单来说,“=”左边的是左值,“=” 右边的是右值。当一个数据对象被用作右值时,用的是数据对象的值(内容),当数据对象被用 作左值时,用的是数据对象的身份(数据在内存中的位置)。 例如,设i是一个整型变量,对于表达式i=i+1,右边的i用的就是其右值,左边i用的就 是其左值,具体执行过程是:先从内存中读出i的取值(右值),把该值复制一份,与整数1做加 法运算,运算的结果根据i的内存地址(左值)存储到内存对应的位置。 新手尤其需要注意上述赋值表达式,它是一种赋值运算,最后的结果就是变量i存储的值 加一,不要与相等判断运算混淆,相等判断是逻辑运算符“==”。 需要指出的是,如果赋值运算中,赋值运算符两边的变量和表达式的类型不同,C++先隐 含地将表达式的结果转换成变量的类型,再将转换的结果赋给变量。当然也可以显式地转换 表达式的值的类型。因此,赋值运算的结果的类型总是与赋值运算符左边的变量的类型一致。 例如,如果a为浮点变量,b和c为整型变量,那么a=b+c计算过程为:先对整型变量b与c 的值进行整数求和,然后将结果转变成浮点类型,再赋给浮点型的变量a,同时,该浮点值也是 整个表达式的值。下面的表达式具有同样的效果。 a = (float)(b + c) 3.3节将会进一步介绍类型转换的功能。 2.复合赋值运算 程序中经常需要对变量进行某种运算,然后把计算的结果再赋给该变量。这种复合操作 可以通过复合赋值运算进行简化。复合赋值运算符包括*=、/=、%=、+=、-=、>>=、 <<=、&=、^=、|=。 注意,复合赋值运算符的两个字符之间不能留空格。复合赋值运算符的使用,都可以概括 为下面的模式。 变量操作符= 表达式 其功能相当于 变量= 变量操作符表达式 具体操作是:先计算赋值运算符右边的表达式的值,将作为左操作数的变量的当前值与 该值做操作符代表的运算,运算的结果复制给变量并作为整个表达式的结果。例如,假设a为 整型变量,当前值为10,那么a+=18/3的计算过程为:先计算18除以3的整商,得到整数 6,将变量a的当前值10与6求和,得到16,再将16赋给变量a,整个表达式的结果也是16。 在这个过程中,a的值被使用了两次,第一次使用a的右值,第二次使用a的左值。 此外,C++11标准新引入了关键字and_eq、or_eq、xor_eq,功能分别与复合赋值运算符&=、 表达式 第3章 4 5 |=、^=等价。 3.变量初始化 在2.3.2节介绍过,C++ 允许在声明变量时通过“=”对变量进行初始化,给变量定初始 值。这个初始值可以是一个表达式,但是要求这个表达式的值在编译时是可计算的,即要求该 表达式的值是一个常量。下面是一些合法的带初始化的变量声明。 int i = 2; double x = i + 1.0; double a = i * x; 经过声明和初始化后,变量i的值为2,x的值为3.0,a的值为6.0。 在C++新标准中,允许使用花括号括起来的初始化值列表。例如: double pi = {3.14}; int array[5] = {1, 2, 3, 4, 5}; 其中,array是一个数组,初始化的结果是数组中5个元素值依次取1、2、3、4、5(详见第6章)。 3.2.6 逗号运算 C++提供了一种特殊的运算符———逗号运算符(,)。该运算符将两个表达式连接起来构 成逗号表达式。逗号运算符是一个二元运算符,具有左结合性质,其优先级比前面介绍的运算 都要低。其计算过程是:先计算逗号左边的表达式,再计算逗号右边的表达式,并且将右边的 表达式的计算结果作为整个表达式的结果。例如: 12 + 4, 3 * 5 表达式的结果为15。 12 + 4, 3 * 5, 4 - 1 表达式的结果为3。 大多数情况,使用逗号表达式的目的是为了顺序计算多个表达式的值,而并非一定要获得 逗号运算的结果。第4章将会看到逗号表达式常用于for循环语句中。 3.2.7 条件运算符 条件运算符是一个三目运算符。该运算的一般形式如下: <表达式1> ? <表达式2> : <表达式3> ; 条件运算符的含义是:先计算<表达式1>的值,如果为true(非0),则计算<表达式2>的 值,并把该值作为整个表达式的值;如果表达式1的值为false(为0),则直接计算<表达式3>的 值,并把它作为整个表达式的值。 例如,假设浮点型变量grade的值为70,result为字符型变量,表达式 result = grade >= 60 ? 'P' : 'F' 的计算过程是:计算条件表达式grade>=60的值,结果为true,所以直接将'P'作为赋值运算 符右边表达式的值,并赋给result,该值也是整个表达式的计算结果。可见,利用条件运算符, 可以根据条件完成不同的计算。 条件运算符提示:条件运算表达式允许嵌套使用,即<表达式2>和<表达式3>也可以 C++程序设计(第4 版) 4 6 是一个条件运算表达式,但是随着嵌套层数的增加,代码的可读性极具下降,所以一般建议嵌 套不超过两层。更复杂的条件判断嵌套应该使用第4章介绍的选择结构。 3.2.8 sizeof运算符 sizeof运算符返回一个表示式结果值或者一个类型名字所占的字节数,所得到的值是一 个类型为size_t的无符号整数。例如: cout << sizeof(3 + 5) << endl; //输出结果为4 cout << sizeof(int) << endl; //输出结果也为4 需要说明,sizeof运算符用于一个表达式时,并不实际计算该表达式的值,只需确定表达 式结果的类型。 3.3 类型转换 在表达式求解过程中,参与运算的操作数的类型可能不完全相同,所以常常需要由一种类 型转换成另一种类型。C++允许不同类型的数据进行转换,即可以将一种数据类型的数据转 换成另一种类型的数据。 但是,由于各种数据类型在表示范围和精度上是不同的,所以有的转换不会丢失数据的精 图3-1 数据类型的排序 度,而有的转换则会丢失数据的精度。例如,将int类型数据 转换成double类型,不会导致数据的改变,而将double类型 的数据转换成int类型,则会截去double的小数部分,从而可 能改变数据的值;与此类似,将大的整数类型变为较小的整数 类型,如long转换成short,也可能改变数据的值。为此,C++ 规定了一个“提升规则”,说明如何保证当一种数据类型转换为 另一种数据类型时不会丢失数据的精度。C++按照各种数据 类型的表示范围和精度,将各种数据类型由高到低进行排序, 如图3-1所示。 类型转换是指把一种类型的数据转换成另一种类型的数 据,将数据转换成较低的类型可能导致取值不正确。C++的类型转换有两种:隐式类型转换和 强制类型转换。C++语言要求,如果要将数据转换成较低的类型,必须显式地使用强制类型 转换;如果将数据转换成较高的类型则可以通过隐式类型转换。 1.隐式类型转换 隐式类型转换由系统自动隐含地进行。当表达式中操作数据的类型不同时,要进行隐式 类型转换,使表达式中的数据类型相同。例如,在算术表达式和赋值表达式中类型不同时,就 进行隐式类型转换。 下面是算术表达式中隐式类型转换规则。 (1)表达式中如有char、short和enum 类型的数据时,自动将它们转换成int类型。 (2)把表达式中不同类型的数据转换成精度最高、占用内存最多的那个数据的类型。例 如,3.14/2,由于被除数是浮点数,其精度比除数要高,所以在执行除法运算之前,首先把除数 转换为浮点数,然后再执行浮点数的除法。 表达式 第3章 4 7 注意,正如3.2.5节的赋值表达式的介绍,自动将赋值运算符右边表达式值的类型转换成 左边变量的类型,这时如果左边变量类型的精度低于右边表达式值的类型时,可能会丢失数据 的精度,虽然这种转换是由系统自动进行的。 2.强制类型转换 强制类型转换又称显式类型转换,它把表达式值的类型强制转换成指定的类型。早期的 C++语言使用C风格的强制类型转换,其一般形式如下: (<目标数据类型>) <原数据类型的数据> 例如: (int) 3.14 将3.14转换成整数3。 在这种方式中,类型转换也可以看作一种单目运算符,其优先级比乘、除运算符要高。 C++语言也可以使用如下的类型转换形式。 <目标数据类型> (<原数据类型的数据>) 例如: double (3) 将3转换成双精度浮点数3.0。 这种方式把类型转换当成函数使用。两种类型转换方式等价。再看以下示例。 (double)3/2 或 double(3)/2 是先把整数3强制转换成双精度类型,再把2隐式转换成双精度类型,最后得到的值是双精度 数1.5。如果修改括号的位置,把上式写成 (double)(3/2) 或 double(3/2) 则先计算3/2得到整数值1,再把1转换成双精度类型1.0。 3.命名类型转换 在类型转换(主要是强制类型转换)中,由精度高、占用内存多的数据类型转换成精度低、 占用内存少的数据类型时,不仅改变数据的类型,也可能改变其值,所以使用类型转换时要非 常小心。 C++ 语言为了进一步强调类型转换的风险,使得问题追溯更加方便,从C++11标准开 始,为强制类型转换专门引入了4 个关键字:static_cast、const_cast、reinterpret_cast和 C++程序设计(第4 版) 4 8 dynamic_cast,称为命名类型转换,其基本形式如下: xxx_cast < <目标数据类型> > (<原数据类型的数据>) 例如: double score = 95.6; int n = static_cast(score); 这两行代码的作用是,先初始化浮点变量score为95.6,然后读取其值,构造一个副本,转换为 整数95(注意不是四舍五入),用该整数值初始化整型变量n。所以,static_cast与之前介绍的 强制类型转换功能基本一致。但是static_cast要比传统强制类型转换更加严格,使用时编译 器会对转换过程的安全性进行检查,尤其是对于后面将介绍的指针转换和对象之间的转换 来说。其 他3种转换各有其适用场景,基本情况如表3-5所示。 表3-5 命名类型转换说明 关 键 字说 明 static_cast 用于风险较低的良性转换,一般不会导致意外发生 const_cast 用于const与非const、volatile与非volatile之间的转换 reinterpret_cast 最危险也是最灵活的类型转换,仅仅是对二进制位的重新解释,不会借助已有的转换规 则对数据进行调整 dynamic_cast 主要用于C++类层次间之的上行转换和下行转换 3.4 表达式语句 表达式语句是C++最基本的语句。在任何表达式之后加上分号“;”,就是一个表达式语 句。表达式语句的形式如下: 表达式; 表达式语句的功能是计算分号前表达式的结果,但该计算的结果并没有被再利用。需要 特别说明的是,分号是表达式语句的组成部分。 常用的表达式语句是赋值语句和具有返回值的函数调用语句,其中赋值语句就是赋值运 算表达式构成的语句,而函数调用语句就是由函数调用构成的语句,第5章中将介绍。 注意:C++允许仅以分号“;”构成一个语句,这种语句称作空语句。空语句仅起标识作 用,不做任何事情,一般起到占位作用。 下面是一些表达式语句的示例。 a = a + 3; //赋值语句 x = y = z = 0; //多重赋值语句 t = 2, t + x + a; //逗号表达式语句 z = i #include using namespace std; int main() { double a, b, c; //存放3 个系数 double root1, root2; //存放2 个实根 cout << "Please input three coefficients in order: "; cin >> a >> b >> c; double delta; delta = b * b - 4 * a * c; root1 = (-b + sqrt(delta)) / (2 * a); root2 = (-b - sqrt(delta)) / (2 * a); cout << "Two real roots are: " << root1 << "\t" << root2 << endl; return 0; } 程序运行结果: Please input three coefficients in order: 1 -5 6 Two real roots are: 3 2 因为用到开根号函数,所以程序引入了数学库cmath。需要说明的是,这里并没有考虑Δ 小于或等于0和系数a等于0这些异常情况。如果要改进这一问题,需要引入第4章的判断 选择语句。 习 题 3.1 将下列数学表达式改写为程序表达式。 (1) cosx a+b+c (2) (x+y)2+(x-y)2 (3)sinα/cosβ+tanδ (4)|x-y| z+x 3.2 假设a=4,b=10,c=2,计算下面算术表达式的值。 (1)a+b*c/(a+c)%3/a (2)(float)(a+c)/3+(b-a)%a C++程序设计(第4 版) 5 0 (3)a= b= (c=a+=6) 3.3 假设x=3,y=10,z=12,判断下面关系表达式或者逻辑表达式的真假。 (1)x-y>y-z (2)x<=y&&(x>0)||(y>0) (3)!(x-y)>0||(y-z>0) (4)x+z==y||z-x<0 3.4 执行下列语句后,3个变量a、b、c的值各为多少? int a, b, c; a=20; b=++a; c=a++; 3.5 若x=3,y=2,z=1,下列各式的结果是什么? (1)x|y&z (2)x|y-z (3)x^y&-z (4)x<<=2 (5)y<<2 3.6 若x=1,y=-1,下列各式的结果是什么? (1)!x|x (2)~x|x (3)x^x (4)x<<=2 (5)y<<2 3.7 设计一个程序,测试你的计算机是如何处理下面的移位运算的: (1)如果向左移位运算>>和向右移位运算<<的右操作数是负数,结果是什么? (2)如果向右移位运算<<的左操作数是有符号类型,并且为负数(最高位为1),高位填 充的值是0还是1? 3.8 写出下面表达式运算后a的值,设原来a的初始值为12。 (1)a+=a (2)a-=2 (3)a*=2+3 (4)a/=a+a (5)a%=(n%=2),n的值等于5 (6)a+=a-=a*=a 3.9 先分析下面程序的代码,得出其预期运行结果,然后上机实际运行程序,对比验证与 分析的结果是否一致。 #include using namespace std; int main() { int a = 3, b = 7, c, d; c = ++a+b++; d = (++a)+(++b); cout << a << '\t' << b << '\t' << c << '\t' << d << endl; return 0; } 3.10 设计一个计算体重指数BMI(BodyMassIndex)的程序,BMI是体重(kg)除以身高 (m)的平方,从键盘输入一个人的体重和身高,计算其BMI指数并输出。 3.11 设计一个程序,从键盘输入一个圆的半径,求其周长和面积。 3.12 设计一个程序,从键盘输入一个圆柱底面圆的半径、圆柱的高,求其表面积和体积。 3.13 设计一个程序,从键盘输入一个3位正整数,程序逆序输出该整数。例如,若输入 123,则程序输出321。 3.14 设计一个程序,输入一个整数,判断是否同时满足除3余2、除5余3和除7余2,如 果满足则输出Yes,否则输出No。 表达式 15 设计一个程序,从键盘输入以秒数表示的时间段,然后换算成以天、小时、分钟和秒 的组合方式来表示这个时间段,并打印输出。 3.785L) il 3. 16 美国汽车的油耗量指标是以1加仑(等于3.燃油的行驶里程(以me为单 位,100km等于62.e)来表示,单位为mpg(n),中国的汽车油耗量指标一 14milmilespergalo 般是行使100km消耗的燃油量,单位为L/100km 。所以,7L/100km 。设计一 27mpg约合8. 个程序,用户输入美国标准的汽车油耗量指标,计算转换为中国标准的汽车油耗量,并输出 结果。 第 章 51