第4章 表达式
介绍由语言本身定义,并用于内置类型运算对象的运算符,还有几种标准库定义的运算符、运算符的优先级,以及类型转换。
4.1 基础
4.1.1 基本概念
C++定义了一元运算符和二元运算符。作用于一个运算对象的是一元运算符,如取地址符(&)和解引用符(*),作用于两个对象的运算符是二元运算符,如相等运算符(==)和乘法运算符(*),函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。
有的符号如(*)既可以作一元运算符也可以作二元运算符,如何使用由上下文决定,两种用法互不相干。
运算对象转换
表达式求值的过程中运算对象常由一种类型转换成另一种类型。如整数能转换成浮点数,浮点数也能转换成整数,但指针不能转换成浮点数。小整数类型(如bool、char、short等)通常会被提升成较大的整数类型。
重载运算符
C++定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作,当运算符作用于类类型的运算对象时,用户可以自行定义其含义,所以称之为重载运算符。使用重载运算符时,其包括运算对象的类型和返回值的类型,都是由该运算符定义的,但是运算对象的个数,运算符的优先级和结合律都是无法改变的。
左值和右值
左值是有地址的,变量就是左值;右值没有地址,如数字1,就是右值。左值能出现在赋值语句左边与右边,右值只能出现在赋值语句右边,如函数返回值只能在赋值语句右边。
当一个对象被用作右值的时候,用的是对象的值(内容),当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。在需要右值的地方可以用左值来代替,但是不能把右值当成左值使用。
4.1.2 优先级与结合律
求复合表达式需要将运算符和运算对象合理组合,优先级和结合律决定了运算对象组合的方式,表达式中的括号无视上述规则,将表达式的局部括起来可以得到优先运算。
- 高优先级运算符的运算对象要比低优先级运算符的运算对象组合得更紧密。
- 优先级相同则由结合律确定。
- 算术运算符满足左结合律,运算符优先级相同的将按照从左向右的顺序组合运算对象。
- 括号无视优先级与结合律。
4.1.3 求值顺序
运算对象的求值顺序与优先级和结合律无关。
int i = 0;
cout << i << " " << ++i << endl;
// 上述表达式是未定义的,编译器可能先求++i的值再求i的值,也可能先求i的值再求++i的值
有4种运算符规定了运算对象的求值顺序:
- 逻辑与
&&
运算符,规定先求左侧运算对象的值,只有当左侧对象值为真时才继续求右侧。 - 逻辑或
||
运算符。 - 条件
?:
运算符。 - 逗号
,
运算符。
在表达式f() + g() * h() + j()
中,优先级规定g()
的返回值和h()
的返回值相乘,结合律规定f()
的返回值先和g()
和h()
的乘积相加,所得结果再与j()
返回值相加。但对这些函数的调用顺序没有明确规定。
如果f、g、h、j是无关函数,即它们不会改变同一个对象的状态也不执行I/O任务,则函数的调用顺序不受限制。反之,如果其中某几个函数影响同一个对象,则它是一条错误的表达式。
4.2-4.10 运算符
优先级 | 运算符 | 含义 | 用法 | 结合方向 | 说明 |
---|---|---|---|---|---|
1 | [] | 数组下标 | 数组名[常量表达式] | 从左到右 | |
() | 圆括号 | (表达式)/函数名(形参表) | |||
. | 成员选择(对象) | 对象.成员名 | |||
-> | 成员选择(指针) | 对对象指针->成员名 | |||
2 | - | 一元负号 | -表达式 | 从右到左 | bool b = true; bool b2 = -b; // b2是true |
~ | 按位取反 | ~表达式 | 将1置为0,将0置为1。 | ||
++ | 自增 | ++变量名/变量名++ | int i = 0, j; j = ++i; // j = 1, i = 1; 前置版本得到递增之后的值 j = i++; // j = 1, i = 2; 后置版本得到递增之前的值 | ||
-- | 自减 | --变量名/变量名-- | |||
* | 取值 | *指针变量 | |||
& | 取地址 | &变量名 | |||
! | 逻辑非 | !表达式 | 将运算对象取反后返回。 | ||
(类型) | 强制类型转换 | (数据类型)表达式 | |||
sizeof | 长度运算符 | sizeof(表达式) | 返回一条表达式结果类型的大小或一个类型名字所占的字节数,并不实际计算其运算对象的值。 sizeof:char类型(1)、引用类型(被引用对象空间大小)、指针(指针本身大小)、解引用指针(所指对象大小,指针不需有效)、数组(整个数组大小,即对数组中所有元素sizeof后求和)、string或vector对象(该类型固定部分的大小,不计算对象中的元素占用的空间)。 | ||
3 | * | 乘 | 表达式*表达式 | 从左到右 | |
/ | 除 | 表达式/表达式 | 运算对象符号相同则为正,否则商为负,商一律向0取整(即直接切除小数部分)。 | ||
% | 余数(取模) | 整型表达式%整型表达式 | (-m)/n和m/(-n)都等于-(m/n),即如果m%n不等于0,则符号和m相同。 | ||
4 | + | 加 | 表达式+表达式 | 从左到右 | |
- | 减 | 表达式-表达式 | |||
5 | << | 左移 | 变量<<表达式 | 从左到右 | 基于二进制位的移动操作,令左侧对象按右侧对象的要求移动指定位数,然后将经过移动的左侧对象的拷贝值作为求值结果,右侧对象不能为负,且值必须严格小于结果的位数。移出边界之外的位被舍弃,在右侧插入值为0的二进制位。 |
>> | 右移 | 变量>>表达式 | 如果左侧运算对象是无符号类型,在左侧插入值为0的二进制位,如果该运算对象是带符号类型,在左侧插入符号位的副本或值为0的二进制位。 | ||
6 | > | 大于 | 表达式>表达式 | 从左到右 | |
>= | 大于等于 | 表达式>=表达式 | |||
< | 小于 | 表达式<表达式 | |||
<= | 小于等于 | 表达式<=表达式 | |||
7 | == | 等于 | 表达式==表达式 | 从左到右 | |
!= | 不等于 | 表达式!= 表达式 | |||
8 | & | 按位与 | 表达式&表达式 | 从左到右 | 两个运算对象的对应位置都是1则运算结果中该位为1。 |
9 | ^ | 按位异或 | 表达式^表达式 | 从左到右 | 两个运算对象的对应位置有且只有一个为1则该位为1,否则为0。 |
10 | | | 按位或 | 表达式|表达式 | 从左到右 | 两个运算对象的对应位置至少有一个为1则该位为1,否则为0。 |
11 | && | 逻辑与 | 表达式&&表达式 | 从左到右 | 两个运算对象都为真时结果为真,当且仅当左侧为真时才对右侧求值。 |
12 | || | 逻辑或 | 表达式||表达式 | 从左到右 | 两个运算对象中的一个为真结果就为真,当且仅当左侧为假时才对右侧求值。 |
13 | ?: | 条件运算符 | 表达式1?表达式2: 表达式3 | 从右到左 | 先求表达式1的值,如果条件为真对表达式2求值并返回该值,否则对表达式3求值并返回该值(当表达式2和3都是左值或都能转换成同一种左值类型时,运算结果是左值,否则是右值)。 |
14 | = | 赋值 | 变量=表达式 | 从右到左 | 左侧运算对象必须是一个可修改的左值,若左右两侧对象类型不同,则右侧运算对象将转换成左侧运算对象的类型。 |
*= | 乘后赋值 | 变量*=表达式 | |||
/= | 除后赋值 | 变量/=表达式 | |||
%= | 取模后赋值 | 变量%=表达式 | |||
+= | 加后赋值 | 变量+=表达式 | |||
-= | 减后赋值 | 变量-=表达式 | |||
<<= | 左移后赋值 | 变量<<=表达式 | |||
>>= | 右移后赋值 | 变量>>=表达式 | |||
&= | 按位与后赋值 | 变量&=表达式 | |||
^= | 按位异或后赋值 | 变量^=表达式 | |||
|= | 按位或后赋值 | 变量|=表达式 | |||
15 | , | 逗号 | 表达式,表达式,… | 从左到右 |
4.11 类型转换
4.11.1 算术转换
把一种算术类型转换成另外一种算术类型,其中运算符的运算对象将转换成最宽的类型。
- 只要有long double,无论其余的操作数是什么类型,都将转化为long double类型。
- 整数提升:对于所有比int小的整型,如果该类型的取值范围包含在int内,则提升为int类型,否则提升为unsigned int类型。
- long足够包含unsigned int则unsigned int转化为long,否则转化为unsigned long。在32位的机子上,表达式包含unsigned int和long两种类型,其操作数都应转化为unsigned long。
- 对于包含同级别的signed和unsigned int表达式,则从signed型转化为unsigned型数据。
4.11.2 其他隐式类型转换
数组转换成指针:大多数用到数组的表达式中数组自动转换成指向数组首元素的指针。但当数组被用作decltype关键字的参数,或者作为取地址符(&)、sizeof及typeid等运算符对象时,上述转换不会发生。用引用来初始化数组上述转换也不会发生。
指针的转换:常量整数值0或者字面值nullptr能转换成任意指针类型,指向任意非常量的指针能转换成void*,指向任意对象的指针能转换成const void*。在有继承关系的类型间还有另外一种指针转换的方式。
转换成布尔类型:从算术类型或指针类型向布尔类型存在自动转换的机制。若指针或算术类型的值是0,则转换结果是false,否则转换结果是true。
转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用同样如此。即若T是一种类型,则能将指向T的指针或引用分别转换成指向const T的指针或引用。相反的转换不存在。
类类型定义的转换:类类型能定义由编译器自动执行的转换,但每次只能执行一种类类型的转换。
4.11.3 显式转换
命名的强制转换有如下格式:cast-name<type>(expression)
,其中type是转换的目标类型而expression是要转换的值,如果type是引用类型,则结果是左值。cast-name是static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种,指定了执行的是哪种转换。
- static_cast:任何具有明确定义的类型转换,只要不包含底层const,都能使用static_cast(当把指针存放在void*中,并使用static_cast将其转换回原来的类型时,应确保指针的值保持不变,强制转换的结果将与原始的地址值相等,因此必须确保转换后所得的类型就是指针所指的类型)。
- const_cast:只能改变运算对象的底层const。可去除对象的常量性(const),const_cast 的唯一职责就在于此,若将 const_cast 用于其他转型将会报错。
- reinterpret_cast:reinterpret_cast用来执行低级转型,如将执行一个 int的指针强制转为int。其转换结果与编译平台相关,不具有可移植性,因此不常见。
旧式的强制类型转换,早期版本中显式进行强制类型转换包含两种形式:type (expr);
和(type) expr;
。当在某处执行旧式强制类型转换时,如果换成const_cast和static_cast也合法,则其行为与对应的命名转换一致,如果替换后不合法,则执行与reinterpret_cast类似的功能。