4.表达式
表达式
1 表达式基础
1.1 基本概念
1.1.1 表达式
- 表达式由一个或多个运算对象(operand)组成,对表达式求值将得到一个结果。
- 字面值和变量是最简单的表达式。
- 把运算符和运算对象结合起来可以生产较复杂的表达式。
1.1.2 运算符
- 一元运算符:作用于一个运算对象的运算符,如取地址符(&)和解引用符(*)
- 二元运算符:作用于两个运算对象的运算符,如相等(==)和乘法(*)
- 还有一个作用于三个运算对象的三元运算符
1.1.3 运算对象转换:
- 小整数类型(bool,char,short)会被提升为较大的整数类型(主要是 int)
1.1.4 运算符重载
- 当运算符作用于类类型的运算对象时,用户可以自行定义其含义。
- 例:IO 库的»和«,string 对象、vector 对象和迭代器使用的运算符
重载运算符时,运算对象的个数、运算符的优先级、结合律都是无法改变的。
1.1.5 左值和右值
- C++表达式要么是左值,要么是右值
- C 语言:左值可以位于赋值语句的左侧、右值不能。
- C++语言中,要复杂得多
- 右值:取不到地址的表达式
- 左值:能取到地址的表达式
- 常量对象为代表的左值不能作为赋值语句的左侧运算对象
- 某些表达式的求值结果是对象,但他们是右值
当一个对象被作用右值的时候,用的是对象的值(内存中的内容) 当一个对象被当做左值的时候,用的是对象的身份(内存中的位置)
通常情况:
- 左值可以当成右值,实际使用的是它的内容(值)
- 不能把右值当成左值(也就是位置)
- 取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。
- 内置解引用运算符、下标运算符、迭代器解引用运算符、string 和 vector 的下标运算符的求值结果都是左值。
1.1.6 decltype 与表达式
- 如果表达式的求值结果是左值,decltype 作用于该表达式(不是变量)得到一个引用类型。例如,对于 int *p:
- 因为解引用运算符生成左值,所有 decltype(*p)的结果是 int&
- 因为取地址运算符生成右值,所以 decitype(&p)的结果是 int *
如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。
1.2 优先级与结合律
-
表达式的计算结果,依赖运算符的优先级 (precedence) 以及运算对象的求值顺序 (order of evaluation)以及结合律 (associativity)
-
左结合律就是从左往右算,右结合律反之
这个靠常识吧~如果不清楚,建议加括号~
1.3 求值顺序
- 求值顺序:
int i = f1() + f2()
- 先计算
f1() + f2()
,再计算int i = f1() + f2()
。但是 f1 和 f2 的计算先后不确定 - 但是,如果 f1、f2 都对同一对象进行了修改,因为顺序不确定,所以会编译出错,显示未定义
- 先计算
2 算术运算符
- 运算符优先级,这个实践中感受,不用刻意记
- 溢出:当计算的结果超出该类型所能表示的范围时就会产生溢出。
- bool 类型不应该参与计算
bool b=true;
bool b2=-b; //仍然为true
//b为true,提升为对应int=1,-b=-1
//b2=-1≠0,所以b2仍未true
- 取余运算 m%n,结果符号与 m 相同
3 逻辑和关系运算符
- 短路求值:逻辑与运算符和逻辑或运算符, 都是先求左侧运算对象的值, 再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时, 才会计算右侧运算对象的值。先左再右
4 赋值运算符
- 赋值运算满足右结合律
- 赋值运算符优先级低,使用其当条件时应该加括号
初始化不是赋值,赋值有替换的意思
5 递增和递减运算符
- 前置版本
j = ++i
,先加一后赋值 - 后置版本
j = i++
,先赋值后加一
优先使用前置版本,后置多一步储存原始值。(除非需要变化前的值)
5.1 混用解引用和递增运算符
*iter++
等价于*(iter++)
,递增优先级较高
auto iter = vi.begin();
while (iter!=vi.end()&&*iter>=0)
cout<<*iter++<<endl; // 输出当前值,指针向前移1
6 成员访问运算符
- 点运算符和箭头运算符都可以访问成员
- ptr->mem 等价于(*ptr).mem
- 注意
.
运算符优先级大于*
,所以记得加括号
7 条件运算符
-
三目运算符(右结合律)
-
条件运算符(
?:
)允许我们把简单的if-else
逻辑嵌入到单个表达式中去,按照如下形式:cond? expr1: expr2
-
可以嵌套使用,右结合律,从右向左顺序组合
finalgrade = (grade > 90) ? "high pass"
: (grade < 60) ? "fail" : "pass";
//等价于
finalgrade = (grade > 90) ? "high pass"
: ((grade < 60) ? "fail" : "pass");
- 输出表达式使用条件运算符记得加括号,条件运算符优先级太低。
8 位运算符
- 作用于整数类型的运算对象,把运算对象看成是二进制位的集合
- 检查和设置二进制位
- 左结合律
运算符 | 功能 | 备注 |
---|---|---|
~ | 位求反 | |
« | 将其左侧运算对象 每一位的值向左移动 其右侧运算 对象指定的位数 。 |
移出边界外的位就被舍弃掉了。用 0 填充右侧空出的位置。 |
» | 将其左侧运算对象每一位的值向右移动其右侧运算对象指定的位数。 | 移出边界外的位就被舍弃掉了。对于无符号类型,用 0 填充空出的位置。对于有符号类型,其结果取决于机器。空出的位置可用 0 填充,或者用符号位(即最左端的位)的副本填充。 |
& | 位与,相当于 乘法 | 重点:任何位与 1 组合都得本身。任何位与 0 组合都得 0。 |
^ | 位异或,相当于 无进位加法 | 重点:任何位与 0 异或,结果都为本身;任何位与 1 异或,结果为相反。 |
| | 位或 | 重点:任何位与 0 组合,结果都为本身;任何位与 1 组合,结果都为 1。 |
应用
unsigned long quiz1 = 0; // 每一位代表一个学生是否通过考试
1UL << 12; // 代表第12个学生通过
quiz1 |= (1UL << 12); // 将第12个学生置为已通过
quiz1 &= ~(1UL << 12); // 将第12个学生修改为未通过
bool stu12 = quiz1 & (1UL << 12); // 判断第12个学生是否通过
如果运算对象是“小整形”、则它的值会被自动提升
关于符号位如何处理没有明确的规定,所以强烈建议仅将位运算符用于处理无符号类型
更多位运算的示例可以看位操作
9 sizeof 运算符
- 返回一条表达式或一个类型名字所占的字节数
- 返回的类型是
size_t
的常量表达式。 - sizeof 一个字符串或一个向量只返回这些类型的固定部分的大小;它不返回对象元素使用的大小。
- 两种形式:
sizeof (type)
,给出类型名sizeof expr
,给出表达式
- 可用 sizeof 返回数组的大小
int ia[10];
// sizeof(ia)返回整个数组所占空间的大小
// sizeof(ia)/sizeof(*ia)返回数组的大小
constexpr size_t sz = sizeof(ia) / sizeof(*ia);
int arr[sz];
cout << sizeof(ia) << " byte" << endl;
cout << sz << " byte" << endl;
/*
output:
sizeof(ia) 40 byte
sizeof(ia) / sizeof(*ia) 10 byte
*/
10 逗号运算符
- 含有两个运算对象,按照从左向右的顺序依次求值
- 返回结果为右侧表达式的值,左侧求值结果丢弃
11 类型转换
11.1 隐式类型转换
设计为尽可能避免损失精度,即转换为更精细类型。
- 比
int
类型小的整数值先提升为较大的整数类型。 - 条件中,非布尔转换成布尔。
- 初始化中,初始值转换成变量的类型。
- 算术运算或者关系运算的运算对象有多种类型,要转换成同一种类型。
- 函数调用时也会有转换。
11.1.1 算数转换
- 隐式转换:无需程序员介入
- 算术转换:理解算术转换,办法之一就是研究大量的例子
- 整型提升
- 常见的 char、bool、short 能存在 int 就会转换成 int,否则提升为
unsigned int
wchar_t,char16_t,char32_t
提升为整型中int,long,long long ……
最小的,且能容纳原类型所有可能值的类型。
- 常见的 char、bool、short 能存在 int 就会转换成 int,否则提升为
11.1.2 指针的转换
- 0 或字面值 nullptr 能够转换成任意指针类型
- 指向任意非常量的指针能够转换成 void*
- 指向任意对象的指针能够转换成 const void*
11.1.3 其他隐式类型转换
数组转换成指针
数值转换为 bool
11.2 显式类型转换(尽量避免)
-
static_cast:任何明确定义的类型转换,只要不包含底层 const,都可以使用。
double slope = static_cast<double>(int);
-
dynamic_cast:支持运行时类型识别。19.2 节详细讲解
-
const_cast:只能改变运算对象的底层 const,一般可用于去除 const 性质。
const char *pc; char *p = const_cast<char*>(pc)
只有其可以改变常量属性
-
reinterpret_cast:通常为运算对象的位模式提供低层次上的重新解释。比方,可以将 int _,转换为 char _
- 建议不使用,reinterpret_cast 本质上依赖于机器,干扰了正常的类型检查,对编译器的实现转换的过程有要求,使用时开发者最好对涉及的类型和编译器都非常了解
11.2.1 旧式强制类型转换
早期版本的 C++语言和 C 语言都使用函数形式的强制类型转换
// int 转换成 double
int i;
double (i); // C++函数形式的强制类型转换
(double) i; // C风格的强制类型转换
12 运算符优先级表
详见 CPP 语言运算符优先级
13 小结
C++ 语言提供了一套丰富的运算符,并定义了这些运算符作用于内置类型的运算对象时所执行的操作。此外,C++语言还支持运算符重载的机制,允许我们自己定义运算符作用于类类型时的含义。第 14 章将介绍如何定义作用于用户类型的运算符。
对于含有超过一个运算符的表达式,要想理解其含义关键要理解优先级、结合律和求值顺序。每个运算符都有其对应的优先级和结合律,优先级规定了复合表达式中运算符组合的方式,结合律则说明当运算符的优先级一样时应该如何组合。
大多数运算符并不明确规定运算对象的求值顺序:编译器有权自由选择先对左侧运算对象求值还是先对右侧运算对象求值。一般来说,运算对象的求值顺序对表达式的最终结果没有影响。但是,如果两个运算对象指向同一个对象而且其中一个改变了对象的值,就会导致程序出现不易发现的严重缺陷。
最后一点,运算对象经常从原始类型自动转换成某种关联的类型。例如,表达式中的小整型会自动提升成大整型。不论内置类型还是类类型都涉及类型转换的问题。如果需要,我们还可以显式地进行强制类型转换。
14 术语表
算术转换(arithmetic conversion): 从一种算术类型转换成另一种算术类型。在二元运算符的上下文中,为了保留精度,算术转换通常把较小的类型转换成较大的类型(例如整型转换成浮点型)。
集合律(associativitay): 规定具有相同优先级的运算符如何组合在一起。结合律分为左结合律(运算符从左到右组合)和右集合律(运算符从右到左组合)。
二元运算符(binary operator): 有两个运算对象参与运算的运算符。
强制类型转换(cast): 一种显式的类型转换。
复合表达式(compound expression): 含有多于一个运算符的表达式。
const_cast: 一种涉及 const 的强制类型转换。将底层 const 对象转换成对应的非常量类型,或者执行相反的转换。
转换(conversion): 一种类型的值改变成另一种类型的值的过程。
dynamic_cast: 和继承及运行时类型识别一起使用。
表达式(expression): C++程序中最低级别的计算。表达式将运算符作用于一个或多个运算对象,每个表达式都有对应的求值结果。表达式本身也可以作为运算兑现转换成所需的类型。
隐式转换(implicit conversoin): 由编译器自动执行的类型转换。假如表达式需要某种特定的类型而运算对象是另外一种类型,此时只要规则允许,编译器就会自动地将运算对象转换成所需的类型。
整型提升(integral promotion): 把一种较小的整型类型转换成与之最接近的较大整数类型的过程。不论是否真的需要,小数类型(即 short, char 等)总会得到提升。
左值(lvalue): 是指那些求值结果为对象或函数的表达式。一个表示对象的非常量左值可以作为赋值运算符的左侧运算对象。
运算对象(operand): 表达式在某些值执行运算,这些值就是运算对象。一个运算符由一个或多个相关的运算对象。
运算符(operator): 决定表达式所做操作的符号。C++语言定义了一套运算符并说明了这些运算符作用于内置类型时的含义。C++还定义了运算符的优先级和结合律以及每种运算符处理的运算对象数量。可以重载运算符使其能处理类类型。
求值顺序(order of evaluation): 是某个运算符的运算对象的求值顺序。大多数情况下,编译器可以任意选择运算对象求值的顺序。不过运算对象一定要在运算符之前得到求值结果。只用 &&,|| ,条件和逗号四种运算符明确规定了求值顺序。
重载运算符(overloaded operator): 针对某种运算符重新定义的适用于类类型的版本。
优先级(precedence): 规定了复合表达式中不同运算符的执行顺序。与低优先级的运算符相比,高优先级的运算符组合得更紧密。
提升(promoted): 参见整型提升。
reinterpret_cast: 把运算对象的内容解释成另外一种。这种强制类型转换本质上依赖于机器而且非常危险。
结果(result): 计算表达式得到的值或对象。
右值(rvalue): 是指一种表达式,其结果是值而非值所在的位置。
短路求值(short-circuit evaluation): 是一种专有名词,描述逻辑与和逻辑或运算符的执行过程。如果根据运算符的第一个运算对象就能确定整个表达式的结果,求值终止,此时第二个运算对象将不会被求值。
sizeof: 是一个运算符,返回存储对象所需的字节数,该对象的类型可能是某个给定的类型名字,也可能由表达式的返回结果确定。
static_cast: 显式地执行某种定义明确的类型转换,常用于替换由编译器隐式执行的类型转换。
一元运算符(unary operators): 只用一个运算对象参与对象参与运算的运算符。
** , 运算符( , operator):** 逗号运算符,是一种从左到右求值的二元运算符。逗号运算符的结果是右侧运算对象的值,当且仅当右侧运算对象是左值时逗号运算符的结果时左值。
** ? 运算符( ?operator):** 条件运算符,以下述形式提供 if-then-else 逻辑的表达式 cond ? expr1 : expr2; 如果条件 cond 为真,对 exp1 求值;否则对 expr2 求值。 expr1 和 expr2 的类型应该相同或者能转换成同一种类型。expr1 和 expr2 中只有一个会被求值。
&& 运算符(&& operator): 逻辑与运算符,如果两个运算对象都是真,结果才为真。只有当左侧运算对象为真时才会检测右侧运算对象。
**& 运算符(& operator):**位与运算符,由两个运算对象生成的一个新的整型值。如果两个运算对象对应的位都是 1,所得结果中该位 i 二位 1;否则所得结果中该位为 0。
^ 运算符(^ operator): 位异或运算符,由两个元对象生成一个新的整型值。如果两个运算对象对应的为有且只有一个是 1,所得结果中该位为 1;否则所得结果中该位为 0。
|| 运算符(|| operator): 逻辑或运算符,任何一个运算对象是真,结果就为真。只有当左侧运算对象为假时才会检查右侧运算对象。
| 运算符(| operator): 位或运算符,由两个运算对象生成一个新的整型值。如果两个运算对象对应的位至少由一个是 1,所得结果中该位为 1;否则所得结果中该位为 0。
++ 运算符(++ operator): 递增运算符。包括两种形式:前置版本和后置版本。前置递增运算符得到一个左值,它给运算符加 1 并得到运算对象改变后的值。后置递增运算符得到一个右值,它给运算符加 1 并得到运算对象原始的,未改变的值的副本。注意:即使迭代器没有定义 + 运算符,也会有++运算符。
– 运算符(– operator): 递减运算符。包括两种形式:前置版本和后置版本。前置递减运算符得到一个左值,它从运算符减 1 并得到运算对象改变后的值。后置递减运算符得到一个优质,它从运算符减 1 并得到运算对象元素的,未改变的值或副本。注意:即使迭代器没有定义 –运算符,也会有 –运算符。
« 运算符(« operator): 左移运算符,将左侧运算对象的值的(可能是提升后的)副本向左移位,移动的位数有右侧运算对象确定。右侧运算对象必须大于等于 0 而且小于结果的位数。左侧运算对象应该是无符号类型,如果它是带符号类型,则一旦移动改变了符号位的值就会产生未定义的结果。
» 运算符(» operator): 右移运算符,除了移动方向相反,其他性质都和座椅运算符类似。如果左侧运算对象是带符号类型,那么根据实现的不同新移入的内容也不同,新移入的位可能都是 0, 也可能都是符号位的副本。
~ 运算符(~ operator): 位求反运算符,生成一个新的整型值。该值的每一位恰好与(可能是提升后的)运算对象的对应位相反。
!运算符(!operator): 逻辑非运算符,将它的运算对象的布尔值取反。如果运算对象是假,则结果位真,如果运算对象是真,则结果为假。