第四章:表达式
4.1 基础
1.表达式:由一个或多个运算对象组成,对表达式求值将得到一个结果,字面值和变量是最简单的表达式,讲一个运算符和一个或多个运算对象组合起来可以生成较为复杂的表达式。
2.左值和右值:当对象被用作左值的时候,用的是对象的身份(在内存中的位置);
当一个对象被用作右值时,用的是对象的值(内容)
使用关键字 decltype 时,左值和右值也有所不同,若表达式求值结果为左值,decltype 作用于该表达式得到一个引用类型。
int *p;
//解引用运算符生成左值,所以得到的结果为 int&
decltype(*p);
//取地址运算符生成右值,所以得到的结果为 int**
decltype(&p);
3.复合表达式:指含有两个或多个运算符的表达式,对于含有多个运算符的复杂表达式来说,要理解它的含义首先要理解运算符的优先级、结合律以及运算对象的求值顺序
1)左结合律:若运算符优先级相同,则按照从左到右的顺序组合运算对象
2)求值顺序:优先级规定了运算对象的组合方式,但没说明运算对象按照什么顺序求值,在多数情况不会明确指定求值顺序;
对于没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并昌盛未定义的行为;
有4种运算符明确规定了运算对象的求值顺序:
逻辑与( && )运算符:规定先求左侧运算对象的值,只有当左侧运算符为真时才继续求右侧运算对象的值
逻辑或( || )运算符:对顶先求左侧运算对象的值,只有左侧运算对象为假时擦对右侧运算对象求值
条件( ?: )运算符:先对条件进行判断,为真对表达式1计算,为假对表达式2计算
逗号(,)运算符:按照从左向右的顺序依次求值
3)求值顺序、优先级、结合律:运算对象的求值顺序与优先级和结合律无关,e g:表达式 f() + g() * h() + j()
优先级规定:g() 的返回值和 h() 的返回值相乘
结合律规定:f() 的返回值先与 g() 和 h() 的乘积相加,所得结果再与 j() 的返回值相加
求值顺序:对于这些函数的调用顺序没有明确规定
注:如果f、g、h、j是无关函数,既不会改变同一对象状态,也不执行IO任务,那么函数调用顺序不受限制,反之若其中几个函数影响同一对象则是错误表达式,将产生未定义行为
4.2 算术运算符
1.优先级:一元运算符优先级最高,之后是乘法和除法,加法和减法优先级最低
结合律:左结合律,优先级相同时按照从左到右的顺序结合
算数运算符的运算对象和求值结果都是右值
在表达式求值之前,小整数类型的运算对象被提升成较大的整数类型,所有运算对象最终会转换成同一类型
4.3 逻辑和关系运算符
1.运算对象和求值结果均为右值
逻辑与和逻辑或运算符都有明确的求值顺序
关系运算符都满足左结合律
2.进行比较运算时除非比较的对象是布尔类型,否则不要使用布尔字面值 true 和 false 作为运算对象
if(val) {} //若val是任意非0值,条件为真
if(!val) {} //若val是0,条件为真
if(val == true) {} //只有当 val 为1时为真
4.4 赋值运算符
1.赋值运算符的左侧运算对象必须是一个可修改的左值
int i = 0, j = 0, k = 0; //初始化而非赋值
const int ci = i; //初始化而非赋值
i + j = k; //错误:算术表达式是右值
ci = k; //错误:ci是常量(不可修改)左值
2.结合律:赋值运算符满足右结合律
优先级:赋值运算符优先级比较低,所以通常需要给赋值部分加上括号使其符合我们的原意
4.5 递增和递减运算符
1.前置版本和后置版本:前置版本运算符首先将运算对象加1(或减1),然后将改变之后的对象作为求值结果
后置版本运算符也会将运算对象加1(或减1),但是求值结果是运算对象改变之前那个值的副本
两种运算符都必须作用于左值运算对象
前置版本将对象本身作为左值返回
后置版本则将对象原始值的副本作为右值返回
注:除非必须,否则不用递增递减运算符的后置版本,因为前置版本的递增运算符避免了不必要的作用,它将值加1后直接返回改变了的运算对象,而后置版本需将原始值存储下来以便于返回这个未修改的内容,若我们不需要修改前的值则是一种浪费,因此应养成使用前置版本的习惯
2.运算对象可按任意顺序求值:大多数运算符没规定运算对象的求值顺序,一般没什么影响,但若一条子表达式改变了某个运算对象的值,另一条子表达式又要用该值的话,求值顺序就很关键了
*beg = toupper(*beg++); //错误:该赋值语句未定义
//编译器可能按照下面的任意一种思路处理
*beg = toupper(*beg); //如果先求左侧的值
*(beg + 1) = toupper(*beg); //如果先求右侧的值
4.6 成员访问运算符
1.解引用运算符的优先级低于点运算符,因此执行解引用运算的子表达式两端必须加括号,否则代码含义完全不同
(*p).size(); //正确
*p.size(); //错误,p是一个指针
4.7 条件运算符
1.当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值,否则运算的结果是右值
2.由于条件运算符的优先级非常低,因此当一条长表达式嵌套了条件运算子表达式时,通常需要在它两端加上括号。
cout << ((grade < 60) ? "fail" : "pass"); //正确,输出 fail 或者 pass
cout << grade < 60 ? "fail" : "pass"; //错误,因为试图比较 cout 和 60
4.8 位运算符
1.位运算符作用于整数类型的运算对象,并把运算对象视为二进制的集合
如果运算对象是“小整型”,则它的值会被自动提升成较大的整数类型,e g:char 类型会被提升为 int 类型
关于符号位的处理没有明确的规定,因此建议仅将位运算符用于处理无符号类型
2.移位运算符:优先级:比高不低处于中间,比算数运算符的优先级低,但比关系运算符、赋值运算符和条件运算符优先级高
结合律:满足左结合律,
4.9 sizeof 运算符
1.sizeof 运算符返回一条表达式结果类型或一个类型名字所占的字节数
size (type)
size expr //返回的是表达式结果类型的大小,但并不实际计算运算对象的值
2.1)sizeof 运算符满足右结合律
2)sizeof 所得的值是一个 size_t 类型的常量表达式
3)对引用类型执行 sizeof 运算得到被引用对象所占空间的大小
4)对解引用指针执行 sizeof 运算得到指针指向的对象所占空间大小,指针不需要有效
5)对数组执行 sizeof 运算得到整个数组所占空间得到小,等价于对数组中所有元素各执行一次 sizeof 运算并求和,sizeof 不会把数组转换成指针处理
6)对 string 对象和 vector 对象执行 sizeof 运算只返回该类型固定部分大小,不会计算对象中的元素占用了多少空间
Sales_data data, *p;
sizeof(Sales_data); //存储Sales_data类型的对象所占空间大小
sizeof data; //data的类型大小,即sizeof(Sales_data)
sizeof p; //指针所占的空间大小
//sizeof 和 * 优先级相同,而sizeof满足右结合律,所以等价于 sizeof(*p)
//由于 sizeof 不会实际求运算对象的值,所以即使p是无效(未初始化)的指针,依旧是安全的行为,因为指针没有实际的使用
sizeof *p;
sizeof data.revenue; //Sales_data 的 revenue 成员对应类型大小
sizeof Sales_data::revenue; //另一种获得 revenue 大小的方式
4.10 逗号运算符
1.含有两个运算对象,规定了运算对象求值顺序,按照从左向右的顺序依次求值。
2.对逗号运算符来说,首先对左侧的表达式求值,然后将求值结果丢弃掉,逗号运算符的真正结果是右侧表达式的值
若右侧运算对象是左值,则最后结果也是左值
int j = 10;
int i = (j++, j + 100, 999 + j); //最终结果为最右侧表达式的值,即1010
4.11 类型转换
1.C++语言不会直接将两个不同类型的值相加,而是根据类型转换规则设法将运算对象的类型统一后再求值,该类型转换是自动执行的,无需程序员介入,被称为隐式转换
2.在下述情况,编译器会自动地转换运算对象的类型
1)在大多数表达式中,比 int 类型小的整型值首先提升为较大的整数类型
2)在条件中,非布尔值转换成布尔类型
3)初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型
4)如果算术运算或关系运算的运算对象有多种类型,需转换成同一类型
5)在第六章,函数调用也会发生类型转换
3.无符号类型的运算对象:若某个运算对象的类型是无符号类型,那么转换结果要依赖于机器中各个整数类型的相对大小
1)首先执行整型提升
2)若一个运算对象是无符号类型、另一个运算对象是带符号类型,而其中的无符号类型不小于带符号类型,则带符号类型转为无符号,但如果 int 类型恰好为负值,会带来副作用
若带符号类型大于无符号类型,此时转换结果依赖于机器,若无符号类型的所有值都能存于带符号类型中,则无符号类型的运算对象转换成带符号类型,如果不能,那么带符号类型转换为无符号类型
4.其他隐式类型转换:
1)数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针,但当数组被用作 decltype 关键字的参数,或者作为取地址符(&)、sizeof 及 typeid 等运算符的运算对象时,上述转换不会发生,同时,若用一个引用来初始化数组也不会发生
int ia[10]; //含有10个整数的数组
int *ip = ia; //ia转换成指向数组首元素的指针
2)指针的转换:常量整型值 0 或者字面值 nullptr 能转换成任意指针类型;指向任意非常量的指针能转换成void*;指向任意对象的指针能转换成const void*
3)转换成布尔类型:存在一种从算数类型或指针类型向布尔类型自动转换的机制
4)转换成常量:允许将指向非常量类型的指针转换成指向响应的常量类型的指针,引用也一样;但相反的转换不存在,因为其试图删除底层 const
int i;
const int &j = i; //非常量转换成const int的引用
const int *p = &i; //非常量的地址转换成const的地址
int &r = j, *q = p; //错误:不允许const转换成非常量
5)类类型定义的转换:类类型能定义由编译器自动执行的转换,但编译器每次只能执行一种类类型的转换,若同时提出多个转换请求将会被拒
string s, t = "a value"; //字符串字面值转换成string类型
//条件部分本来需要以一个布尔值,但是这里实际检查的是istream类型的值
//但IO库中定义了从istream到布尔值的转换,从而将cin自动转换成布尔值
//布尔值到底是什么由输入流状态决定,最后一次读入成功则布尔值为true,若读入不成功则布尔值为false
while(cin >> s) //while的条件部分把cin转换成布尔值
5.显示转换:有时希望显示地将对象强制转换成另一种类型,虽然有时不得不使用强制类型转换,但这种方法本质上是十分危险的。
一个命名的强制类型转换具有以下形式:
cast-name(expression);
type是转换的目标类型,若type是引用类型,则结果为左值;expression是要转换的值;
cast-name 是 static_cast,dynamic_cast,const_cast 和 reinterpret_cast 中的一种,cast-name 指定了执行的是哪种转换
1)static_cast:任何具有明确定义的类型转换,只要不包含底层const,都可以使用 static_cast
int i, j;
//进行强制类型转换以便执行浮点数除法
double slope = static_cast<double>(j) / i;
static_cast 对于编译器无法自动执行的类型转换也很有用
void *p = &d; //正确:任何非常量对象的地址都能存入void*
//正确:将void*转换回初始的指针类型,必须确保转换后所得类型就是指针所指类型,类型一旦不符将产生未定义后果
double *dp = static_cast<double*>(p);
2)const_cast:const_cast 只能改变运算对象的底层 const,只有 const_cast 能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都讲引发编译错误,但同样的,也不能使用 const_cast 改变表达式的类型。const_cast 常常用于有函数重载的上下文中(p.208)
将常量对象转换成非常量对象的行为,称为“去掉 const 性质”,一旦去掉某个对象的 const 性质,编译器就不再组织我们对该对象的写操作,如果对象本身不是常量,使用强制类型转换获得写权限是合法行为,然而如果对象是常量,再使用 const_cast 执行写操作会产生未定义的后果。
const char *pc;
char *p = const_cast<char*>(pc);
const char *cp;
//错误:static_cast 不能转换掉const性质
char *q = static_cast<char*>(cp);
static_cast<string>(cp); //正确:字符串字面值转换成string类型
const_cast<string>(cp); //错误:const_cast只能改变常量属性
3)reinterpret_cast:reinterpret_cast 通常为运算对象的位模式提供较低层次上的重新解释。使用 reinterpret_cast 是十分危险的,要想安全的使用 reinterpret_cast 必须对设计的类型和编译器实现转换的过程都非常了解。
个人理解:用于处理无关类型之间的转换,会产生一个新值,这个值与原始参数有相同的比特位
int *ip;
//pc所指的真实对象是一个int而非字符,若把pc当成普通字符指针可能会在运行时发生错误
//用一个int的地址初始化pc,由于显示声明这种转换合法,所以编译器不会发出任何警告或错误信息
//仅从语法上看无可指摘,但查找这类问题的原因十分困难
char *pc = reinterpret_cast<char*>(ip);
4)dynamic_cast:支持运行时类型识别,将在19.2节(第730页)进行详细介绍
**总结:**强制类型转换干扰了正常的类型检查,因此建议程序员避免使用强制类型转换,尤其是 reinterpret_cast,因为此类类型转换总是充满风险,在有重载函数的上下文使用 const_cast 无可厚非,但其他情况下使用 const_cast 则意味着程序存在某种设计缺陷,而其他类型转换也都不应频繁使用。每次书写一条强制类型转换语句,都应反复斟酌能否以掐他方式实现相同的目标。