第四章 表达式
表达式是由一个或多个运算对象组成,对表达式求值将得到一个结果。字面值和变量是最简单的表达式,其结果就是字面值和变量的值。
4.1 基础
4.1.1 基本概念
一元运算符:作用于一个运算对象的运算符
二元运算符:作用于两个运算对象的运算符
三元运算符
函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。
在表达式求值的过程中,运算对象常常由一种类型转换成另外一种类型。
我们使用重载运算符时,其包括运算对象的类型和返回值的类型,都是由该运算符定义的;但是运算对象的个数、运算符的优先级和结合律都是无法改变的。
左值和右值
C++的表达式要不然是右值,要不然就是左值。
当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
一个重要的原则:在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。当一个左值被当成右值使用时,实际使用的是它的内容(值)。
1、赋值运算符:需要左值作为其左侧运算对象,得到的结果也是一个左值
2、取地址符作用于一个左值运算对象,返回一个右值
3、内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值
4、内置类型和迭代器的递增递减运算符作用于左值对象,所得的结果也是左值
关键字decltype:如果表达式的求值结果是左值,则得到一个引用类型。
4.1.2 优先级与结合律
复合表达式:含有两个或多个运算符的表达式
优先级和结合律(算数运算符满足左结合律)
括号无视优先级与结合律
4.1.3 求值顺序
在大多数情况下,不会明确指定求值的顺序。
如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。
有四种运算符明确规定了运算对象的求值顺序:&&、||、:?(条件运算符)、,(逗号运算符)
如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。
4.2 算术运算符
+(正号)、-(负号)> *(乘法)、/(除法)、%(求余)> +(加法)、-(减法)
除非另做特殊说明,算术运算符都能作用于任意算术类型以及任意能转换为算术类型的类型。算术运算符的运算对象和求值结果都是右值。
算数表达式有可能产生未定义的结果。一部分原因是数学性质本身:例如除数是0的情况;另外一部分则源于计算机的特点:例如溢出。
负数取整也是直接切除小数部分。
参与取余运算的运算对象必须是整数类型。
如果m%n不等于0,则它的符号和m相同:m%(-n) = m%n、(-m)%n = -(m%n)
4.3 逻辑和关系运算符
关系运算符作用与算术类型或指针类型
逻辑运算符作用于任意能转换成布尔值的类型
0表示假,否则表示真
对于这两类运算符来说,运算对象和求值结果都是右值。
! > (<、<=、>、>=) > (==、!=) > && > ||
逻辑与运算符和逻辑或运算符:短路求值
使用引用避免拷贝:拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。
进行比较运算时除非比较的对象是布尔类型,否则不要使用布尔字面值true和false作为运算对象。
4.4 赋值运算符
赋值运算符的左侧运算对象必须是一个可修改的左值。
区分初始化和赋值。
允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象。如果左侧运算对象是内置类型,那么初始值列表最多只能包含一个值,并且该值即使转换的话其所占空间也不应该大于目标类型的空间(窄化转换,如double转int)。
无论左侧运算对象的类型是什么,初始值列表都可以为空,此时编译器创建一个值初始化的临时量并将其赋给左侧运算对象。
赋值运算满足右结合律。
赋值运算符的优先级低于关系运算符的优先级,通常需要给赋值部分加上括号使其符合我们的原意。
复合运算符:a = a op b;
唯一的区别是左侧运算对象的求值次数:使用复合运算符只求值一次,使用普通的运算符则求值两次。
4.5 递增和递减运算符
必须作用于左值运算对象。
前置版本:首先将运算对象加1或减1,然后将改变后的对象作为求值结果,左值返回;
后置版本:也会将运算对象加1或减1,但是求值结果是运算对象改变之前那个值的副本,右值返回
int i = 0, j;
j = ++i; // j = 1, i = 1
j = i++; // j = 1, i = 2
除非必须,否则不用递增递减运算符的后置版本。
后置递增运算符的优先级高于解引用运算符,书写
cout << *iter++ << endl;
要比书写下面的等价语句更简洁、也更少出错
cout << *iter << endl;
++iter;
因为没有规定运算对象的求值顺序,而且递增和递减运算符会改变运算对象的值,所以要提防在复合表达式中错用这两个运算符,否则也会产生未定义的错误。
4.6 成员访问运算符
点运算符和箭头运算符都可用于访问成员。
解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号。
箭头运算符作用于一个指针类型的运算对象,结果是一个左值。
点运算符分成两种情况:如果成员所属的对象是左值,那么结果是左值;反之,如果成员所属的对象是右值,那么结果是右值。
4.7 条件运算符
条件运算符:cond ? expr1 : expr2
expr1和expr2是两个类型相同或可能转换为某个公共类型的表达式。两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值,否则运算的结果是右值。
允许在条件表达式的内部嵌套另外一个条件表达式。
条件表达式满足右结合律。
条件运算符的优先级非常低,因此当一条长表达式中嵌套了条件运算子表达式时,通常需要在它两端加上括号(包括输出)。
4.8 位运算符
位运算符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合,同样能用于bitset类型。
运算对象可以是有符号的,也可以是无符号的。如何处理运算对象的符号位依赖于机器,左移操作可能会改变符号位的值,因此是一种未定义的行为。
移位运算符
移位运算符右侧的运算对象一定不能为负,而且值必须严格小于结果的位数,否则就会产生未定义的行为。二进制位或者向左移或者向右移,移出边界之外的位就被舍弃掉了。
位求反运算符
将运算对象逐位求反后生成一个新值,将1置为0,将0置为1。
位与、位或、位异或运算符
都是逐位执行逻辑操作。
位与:两个运算对象的对应位置都是1,则运算结果中该位为1,否则是0;
位或
位异或:如果两个运算对象的对应位置有且只有一个为1,则运算结果中该位为1,否则是0
移位运算符满足左结合律。优先级比算术运算符低,但比关系运算符、赋值运算符和条件运算符的优先级高。
4.9 sizeof运算符
返回一条表达式或一个类型名字所占的字节数吧,满足右结合律,返回的值是size_t类型的常量表达式。运算符的运算对象有两种形式:1、sizeof(type);2、sizeof expr。
在第二种形式中,sizeof返回的是表达式结果类型的大小,并不实际计算其运算对象的值。因此,可以解引用一个无效指针作为表达式,sizeof不需要真的解引用指针也能知道它所指对象的类型。
sizeof运算符可以使用作用域运算符来获取类成员的大小,无须我们提供一个具体的对象。
对引用类型执行sizeof运算得到被引用对象所占空间的大小。
对数组执行sizeof运算得到整个数组所占空间的大小,而不是将数组转换成指针来处理。
对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。
4.10 逗号运算符
逗号运算符含有两个运算对象,按照从左到右的顺序依次求值,逗号运算符规定了运算对象求值的顺序(与逻辑与、逻辑或、条件运算符一样)。
逗号运算符真正的结果是右侧表达式的值,如果右侧运算对象是左值,那么最终的求值结果也是左值。
4.11 类型转换
隐式转换:类型转换是自动执行的
算数类型之前的隐式转换被设计得尽可能避免损失精度。
何时发生隐式类型转换?
1、在大多数表达式中,比int类型笑的整型值首先提升为较大的整数类型
2、在条件中,非布尔值转换为布尔类型
3、初始化和赋值
4、算数运算或关系运算的运算对象有多种类型,需要转换成同一种类型
5、函数调用时
4.11.1 算数转换
算数转换的规则定义了一套类型转换的层次,其中运算符的运算对象将转换成最宽的类型,
整型提升:负责把小整数类型(bool、char、short)转换成较大的整数类型。
无符号类型的运算对象
首先进行整型提升,如果类型匹配,无须进行进一步的转换。如果两个提升后的运算对象的类型要么都是带符号的、要么都是无符号的,则小类型的运算对象转换成较大的类型。
如果一个运算对象是无符号类型、另外一个运算对象是带符号类型:
1、无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的;
2、带符号类型大于无符号类型,此时的转换结果依赖于机器:
(1)无符号类型的所有值都能存在带符号类型中,则无符号类型的运算对象转换成带符号类型
(2)如果不能,那么带符号类型的运算对象转换成无符号类型
4.11.2 其他隐式类型转换
除了算数转换之外还有几种隐式类型转换:
1、数组转换成指针:除decltype、&、sizeof、typeid以及作为引用来初始化数组时
2、指针的转换:(1)常量整数值0或者字面值nullptr能转换成任意指针类型;(2)任意非常量的指针能转换成void*;(3)任意指向对象的指针能转换成const void*;(4)继承关系的类型
3、算术类型或指针类型向布尔类型自动转换
4、转换成常量:非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样
5、类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。如:字符串字面值转换成string类型、cin转换成布尔值
4.11.3 显式转换
有时我们希望显式地将对象强制转换成另外一种类型,这种方法称作强制类型转换。强制类型转换干扰了正常的类型检查,尽量避免使用强制类型转换。
cast-name<type>(expression)
如果type是引用类型,则结果是左值。
static_cast
只要不包含底层const,都可以使用static_cast
当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用。
还可以使用static_cast找回存在于void*指针中的值,但我们必须确保转换后所得的类型就是指针所指的类型,类型一旦不符,将产生未定义的后果。
在一个方向上可以作隐式转换,在另外一个方向上就可以作静态转换。
const_cast
const_cast只能改变运算对象的底层const,不能用来改变表达式的类型。
注意:const_cast只能用于指针或引用,不能用于变量。
脱掉const后的引用或指针可以改吗?:可以改变const自定义类的成员变量,但是对于内置数据类型,却表现未定义行为。总之,不要试图去改变const修饰的对象!
reinterpret_cast
通常为运算对象的位模式提供较低层次上的重新解释,也就是说将数据以二进制存在形式的重新解释。
在双方向上都不可以隐式类型转换的,则需要重解释类型转换。
可以用于任意类型的指针之间的转换,对转换的结果不做任何保证,尽量少用。
旧式的强制类型转换
type (expr); // 函数形式的强制类型转换
(type) expr; // C语言风格的强制类型转换
4.12 运算符优先级表
后置递增递减运算符的优先级 大于 前置递增递减运算符
术语表
const_cast:一种涉及const的强制类型转换。将底层const对象转换成对应的非常量类型,或者执行相反的转换。
整型提升:把一种较小的整数类型转换成与之最接近的较大整数类型的过程。不论是否真的需要,小整数类型总是会得到提升。