C++系统学习之五:表达式
表达式由一个或多个运算对象组成,对表达式求值将得到一个结果。字面值和变量是最简单的表达式,其结果就是字面值和变量的值。把一个运算符和一个或多个运算对象组合起来可以生成较复杂的表达式。
基础
1、基本概念
- 一元运算符
- 二元运算符
- 三元运算符
左值和右值
C++的表达式要么是左值,要么就是右值。
当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
在需要右值的地方可以用左值来代替,但是不能把右值当成左值使用。
2、优先级与结合律
复合表达式是指含有两个或多个运算符的表达式。求复合表达式的值需要首先将运算符和运算对象合理地组合在一起,优先级与结合律决定了运算对象组合的方式。
括号无视优先级与结合律
3、求值顺序
优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值。在大多数情况下,不会明确指定求值的顺序。
int i=f1()*f2();
上述就无法知道到底f1在f2之前调用,还是f2在f1之前调用。
NOTE:对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。
int i=0; cout<<i<<" "<<++i<<endl; //未定义
(但是VS2013中测试结果是先进行++i运算)
有四种运算符明确规定了运算对象的求值顺序。
- 逻辑与(&&)运算符:先求左侧对象的值,只有当左侧运算对象的值为真时才继续求右侧对象的值
- 逻辑或(||)运算符
- 条件(?:)运算符
- 逗号运算符
求值顺序、优先级、结合律
运算对象的求值顺序与优先级和结合律无关。当表达式中对象不改变同一对象时,其求值顺序不受限制,如果有对同一对象产生影响,则会是一条错误的表达式,产生未定义的行为。(VS13中已经不报错,而是按照从左到右的顺序求值,如果表达式中只有一个对象改变其值,则先求改变对象的值)
int n = 0; int f1() { return ++n; } int f2() { return --n; } int main() { int a = f1() + f2(); cout << a << endl; system("pause"); return 0; }
输出结果是1.
因为f1和f2都对n有改变,所以先求f1的值再求f2的值。
int i = 0; cout << i << " " << ++i << endl;
输出结果是1,1.
因为++i会改变对象,所以先对其求值,再从左到右的结合律执行。
算术运算符
算术运算符的运算对象和求值结果都是右值。在表达式求值之前,小整数类型的运算对象被提升成较大的整数类型(int),所有运算对象最终会转换成同一类型。
对大多数运算符来说,布尔类型的运算对象将被提升为int,对于一元运算符+或-,布尔值不应该参与运算。
整数相除的结果还是整数,小数部分直接弃掉。
%取余:计算两个整数相除所得的余数,运算对象必须都是整数。
逻辑和关系运算符
关系运算符作用域算术类型或指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。二者的运算对象和求值结果都是右值。
逻辑与和逻辑或运算符
二者都是先求左侧运算对象的值再求右侧运算对象的值。
短路求值:当左侧运算对象的值无法确定表达式的结果时才会计算右侧运算对象的值。
赋值运算符
赋值运算符的左侧运算对象必须是一个可修改的左值。
赋值运算的结果是它的左侧运算对象,并且是一个左值。相应的,结果的类型就是左侧运算对象的类型。如果赋值运算符的左右两个运算对象类型不同,则右侧运算对象转换成左侧运算对象的类型。
赋值运算满足右结合律
其他二元运算符满足左结合律,赋值运算返回的是其左侧运算对象。
赋值运算优先级较低
通常需要给赋值部分加上括号使其符合我们的原意。
不要混用赋值运算符和相等运算符
复合赋值运算符
任意一种复合运算符都完全等价于:
a=a op b;
区别在于左侧运算对象的求值次数:使用复合运算符只求值一次,使用普通运算符则求值两次。
递增和递减运算符
递增和递减运算符有两种形式:前置版本和后置版本。
前置版本是先对对象进行递增或递减运算并返回运算之后的对象值,对象本身已经发生改变。
后置版本是先返回未改变之前的对象的值,然后再对对象进行递增或递减操作。
NOTE:除非必须,否则不用递增或递减运算符的后置版本。
在一条语句中混用解引用和递增运算符
如果想在一条复合表达式中既将变量加1或减1又能使用它原来的值,这时就可以使用递增或递减的后置版本。
NOTE:后置运算符的优先级高于解引用运算符,因此*p++等价于*(p++)
成员访问运算符
点运算符和箭头运算符
NOTE:解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号。
条件运算符
条件运算符(?:)允许把简单的if-else逻辑嵌入到单个表达式中
cond?expr1:expr2
cond是判断条件的表达式,expr1和expr2是两个类型相同或可能转换为某个公共类型的表达式。
表达式的结果是expr1或expr2的返回值。当两个表达式都是左值或能转换成同一种左值类型时,运算的结果哦是左值;否则运算的结果是右值。
嵌套条件运算符
条件运算的嵌套最好别超过两到三层。
在输出表达式中使用条件运算符
条件运算符的优先级非常低,因此当一条长表达式中嵌套了条件运算子表达式时,通常需要加上括号。
位运算符
位运算符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合。位运算符提供检查和设置二进制位的功能。
一般来说,如果运算复习时小整型,则会自动提升成较大的整数类型。运算对象可以是带符号,也可以是无符号。如果运算对象是带符号的且它的值为负,那么位运算符如何处理对象的“符号位”依赖于机器,左移操作可能改变符号位的值,因此是一种未定义的行为。
NOTE:强烈建议仅将位运算符用于处理无符号类型。
移位运算符
首先令左侧运算对象的内容按照右侧运算对象的要求移动指定位数,然后将经过移动的左侧运算对象的拷贝作为求值结果。
其中,右侧的运算对象一定不能为负,而且值必须严格小于结果的位数,否则产生未定义的行为。二进制位移出边界之外的位被舍弃掉。
位求反运算符
位与、位或、位异或运算符
sizeof运算符
sizeof运算符返回一条表达式或一个类型名字所占的字节数。其值是一个size_t类型的常量表达式。
sizeof并不实际计算其运算对象的值。
- 对char或者类型为char的表达式执行sizeof运算,结果为1
- 对引用和解引用指针执行sizeof,得到的是实际对象所占空间的大小
- 对指针的运算结果是指针本身所占空间的大小
- 对数组的运算结果是整个数组所占空间的大小,等价于对数组中所有的元素各执行一次的结果之和
- 对string和vector的执行结果是该类型固定部分的大小,不会计算对象中的元素占用了多少空间
NOTE:sizeof的返回值是常量表达式,因此可以用来定义数组。
逗号运算符
逗号运算符含有两个运算对象,安装从左向右的顺序依次求值。
首先对左侧的表达式求值,然后将求值结果丢弃掉,逗号运算符真正的结果是右侧表达式的值。如果右侧对象是左值,最终结果也是左值。
逗号运算符经常被用在for循环中。
类型转换
算术类型之间的隐式转换被设计得尽可能避免损失精度。
何时发生隐式转换
- 大多数表达式中,比int类型小的整型值首先提升为较大的整数类型
- 在条件中,非布尔值转换成布尔类型
- 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型
- 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
- 函数调用时也会发生类型转换
算术转换
运算符的运算对象转换成最宽的类型。当表达式中既有浮点又有整型的时候,整型转换为浮点。
整型提升
- 小整数类型(bool、char、signed char、unsigned char、short和unsigned short)转换成int类型
- 较大的char类型(wchar_t、char16_t、char32_t)提升为int、unsigned、long、unsigned long、long long和unsigned long long中能容纳其值的最小的一种
无符号类型的运算对象
- 无符号类型大于带符号类型,则带符号类型转换成无符号类型
- 无符号类型小于带符号类型,转换结果依赖于机器。
其他隐式类型转换
数组转换成指针
在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针;
当数组被用作decltype关键字的参数,或者作为取地址符、sizeof、typeid等运算符的运算对象时,上述转换不会发生。
指针的转换
转换成布尔类型
如果指针或算术类型的值为0,转换结果是FALSE,否则是TRUE
转换成常量
允许将非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。相反的转换并不存在,因为它试图删除掉底层const。
类类型定义的转换
显式转换
也称为强制类型转换。
NOTE:强制类型转换很危险
命名的强制类型转换
cast-name<type>(expression);
其中,type是转换的目标类型而expression是要转换的值。
cast-name是static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种。
static_cast
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast.
当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用。它会让编译器知道我们不在乎精度损失,而不报警。
static_cast对于编译器无法自动执行的类型转换也非常有用。
const_cast
const_cast只能改变运算对象的底层const。常用于有函数重载的上下文中。
只有const_cast能改变表达式的常量属性,使用其他形式的命名类型转换改变表达式的常量属性都将引发编译器错误。
const char *p; char *q = const_cast<char*>(p);
reinterpret_cast
通常为运算对象的位模式提供较低层次上的重新解释。尽量不使用。主要是指针之间的转换
int *ip; char *q = reinterpret_cast<char*>(ip);