第4章 语句和表达式
本章包括
◆ 语句和语句块的概念
◆ 表达式中的各种运算符
◆ 类型转换
◆ 表达式的概念
◆ 运算符的优先级和结合性
通过上一章的学习,读者已经了解了在程序中如何存取数据,这一章我们来学习如何处理数据。在程序中,数据处理是通过语句和表达式完成的。一条语句或一个表达式就是数据处理的一个步骤,多个表达式和语句按照一定顺序组合在一起,就构成了一次完整的数据处理过程。
4.1 语句和语句块
语句是数据处理过程中的最小步骤,其标志是结尾处的分号“;”,例如:
int a = 0; int b = 1; int x = 0; ; // 空语句 x = a + b; x = add( a, b ); // add是一个函数,接受两个整型参数
上述这些代码都是语句。请注意第4行并不是一个书写错误,而是表示什么都不做的空语句。虽然空语句没有什么意义,但在C++程序中是允许的。
4.1.1 空格的作用
语句中的空格有的是必需的,而有的则完全是出于美观的目的,例如:
int a = 0;
这是一个变量声明及初始化的语句。其中关键字“int”和变量“a”之间的空格是必需的,否则就构成了一个新的标识符“inta”。而“a”,“=”和“0”之间的空格则仅仅是美观的需要,如果不加空格,写成:
int a=0;
虽然表达的意思是一样的,但看上去不如带有空格那么清楚。下面的语句更加明显地突出了空格的重要性:
x=add(a,b);
4.1.2 语句块的组织
在C++程序中,多个连续的语句可以组成语句块(也称为复合语句)。语句块必须用花括号“{”和“}”包围起来,例如:
double area = 0; { // 语句块开始 double width = 3; double height = 4; area = width * height; } // 语句块结束 cout<<"The area is "<<area<<endl;
语句块可以嵌套,例如:
{ // 块1开始 ⋯⋯ { // 块2开始 ⋯⋯ } // 块2结束 ⋯⋯ } // 块1结束
4.1.3 语句块中的变量
虽然看起来,语句块好像没什么作用,加不加花括号并没什么影响,但实际上语句块限定了变量的作用域(作用范围)。在语句块中声明的变量,其作用范围从声明语句开始,直到语句块结束。一旦超出作用范围,则该变量就不再有效了。
例如,在4.1.2节的例子中,变量width和height的作用范围分别从第3行和第4行开始,直到第6行结束。从第7行开始,就不能再使用这两个变量了,否则会导致编译错误。变量area是在语句块之外定义的,所以area可以在第7行以后使用。
4.2 什么是表达式
所谓表达式就是一个对各种数值(包括对象)进行计算的过程。既然是计算,就会有结果。从这个意义上来说,表达式表达的就是一个数值,而这个数值是经过各种计算得到的。例如“3+2”就是一个表达式,其结果是“5”。
参与计算过程的数值称为操作数,而表示操作的符号称为运算符。一个表达式由一个或多个操作数以及零或多个运算符构成。最简单的表达式由一个字面常量或一个变量构成,例如:
123 'a' Variable
说明
表达式的结尾没有分号“;”。如果带上分号,则构成了一条语句。例如“123”是一个表达式,而“123;”则是由一个字面常量表达式构成的语句。
字面常量表达式的结果是其字面值,而变量表达式的结果就是这个变量的值。通常情况下,表达式中有一个或多个运算符。例如在“3+2”这个表达式中,“+”是一个运算符,表示对操作数“3”和“2”进行相加运算。常见的表达式还有:
PI * radius * radius // 计算圆面积的表达式 1.2 * (baisicSalary + bonus – penalty) // 计算薪水的表达式
如果一个表达式的操作数也是一个表达式,则前者称为复合表达式,而后者称为子表达式。例如,在计算薪水的例子中,表示相乘操作(*)的第二个操作数“(baisicSalary + bonus - penalty)”就是一个子表达式,而整个计算薪水的表达式就是一个复合表达式。
注意
子表达式的计算顺序是由运算符决定的,准确地说是由运算符的优先级和结合性决定的,这将在后文中进行说明。
4.3 运算符分类
C++为了实现各种操作,引入了几十种运算符。如算术运算符(+,-,*,/),求变量内存地址的取址运算符(&),以及比较大小的关系运算符(<,>,==,<=,>=)等。按照运算符作用的操作数个数不同,可以将运算符分为一元运算符、二元运算符和三元运算符。
◆ 一元运算符:如取址运算符(&),其操作数只有一个变量。常见的取址表达式如“&variable”,其结果是变量variable在内存中的地址。
◆ 二元运算符:如算术运算符,每个都需要两个操作数,如“1+2”,“3*4”等。
◆ 三元运算符:在C++中只有一个接受三个操作数的运算符“?:”,常见的表达式如“a>b?a:b”,其含义是如果a大于b,则表达式的结果是a的值,否则是b的值。
按照运算符的操作效果,可以分为算术运算符、赋值运算符、关系运算符、条件运算符、sizeof运算符、位操作运算符等。
4.3.1 算术运算符
算术运算符包括加、减、乘、除(四则运算)运算符(+、-、*、/)和求模运算符(%)。算术运算符是二元运算符,其操作数一般是整数或浮点数(或者是结果为整数或浮点数的表达式)。如果两个操作数都是整数,则运算的结果也是整数,例如:
1 + 2 // 结果是整数3 3 – 4 // 结果是整数-1 5 * 6 // 结果是整数30 6 / 2 // 结果是整数3 5 / 2 // 结果是整数2
在除法操作中,如果商中含有小数部分,则小数将被截掉(舍尾法),如1 / 3的结果是0,5/ 2的结果是2。C++中的除法运算同算术中的除法运算一样,除数都不可为0,否则会导致程序崩溃。在编程时要注意避免除数为0的情况,例如:
int a, b; // a是被除数,b是除数 cin>>a; // 输入被除数 cin>>b; // 输入除数 if ( 0 != b ){ // 如果b不是0,才执行“{}”中的语句 cout<< a / b <<endl; // 输出a/b的值 }
求模运算符(或称取余运算符)%用于计算两个数相除的余数,左边的操作数被右边的操作数除。该运算符只能用来处理整数,包括char,short,int和long型的数。当两个操作数都是正数时,结果为正。但是,如果有一个或两个操作数是负数,则余数的符号取决于编译器的实现,因此可移植性无法保证。求模运算举例如下:
3.14 % 3; // 编译错误,%不能用于浮点数 21 % 6; // 结果是 3 21 % 7; // 结果是 0 21 % -5; // 编译器相关: 结果为 -1或1
4.3.2 算术运算的溢出
在计算机中,数据的取值范围是有限制的,取决于其类型的尺寸,例如short型数据只能取-32768~32767之间的数。如果某些算术运算的结果超出了范围,则会导致数据的“上溢出”或“下溢出”。一旦发生溢出,则其结果是未定义的,例如:
short a = 32767; // 声明短整型变量a,并赋初值为上限 a = a + 1; // 加1,试图超过上限 cout<<a<<endl; // 输出变量的值 unsigned short b = 65535; // 声明无符号短整型变量,并赋初值为上限 b = b + 1; // 加1,试图超过上限 cout<<b<<endl; // 输出变量的值
其输出结果并不是32768和65536。不管实际输出的是什么结果,都是没有意义的,因为C++标准并没有对此做出规定。编程时,依据一种没有定义的行为是很危险的,而且其可移植性无法保证。
4.3.3 赋值运算符
C++的赋值运算符是“=”,这是一个二元运算符,接受两个操作数,其使用方法如下:
a = b;
其含义是将右操作数b的值赋给左操作数a,即用b的值覆盖a的值。右操作数可以是常量,也可以是变量;而左操作数必须是一个左值,即该操作数有一个相关联的、可写的地址值。下面是一个明显的非左值赋值的例子:
1024 = ival; // 错误
一种可能的解决方案为:
int value = 1024; value = ival; // 正确
给const常量赋值也是错误的,例如:
const int value = 1024; // const常量的声明和初始化 value = ival; // 错误,不能给const常量赋值
提示
语句“a=b;”和“int a=b;”是不同的。前者是赋值,而后者是声明一个变量并初始化。初始化与赋值不同,初始化只能发生在变量声明时,而在程序中,一个变量可以多次赋值。
由“=”连接两个操作数,构成一个赋值表达式,其结果值是赋值完成后左操作数的值。例如,在计算表达式“a = 3”时,先将3赋给左操作数a,然后取a的值作为这个表达式的结果值。
4.3.4 自增和自减运算符
自增运算符(++)的目的是简化变量加1的操作,如操作a = a + 1可以简化为:
a++; // 即a = a + 1;
同样,自减运算符(--)的目的是简化变量减1的操作,如a = a -1可以简化为:
a--; // 即a = a – 1;
在C++程序中这种加1和减1的操作是非常普遍的,在以后的编程中将经常看到,所以对这两种操作的简化很有必要。C++也支持这两个运算符的前置版本,即运算符在前、变量在后,例如:
++a; --a;
自增和自减运算符“++”和“--”后置和前置所产生的结果是不同的。后置版本是先使用变量的值,然后再对变量施行自加和自减操作,例如:
int a = 6; int b = a++; // 该条语句运行过后,b的值是6,a的值是7 int c = a--; // 该条语句运行过后,c的值是7,a的值是6
上面语句中b的值是6,而不是7。其中第2行中b的初始化值即“a++”,是一个表达式。由于是后置版本,所以表达式的值是a自加之前的值,即6,b也初始化为6。基于同样的原理,读者可以自行分析一下第3行的运行结果。前置版本是先对变量施行自加和自减操作,然后再使用变量的值,例如:
int a = 6; int b = ++a; // 该条语句运行过后,b的值是7,a的值是7 int c = --a; // 该条语句运行过后,c的值是6,a的值是6
上面语句中b的值是7,而不是6。其中第2行中表达式“++a”由于是前置版本,所以表达式的值是a自加之后的值,即7,b也初始化为7。基于同样的原理,读者可以自行分析一下第3行的运行结果。
4.3.5 关系运算符
在C++中,常见的关系运算符如表4-1所示。
表4-1 常见的关系运算符
关系运算符是二元运算符,用于比较两个操作数的大小关系。关系运算符的操作数是两个表达式,比较的结果是一个bool型的值,即true或false,例如:
bool res = false; // 声明并初始化bool型变量 res = 3 < 4; // res的值是true res = 4 < 3; // false int a = 5; int b = 3; es = a >= b; // true res = a + 1 == 2 * b -1; // true res = a != b; // true
4.3.6 逻辑运算符
在C++中,常见的逻辑运算符如表4-2所示。
表4-2 常见的逻辑运算符
逻辑非是一元运算符,其含义是求表达式的相反值,例如:
bool a = false; bool res = !a; // res的值是true res = !res; // res的值是false
逻辑与运算符(&&)和逻辑或运算符(||)都是二元运算符,其结果值的计算方法如下:
◆ 只有当逻辑与(&&)运算符的两个操作数都为true时,结果值才为true。
◆ 对于逻辑或(||)运算符,只要两个操作数之一为true,结果值就为true。
例如:
bool res = false; res = true && false; // res为false,因为两个操作数中有一个是false res = true || false; // res为true,因为两个操作数中有一个是true
这些操作数按从左至右的顺序计算。只要能够得到表达式的值true或false,运算就会结束。例如给定以下表达式:
expr1 && expr2 expr1 || expr2
如果下列条件有一个满足:则保证不会计算expr2。
◆ 在逻辑与表达式中expr1的计算结果为false。
◆ 在逻辑或表达式中expr1的计算结果为true。
4.3.7 条件运算符
条件运算符(?:)是唯一的一个三元运算符,其语法格式如下:
expr1 ? expr2 : expr3;
其含义是:如果子表达式expr1的结果是true,则整个表达式的结果是子表达式expr2的值;否则,整个表达式的结果是子表达式expr2的值。
条件表达式的典型用法如下:
int a = 20, b = 30; … int larger = ( a > b ) ?a : b; int smaller = ( a < b) ? a : b;
上面代码用于求两个数中的较大者和较小者。
4.3.8 逗号运算符
逗号表达式是一系列由逗号分开的表达式,这些表达式从左向右计算,逗号表达式的结果是最右边表达式的值。例如,在“a += 3, c * 5;”中,程序依然会先执行a的自增,再执行c * 5。
4.3.9 位运算符
在C++中,常见位运算符的功能与用法如表4-3所示。
表4-3 常见位运算符的功能与用法
位运算符把操作数解释成有序的位集合,每个位的值是0或1。位运算符允许程序员设置或测试独立的位。
当心
位运算符一般用于处理整数。虽然整数可以是有符号的,也可以是无符号的,但一般是无符号的。因为在大多数的位操作中符号位的处理是未定义的,因此不同的编译器对符号位的处理可能会不同。
1. 按位非运算符~
该操作符的作用是翻转操作数的每一位。如果某位是1,则改为0;如果某位是0,则改为1。例如:
short a = 5; short b = ~a; // b的值是-6
短整型5的二进制是00000101,每位取反,则结果值的二进制是11111010,即-6。
2. 左移运算符<<和右移运算符>>
该操作符的作用是将其左边操作数的位向左或向右移动,移动的位数是右边的操作数。例如:
short a = 5 << 1; // a的值是10,5左移1位的二进制结果是00001010,即十进制的10 short b = 5 >> 2; // b的值是1,5右移2位的二进制结果是00000001,即十进制的1
在移位过程中,某些位被移到存储区外面,则该位将被丢弃。左移运算符(<<)从右边开始用0补空位。对于右移运算符(>>),如果操作数是无符号数,则从左边开始插入0;如果操作数是有符号数,则插入符号位的复制值或者插入0,这由具体的编译器决定。
3. 按位与运算符&
对于进行按位与运算的两个整数操作数的每个位,如果两个操作数在该位处都是1,则结果值的该位为1,否则为0。例如:
short a = 5 & 4;// 即00000101 & 00000100,得二进制结果00000100,即十进制的4
注意
请不要把该运算符与逻辑与运算符&&相混淆。
4. 按位异或运算符
对于进行按位异或运算的两个整数操作数的每个位,如果两个操作数在该位处只有一个是1(注意不是两个同时为1),则结果值的该位为1,否则为0。例如:
short a = 5 ^ 4;// 即00000101 & 00000100,得二进制结果00000001,即十进制的1
5. 按位或运算符|
对于按位或运算的两个整数操作数的每个位,如果两个操作数在该位处只有一个是1,则结果值的该位为1,如果都是0,则结果值的该位为0。例如:
short a = 5 ^ 4;// 即00000101 & 00000100,得二进制结果00000101,即十进制的5
4.3.10 复合赋值运算符
除了普通的赋值运算符“=”之外,在C++程序中还可以将其他运算符与“=”相结合,构造一种复合赋值运算符,如“+=”、“*=”、“&=”等。这种运算符表示的是先用其他运算符计算左操作数和右操作数,然后将结果赋给左操作数。例如:
int a = 5; a += 3; // 实际是a = a + 12; a *= 4; // 实际是a = a * 4; a &= 1; // 实际是a = a & 1;
采用复合赋值运算符,可以使程序更加简洁明了。
4.4 运算符的优先级和结合性
程序中的优先级同算术中的优先级概念一样,都指的是哪一个运算符先计算。例如在下面的语句中:
int x = 6 + 3 * 4 / 2-2;
表示乘除的运算符(*和/)先于加减运算符(+和-)计算。运算符的结合性指的是同样优先级的两个运算符相邻时,先计算哪一个。如果左面的先计算,则该级运算符具有左结合性,否则具有右结合性。
算术运算符具有左结合性。例如在表达式“a + b - c”中,加减运算符的优先级相同,并具有左结合性,所以首先计算左面的加号,然后将计算的结果减去c,就是表达式的结果,即语句的计算过程是:
step 1 乘除的优先级高,所以先计算“3*4/2”这个表达式。
step 2 乘除具有左结合性,所以先计算“3*4”,得12,再除以2得6。
step 3 再计算加减,即“6+6-2”。
step 4 加减具有左结合性,所以先计算左边的“6+6”,得12,再算“12-2”,得10。
C++中各种运算符的优先级和结合性如表4-4所示。
表4-4 运算符的优先级和结合性
4.5 类型转换
在表达式的计算过程中,经常需要计算两个不同类型的操作数,或者将一种类型的值赋给另一种类型的变量。例如下列表达式:
1.2 + 3; // double型数据与int型数据相加 int a = 4.56; // 将double型数据赋给int型变量
不同类型的数据在计算机中的处理方式不一样,例如double型和int型数据在内存组织和计算方法的实现上都不相同,所以为了完成上述操作必须进行类型转换。
4.5.1 隐式类型转换
对于内置类型,如char,int,double等,C++定义了一个标准转换规则。如int型转换为double型时,数值不变;double型转换为int型,用舍尾法;char型转换为int型时,采用对应的ASCII码。必要时,编译器隐式地按照这个规则对数据类型进行转换。转换的原则是:小类型总是被提升成大类型,以防止精度损失;大类型转换为小类型时,给出警告(不是编译错误)。所谓的小类型、大类型是从类型尺寸的角度来讲的。也就是说,占用内存小的类型总是转换成占用内存大的类型,如char型到int型、int型到double型。
隐式类型转换发生在下列这些典型的情况下:
◆ 混合类型的算术表达式中。
◆ 用一种类型的表达式给另一种类型的变量赋值。
◆ 把一个表达式传递给一个函数调用,其类型与形参的类型不相同。
◆ 从一个函数返回一个表达式,其类型与函数的返回类型不相同。
如果一个算术表达式中,参与运算的各个数据的类型不相同,则其中尺寸最大的数据类型成为目标类型。运算之前,编译器先将各个操作数转换成目标类型,这种转换也被称为算术转换。例如在表达式“1.2 + 3”中,两个操作数分别为double型(两个机器字)和int型(一个机器字),double比int的尺寸大,所以在计算前先将3转换为double型,其值为3.0。这样表达式的结果也是double型。
当用一种类型的表达式给另一种类型的变量赋值时也会发生类型转换,此时目标类型是被赋值变量的类型。例如下列表达式:
double a = 123; // 标准的隐式转换 int b = 3.14159; // 精度损失,会导致一个编译警告
为一个函数传递参数时,如果实际传递的参数与函数声明中所用参数的类型不相同,那么就需要将所传递参数转换成函数所需参数的类型。例如C++中求平方根的库函数sqrt接受一个double型参数,如果传入一个int型数据,则该数据会被提升为double型。
在函数的return(返回)语句中,如果return表达式的类型与函数声明的返回类型不相同,就需要将实际返回值转换成函数的返回类型。在这种情况下目标转换类型是函数的返回类型。例如:
double difference( int ival1, int ival2 ) { // 返回值被提升为double类型 return ival1- ival2; }
提示
大类型向小类型转换在算术表达式中不会出现,但对于其他几种情形,却可能存在。虽然不会导致编译错误,但最好还是尽量避免,因为这会导致数据的精度损失。
4.5.2 特殊的隐式转换
在C++中有一些比较特殊的类型转换,包括:
◆ char型数据可以当做整型数使用。
◆ bool型数据与其他数据类型的转换。
◆ 在算术表达式中,尺寸小于整型的类型总是先被提升为整型。
由于在C++程序中字符是用ASCII码表示的,所以字符可以当做整型数使用,其值是对应的ASCII码值。例如'A'可以当做65使用,'a'可以当做97使用。
int iVal = 'A'; // iVal的值是65 iVal = 'a'; // 97
同样,在ASCII码表范围内的整数(0~256),也可以当做对应的字符使用。例如:
char cVal = 66; // cVal的值是'B' cVal = 98; // 'b'
当程序中需要将其他数据类型转换为bool型时,如果传入的数据为零,则转换为false;如果不为0,则转换为true。将bool型数据转换为其他类型数据时,如果传入的数据为true,则转换为1;否则,转换为0。例如:
bool res = 0; // res为false res = 1; // true res = -1; // true res = 1.234; // true res = 0.0; // false res = 'a'; // true int iVal = true; // iVal为1 iVal = false; // 0 double dVal = true; // dVal为1.0 dValue = false; // 0.0
算术表达式中小于整型的类型,总是先被提升为整型。在算术表达式中,如果只含有char型、short型等数据,则这些数据先会被转换成整型。例如:
short sVal = 1; char cVal = 'a'; cout<< sVal + cVal <<endl; // sVal转换成int型的1,cVal转换成int型的97
程序的输出结果是98。
4.5.3 显式类型转换
如果要将某个数据显式地转换为其他数据类型,则可以使用下面的表达式:
类型(数据) (类型)数据
上述表达式是等价的。例如把一个浮点型数据转换成整型,可以写为:
int a = (int) 3.1416;
也可以写为:
int a = int (3.1416 );
上述类型转换的方法继承自C语言,C++也提供了自己的类型转换运算符static_cast。例如,为了将double型数据转换成int型数据,应当如下写:
int a = static_cast<int>( 3.1416 );
提示
虽然C++的运算符写起来比较复杂,但是这样写可以明确地告诉程序的阅读者:“这里需要一个类型转换”,因此也方便了程序的维护。
4.6 综合实例
在本节中,将选择几个典型的综合实例讲解如何在C++中使用表达式和语句。希望读者在学习这些例子后,能对C++的基本语法有更详细的了解。
4.6.1 找出某个范围内的素数
所谓素数就是只能被1和自身整除的正整数。比如,1就是一个素数,2和3也是,但4不是,4除了可以被1和自身整除外,还可以被2整除。从这个定义出发,要判断一个数是不是素数可以采用下面的算法:先判断这个数是不是1或2,如果是则返回true。如果不是,则求出该数的平方根,然后遍历大于等于2并小于这个平方根的所有整数,如果该数能够被这样的整数整除,则不是素数,否则就是一个素数。程序如示例代码4.1所示。
示例代码4.1
#include <cstdlib> #include <iostream> #include <cmath> using namespace std; // 使用名称空间std int main(int argc, char *argv[]) // 主函数 { cout<<"——求素数——"<<endl; // 输出提示信息 int min = 0, max = 0; // 素数的范围 cout<<"请输入一个范围:"<<endl; // 输出提示信息 cout<<"大于等于:"; cin>>min; // 输入最小值 cout<<endl; // 换行 if( min < 1 ) // 求取范围只能是正整数 { min = 1; } cout<<"小于:"; // 输出提示信息 cin>>max; // 输入最大值 cout<<endl; // 换行 while( max <= min ) // 如果输入有误,直到用户输入正确的最大值为止 { cout<<"范围的上限必须大于下限。" // 输出出错信息 <<"请重新输入:"<<endl; cin>>max; // 重新输入最大值 } cout<<"上述范围内的素数是: "<<endl; // 输出提示信息 for( int i=min; i<max; i++ ) // 遍历范围内的所有整数 { bool flag = true; // 标明当前整数是否是素数的标志 if( 1 == i || 2 == i ) // 如果1或2在范围内直接输出 { flag = true; } else // 除1和2之外的其他数 { int s = static_cast<int>(sqrt(i)); // 求平方根 for( int j=2; j<=s; j++ ) // 从2开始遍历,直到平方根 { if( (i % j) == 0 ) // 如果可以被整除,则不是素数 { flag = false; } } } if( flag ) // 如果是素数 { cout<<i<<endl; // 输出 } } system("PAUSE"); // 等待用户反应 return EXIT_SUCCESS; // 主函数返回 }
建立一个控制台工程,并将上述代码复制到源文件中,编译、链接并执行,其结果如图4-1所示。
图4-1 求素数输出结果
4.6.2 求最大值
假设允许用户输入5个整数,求其中的最大值,程序如示例代码4.2所示。
示例代码4.2
#include <cstdlib> #include <iostream> using namespace std; // 使用名称空间std int main(int argc, char *argv[]) // 主函数 { cout<<"——求最大值——"<<endl; // 输出提示信息 int temp = 0; // 保存输入值的变量 int max = 0; // 保存最大值的变量 cout<<"请输入5个整数:"<<endl; // 输出提示信息 for( int i=0; i<5; i++ ) // 循环5次 { cin>>temp; // 输入数据 if( temp > max ) // 如果当前输入的数据比最大值还大 { max = temp; // 修改最大值 } } cout<<"最大值是:"<<max<<endl; // 输出最大值 system("PAUSE"); // 等待用户反应 return EXIT_SUCCESS; // 主函数返回 }
建立一个控制台工程,并将上述代码复制到源文件中,编译、链接并执行,其结果如图4-2所示。
图4-2 求最大值输出结果
4.7 小结
本章主要讲述了C++中的语句、表达式和运算符。在C++程序中,数据通过运算符组合成表达式。一个表达式后面加上分号,则该表达式就变成了一条语句。本章的重点是各种运算符的使用方法,以及运算符的优先级和结合性。另外,类型转换也是读者需要掌握的。