【C++】【C++ Primer】4-表达式
1 基础
1.1 基本概念
1.1.1 一元运算符
作用于一个运算对象的运算符是一元运算符,如取地址符&和解引用符*。
1.1.2 二元运算符
作用于两个运算对象的运算符是二元运算符,如相等运算符==和乘法运算符*。
1.1.3 三元运算符
condition ? expression1 : expression2
1.1.4 组合运算符和运算对象
对于含有多个运算符的复杂表达式来说,想理解它的含义,首先要理解运算符的优先级、结合律以及运算对象的求值顺序。
1.1.5 运算对象转换
二元运算符通常要求两个运算对象的类型相同,如果类型不相同,能转换成同一种类型也可以。
类型转换的规则比较复杂,譬如:
- 整数能转换成浮点数
- 浮点数能转换成整数
- 指针不能转换成浮点数
- 小整数类型(bool、char、short)会被提升为较大的整数类型,通常是int
1.1.6 重载运算符
C++中,用户可以自定义运算符如何作用在类类型上。这为已存在的运算符赋予了新的含义,所以称作重载运算符。
使用重载运算符时,运算对象的类型、返回值的类型,均由运算符决定。但运算符对象的个数、运算符优先级和结合律是无法改变的。
1.1.7 左值和右值
C++的表达式要么是左值,要么是右值。
当对象被用做左值时,用的是对象的身份(在内存中的位置)。当对象被用做右值时,用的是对象的值(内容)。
需要右值的地方可以用左值来代替,此时实际使用的时它的值(内容)。但右值不能作为左值使用。
几种运算符的左右值区分:
- 赋值运算符的左侧运算对象是左值(非常量),其结果也是一个左值。
- 取地址符作用于左值,返回一个指向该运算对象的指针,这个指针是右值
- 内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符,求值结果都是左值
- 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得结果也是左值。
使用关键字decltype时,左值和右值也有所不同。如果表达式求值结果是左值,decltype作用于该表达式(不是变量)得到一个引用类型。
1.2 优先级与结合律
优先级和结合律记忆起来比较复杂,实际编程中用括号确定即可。
1.3 求值顺序
大多数情况下,不会明确指定求值的顺序。譬如以下表达式,函数f1和函数f2的调用顺序是不确定的。
int i = f1() * f2();
对于没有指定执行顺序的运算符来说,如果表达式指向并修改同一个对象,将引发错误并产生未定义的行为。
有四种运算符明确规定了运算对象的求值顺序:
- &&
- ||
- ?:
- ,逗号运算符
2 算术运算符
3 逻辑和关系运算符
4 赋值运算符
赋值运算符的左侧运算对象必须是一个可修改的左值。赋值运算的结果是它的左侧运算对象,并且是一个左值。如果赋值运算符左右两个运算对象类型不同,则右侧运算对象转换为左侧运算对象的类型。
4.1 列表初始化
C++11允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象。如果左侧运算对象是内置类型,初始值列表最多只能包含一个值,而且该值类型转换后所占空间不应大于目标类型空间。
int k = {3.14}; // 错误,窄化转换
vector<int> vi; // 初始为空
vi = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // vi含有10个元素,值从0到9
无论左侧运算对象是什么类型,初始值列表都可以为空。编译器会创建一个值初始化的临时量,将其赋给左侧运算对象。
4.2 赋值运算满足右结合律
我们以下面的多重赋值语句为例来分析:
int ival, jval;
ival = jval = 0;
由于赋值运算符满足右结合律,所以先算靠右的jval=0。又由于赋值运算的返回值是左侧对象,所以左侧赋值运算实际是ival=jval,因此ival和jval都被赋值为0。
多重赋值语句中的每一个对象,要么与右侧对象的类型相同,要么可以由右侧类型转换得到。
int ival, *pval;
ival = pval = 0; // 错误,pval是int *类型,无法转换为int类型
4.3 赋值运算符优先级较低
赋值运算符的优先级较低,应用在循环条件等场合时应用括号括起来,以保证语义正确。
while ((i = get_value()) != 42) {
// 其他代码
}
5 递增、递减运算符
5.1 递增、递减运算符基本概念
递增、递减运算符有前置版本、后置版本。
- 前置版本:先将运算对象加一/减一,然后将改变后的对象作为左值返回;
- 后置版本:也将运算对象加一/减一,但求值结果是运算对象改变之前那个值的副本,将这个副本作为右值返回。
后置版本需要将原始值存储下来,以便返回这个未修改的内容。因此存在一定浪费。除非必须,尽量使用前置版本。
5.2 解引用运算符与递增运算符的优先级
以下两种代码作用相同:
*p++
*(p++)
6 成员访问运算符
点运算符和箭头运算符都可用于访问成员。
解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号:
*p.size(); // 这是对p.size()进行解引用
(*p).size(); // 这是p所指对象的成员,等同于 p->size()
箭头运算符作用于指针类型的运算对象,结果是一个左值。
点运算符分为两种情况:
- 如果成员所属的对象是左值,结果是左值;
- 如果成员所属的对象是右值,结果是右值。
7 条件运算符
7.1 条件运算符语法格式
condition ? expression1 : expression2;
7.2 条件运算符的嵌套使用
condition1 ? expression1 : (condition2 ? expression2 : expression3)
8 位运算符
位运算符作用于整数类型的运算对象,并将运算对象看作二进制位的集合。位运算符提供检查、设置二进制位的功能。
一种名为bitset的标准库类型也可以标识任意大小的二进制位集合,因此位运算符也可以作用于bitset类型。
8.1 位运算符的类型提升
通常来说,如果运算对象是小整型(bool、char、short),会被自动提升为较大的整数类型(int)。
8.2 位运算符的符号问题
运算对象可以是带符号的,也可以是无符号的。
如果运算对象是带符号的,且其值为负,则位运算符如何处理对象的符号位依赖于机器。此外,左移操作可能会改变符号位的值,因此是未定义行为。
由于位运算符对符号位的处理没有明确规定,因此应仅将位运算符用于处理无符号类型。
9 sizeof运算符
sizeof运算符返回一条表达式或一个类型名字所占的字节数,返回值是size_t类型的常量表达式,可用于声明数组维度。
运算符的运算对象有两种形式:
sizeof(type)
sizeof expr
sizeof运算符的结果部分依赖于其作用的类型:
- char或类型为char的表达式:结果为1。
- 引用类型:结果为被引用对象所占空间。
- 指针:结果为指针本身所占空间的大小。
- 解引用指针:指针指向对象所占空间大小。
- 数组:整个数组所占空间的大小。
- string对象或vector对象:结果为该类型固定部分的大小,不计算对象中的元素占用了多少空间。
10 逗号运算符
逗号运算符含有两个运算对象,按从左向右顺序一次求值。在本文1.3小节中曾提到过运算对象求值顺序问题,逗号运算符也是其中一种。
对于逗号运算符来说,首先对左侧表达式求值,然后将求值结果丢掉。逗号运算符的真正结果是右侧表达式的值。如果右侧运算对象是左值,最终的求值结果就是左值。
逗号运算符通常被用在for循环中。
for (vector<int>::size_type ix = 0; ix != ivec.size(); ++ix, --cnt) {
// code
}
11 类型转换
11.1 隐式转换
在C++语言中,如果两种类型可以相互转换,它们就是关联的。当程序需要其中一种类型的运算对象时,可以用另一种关联类型的对象或值来替代。
C++语言不会直接将两个不同类型的值相加,而是先根据类型转换规则设法统一运算对象的类型,然后再求值。这个过程时自动执行的,无需程序员介入,所以被称为隐式转换。算术类型之间的隐式转换被设计得尽可能避免损失精度。如果表达式中既有整型对象,也有浮点型对象,则将整型转换成浮点型。
我们用以下代码来分析:
int ival = 3.541 + 3;
以上代码分为两个步骤:
- 右侧表达式,将3提升为浮点型,和3.541相加
- ival是int类型,无法改变,因此将右侧表达式的结果转换成int类型,用于初始化ival。
在下列情况下,编译器会自动转换运算对象的类型:
- 比int类型小的整型值提升为int类型
- 在条件表达式中,非布尔值转换为布尔类型
- 初始化过程中,初始值转换成变量的类型
- 赋值语句中,右侧运算对象转换成左侧运算对象的类型
- 算术运算或关系运算的运算对象有多种类型时,需转换成同一种类型
- 函数调用时也会发生类型转换
11.2 算术转换
算术转换的含义是把一种算术类型转换成另一种算术类型。
算术转换的规则定义了一套类型转换的层次,其中运算符的运算对象转换成最宽的类型。譬如一个运算对象的类型是long double,只要另一个对象类型比它小,就会转换成long double。当表达式中既有浮点类型也有整数类型时,整数值转换成相应的浮点类型。
11.2.1 整型提升
整型提升负责将小整数类型转换成较大的整数类型。
对于bool、char、signed char、unsigned char、short、unsigned short等类型,如果它们的取值范围能被int覆盖,就提升成int类型。否则提升成unsigned int类型。
对于较大的char类型,如wchar_t、char16_t、char32_t,提升成int、unsigned int、long、unsigned long、long long、unsigned long long中,能容纳原类型所有值的最小的一种类型。
11.2.2 无符号类型的运算对象
如果某个运算符的运算对象类型不一致,这些运算对象将转换为相同类型。但如果某个运算对象是无符号类型,转换的结果就依赖于机器中各个整数类型的相对大小了。
首先,执行整型提升。
- 如果结果的类型匹配,则无需进行进一步的转换。
- 如果两个运算对象都是带符号类型或无符号类型,则较小类型的运算对象转换为较大类型的运算对象。
- 如果一个运算对象是带符号类型,另一个运算对象是无符号类型
- 无符号类型不小于带符号类型,则带符号类型的运算对象转换为无符号的。譬如一个是unsigned int,另一个是int,则int类型的运算对象转换为unsigned int类型。如果int类型的运算对象恰好为负值,结果为这个负数加上无符号数的模。
- 带符号类型大于无符号类型,转换结果取决于机器。如果无符号类型的所有值都能存在该带符号类型中,则无符号类型的运算对象转换成带符号类型。如果不能,则带符号类型的运算对象转换为无符号类型。
11.3 其他隐式类型转换
11.3.1 数组转换成指针
在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针。当数组被用做decltype关键字的参数,或者作为取地址符&、sizeof及typeid等运算符的运算对象时,上述转换不会发生。如果用一个引用来初始化数组,上述转换也不会发生。在表达式中使用函数类型时,会发生类似的指针转换。
11.3.2 指针的转换
C++规定了几种其他的指针转换方式:
- 常量整数值0或字面值nullptr能转换为任意指针类型;
- 指向任意非常量的指针能转换成void *;
- 指向任意对象的指针能转换成const void *。
- 有继承关系的类型间还有另一种指针转换的方式。
11.3.3 转换成布尔类型
如果指针或算术类型的值为0,转换结果为false,否则转换结果为true。
11.3.4 转换成常量
允许将指向非常量的指针或引用转换成相应的常量类型的指针或引用。简而言之,对象本身可以修改,但我们可以对它使用不可修改的指针或引用。
11.3.5 类类型定义的转换
类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。如果同时提出多个转换请求,这些请求将被拒绝。
在之前的程序中,我们已经使用了两种类类型转换:
string s = "a value"; // 字符串字面值转换为string类型
while (cin >> s) { // 将istream的返回值作为条件判断
// code
}
11.4 显式转换
如果想显式地将对象强制转换为另一种类型,就要强制类型转换。
命名的强制类型转换具有如下形式:
cast-name<type>(expression);
- cast-name指定执行哪种转换
- static_cast
- dynamic_cast
- const_cast
- reinterpret_cast
- type是转换的目标类型
- 如果type是引用类型,结果是左值
- expression是要转换的值
11.4.1 static_cast
任何具有明确定义的类型转换,只要不包含底层const,就可以使用static_cast。
譬如通过强制类型转换,执行浮点数除法:
int i = 1, j = 2;
double slope = static_cast<double>(j) / i;
需要将一个较大的算术类型赋值给较小的类型时,static_cast非常有用。编译器发现试图将较大的算术类型赋值给较小的算术类型时,就会给出警告信息。但执行显式类型转换后,警告信息就会被关闭了。
static_cast对于编译器无法自动执行的类型转换也非常有用。譬如使用static_cast找回存在于void *指针中的值:
double d = 3.14;
void *p = &d;
double *dp = static_cast<double *>(p);
11.4.2 const_cast
const_cast只能改变运算对象的底层const。将常量对象转换成非常量对象的行为,称作去掉const性质。
一旦我们去掉某个对象的const性质,编译器就不再阻止我们对该对象进行写操作。如果对象本身不是一个常量,使用强制类型转换获得写权限是合法行为。如果对象是常量,使用const_cast执行写操作就会产生未定义的后果。
const char *pc;
char *p = const_cast<char *>(pc); // 正确,但通过p写值是未定义行为
四种强制类型转换中,只有const_cast能改变表达式的const属性。const_cast只能修改const属性,不能修改表达式的类型。
const char *cp;
char *q = static_cast<char *>(cp); // 错误。static_cast不能修改表达式的const属性
string s = const_cast<string>(cp); // 错误。const_cast不能用于转换类型
const_cast通常用于有函数重载的上下文中。
11.4.3 reinterpret_cast
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。
我们用以下代码来分析:
int *ip;
char *pc = reinterpret_cast<char *>(ip);
pc所指的真实对象是int类型,而非char类型。如果把pc当成普通的字符指针使用就可能在运行时发生错误。
reinterpret_cast要尽量避免使用。
11.4.4 旧式的强制类型转换
在早期的C++版本中,强制类型转换有两种形式:
type (expr); // 函数形式的强制类型转换
(type) expr; // C语言风格的强制类型转换
已完成