目录
第4章 优先级和运算符
4.1 基础
表达式由一个或多个运算对象(operand)组成,对表达式求值将得到一个 结果(result)。字面值和变量是最简单的表达式(expression),其结果就是字面值和变量的值。把一个运算符(operator)和一个或多个运算对象组合起来可以生成较复杂的表达式。
1)基础概念
C++定义了一元运算符(unary operator)和二元运算符(binary operator),作用于一个运算对象的运算符是一元运算符,如取 地址符( & ) 和 解引用符( * );作用于两个运算对象的运算符是二元运算符,如 相等运算符( == ) 和 乘法运算符( * )。除此之外,还有一个作用于三个运算对象的 三元运算符(Ternary Operator)。函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。
可以看到 * 既能作为一元运算符也能作为二元运算符。。。
表达式求值过程中,类型转换的规则有点复杂,如:整数能转换成浮点数,浮点数也能转换成整数,但是指针不能转换成浮点数。而小整数类型(如 bool、char、short 等)通常会被 提升(promoted) 为较大的整数类型,主要是 int。
重载运算符
C++定义了运算符作用于 内置类型 和 复合类型 的运算对象时所执行的操作。当运算符作用于 类类型 的运算对象时,用户可以自定义其含义,这被称作 运算符重载(overloaded operator),如:IO 库的 >> 和 << 运算符以及 string 对象、vector 对象和迭代器使用的运算符等。
左值和右值
C++的表达式分为 右值(rvalue) 和 左值(lvalue):
- 当一个对象被用作右值的时候,用的是 对象的值(内容);
- 当一个对象被用作左值的时候,用的是 对象的身份(在内存中的位置)。
其中,需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。
- 赋值运算符需要一个非常量左值作为其左侧运算对象,得到的结果也仍然是一个左值;
- 取地址符作用于一个左值运算对象,返回指向该运算对象的指针,该指针是一个右值;
- 内置解引用运算符、下标运算符、迭代器解引用运算符、string 和 vector 的下标运算符都返回左值;
- 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本返回左值,后置版本返回右值。
如果decltype作用于一个求值结果是左值的表达式,会得到一个引用类型。
2)优先级与结合律
复合表达式(compound expression)指含有两个或多个运算符的表达式。运算符和运算对象合理地组合在一起,优先级与结合律决定了运算对象的组合方式,高优先级运算符先运行(乘法和除法,然后加法和减法),如果优先级相同,则其组合规则由结合律确定,从左向右顺序运行。
括号无视优先级与结合律,表达式中括号括起来的部分被当成一个单元来求值,然后再与其他部分一起按照优先级组合。
3)求值顺序
对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。
有4 种运算符明确规定了运算对象的求值顺序:
- 逻辑与 ( && ) 运算符
- 逻辑或 ( || ) 运算符
- 条件 ( ? : ) 运算符
- 逗号 ( , ) 运算符
处理复合表达式时建议遵循以下两点是有益的:
- 不确定求值顺序时最好使用括号来强制让表达式的组合关系符合程序逻辑的要求;
- 如果表达式改变了某个运算对象的值,则在表达式的其他位置不要再使用这个运算对象。
不过,第2条规则有一个重要例外,当改变运算对象的子表达式本身就是另一个子表达式的运算对象时,规则无效。例如表达式 *++iter,递增运算符改变了 iter的值,而改变后的 iter的值又是解引用运算符的运算对象。这种或者类似情况下,求值的顺序不会成为问题,因为递增运算(即改变运算对象的子表达式)必须先求值,然后才轮到解引用运算。
4.2 算术运算符
算术运算符(按照运算符的优先级排序):
上面的所有运算符都满足左结合律, 意味着当优先级相同时按照从左向右的顺序进行组合。
除法运算:
整数相除( / )结果还是整数,即直接弃除商的小数部分;
取余 或 取模 运算符( % )计算整数相除的余数。
在除法运算中,C++语言的早期版本允许结果为负数的商向上或向下取整,C++11新标准则规定商一律向0取整(即直接去除小数部分)。
除了-m导致溢出的特殊情况,其他时候(-m)/n和m/(-n)都等于-(m/n),m%(-n)等于m%n,(-m)%n都等于-(m%n)。
21 % 6; /* 结果是3 */ 21 / 6; /* 结果是3 */
21 % 7; /* 结果是0 */ 21 / 7; /* 给果是3 */
-21 % -8; /* 结果是-5 */ -21 / -8; /* 结果是2 */
21 % -5; /* 结果是1 */ 21 / -5; /* 结果是-4 */
4.3 逻辑和关系运算符
关系运算符作用于算术类型和指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。
逻辑与 运算符 && 和 逻辑或 运算符 || 都是先计算左侧运算对象的值再计算右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会去计算右侧运算对象的值,这种策略称为 短路求值(short-circuit evaluation)。
- 对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值。
- 对于逻辑或运算符来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。
if 语句的条件部分首先检查 s 是否是一个空 string,如果是,则不论右侧如何都换行;只有当 string 对象非空时才求第二个运算对象的值,即是否是以句号结束。
逻辑非 运算符 ! 将运算对象的值取反后返回。
关系运算符 比较运算对象的大小关系并返回布尔值,关系运算符都满足左结合律。
进行比较运算时,除非比较的对象是布尔类型,否则不要使用布尔字面值
true
和false
作为运算对象。
4.4 赋值运算符
赋值运算符 = 的左侧运算对象必须是一个 可修改 的 左值。
赋值运算符满足右结合律。
因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号。
复合赋值运算符包括 +=、-=、*=、/=、%=(算术运算符);
<<=、>>=、&=、^= 和 |=(位运算符)。
任意一种复合运算都完全等价于 a = a op b。
4.5 递增和递减运算符
递增( ++ )和递减( -- )运算符是为对象加1或减1的简洁书写形式。这两个运算符还可应用于迭代器,因为很多迭代器本身不支持算术运算。
递增和递减运算符分为前置版本和后置版本:
- 前置版本:首先将运算对象加1(或减1),然后将改变后的对象作为求值结果。
- 后置版本:也会将运算对象加1(或减1),但求值结果是运算对象改变前的值的副本。
除非必须,否则不应该使用递增或递减运算符的后置版本。因为后置版本需要将原始值存储下来以便于返回修改前的内容,如果我们不需要这个值,那么后置版本的操作就是一种浪费。建议养成使用前置版本的习惯,这样不仅不需要担心性能的问题, 而且更重要的是写出的代码会更符合编程的初衷。
后置递增运算符的优先级高于解引用运算符,因此*pbeg++等价于 *(pbeg++)。pbeg++ 把 pbeg 的值加1, 然后返回 pbeg 的初始值的副本作为其求值结果,此时解引用运算符的运算对象是 pbeg 未增加之前的值。最终,这条语句输出 pbeg 开始时指向的那个元素,并将指针向前移动一个位置。
如果返回的是加1之后的值,解引用该值将产生错误的结果。不但无法输出第一个元素,而且更糟糕的是如果序列中没有负值,程序将可能试图解引用一个根本不存在的元素。
在某些语句中混用解引用和递增运算符可以使程序更简洁!!!
cout << *iter++ << endl;
// cout << *iter << endl;
// ++iter;
4.7 条件运算符
条件运算符(? :)的使用形式如下:
cond ? expr1 : expr2;
其中cond是判断条件的表达式,而expr1和expr2是两个类型相同或可能转换为某个公共类型的表达式。先求 cond 的值,如果cond为真则对 expr1 求值并返回该值,否则对 expr2 求值并返回该值。
只有当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果才是左值,否则运算的结果就是右值。
条件运算符可以嵌套,但是考虑到代码的可读性,运算的嵌套层数最好不要超过两到三层。
4.8 位运算符
位运算符(左结合律):
在位运算中符号位如何处理并没有明确的规定,所以强烈建议仅将位运算符用于无符号类型的处理。
左移运算符 << 在运算对象右侧插入值为0的二进制位;右移运算符 >> 的行为依赖于其左侧运算对象的类型:如果该运算对象是无符号类型,在其左侧插入值为0的二进制位;如果是带符号类型,在其左侧插入符号位的副本或者值为0的二进制位,如何选择视具体环境而定。
位求反运算符( ~ )将运算对象逐位求反而生成一个新值,将1置为0、将0置为1。
char 类型的运算对象首先提升成 int 类型,提升时运算对象原来的位保持不变, 往 高位(high order position) 添0即可。
与( & )、或( | )、异或( ^ )运算符在两个运算对象上逐位执行相应的逻辑操作。
4.9 sizeof运算符
sizeof 运算符返回一个表达式或一个类型名字所占的字节数,返回值是 size_t 类型。
在 sizeof
的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用。
sizeof
运算符的结果部分依赖于其作用的类型:
- 对 char 或者类型为 char 的表达式执行 sizeof 运算,返回值为1。
- 对引用类型执行 sizeof 运算得到被引用对象所占空间的大小。
- 对指针执行 sizeof 运算得到指针本身所占空间的大小。
- 对指针执行 sizeof 运算得到指针本身所占空间的大小。对解引用指针执行 sizeof 运算得到指针指向的s对象所占空间的大小,指针不需要有效。
- 对数组执行 sizeof 运算得到整个数组所占空间的大小。
- 对 string 或 vector 对象执行 sizeof 运算只返回该类型固定部分的大小,不会计算对象中元素所占空间的大小。
4.10 逗号运算符
逗号运算符 , 含有两个运算对象,按照从左向右的顺序依次求值,最后返回右侧表达式的值。逗号运算符经常用在 for 循环中。
4.11 类型转换
在C++语言中,某些类型之间有关联。如果两种类型有关联, 那么当程序需要其中一种类型的运算对象时,可以用另一种关联类型的对象或值来替代。换句话说,如果两种类型可以 相互转换(conversion),那么它们就是关联的。
无须程序员介入,会自动执行的类型转换叫做 隐式转换(implicit conversions),可以尽可能地避免损失精度。
在下面这些情况下, 编译器会自动地转换运算对象的类型:
- 在大多数表达式中,比 int 类型小的整型值首先提升为较大的整数类型。
- 在条件中,非布尔值转换成布尔类型。
- 在初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
- 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
- 函数调用时也会发生类型转换。
1)算术转换
把一种算术类型转换成另一种算术类型叫做 算术转换(arithmetic conversion),其中运算符的运算对象将被转换成最宽的类型。
整型提升(integral promotions) 负责把小整数类型转换成较大的整数类型。
如果某个运算符的运算对象类型不一致, 这些运算对象将转换成同一种类型。但是如果某个运算对象的类型是无符号类型,那么转换的结果就要依赖于机器中各个整数类型的相对大小了。
要想理解算术转换,办法之一就是研究大量的例子:
2)其他隐式类型转换
数组转换成指针:在大多数表达式中,数组名字自动转换成指向数组首元素的指针。
指针的转换:常量整数值0或字面值 nullptr
能转换成任意指针类型;指向任意非常量的指针能转换成void*;指向任意对象的指针能转换成const void*。
转换成布尔类型:任意一种算术类型或指针类型都能转换成布尔类型。如果指针或算术类型的值为0,转换结果是 false,否则是 true。
转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。
类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。
3)显式转换
显式类型转换 也叫做 强制类型转换(cast)。
int i, j;
double slope = i/j;
虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的。建议尽量避免强制类型转换。
命名的强制类型转换(named cast) 形式如下:
cast-name<type>(expression);
其中 type 是转换的目标类型,expression 是要转换的值。如果 type 是引用类型,则转换结果是左值。cast-name 是 static_cast
、dynamic_cast
、const_cast
和reinterpret_cast
中的一种,用来指定转换的方式。
dynamic_cast
支持运行时类型识别- 任何具有明确定义的类型转换,只要不包含底层
const
,都能使用static_cast
。当需要把一个较大的算术类型赋值给较小的类型时,static cast
非常有用。static cast
对于编译器无法自动执行的类型转换也非常有用 const_cast
只能改变运算对象的底层const
,同时也只有const_cast
能改变表达式的常量属性。const_cast
常常用于函数重载。- reinterpret_cast 通常为运算对象的位模式提供底层上的重新解释。reinterpret_cast 本质上依赖于机器。要想安全地使用reinterpret_cast 必须对涉及的类型和编译器实现转换的过程都非常了解。
- 早期版本的C++语言中,显式类型转换包含两种形式:
- type (expression); // 函数形式的强制类型转换
- (type) expression; // C语言风格的强制类型转换
4.12 运算符优先级表