1. 表达式基本概念
-
运算对象类型转换过程中,小整数类型(bool,char,short)通常被提升(promoted)为较大的整数类型(int)。
-
C++语言定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。当运算符作用于类类型的运算对象时,用户可以自定义其含义,这个过程称为重载运算符。
IO库的>>和<<以及string对象,vector对象和迭代器使用的都是重载的运算符。
重载某个运算符时运算对象的类型和返回值的类型都是可以自定义的,但是运算对象的个数、运算符的优先级和结合律是无法改变的。 -
当一个对象被用作右值时,用的是对象的内容/值,当一个对象被用作左值时,用的是对象的身份/内存中的位置。不能用C语言的简单归纳(左值可以位于赋值语句的左侧,右值不可),理由如下:
左值右值的形式区分(或者称语法区分)是能否用取地址&运算符;语义区分(即其本质涵义)在于表达式代表的是持久对象还是临时对象
在运算符运算的过程中,有一条重要的原则,在需要右值的地方可以用左值来代替,此时使用的左值的内容,但是需要左值的地方不能用右值。
常见的运算符的参数和返回结果:
- 赋值运算符:需要非常量左值作为左侧运算对象,返回一个左值。
- 取地址运算符:作用于左值对象,返回右值
- 内置解引用运算符、下标运算符、迭代器解引用运算符:求值结果是左值
- 内置类型和迭代器的递增递减运算符:作用于左值,结果为左值。(迭代器也可以取地址)
- 算术运算符(+,-,*,/,%)的运算对象和求值结果都是右值。
使用关键字decltype时,如果表达式求值结果是左值,那么得到的结果是一个引用类型的
:
2.表达式的求值顺序
优先级规定了运算对象的组合方式,但是没有规定运算对象按照什么顺序求值。
大多数情况下,运算符没有明确确定求值顺序,对那些没有明确求值顺序的运算符,如果一条语句的多个表达式指向并且修改了同一对象,会产生未定义行为。如下,<<
运算符没有规定求值顺序:
有四种运算符明确规定了运算对象的求值顺序,分别为&&
,||
,?:
,,
关于复合表达式编写的一些建议:
3. 算术运算符
算术运算符优先级(从上至下优先级逐渐降低,同一栏内的优先级相同):
*
,/
优先级相同,遵循左结合律,从左向右运算。
表达式求值前,小整数类型被提升为较大整数类型,所以最好不要用bool参与运算,如下所示:
求余运算的两个操作对象必须是整数。如下:
如果m%n!=0,那么他的符号和m相同。
4. 逻辑运算符
逻辑(与或非)和关系(剩下的)
运算符优先级(从上至下优先级逐渐降低,同一栏内的优先级相同):
一个小技巧,在循环遍历中,可将遍历工具设为引用
防止拷贝的发生。如:
关系运算符都满足左结合律。 且返回一个布尔值。如下:
在将算术对象或指针对象作为判断条件时,会先将其转化为bool
值:
但是如果Bool值参与运算,则会转为整形的值(false->0,true->1):
也就是说,只有val
是bool
值才用bool
值参与逻辑运算。
5. 赋值运算符
赋值运算满足右结合律(从右向左运算)。
注:返回的左侧运算对象当成左值
上述类型不同且无法转化的赋值是非法的。
利用赋值语句会返回左侧运算对象的特性,可以写出更简介的代码:
在使用上述结构时,注意一点:
每种运算符都有对应的复合形式:
6. 递增和递减运算符
递增和递减符有两个版本,前置版本和后置版本,都作用于左值对象,前置版本将对象本身作为左值返回,后置版本将对象的副本作为右值返回。
后置递增运算符的优先级高于解引用运算符,所以可以用:
这是一种简洁的写法。但是这种写法也不是一直能用,其中一种情况就是因未定义的计算顺序导致的问题:
7. 成员访问运算符(点运算符和箭头运算符)
解引用运算符优先级低于点运算符(访问成员函数,成员变量等)。
箭头运算符的返回结果为左值
。点运算符的返回随成员所属的对象类型不同而不同
,如果成员所属对象为左值,那么点运算符返回左值,如果成员所属对象为右值,那么点运算符返回右值。
8. 条件运算符
格式如下:
允许在条件运算符内嵌套另一个条件运算符。即条件表达式可以作为另一个条件运算符的cond或者expr。比如:
条件运算符的优先级非常低,需要加括号。否则:
9. 位运算符
在位运算中,如果涉及到提升,那需要先提升后再进行位运算。
鉴于移位可能会改变符号位的值,关于符号位如何处理没有明确的规定,随具体机器而定,所以建议只将移位作用于无符号数。
IO运算符<<和>>是移位运算符的重载版本,其优先级和结合律与移位版本的一致。满足左结合律,也就是从左至右运算,至于优先级:
10. sizeof运算符
sizeof
返回size_t类型的大小,运算对象有两种形式:
sizeof
满足右结合律。从右至左运算。计算大小时并不会实际计算对象的值
:
11. 逗号运算符
12. 类型转换
如果两种类型可以相互转换,那么它们就是关联的。
12.1 隐式转换
数组隐式转换:数组会被隐式转化为指针。在如下四种情况除外:
- 数组作为
decltype
的参数 - 数组作为
&
取地址符的参数 - 数组作为
sizeof
的参数 - 数组作为
typeid
的参数
指针隐式转换:常量整形值0或者字面值nullptr
能转化为任意指针类型。指向任意非常量的指针能转化为void*
,指向任意对象的指针能转化为const void *
。
类类型定义的转换:
上述转换中有两个类类型的转换。
12.2 算术转换
把一种算术类型转换为另一种算术类型。比如,在使用运算符时,会将运算对象转换为最宽的类型。
整形提升
:将小整数类型转化为较大的整数类型。bool,char,short都属于小整数,一般被提升到int,较大的char类型如wchar_t,char16_t等等会被提升为int、long等中能容纳原类型所有可能的值的最小类型。
在实际的运算中,先执行整形提升,然后执行类型转换。
无符号类型的算术转换(特殊处理):
运算符的两个运算对象都是有符号或者无符号。那么小类型先提升后转向大的类型。
运算符的两个运算对象一个是有符号一个是无符号:
- 无符号类型大于等于(指从字面上)有符号类型。将有符号转为无符号。
- 有符号类型大于(指从字面上)无符号类型。如果有符号类型能完整存储无符号(类型大小的比较)的所有值,把无符号转化为有符号。否则把有符号转化为无符号。
下面是实际的算术转换例子:
12.3 显式类型转换
命名的强制类型转换:
type为要转换的目标类型,expression是要转换的值。如果type是引用类型,则结果为左值。cast-name指明了是哪种类型的转换。
转换名 | 说明 |
---|---|
static_cast | ① 任何不包含底层const的有明确定义的类型转换,都可以用这个。 ②在把较大的算术类型赋值给较小类型时非常有用,static_cast告诉读者和编译器,不在乎精度损失,编译器也不会给出警告。![]() |
dynamic_cast | |
const_cast | 只能改变运算对象的底层const (与其说是该,不如说是提供了另一个接口访问对象),也只有它能改变底层const属性,使用其他的命名强制类型转换试图修改const属性都会报错。而const_cast不能改变对象的类型,只能该改const属性:![]() |
reinterpret_cast | 提供位层次的重新解释(内容不变,类型改变)。下图即将一个int * 类型的指针转化为char指针,后续就会把他当成char使用。![]() ![]() |