第4章 表达式
表达式由一个或多个运算对象(operand) 组成,对表达式求值将得到一个结果(result)。字面值和变量是最简单的表达式(expression),其结果就是字面值和变量的值。把一个**运算符(operator)**和一个或多个运算对象组合起来可以生成较复杂的表达式。
4.1 基础
4.1.1 基本概念
C++定义了一个一元运算符(unary operator) 和 二元运算符(binary operator) 以及 三元运算符(唯一 b ? x:y)。
作用于几个运算对象就叫几元。
函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。
一些符号既能作为一元运算符也能作为二元运算符。
组合运算符和运算对象
对于含有多个运算符的复杂表达式来说,要想理解它的含义首先要理解运算符的优先级(precedence)、结合律(associativity)以及运算对象的求值顺序(order of evaluation)。
运算对象转换
在表达式求值的过程中,运算对象常常由一种类型转换成另外一种类型。
类型转换的规则虽然有点复杂,但大多数都合乎情理、容易理解。让人稍稍有点意外的是,小整数类型(如bool、char、short等)通常会被 提升(promoted) 成较大的整数类型,主要是int。
重载运算符
C++语言定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。当运算符作用域类类型的运算对象时,用户可以自行定义其含义。因为这种自定义的过程事实上是为已存在的运算符赋予了另外一层含义,所以称之为重载运算符(overloaded operator)。
使用重载运算符时,运算对象的个数、运算符的优先级和结合律是无法改变的。
左值和右值
C++的表达式要不然是右值(rvalue),要不然就是左值(lvalue)。左值可以位于赋值语句的左侧,右值则不能。
到目前为止,已经有几种我们舒徐的运算符是要用到左值的。
· 赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也仍然是一个左值。
· 取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。
· 内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值。
· 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得的结果也是左值。
使用关键字decltype的时候,左值和右值也有所不同。假定p的类型是int*,那么decltype(*p)返回的类型是int&,decltype(&p)的结果是int **。
4.1.2 优先级与结合律
复合表达式(compound expression) 是指含有两个或多个运算符的表达式。求复合表达式的值需要首先将运算符和运算对象合理地组合在一起,优先级与结合律决定了运算对象组合的方式。
一般来说,表达式最终的值依赖于其子表达式的组合方式。
括号无视优先级与结合律
括号无视普通的组合规则,表达式中括号括起来的部分被当成一个单元来求值,然后再与其他部分一起按照优先级组合。
优先级与结合律有何影响
优先级影响程序的正确性。结合律对表达式产生影响的一个典型示例是输入输出运算。
4.1.3 求值顺序
优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值。在大多数情况下,不会明确指定求值的顺序。
对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。
int i = 0;
cout<<i<<" "<<++i<<endl;//未定义的
因为程序是未定义的,所以编译器可能先求++i的值再求i的值,也可能先求i的值再求i++的值,甚至编译器还可能做完全不同的操作。
有4种运算符明确规定了运算对象的求值顺序,分别是逻辑与运算符(&&),逻辑或运算符(||),条件运算符(?;)和逗号运算符(,)。
求值顺序、优先级、结合律
运算对象的求值顺序与优先级和结合律无关。
建议:处理符合表达式
1.拿不准的时候最好用括号来强制让表达式的组合关系复合程序逻辑的要求。
2.如果改变了某个运算对象的值,再表达式的其他地方不要再使用这个运算对象。(当改变运算对象的子表达式本身就是另外一个子表达式的运算对象时该规则无效。)
4.2 算术运算符
除非另做特殊说明,算术运算符都能作用于任意算术类型以及任意能转换为算术类型的类型。算术运算符的运算对象和求值结果都是右值。
提示:溢出和其他算术运算异常
算术表达式有可能产生未定义的结果。一部分原因是数学性质本身:例如除数是0的情况;另外一部分则源于计算机的特点:例如溢出,当计算的结果超出该类型所能表示的范围时就会产生溢出。
假设某个机器的short类型占16位,则最大的short数值是32767。在这样一台机器上,下面的复合赋值语句将产生溢出:
short short_value=32767;//如果short类型占16位,则能表示的最大值是32767
short_value += 1;//该计算导致溢出
cout<<"short_value:"<<short_value<<endl
;
给short_value赋值的语句是未定义的,这是因为表示一个带符号数32768需要17位,但是short类型只有16位。很多系统在编译和运行时都不报溢出错误,像其他未定义的行为一样,溢出的结果是不可预知的。在我们的系统中,程序的输出结果是:
short_value:-32768
该值发生了“环绕(wrapped around)”,符号位本来是0,由于溢出被改成了1,于是结果变成了一个负值。在别的系统中也许会有其他结果,程序的行为可能不同甚至直接崩溃。
运算符%俗称“取余”或“取模”运算符,负责计算两个整数相除所得的余数,参与的取余运算的运算对象必须是整数类型。
根据取余运算的定义,如果m和n是整数且n非0,则表达式(m/n)*n+m%n的求值结果与m相等(注意此时m/n得到的是取整后的整数,因此(m/n)*n不等于m)。
4.3 逻辑和关系运算符
关系运算符作用于算术类型或指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。
逻辑与和逻辑或运算符
对于逻辑与运算符(&&) 来说,当且仅当两个运算对象都为真时结果为真;对于逻辑或运算符(||) 来说,只要两个运算对象中的一个为真结果就为真。
逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略称为短路求值(short-circuit evaluation)。
· 对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值。
· 对于逻辑或运算符来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。
逻辑非运算符
逻辑非运算符(!) 将运算对象的值取反后返回。
关系运算符
顾名思义,关系运算符比较运算对象的大小关系并返回布尔值。
因为关系运算符的求值结果是布尔值,所以将几个关系运算符连写在一起会产生意想不到的结果:
if(i<j<k)//若i<j成立,那么k大于1则为真
相等性测试与布尔字面值
如果想测试一个算术对象或指针对象的真值,最直接的方法就是将其作为if语句的条件:
if(val){/*...*/};//如果val是任意的非0值,条件为真
if(!val){/*...*/};//如果val是0,条件为真
有时会错误地将上面的真值测试写成如下形式:
if(val==true){/*...*/}//只有当val等于1时条件才为真
如果val不是布尔值,那么进行比较之前会首先把true转换成val的类型。也就是说,如果val不是布尔值,则代码可以改写成如下形式:
if(val==1){/*...*/};
注:进行比较运算时除非比较的对象是布尔类型,否则不要使用布尔字面值true和false作为运算对象。
4.4 赋值运算符
赋值运算符的左侧运算对象必须是一个可修改的左值。
赋值运算符的结果是它的左侧运算对象,并且是一个左值。相应的,结果的类型就是左侧运算对象的类型。如果赋值运算符的左右两个运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型。
C++11新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象。
无论左侧运算对象的类型是什么,初始值列表都可以为空。此时,编译器创建一个值初始化的临时量并将其赋给左值运算对象。
赋值运算满足右结合律
int ival,jval;
ival = jval = 0;//正确,都被赋值为0
赋值运算优先级较低
赋值语句经常会出现在条件当中。因为赋值运算的优先级相对较低,所以通常需要赋值部分加上括号使其符合我们的原意。
切勿混淆相等运算符和赋值运算符
C++允许用赋值运算作为条件。
if(i==j)
此时,if语句的条件部分把j的值赋给i,然后检查赋值的结果是否为真。
复合赋值运算符
我们经常需要对对象施以某种运算,然后把运算的结果再赋给该对象。
每种运算符都有相应的复合赋值形式。两者的唯一区别是左侧运算对象的求值次数:使用复合运算符只求值一次,使用普通的运算符则求值两次。
4.5 递增和递减运算符
递增运算符(++)和递减运算符(–)为对象的加1和减1提供了一种简介的书写形式。这两个运算符还可应用于迭代器。
递增和递减运算符有两种形式:前置版本和后置版本。
前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。
建议:除非必须,否则不用递增递减运算符的后置版本
前置版本的递增运算符避免了不必要的工作,它把值加1后直接返回改变了的运算对象。与之相比,后置版本需要将原始值存储下来以便于返回这个未修改的内容。如果我们不需要修改前的值,那么后置版本的操作就是一种浪费。
在一条语句中混用解引用和递增运算符
auto pbeg = v.begin();
//输出元素直至遇到第一个负值为止
while(pbeg != v.end() && *beg >=0)
cout<<*pbeg++<<endl;//输出当前值并将pbeg向前移动一个元素
后置递增运算符的优先级高于解引用运算符,因此 *pbeg++等价于 *(pbeg++)。最终,这条语句输出pbeg开始时指向的那个元素,并将指针向前移动一个位置。
建议:简介可以成为一种美德
书写cout << *iter++ << endl
要比书写下面的等价语句更简洁、也更少出错
cout << *iter << endl;
++iter;
运算对象可按任意顺序求值
大多数运算符都没有规定运算对象的求值顺序。然而,如果一条子表达式改变了某个运算对象的值,另一条子表达式又要使用该值的话,运算对象的求值顺序就很关键了。因为递增运算夫和递减运算符会改变运算对象的值,所以要提防在复合表达式中错用这两个运算符。
//该循环的行为是未定义的!
while (beg != s.end() && !isspcae(*beg))
*beg = toupper(*beg++);//错误:该赋值语句未定义