文章目录
- 第4章 表达式
- 4.1 基础
- 4.1.1 基础概念
- 4.1.2 优先级与结合律
- 4.1.3 求值顺序
- 4.2 算术运算符
- 4.3 逻辑和关系运算符
- 4.4 赋值运算符
- 4.5 递增和递减运算符
- 4.6 成员访问运算符
- 4.7 条件运算符
- 4.8 位运算符
- 4.9 sizeof运算符
- 4.10 逗号运算符
- 4.11 类型转换
- 4.11.1 算术转换
- 4.11.2 其他隐式类型转换
- 4.11.3 显式转换
- 4.12 运算符优先级表
- 小结
- 术语表
第4章 表达式
- 表达式由运算对象组成 对表达式求值得到一个结果
- 最简单的表达式:字面值和变量
- 复杂表达式:将一个运算符和一个或多个运算对象组合
4.1 基础
4.1.1 基础概念
- 一元/二元/三元运算符 某些符号元数由上下文决定
- 组合运算符和运算对象
- 优先级 结合律 求值顺序
- 运算对象转换
- 运算对象转换为同一种类型
- 整数↔浮点数 ,但指针不能转换为浮点数
- 小整数类型通常会被提升为较大的整数类型
- 重载运算符
- 当运算符作用在类类型的运算对象时,用户可以自行定义其含义。为已存在运算符赋予另外一层含义
- 重载时 运算对象/返回值类型由运算符定义 但运算对象个数/运算符优先级/结合律无法改变
- 左值和右值
- C: 左值可以位于赋值语句左边,右值则不能
- C++:右值:用的是对象的值(内容);左值:用的是对象的身份(在内存中的位置)
- 右值可以由左值代替 反之不行
- 需要用到左值的运算符
- 赋值运算符 左侧运算对象
- 取地址符作用于左值运算对象
- 内置解引用/下标运算符 迭代器解引用 string/vector下标运算符 的求值结果均为左值
- 内置类型/迭代器的递增递减运算符作用于左值运算对象 前置版本所得结果也是左值
- 关键字
decltype
- 作用于求值结果是左值的表达式 得到的是引用类型
int *p; decltype(*p)->int&
//解引用生成左值 得到类型为引用int *p; decltype(&p)->int**
//取地址符生成右值 结果为一个指向指针的指针
4.1.2 优先级与结合律
- 复合表达式 含有两个或多个运算符的表达式
- 优先级和结合律决定了运算对象组合的方式 括号(优先运算)无视上述规则
- 高优先级运算符的运算对象要比低优先级的运算对象更为紧密地结合在一起
- 如果优先级相同 组合规律由结合律确定
- 括号无视优先级和结合律
- 优先级和结合律 影响程序的正确性
4.1.3 求值顺序
- 不会明确指定求值的顺序,如
int i = f1() + f2();
不能确定是先调用f1还是f2 - 对于没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为
- 4种明确规定运算对象求值顺序的运算符
- 逻辑与
&&
先求左侧运算对象为真才继续求右侧运算对象(短路求值) - 逻辑或
||
先求左侧运算对象为假才继续求右侧运算对象(短路求值) - 条件运算符
?:
- 逗号运算符
,
- 逻辑与
- 求值顺序 优先级 结合律
- 求值顺序与优先级和结合律无关
- 如
f()+g()*h()+j();
- 优先级规定 g()的返回值和h()的返回值相乘
- 结合律规定 f()的返回值先与g()和h()的乘积相加,所得结果再与j()的返回值相加
- 对于这些函数的调用顺序没有明确规定 若fghj无关不会改变同一对象状态也不执行IO则函数调用顺序不受限制;若影响同一对象,则是一条错误的表达式,将产生未定义的行为
- 处理复合表达式
- 拿不准最好加括号来强制使表达式组合关系符合程序逻辑
- 若改变了某个运算对象的值,在表达式的其他地方不要再使用该运算对象(例外
*++iter
,递增运算必须先求值 然后才轮到解引用)
- c++未明确规定大多数二元运算符的求值顺序给编译器优化留下了余地,该策略实际上是在代码生成效率和程序潜在缺陷之间间进行权衡
4.2 算术运算符
- 优先级由高到低:一元运算符 乘法和除法 加法和减法,优先级高的运算符比低的更紧密,上述都满足左结合律,当优先级相同时按照从左向右的顺序进行组合
- 算术运算符都能作用于任意算术类型 以及任意能转换为算术类型的类型
- 算数运算符的运算对象和求值结果都是右值
- 表达式求值前小整数运算对象都被提升为较大的整数类型 所有运算对象最终都会转换成同一类型
- 一元正号运算符 加法运算符 减法运算符都能作用于指针 (二元加法减法)
- 一元正号运算符作用于一个指针或者算数值时 返回运算对象值一个(提升后的)副本
- 布尔值不应该参与运算 对被提升为int型 对其求负true的仍然为true
bool b=true; bool b2=-b;//b2为true 因b被提升为1,-b为-1不为零仍是true
- 算术表达式有可能产生未定义的结果原因
- 数学性质本身 如除数是0
- 计算机特点 如溢出 计算的结果超出该类型所能表示的范围时就会产生溢出
+ - * /
整数相除
结果还是整数 若商含有小数部分则直接舍弃%
取余/取模运算符 参与取余运算的运算对象必须是整数类型/
除法运算 两运算对象符号相同则商为正 否则商为负&
取余运算 如m%n
若值不为零则符号与m相同(-m)/n
和m/(-n)
都等价于-(m/n)
;m%(-n)
等价于m%n
,(-m)%n
等价于-(m%n)
4.3 逻辑和关系运算符
- 关系运算符作用于 算术类型或指针类型 返回值为布尔类型
- 逻辑运算符作用于 任意能转换为布尔值的类型 返回值为布尔类型
- 以上两类运算符求值结果都是右值
- 逻辑与和逻辑或运算符 都是先求左侧运算对象的值再求右侧
- 逻辑与
&&
全真为真 其余为假 左侧为真才求右侧 - 逻辑或
||
全假为假 其余为真 左侧为假才求右侧 - 短路求值:当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值
- 逻辑与
- 声明为引用类型可以避免对元素的拷贝,如不需写操作可声明为常量的引用
- 逻辑非运算符
!
将运算对象的值取反后返回
- 关系运算符
- 比较运算对象的大小并返回布尔值 满足左结合律
- 不要连写关系运算符 如
if(i<j<k)
是拿i<j
的布尔值结果与k比较;正确应该写为if(i<j&&j<k)
- 相等性测试与布尔字面值
- 测试一个算数对象或指针对象的真值 最直接方法是将其作为if语句的条件 如
if(val){...}//任意非零值为真
或if(!val){...}//val为零值为真
- 进行比较运算时除非比较的对象是布尔类型 否则不要使用字面值true和false作为运算对象如
if(val==true)若val不是布尔类型会变为if(val==1)
(false会转换为0),若要进行值比较应直接写1而不是true
- 测试一个算数对象或指针对象的真值 最直接方法是将其作为if语句的条件 如
4.4 赋值运算符
- 赋值运算 左侧运算对象必须是一个可修改的左值 其的结果是它的左侧运算对象 是一个左值 类型就是左侧对象的类型,如果赋值运算的左右侧运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型
- 右侧运算对象 允许使用花括号括起来的初始值列表;若左侧运算对象是内置类型,则初始化列表最多包含一个值 即使转换所占空间也不应该大于目标类型;无论左侧运算对象啊是什么类型,初始值列表都可以为空,编译器创建一个值初始化的临时量赋给左侧运算对象
- 赋值运算符满足右结合律
- 赋值返回的是其左侧运算对象
- 多重赋值中每一对象 类型或者与右边对象的类型相同 或可由右边对象的类型转换得到
- (不能将指针的值赋给int)
- 赋值运算优先级比较低
- 赋值部分加上括号
- (低于关系运算符优先级) 所以加括号这样写
while((i=get_value)!=42)
- 切勿混淆相等运算符和赋值运算符
==
=
- 复合赋值运算符
- 等价于
a=a op b;
但复合运算符只求值一次,普通运算符求值两次
- 等价于
4.5 递增和递减运算符
- 前置版本
j = ++i
,求值结果为改变后的对象 - 后置版本
j = i++
,求值结果为运算对象改变之前的那个值的副本 - 两者均须作用于左值运算对象
- 除非必须 否则不使用递增递减运算符的后置版本(比前置多一步存储原始值)
- 在一条语句中混用解引用和递增运算符
- 既想变量加一减一又能使用它原来的值 则可以使用后置版本
- 如
auto pbeg=v.begin(); while(pbeg!=v.end() && *pbeg>=0){cout<<*pbeg++<<endl;}
后置递增运算符优先级高于解引用 故*pbeg++
等价于*(pbeg++)
- 运算对象可按任意顺序求值
- 复合表达式多个地方改变同一个对象的值时求值顺序很关键,因为递增递减会改变运算对象的值,所以要提防在复合表达式中错用这两个运算符
4.6 成员访问运算符
- 点运算符 获取类对象的一个成员 成员对象是左值结果为左值 成员对象是右值结果为右值
- 箭头运算符 作用于指针类型运算对象 结果为左值
ptr->mem
等价于(*ptr).mem
- 解引用
*
运算符优先级低于点运算符.
,须加括号
4.7 条件运算符
- 条件运算符(
?:
)if-else
逻辑嵌入到单个表达式中去cond? expr1: expr2
- 先求cond的值 条件为真返回expr1 条件为假返回expr2 只对expr1和expr2中的一个求值
- 当条件运算符的两个表达式都是左值或者能转换为同一种左值类型时 运算结果是左值 否则运算结果是右值
- 嵌套条件运算符
- 右结合律 按照从右向左的顺序组合 靠右边的条件运算构成靠左边的条件运算的:分支
- 嵌套多了可读性下降 最好别超过两到三层
- 在输出表达式中使用条件运算符
- 条件运算符优先级非常低 记得加括号
4.8 位运算符
- 作用于整数类型运算对象 将对象看成二进制位的集合 提供检查和设置二进制位的功能
- 小整型自动提升为较大的整型 运算对象有/无符号的都可;若带符号负数 左移操作可能会改变符号位的值
- 符号位的处理无明确规定 建议仅将位运算用于处理无符号类型
- 移位运算符
- 二进制位向左移(
<<
)(在右侧插入值为0的二进制位)或者向右移(>>
)(无符号 在左侧插入值为0的二进制位;有符号 在左侧插入符号位的副本或者值为0的二进制位 视具体情况),左侧运算对象内容按照右侧运算对象的要求移动指定位数,移出边界外的位就被舍弃掉
- 二进制位向左移(
- 位求反运算符
(~)
将运算对象逐位求反后生成一个新址 1->0 0->1- char提升为int 提升时运算对象原来的位保持不变 往高位添加0即可
- 位与 位或 位异或 运算符
- 与
&
或|
异或^
两运算对象上逐位执行相应的逻辑操作 - 与
&
全1为1 其余为0 - 或
|
全0为0 其余为1 - 异或
^
相异(对应位置有且仅有一个为1)为1 相同为0 - 不要将位运算符和逻辑运算符搞混 如位与
&
位或|
位求反~
和逻辑与&&
逻辑或||
逻辑非!
- 与
- 使用位运算符
- 测试是否通过 位或某一位设为0其余位设为1 只改变其中一位 其他位不变
- 移位运算符(又叫IO运算符)满足左结合律
- 重载优先级和结合律与内置版本一样
- 优先级不高不低 比算术运算符优先级低 比关系运算符、赋值运算符、条件运算符优先级高,一次使用多个运算符有必要适当加括号
4.9 sizeof运算符
- sizeof运算符返回一条表达式或一个类型名字所占的字节数 满足左结合律
-
sizeof (type)
类型名
-
sizeof expr
表达式
-
- 所得值的类型是
size_t
类型的常量表达式,sizeof
返回表达式类型结果,并不实际计算其运算对象的值(sizeof *P等价于sizeof (*P)
,因为不实际计算其运算对象的值,即使P是一个无效(未初始化)的指针也安全,指针并未被真正使用,不需真的解引用也能知道它所指对象的类型) - 允许使用作用域运算符来获取类成员的大小,无需提供具体对象因为想知道类成员大小无须真的获取该成员
- 结果部分依赖于作用的类型
char类型/表达式
结果为1引用类型
被引用对象所占空间大小指针
指针本身所占空间大小解引用指针
指针所指对象所占空间大小 指针不需有效数组
整个数组所占内存空间大小 等价与每个元素sizeof
值之和sizeof
不会将数组转换为指针来处理- 可以用数组大小除以单个元素的大小得到数组元素个数
- 因为
sizeof
返回值是常量表达式 可用于声明数组维度
// sizeof(ia)/sizeof(*ia) 返回整个数组所占空间的大小/返回数组的大小
constexpr size_t sz = sizeof(ia)/sizeof(*ia);
int arr2[sz];//返回值是常量表达式 可用于声明数组维度
string vector
返回类型固定部分大小 不会计算对象元素占用空间
4.10 逗号运算符
(,)
含有两个运算对象,从左向右依次求值- 规定运算对象顺序 先求左侧,将左侧求值结果丢弃,逗号运算符真正结果是右侧表达式的值。如果右侧运算符是左值,那么最终的求值结果也是左值。
4.11 类型转换
- 如果两种类型可以相互转换 那么它们就是关联的
- 隐式类型转换:
- c++根据类型转换规则将运算对象的类型统一(自动执行)后再求值
- 尽可能避免损失精度 如同时有整型和浮点型运算,整型会转换为浮点型
- 何时发生隐式类型转换
- 大多数表达式 比
int
类型小的整数值先提升为较大的整数类型 - 条件 非布尔转换成布尔
- 初始化 初始值转换成变量的类型;赋值 右侧转换为左侧类型
- 算术运算或者关系运算的运算对象有多种类型 要转换成同一种类型
- 函数调用时也发生类型转换
- 大多数表达式 比
4.11.1 算术转换
- 把一种算术类型转换为另外一种算术类型
- 运算符的运算对象将转换为最宽的类型
- 表达式中既有浮点型也有整型时 整型将转换为相应的浮点类型
- 整型提升
- 将较小的整数类型转换为较大的整数类型
bool/char/signed char/unsigned char/short/unsigned short
等可能存在int
里提升为int
型 - 较大的char类型
(wchar_t/char16_t/char32_t)
提升为int/unsigned int/long/unsigned long/long long/unsigned long long
中的一种,前提:转换后的类型要能容纳原类型所有可能的值
- 将较小的整数类型转换为较大的整数类型
- 无符号类型的运算对象
- 转换结果依赖于机器中各个整数类型的相对大小
- 先整型提升 若结果类型匹配则无需进一步的转换
- 若都是带符号/无符号 则小类型运算对象转换为大类型运算对象
- 若一个带符号 一个不带符号
- 无符号不小于带符号 则带符号转换为无符号
- 带符号大于无符号 转换结果依赖于机器
- 若无符号所有值都能存在该带符号类型中,则无符号类型运算对象转换为带符号
- 若不能 则带符号运算对象转换为无符号类型
- 理解算术转换
- 研究大量的例子
- 研究大量的例子
4.11.2 其他隐式类型转换
- 数组转换为指针(指向数组首元素的指针)
- 当数组被用作decltype关键字 或作为取地址符(&)、sizeof及typeid等运算对象时上述转换不会发生
- 如果用一个引用来初始化数组时上述转换也不会发生
- 在表达式中使用函数类型时会发生类似指针转换
- 指针的转换
- 常量整数值0/字面值nullptr 能转换为任意指针类型
- 指向任意非常量的指针能转换为void*
- 指向任意常量的指针能转换为const void*
- 转换为布尔值
- 算术类型/指针类型->布尔类型
- 0->false;否则->true
- 转换为常量
- 能将指向类型T的指针或者引用分别转换成指向const T的指针或引用
- 不允许const转换为非常量 因为它试图删除底层const
- 类类型定义的转换
- 能定义由编译器自动执行的转换 每次只能执行一种类类型转换
//字面值转string
string s,t="a value";
//cin转 布尔值
while(cin>>s);
//最后一次读入成功 转换的布尔值是true
//不成功 布尔值是false
4.11.3 显式转换
- 显示的将对象强制转换为另外一种类型
- 命名的强制类型转换
cast-name<type>(expression);
- static_cast、const_cast、dynamic_cast、reinterpret_cast
- dynamic_cast:
- 支持运行时类型识别
- static_cast:
- 任何具有明确定义的类型转换,只要不包含底层const,都可以使用
- 利用static_cast找回存在于void*指针中的值,应确保指针的值保持不变,强制转换结果与原地址值相等,须确保转换后所得类型就是指针所指的类型,类型一旦不符,将产生未定义的后果
- const_cast:(常用于有函数重载的上下文中)
- 只能改变运算对象的底层const,将常量对象转换为非常量对象的行为一般称为去const性质
- 如果运算结果对象本身不是一个常量 使用强制类型转换获取写权限是合法行为
- 如果运算结果对象本身是一个常量 再使用const_cast强制类型写操作就会产生未定义的行为
- const_cast只能改变表达式的常量属性,使用其他类型的命名强制类型转换改变表达式的常量属性都将引发编译器错误;同样,也不能用const_cast改变表达式的类型
- reinterpret_cast:
- 通常为运算对象的位模式提供低层次上的重新解释
- 使用reinterpret_cast非常危险,本质上依赖机器,想安全使用reinterpret_cast必须对涉及的类型和编译器实现转换的过程都非常了解
- 例子
int *ip; char *pc=reinterpret_cast<char*>(ip); //牢记pc指向的真实对象是一个int而非字符,不能将其当普通字符指针使用
- 建议:避免强制类型转换
- 干扰正常类型检查
- 每次书写一条强制类型转换都应反复斟酌能否以其他方式实现相同的目标
- 实在无法避免 也应尽量限制类型转换值的作用域 记录对相关类型的所有假设可以减少错误发生的机会
- 旧式强制类型转换
- 早期c++显示进行强制类型转换
type (expr);
//函数类型强制类型转换(type) expr;
//c语言风格强制类型转换- 若旧式强制类型转换转换为static_cast、const_cast也合法,则其行为与对应的命名转换一致;若替换后不合法则执行与reinterpret_cast类似的功能
- 与命名的强制类型转换先比旧式强制类型转换不那么清晰明了容易被看漏
4.12 运算符优先级表
小结
- 运算符作用于内置类型运算对象所执行的操作,支持运算符重载,允许我们自己定义运算符作用于类类型时的含义
- 对含超一个运算符的表达式 要理解优先级、结合律、求值顺序
- 每一个运算符都有对应的优先级和结合律
- 优先级规定复合表达式中运算符组合的方式
- 结合律说明当运算符的优先级一样时应该如何组合
- 大多数运算符不规定运算对象的求值顺序
- 若两运算对象指向同一个对象并且其中一个改变了对象的值,会导致程序出现缺陷
- 每一个运算符都有对应的优先级和结合律
- 运算对象经常从原始类型自动转换为某种关联类型,还可显示进行强制类型转换
术语表
- 左值:求值对象结果为对象或函数的表达式。一个表示对象的非常量左值可以作为赋值运算符的左侧运算对象
- 右值:一种表达式,其结果是值而非值所在的位置
- 求值顺序:运算对象一定要在运算符之前得到求值结果。但只有
&&、||、条件、逗号
4中运算符规定了求值顺序 - 优先级:高优先级的运算符结合得更紧密
- 短路求值:描述
&&、||
运算符的执行过程。如果根据运算符的第一个运算对象就能确定整个表达式的结果,求值终止,此时第二个运算对象将不会被求值 - size_of:返回储存对象(类型可能为某个给定类型名字也可能由表达式的返回结果确定)所需字节数