C++基础入门第四章
第四章 表达式
表达式由一个或多个运算对象(operand)组成,对表达式求值将得到一个结果(result)。字面值和变量是最简单的表达式(expression),其结果就是字面值和变量的值。把一个运算符(operator)和一个或多个运算对象组合起来可以生成较复杂的表达式。
4.1 基础
4.1.1 基本概念
C+定义了一元运算符(unary operator)和二元运算符(binary operator)。作用于一个运算对象的运算符是一元运算符,如取地址符&和解引用符*:作用于两个运算
对象的运算符是二元运算符,如相等运算符=和乘法运算符*。除此之外,还有一个作用于三个运算对象的三元运算符。函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。一些符号既能作为一元运算符也能作为二元运算符。
组合运算符和运算对象
对于含有多个运算符的复杂表达式来说,要想理解它的含义首先要理解运算符的优先级(precedence)、结合律(associativity)以及运算对象的求值顺序(order of evaluation)。
1 |
|

运算对象转换
在表达式求值的过程中,运算对象常常由一种类型转换成另外一种类型。类型转换的规则虽然有点复杂,但大多数都合乎情理、容易理解。让人稍微有点意外的是,小整数类型(如bool、char、short等)通常会被提升(promoted)成较大的整数类型,主要是int。
重载运算符
C++语言定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。当运算符作用于类类型的运算对象时,用户可以自行定义其含义。因为这种自定义的过程事实上是为已存在的运算符赋予了另外一层含义,所以称之为重载运算符(overloadedoperator)。
我们使用重载运算符时,其包括运算对象的类型和返回值的类型,都是由该运算符定义的:但是运算对象的个数、运算符的优先级和结合律都是无法改变的。
左值和右值
C++的表达式要不然是右值,要不然就是左值。这两个名词是从C语言继承过来的,原本是为了帮助记忆:左值可以位于赋值语句的左侧,右值则不能。
在C++中,一个左值表达式的求值结果是一个对象或者一个函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。此外,虽然某些表达式的求值结果是对象,但它们是右值而非左值。可以做一个简单的归纳:当一个对象被用作右值的时候,用的是对象的值(内容):当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
不同的运算符对运算对象的要求各不相同,有的需要左值运算对象、有的需要右值运算对象;返回值也有差异,有的得到左值结果、有的得到右值结果。一个重要的原则是在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。当一个左值被当成右值使用时,实际使用的是它的内容(值)。到目前为止,已经有几种我们熟悉的运算符是要用到左值的。
- 赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也仍然是一个左值。
- 取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。
- 内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值。
- 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得的结果也是左值。
使用关键字decltype的时候,左值和右值也有所不同。如果表达式的求值结果是左值,decltype作用于该表达式(不是变量)得到一个引用类型。另一方面,因为取地址运算符生成右值,结果是一个指向整型指针的指针。
4.1.2 优先级与结合律
复合表达式(compound expression)是指含有两个或多个运算符的表达式。求复合表达式的值需要首先将运算符和运算对象合理地组合在一起,优先级与结合律决定了运算对象组合的方式。也就是说,它们决定了表达式中每个运算符对应的运算对象来自表达式的哪一部分。表达式中的括号无视上述规则,程序员可以使用括号将表达式的某个局部括起来使其得到优先运算。
一般来说,表达式最终的值依赖于其子表达式的组合方式。高优先级运算符的运算对象要比低优先级运算符的运算对象更为紧密地组合在一起。如果优先级相同,则其组合规则由结合律确定。算术运算符满足左结合律,意味着如果运算符的优先级相同,将按照从左向右的顺序组合运算对象。
括号无视优先级与结合律
括号无视普通的组合规则,表达式中括号括起来的部分被当成一个单元来求值,然后再与其他部分一起按照优先级组合。
1 |
|

优先级与结合律有何影响
优先级会影响程序的正确性。结合律对表达式产生影响的一个典型示例是输入输出运算。
1 |
|

4.1.3 求值顺序
有4种运算符明确规定了运算对象的求值顺序。第一种是逻辑与(&&)运算符,它规定先求左侧运算对象的值,只有当左侧运算对象的值为真时才继续求右侧运算对象的值。另外三种分别是逻辑或(||)运算符、条件(?:)运算符和逗号(,)运算符。
求值顺序、优先级、结合律
运算对象的求值顺序与优先级和结合律无关。
以下两条经验准则对书写复合表达式有益。
拿不准的时候最好用括号来强制让表达式的组合关系符合程序逻辑的要求。
如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。
第2条规则有一个重要例外,当改变运算对象的子表达式本身就是另外一个子表达式的运算对象时该规则无效。
4.2 算术运算符

按照运算符的优先级将其分组。一元运算符的优先级最高,接下来是乘法和除法,优先级最低的是加法和减法。优先级高的运算符比优先级低的运算符组合得更紧密。上面的所有运算符都满足左结合律,意味着当优先级相同时按照从左向右的顺序进行组合。除非另做特殊说明,算术运算符都能作用于任意算术类型以及任意能转换为算术类型的类型。算术运算符的运算对象和求值结果都是右值。一元正号运算符、加法运算符和减法运算符都能作用于指针。
对大多数运算符来说,布尔类型的运算对象将被提升为int类型。
提示:溢出和其他算术运算异常 算术表达式有可能产生未定义的结果。一部分原因是数学性质本身:例如除数是0的情况;另外一部分则源于计算机的特点:例如溢出,当计算的结果超出该类型所能表示的范围时就会产生溢出。很多系统在编译和运行时都不报溢出错误,像其他未定义的行为一样,溢出的结果是不可预知的。该值发生了“环绕(wrapped around”,符号位本来是0,由于溢出被改成了1,于是结果变成一个负值。在别的系统中也许会有其他结果,程序的行为可能不同甚至直接崩遗。
当作用于算术类型的对象时,算术运算符+、-、*、/的含义分别是加法、减法、乘法和除法。整数相除结果还是整数,也就是说,如果商含有小数部分,直接弃除。
在除法运算中,如果两个运算对象的符号相同则商为正(如果不为0的话),否则商为负。C++11新标准则规定商一律向0取整(即直接切除小数部分)。
根据取余运算的定义,如果m和n是整数且n非0,则表达式(m/n)*n+m号n的求值结果与m相等。隐含的意思是,如果m%n不等于0,则它的符号和m相同。C+语言的早
期版本允许m号n的符号匹配n的符号,而且商向负无穷一侧取整,这一方式在新标准中已经被禁止使用了。
1 |
|

4.3 逻辑和关系运算符
关系运算符作用于算术类型或指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。值为0的运算对象(算术类型或指针类型)表示假,否则表示真。对于这两类运算符来说,运算对象和求值结果都是右值。

逻辑与和逻辑或运算符
对于逻辑与运算符&&来说,当且仅当两个运算对象都为真时结果为真;对于逻辑或运算符||来说,只要两个运算对象中的一个为真结果就为真。逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略称为短路求值。
- 对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值
- 对于逻辑或运算符来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。
1 |
|

逻辑非运算符
逻辑非运算符!将运算对象的值取反后返回。
关系运算符
顾名思义,关系运算符比较运算对象的大小关系并返回布尔值。关系运算符都满足左结合律。因为关系运算符的求值结果是布尔值,所以将几个关系运算符连写在一起会产生意想不到的结果。
1 |
|

相等性测试与布尔字面值
如果想测试一个算术对象或指针对象的真值,最直接的方法就是将其作为if语句的条件:
进行比较运算时除非比较的对象是布尔类型,否则不要使用布尔字面值true和false作为运算对象。
4.4 赋值运算符
1 |
|

赋值运算满足右结合律
赋值运算符满足右结合律,这一点与其他二元运算符不太一样。对于多重赋值语句中的每一个对象,它的类型或者与右边对象的类型相同、或者可由右边对象的类型转换得到。
1 |
|

赋值运算优先级较低
赋值语句经常会出现在条件当中。因为赋值运算的优先级相对较低,所以通常需要给赋值部分加上括号使其符合我们的原意。
因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号。
切勿混淆相等运算符和赋值运算符
相等运算符==
赋值运算符=
复合赋值运算符
我们经常需要对对象施以某种运算,然后把计算的结果再赋给该对象。

每种运算符都有相应的复合赋值形式
+= |
-= |
*= |
/= |
%= |
|---|---|---|---|---|
<<= |
>>= |
&= |
^= |
` |
4.5 递增和递减运算符
递增运算符(++)和递减运算符(-一)为对象的加1和减1操作提供了一种简洁的书写形式。这两个运算符还可应用于迭代器,因为很多迭代器本身不支持算术运算,所以此时递增和递减运算符除了书写简洁外还是必须的。
1 |
|

有C语言背景的读者可能对优先使用前置版本递增运算符有所疑问,其实原因非常简单:前置版本的递增运算符避免了不必要的工作,它把值加1后直接返回改变了的运算对象。与之相比,后置版本需要将原始值存储下来以便于返回这个未修改的内容。如果我们不需要修改前的值,那么后置版本的操作就是一种浪费。
对于整数和指针类型来说,编译器可能对这种额外的工作进行一定的优化;但是对于相对复杂的迭代器类型,这种额外的工作就消耗巨大了。建议养成使用前置版本的习惯,这样不仅不需要担心性能的问题,而且更重要的是写出的代码会更符合编程的初衷。
在一条语句中混用解引用和递增运算符
1 |
|

形如
*pbeg+的表达式一开始可能不太容易理解,但其实这是一种被广泛使用的、有效的写法。当对这种形式熟悉之后,书写cout <*iter++<endl;
要比书写下面的等价语句更简洁、也更少出错cout <*iter <endl;++iter;
不断研究这样的例子直到对它们的含义一目了然。大多数C++程序追求简洁、摒弃冗长,因此C++程序员应该习惯于这种写法。而且,一旦熟练掌握了这种写法后,程序出错的可能性也会降低。
运算对象可按任意顺序求值
1 |
|

用一个看似等价的while循环进行代替:
1
2
3 //该循环的行为是未定义的!
while (beg != s.end() && !isspace(*beg))
*beg = toupper(*beg++);//错误:该赋值语句未定义编译器可能按照下面的任意一种思路处理该表达式:
1
2 *beg = toupper(*beg);//如果先求左侧的值
*(beg + 1) = toupper(*beg);//如果先求右侧的值
4.6 成员访问运算符
点运算符和箭头运算符都可用于访问成员,其中,点运算符获取类对象的一个成员:箭头运算符与点运算符有关,表达式ptr->mem等价于(*ptr).mem
1 |
|

因为解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号。如果没加括号,代码的含义就大不相同了。
1 | //运行p的size成员,然后解引用size的结果 |
箭头运算符作用于一个指针类型的运算对象,结果是一个左值。点运算符分成两种情况:如果成员所属的对象是左值,那么结果是左值:反之,如果成员所属的对象是右值,那么结果是右值。
4.7 条件运算符
条件运算符?:允许我们把简单的if-else逻辑嵌入到单个表达式当中,条件运算符按照如下形式使用:
1 | cond?expr1:expr2 |
条件运算符的执行过程是:首先求cond的值,如果条件为真对expr1求值并返回该值,否则对exp2求值并返回该值。
1 | string finalgrade = (grade < 60) ? "fail" : "pass"; |
当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值:否则运算的结果是右值。
嵌套条件运算符
允许在条件运算符的内部嵌套另外一个条件运算符。也就是说,条件表达式可以作为另外一个条件运算符的cond或expr。
1 | finalgrade = (grade > 90) ? "high pass" : (grade < 60) ? "fail" : "pass"; |
随着条件运算嵌套层数的增加,代码的可读性急剧下降。因此,条件运算的嵌套最好别超过两到三层
在输出表达式中使用条件运算符
条件运算符的优先级非常低,因此当一条长表达式中嵌套了条件运算子表达式时,通常需要在它两端加上括号。
1 | cout << ((grade < 60) ? "fail" : "pass"); // 输出pass或者fail |
4.8 位运算符
位运算符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合。位运算符提供检查和设置二进制位的功能。

关于符号位如何处理没有明确的规定,所以强烈建议仅将位运算符用于处理无符号类型。
移位运算符
之前在处理输入和输出操作时,我们已经使用过标准O库定义的<<运算符和>>运算符的重载版本。这两种运算符的内置含义是对其运算对象执行基于二进制位的移动操作,首先令左侧运算对象的内容按照右侧运算对象的要求移动指定位数,然后将经过移动的(可能还进行了提升)左侧运算对象的拷贝作为求值结果。其中,右侧的运算对象一定不能为负,而且值必须严格小于结果的位数,否则就会产生未定义的行为。二进制位或者向左移(<<)或者向右移(>>),移出边界之外的位就被舍弃掉了。
1 | unsigned char bits = 0233;//0233是八进制的字面值 |

1 | bits << 8;//bits提升成int类型,然后向左移动8位 |

1 | bits << 31;//向左移动31位,左边超出边界的位丢弃掉了 |

1 | bits >> 3;//向右移动3位,最右边的3位丢弃掉了 |

左移运算符(<<)在右侧插入值为0的二进制位。右移运算符(>>)的行为则依赖于其左侧运算对象的类型:如果该运算对象是无符号类型,在左侧插入值为0的二进制位:如果该运算对象是带符号类型,在左侧插入符号位的副本或值为0的二进制位,如何选择要视具体环境而定。
位求反运算符
位求反运算符~将运算对象逐位求反后生成一个新值,将1置为0、将0置为1。
1 | unsigned char bits = 0227; |

1 | ~bits; |

char类型的运算对象首先提升成int类型,提升时运算对象原来的位保持不变,往高位(high order position)添加0即可。因此在本例中,首先将bits提升成int类型,增加24个高位0,随后将提升后的值逐位求反。
位与、位或、位异或运算符
与&、或!、异或^运算符在两个运算对象上逐位执行相应的逻辑操作.

对于位与运算符&来说,如果两个运算对象的对应位置都是1则运算结果中该位为1,否则为0。对于位或运算符|来说,如果两个运算对象的对应位置至少有一个为1则运算结果中该位为1,否则为0。对于位异或运算符^来说,如果两个运算对象的对应位置有且只有一个为1则运算结果中该位为1,否则为0。
有一种常见的错误是把位运算符和逻辑运算符搞混了,比如位与
&和逻辑与&&人位或|和逻辑或||、位求反~和逻辑非!。
使用位运算符
不常用
移位运算符(又叫IO运算符)满足左结合律
尽管很多程序员从未直接用过位运算符,但是几乎所有人都用过它们的重载版本来进行IO操作。重载运算符的优先级和结合律都与它的内置版本一样,因此即使程序员用不到移位运算符的内置含义,也仍然有必要理解其优先级和结合律。
1 |
|

移位运算符的优先级不高不低,介于中间:比算术运算符的优先级低,但比关系运算符、赋值运算符和条件运算符的优先级高。因此在一次使用多个运算符时,有必要在适当的地方加上括号使其满足我们的要求。