第5章 表达式
C++提供了丰富的操作符,并定义操作数为内置类型时,这些操作符的含义,除此之外,C++还支持操作符重载,允许程序员自定义用于类类型时操作符的含义,操作符 执行什么操作 以及 操作的结果的类型,取决于 操作数的类型,操作符 对其操作数的类型 有要求,如果操作符应用于内置或复合类型的操作数,则由C++语言定义其类型要求,如,用于内置类型对象的解引用操作符要求其操作数必须是指针类型
C++提供了一元操作符和二元操作符,作用于一个操作数上的操作符称为一元操作符,作用于两个操作数上的操作符称为二元操作符,二元操作符,要求它的两个操作数具有相同的数据类型,或者其类型可以转换为同一种数据类型,有些符号 既可以表示一元操作也可以表示二元操作,C++还提供了一个使用三个操作数的三元操作符
表达式 由一个或多个操作数 通过操作符 组合而成,最简单 的表达式仅包含一个字面值常量 或 变量,较复杂的表达式 则由操作符以及一个或多个操作数构成,每个表达式都会产生一个结果,如果表达式中没有操作符,则其结果就是操作数本身的值,除了特殊用法( 变量是左值,内存中有类型的区域 ),表达式的结果是右值,可以读取该结果值,但是不允许对它进行赋值,要 理解由多个操作符组成的表达式,必须先理解 操作符的优先级、结合性 和 操作数的求值顺序
5.1 算术操作符
算术操作符都是 左结合,优先级相同时,操作符从左向右依次与操作数结合
一元正号操作符返回操作数本身,对操作数不作任何修改,二元+、-操作符也可以用于指针值,算术操作符 +、-、*、/ 具有直观的含义:加法、减法、乘法和除法,操作符 %称为求余或求模操作符,用于计算左操作数除以右操作数的余数,如果两个操作数都是负,求模的结果则为负或零,如果只有一个操作数为负数,求模的结果及符号取决于机器
5.2 关系操作符和逻辑操作符
关系操作符和逻辑操作符 使用算术 或 指针类型的操作数,并返回bool类型的值
逻辑操作符将其操作数视为条件表达式,首先对操作数求值,若结果为0,则条件为假,否则为真
不应该串接使用关系操作符:关系操作符(<、 <=、 >、 >=)具有左结合性,由于关系操作符返回bool类型的结果,如果把多个关系操作符串接起来使用,结果往往出乎预料
逻辑与和逻辑或操作符总是先计算其左操作数,然后再计算其右操作数,只有在仅靠左操作数的值无法确定该逻辑表达式的结果时,才会求解其右操作数,这种求值策略为短路求值,逻辑非操作符将其操作数视为条件表达式,产生与其操作数值相反的条件值
5.3 位操作符
位操作符 使用 整形 的操作数,还可以用于 bitset类型 的操作数,位操作符将其整形操作数视为二进制位的集合,为每一位提供检验和设置的功能
对于位操作符,由于系统不能确保如何处理其操作数的符号位,建议使用unsigned整形操作数,位求反操作符 将操作数的每一个二进制位取反,移位操作符 的右操作数标志要移动的位数,不可以是负数,而且必须严格小于左操作数位数的值,位与操作符 需要两个整形操作数,位异或(互斥或)操作符,也需要两个整形操作数,在每个位的位置,如果两个操作数对应的位只有一个(不是两个)为1,则操作结果中该位为1,否则为0
5.3.1 bitset对象或整形值的使用
标准库提供的bitset操作更直接、更容易阅读和书写、正确使用的可能性更高,而且bitset对象的大小不受unsigned数的位数限制,优于整形数据的低级直接位操作
bitset<8> bs; bs.set(2 ); //bs|= (1UL<<2); cout << bs[2]; //cout << bs; //bitset对象不能与整形&操作 //bs.reset(2 ); //bs&= ~(1UL<<2);
5.3.2 将移位操作符用于IO
输入输出标准库(IO library)分别重载了位操作符>>和<<用于输入和输出,IO操作符为左结合,移位操作符具有中等优先级:其优先级比算术操作符低,比关系操作符、赋值操作符和条件操作符优先级高,若IO表达式的操作数包含了比IO操作符优先级低的操作符,相关优先级别将影响书写该表达式的方式,通常用圆括号强制先实现右结合
5.4 赋值操作符
赋值操作符的 左操作数 必须是 非const的左值,数组名是不可修改的左值:因此数组不可用作赋值操作的目标,下标和解引用操作符都返回左值,作用于非const数组时,其结果可以作为赋值操作的左操作数,赋值表达式的值是其左操作数的值,其结果的类型为左操作数的类型,赋值操作将其右操作数的值赋给左操作数,当左右操作数的类型不同时,该操作实现的类型转换可能会修改被赋的值
5.4.1 赋值操作的右结合性
赋值操作返回左值,C++语言允许将多个赋值操作写在一个表达式中,与其他二元操作符不同,赋值操作具有右结合性,当表达式含有多个赋值操作符时,从右向左结合,多个赋值操作中,各对象必须具有相同的数据类型,或者具有可转换为同一类型的数据类型
5.4.2 赋值操作具有低优先级
通常的用法,是将赋值操作写在条件表达式中,把赋值操作用作长表达式的一部分,赋值操作上 加上圆括号 是必须的,因为赋值操作符的优先级低于不等操作符
5.4.3 复合赋值操作符
在对某个对象做某种操作(算术操作或位操作)后,再将操作结果重新赋给该对象,称为复合赋值操作,使用复合赋值操作时,左操作数只计算了一次
5.5 自增和自减操作符
自增和自减操作符为对象加1或减1操作提供了方便简短的实现方式,它们有前置和后置两种使用形式,只有在必要时才使用后置操作符,后置操作符必须先保存操作数原来的值,以便返回未加1或未减1之前的值作为操作的结果, 后自增操作的优先级高于解引用操作
5.6 箭头操作符
C++语言为包含点操作符和解引用操作符的表达式( ( * ) . )提供了一个同义词:箭头操作符( -> ),a synonym for a dereference followed by the dot operator.
5.7 条件操作符
条件操作符是C++中唯一的三元操作符,它允许将简单的if-else判断语句嵌入表达式,条件操作符的语法格式为:cond ? expr1 : expr2;,条件操作符的优先级相当低,当我们要在一个更大的表达式中嵌入条件表达式时,通常必须用圆括号把条件表达式括起来
5.8 sizeof操作符
sizeof操作符的作用是返回一个对象 或 类型名的长度,返回值的类型是size_t,长度的单位是字节,sizeof表达式的结果是编译时常量,该操作符有以下三种语法形式:sizeof (type name); sizeof(expr); sizeof expr;,将sizeof应用在表达式expr上,将获得 该表达式的结果的类型长度,并没有计算表达式expr的值
使用sizeof的结果部分地依赖所涉及的类型: 对 char类型 或 值为char类型的表达式 做sizeof操作保证得1; 对引用类型sizeof操作 将返回 存放引用类型对象所需的内存空间大小; 对指针做sizeof操作 将返回 存放指针所需的内存大小,注意:如果要获取该指针所指向对象的大小,则必须对该指针进行解引用; 对数组做sizeof操作 等效于对其元素类型做sizeof操作的结果乘以数组元素的个数,sizeof返回整个数组内存的存储长度,用sizeof数组的结果除以sizeof其元素类型的结果,即可求出数组元素的个数
double *p; //p持有无效地址 cout << sizeof *p; //没有计算表达式 *p ,获取指针指向对象的大小 //8
5.9 逗号操作符
逗号表达式是一组由逗号分隔的表达式,这些表达式从左向右计算,逗号表达式的结果是其最右边表达式的值,如果最右边的操作数是左值,则逗号表达式的值也是左值
5.10 复合表达式的求值
含有两个或更多操作符的表达式称为复合表达式,在复合表达式中,操作数和操作符的结合方式决定了整个表达式的值,表达式的结果会因为操作符和操作数的分组结合方式的不同而不同,操作符的分组结合方式取决于操作符的优先级和结合性,优先级规定的是操作数的结合方式,并没有说明操作数的计算顺序
5.10.1优先级
当操作符的优先级相同时,由其结合性决定求解次序,算术操作具有左结合性,圆括号凌驾于优先级之上,使用圆括号的表达式将用圆括号括起来的子表达式视为独立单元 先计算,其他部分则以普通的优先级规则处理
5.10.2 结合性
结合性规定了具有相同优先级的操作符如何分组
C++全部操作符优先级顺序表:该表以横线分割成不同的段,每段内各个操作符的优先级相同,且都高于后面各段中的操作符
5.10.3 求值顺序
&&和 ||操作符 计算其操作数的次序:当且仅当其右操作数确实影响了整个表达式的值时,才计算这两个操作符的右操作数;规定了操作数计算顺序的操作符还有 条件和逗号操作符,除此之外,其他操作符并未指定其操作数的求值顺序
处理复合表达式的两个指导原则:如果有怀疑,则在表达式上按程序逻辑要求使用圆括号强制操作数的组合;如果要修改操作数的值,则不要在同一个语句的其他地方使用该操作数
//一个表达式里,不要在两个或更多的 子表达式中对同一对象 做自增或自减操作 if( arr[index] < arr[index + 1]){ //do whatever } ++index;
5.11 new 和 delete 表达式
使用new和delete表达式可以动态创建和释放数组或单个对象,动态创建对象,只需指定其数据类型,而不必为该对象命名,new表达式返回指向新创建对象的指针,通过指针来访问对象
C++使用直接初始化语法规则,初始化动态创建的对象,如果 提供了初值,new表达式分配到所需要的内存后,用给定的初值初始化该内存空间,如果不显式初始化,动态创建的对象与 在函数内定义的变量初始化方式 相同
可以在类型名后面使用一对内容为空的圆括号对动态创建的对象做 值初始化,内容为空的圆括号表示虽然做初始化,但实际上并未提供特定的初值,对于提供了默认构造函数的类类型,没有必要对其对象进行值初始化
内置类型const对象 或 未提供默认构造函数的类类型const对象必须显式初始化,尽管程序员不能改变const对象的值,但可以撤销对象本身
const int *p = new const int(1024); //*x = 2048; delete p;
动态创建的对象用完后,程序员必须显式地将该对象用的内存返回给自由储存区,C++提供了delete表达式释放指针所指向的地址空间,如果指针指向的不是用new分配的每次空间,则在该指针上使用delete是不合法的,如果指针的值为0,则在其上做的delete操作是合法的,一旦删除了指针所指向的对象,立即将其置为0
5.12 类型转换
表达式是否合法取决于操作数的类型,合法表达式其含义也由其操作数类型决定,若两种类型相关(如果两个类型之间可以相互转换,称为两个类型相关),则可在需要某种类型的操作数位置上,使用该类型的相关类型对象或值
两个不同类型的值,在执行算术操作之前,将两个操作数转换为同一种数据类型,这些转换规则由编译器自动执行,被称为隐式类型转换
C++定义了算术类型之间的内置转换以尽可能防止精度损失,如果表达式的操作数分别为整形和浮点型,则整形的操作数被转为浮点型,在赋值操作中,因为不可能更改做操作数对象的类型,因此左操作数的类型占主导地位,如果赋值操作的左右操作数类型不同,则右操作数会被转换为左边的类型
5.12.1 何时发生隐式类型转换
在下列情况下,将会发生隐式转换:在混合类型的表达式中,其操作数被转换为相同的类型;用作条件的表达式被转换为bool类型;用一表达式初始化某个变量,或将一表达式赋值给某个变量,则该表达式被转换为该变量的类型
5.12.2 算术转换
C++语言为内置类型提供了一组转换规则,其中最常用的是算术转换,算术转换保证在执行操作之前,将二元操作符的两个操作数转换为同一类型,并使表达式的值也具有相同的类型
5.12.3 其他隐式转换
指针转换:在使用数组时,大多数情况下数组都会自动转换为指向第一个元素的指针,不将数组转换为指针的例外情况:数组用作取地址操作符的操作数或sizeof操作符的操作数时,或用数组对数组的引用进行初始化时,不会将数组转换为指针,另外两种指针转换:指向 任意数据类型的指针 都可以转换为void*类型,整形数值常量0可转换为任意指针类型
算术值和指针都可以转换为bool类型,bool对象也可以转换为int型,C++自动将枚举类型的对象或枚举成员转换为整形,其转换结果可用于任何要求使用整形值得地方
当使用非const对象初始化const对象的引用时,系统将非const对象转换为const对象,将非const对象的地址(或非const指针)转换为指向相关const类型的指针,类类型可以定义由编译器自动执行的类型转换
5.12.4 显式转换
显式转换也称为强制类型转换(cast ),包括一下列名字命名的 强制类型转换操作符:static_cast、 dynamic_cast、 const_cast、 reinterpret_cast
5.12.5 何时需要强制类型转换
因为要 覆盖通常的标准转换,所以需要显式使用强制类型转换,显式使用强制类型转换的另一个原因是:可能存在多种转换时,需要选择一种特定的类型转换
5.12.6 命名的强制类型转换
命名的强制类型转换符号的一般形式:cast-name<type>(expression ); ,其中cast-name为static_cast 、dynamic_cast 、const_cast、 reinterpret_cast之一,type为转换的目标类型,expression是被强制转换的值
dynamic_cast 支持运行时识别指针或引用所指向的对象(见18.2节),const_cast 转换掉表达式的const性质,static_cast编译器隐式执行的任何类型转换都可以由static_cast显式完成,当需要将一个较大的算术类型赋值给较小的类型时,使用强制转换非常有用,如果编译器不提供自动转换,使用static_cast来执行类型转换也是很有用的,reinterpret_cast 通常 为操作数的位模式提供较低层次的重新解释,其本质上依赖于机器,要求完全理解所涉及的数据类型,以及编译器实现强制类型转换的细节
5.12.7 旧式强制类型转换
在引入命名的强制类型转换操作符之前,显式强制转换用圆括号将类型括起来实现,效果与使用reinterpret_cast符号相同,但这种强制转换的可视性比较差,标准C++为了加强类型转换的可视性,引入命名的强制转换操作符,为程序员在必须使用强制转换时提供了更好的工具
旧式强制转换符号有两种形式:type (expr); (type) expr;,旧式强制转换依赖于所涉及的数据类型,具有与const_cast、static_cast、 reinterpret_cast一样的行为,在合法使用static_cast或const_cast的地方,旧式强制转换提供了与各自对应的命名强制转换一样的功能,如果这两种强制转换均不合法,则旧式强制转换执行reinterpret_cast功能
const void* 与 命名的强制转换
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <iostream> using namespace std; struct A{ int a; A():a(128) { } }; void f(const void *p){ //A* ap1 = (A*) p; //const A* ap1 = dynamic_cast<const A*>(p ); //source is not a pointer to class //const A* ap1 = static_cast<const A*>(p ); const A* ap1 = reinterpret_cast<const A*>(p ); { A* ap2 = const_cast<A*> (ap1 ); ap2->a = 256; } //ap1->a = 1024; //read only cout << ap1->a; } int main() { A* ap = new A; f(ap ); delete ap; return 0; }