C++ Primer 第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 位运算符
- 一、移位运算符
- 二、位求反运算符
- 三、位与、位或、位异或运算符
- 四、使用位运算符
- 五、移位运算符(又叫IO运算符)满足左结合律
- 练习
- 4.9 sizeof运算符
- 4.10 逗号运算符
- 练习
- 4.11 类型转换
- 一、何时发生隐式类型转换
- 4.11.1 算术转换
- 一、整型提升
- 二、无符号类型的运算对象
- 三、理解算术转换
- 4.11.2 其他隐式类型转换
- 4.11.3 显式转换
- 一、命名的强制类型转换
- 二、旧式的强制类型转换
- 4.12 运算符优先级表
- 小结
表达式由一个或多个运算对象组成,对表达式求值将得到一个结果。字面值和变量是最简单的表达式,其结果就是字面值和变量的值。把一个运算符和一个或多个运算对象组合起来可以生成较复杂的表达式。
4.1 基础
4.1.1 基本概念
一元运算符:作用于一个运算对象的运算符。
二元运算符:作用于两个运算对象的运算符。
三元运算符
函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。
一、组合运算符和运算对象
二、运算对象转换
三、重载运算符
我们使用重载运算符时,其包括运算对象的类型和返回值的类型,都是由该运算符定义的;但是运算对象的个数、运算符的优先级和结合律都是无法改变的。
四、左值和右值
左值可以位于赋值语句的左侧,右值则不能。
当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
(1)赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也仍然是 一个左值。
(2)取地址符作用域一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。
(3)内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值。
(4)内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得到的结果是左值。
如果表达式的求值结果是左值,decltype作用于该表达式(不是变量)得到一个引用类型。例,假定p的类型是int*,因为解引用运算符生成左值,所以decltype(*p)的结果是int&。另一方面,因为取地址运算符生成右值,所以decltype(&p)的结果是int **,结果是一个指向整型指针的指针。
4.1.2 优先级与结合律
算术运算符满足左结合律,意味着如果运算符的优先级相同,将按照从左向右的顺序组合运算对象。
一、括号无视优先级与结合律
二、优先级与结合律有何影响
4.1.3 求值顺序
对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。
有四种运算符明确规定了运算对象的求值顺序:逻辑与运算符(&&)、逻辑或运算符(||)、条件运算符(?:)、逗号运算符(,)。
一、求值顺序、优先级、结合律
表达式:f() + g() * h() + j()
优先级规定,g()的返回值和h()的返回值相乘
结合律规定,f()的返回值先于g()和h()的乘积相加,所得结果再与j()的返回值相加
对于这些函数的调用顺序没有明确规定
4.2 算术运算符
算术运算符的运算对象和求值结果都是右值。
在表达式求值之前,小整数类型的运算对象被提升成较大的整数类型,所有运算对象最终会转换成同一类型。
// 一元正号运算符作用于一个指针或者算术值时,返回运算对象值的一个(提升后的)副本
// 一元负号运算符对运算对象值取负后,返回其(提升后的)副本
int i = 1024;
int k = -i; // k是-1024
bool b = true;
bool b2 = -b; // b2是true
// 整数相除结果还是整数,商一律向0取整(即直接切除小数部分)
int ival1 = 21 / 6; // 3
// 参与取余运算的运算对象必须是整数类型
int ival = 42;
double dval = 3.14;
ival % 12; // 6
ival % dval; // 错误
// (-m)/n和m/(-n)都等于-(m/n)
// m%(-n)等于m%n,(-m)%n等于-m%n
#include <iostream>
using namespace std;
int main()
{
int i = 10;
int *p = &i;
cout << *p << endl; // 10
cout << &i << endl;
cout << p << endl; // 变量i的地址
int *pt = +p;
cout << *pt << endl; // 10
cout << pt << endl; // 变量i的地址
return 0;
}
练习
溢出是一种常见的算术运算错误。因为在计算机中存储某种类型的内存空间有限,所以该类型的表示能力(范围)也是有限的,当计算的结果值超出这个范围时,就会产生未定义的数值,这种错误称为溢出。
4.3 逻辑和关系运算符
运算对象和求值结果都是右值。
一、逻辑与和逻辑或运算符
先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。
二、逻辑非运算符
三、关系运算符
if (i < j <k) ——if语句的条件部分首先把i、j和第一个<运算符组合在一起,其返回的布尔值再作为第二个<运算符的左侧运算对象。也就是说,k比较的对象是第一次比较得到的那个或真或假的结果。
四、相等性测试与布尔字面值
if (val) // 如果val是任意的非0值,条件为真
if (!val) // 如果val是0,条件为真
if (val == true) // 只有val等于1时条件才为真
// 进行比较运算时除非比较的对象是布尔类型,否则不要使用布尔字面值true和false作为运算对象
4.4 赋值运算符
赋值运算符的左侧运算对象必须是一个可修改的左值。赋值运算的结果是它的左侧运算对象。
int i = 0, j = 0, k = 0; // 初始化而非赋值
const int ci = i; // 初始化而非赋值
1024 = k; // 错误,字面值是右值
i + j = k; // 错误,算术表达式是右值
ci = k; // 错误,ci是常量左值
// C++11新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象
k = {3.14}; // 错误,窄化转换
vector<int> vi;
vi = {0, 1, 2, 3, 4, 5};
一、赋值运算满足右结合律
int ival, *pval;
ival = pval = 0; // 错误,不能把指针的值赋给int
string s1, s2;
s1 = s2 = "OK"; // 正确,字符串字面值"OK"转换成string对象
二、赋值运算优先级较低
三、切勿混淆相等运算符和赋值运算符
if (i = j) // 若j的值非0,则表达式恒为真
四、复合赋值运算符
4.5 递增和递减运算符
两种形式:前置版本和后置版本。前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。
一、在一条语句中混用解引用和递增运算符
auto pbeg = v.begin();
while(pbeg != v.end() && *pbeg >= 0)
cout << *pbeg++ << endl;
// 后置递增运算符的优先级高于解引用运算符,先将pbeg加1,然后返回pbeg的初始值的副本作为其求值结果,此时解引用云端福的运算对象是pbeg未增加之前的值。因此这条语句输出pbeg开始时指向的那个元素,并将指针向前移动一个位置
二、运算对象可按任意顺序求值
4.6 成员访问运算符
string s1 = "a string", *p = &s1;
auto n = s1.size();
n = (*p).size();
n = p->size();
// 解引用运算符的优先级低于点运算符
*p.size(); // 错误,p是一个指针,它没有名为size的成员
箭头运算符作用于一个指针类型的运算对象,结果是一个左值。点运算符分成两种情况:如果成员所属的对象是左值,那么结果是左值;反之,如果成员所属的对象是右值,那么结果是右值。
4.7 条件运算符
cond ? expr1 : expr2;
cond是判断条件的表达式
expr1和expr2是两个类型相同或可能转换为某个公共类型的表达式
当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值;否则运算的结果是右值。
int a = 10;
int b = 20;
((a > b) ? a : b) = 100; // 正确,左侧条件表达式运算结果为左值
一、嵌套条件运算符
string finalgrade = (grade > 90) ? "high pass" : (grade < 60) ? "fail" : "pass";
条件运算符满足右结合律,意味着运算对象(一般)按照从右向左的顺序组合
二、在输出表达式中使用条件运算符
cout << ((grade < 60) ? "fail" : "pass"); // 输出pass或fail
cout << (grade < 60) ? "fail" : "pass"; // 输出1或0
// 等价为
// cout << (grade < 60);
// cout ? "fail" : "pass";
cout << grade < 60 ? "fail" : "pass"; // 错误:试图比较cout和60
4.8 位运算符
位运算符用于整数类型的运算对象,并把运算对象看成是二进制位的集合。位运算符提供检查和设置二进制位的功能。
如果运算对象是带符号的且它的值为负,那么位运算符如何处理运算对象的“符号位”依赖于及其。而且,此时的左移操作可能会改变符号位的值,因此使一种未定义的行为。
异或:不同为1,相同为0
一、移位运算符
令左侧运算对象的内容按照右侧运算对象的要求移动指定位数,然后经过移动的左侧运算对象的拷贝作为求值结果。其中,右侧的运算对象一定不能为负,而且值必须严格小于结果的位数,否则就会产生未定义的行为。二进制位或者向左移(<<)或者向右移(>>),移出边界之外的位就被舍弃掉了。
左移运算符(<<)在右侧插入值为0的二进制位。右移运算符(>>)的行为则依赖于其左侧运算对象的类型:如果该运算对象是无符号类型,在左侧插入值为0的二进制位;如果该运算对象是带符号类型,在左侧插入符号位的副本或者值为0的二进制位,如何选择要视具体环境而定。
二、位求反运算符
位求反运算符(~)将运算对象逐位求反后生成一个新值,将1置为0、将0置为1。
char类型的运算对象首先提升成int类型,提升时运算对象原来的位保持不变,往高位添加0即可。
三、位与、位或、位异或运算符
四、使用位运算符
unsigned long quiz1 = 0; // 把这个值当成是位的集合来使用
// 序号为27的学生对应的位置1,表示他通过了测验
1UL << 27 // 生成一个值,该值只有第27位为1
quiz1 |= 1UL << 27; // 表示学生27通过了测验
quiz1 |= (1UL << 27 | 1UL << 25);
quiz1 &= ~(1UL << 27); // 表示学生27没有通过测验
quiz1 &= ~(1UL << 27 | 1UL << 25)
bool status = quiz1 & (1UL << 27); //学生27是否通过了测验?
五、移位运算符(又叫IO运算符)满足左结合律
练习
4.9 sizeof运算符
sizeof运算符返回一条表达式或一个类型名字所占的字节数。sizeof运算符满足右结合律,其所得的值是一个size_t类型的常量表达式,sizeof并不实际计算其运算对象的值。
Sales_data data, *p;
sizeof *p;
因为sizeof满足右结合律并且与*运算符的优先级一样,所以表达式按照从右向左的顺序结合,因此它等价于sizeof(*p)
因为sizeof不会实际运算对象的值,所以即使p是一个无效的指针也不会有影响
sizeof data.revenue;
sizeof Sales_data::revenue;
对数组执行sizeof运算得到整个数组所占空间的大小。
sizeof(ia) / sizeof(*ia)返回ia的元素的数量。
对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。
vector<int> ivect = {1, 2, 3, 4, 5};
cout << "sizeof int = " << sizeof(int) << endl; // 4
cout << "sizeof ivect = " << sizeof(ivect) << endl; // 24且不变化
vector:
pointer _M_start;
pointer _M_finish;
pointer _M_end_of_storage
vector靠3个指针来管理,每个指针占8个字节(64位操作系统)
4.10 逗号运算符
逗号运算符含有两个运算对象,按照从左向右的顺序依次求值。逗号运算符真正的结果是右侧表达式的值,如果右侧运算对象是左值,那么最终的求值结果也是左值。
练习
4.11 类型转换
int ival = 3.541 + 3;
// 3转换成double类型,然后执行浮点数加法,所得结果的类型是double
// 在初始化过程中,加法运算得到的double类型的结果转换成int类型的值,这个值用来初始化ival
一、何时发生隐式类型转换
编译器自动地转换运算对象的类型:
(1)在大多数表达式中,比int类型小的整型值首先提升为较大的整数类型
(2)在条件表达式中,非布尔值转换成布尔类型
(3)初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型
(4)如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型
(5)函数调用时也会发生类型转换
4.11.1 算术转换
一、整型提升
整型提升负责把小整数类型转换成较大的整数类型。对于bool、char、signed char、unsigned char、short、unsigned short等类型来说,只要它们所有可能的值都存在int里,它们就会提升成int类型;否则,提升成unsigned int类型。较大的char类型提升成int、unsigned int、long、unsigned long、long long和unsigned long long中最小的一种类型,前提是转换后的类型要能容纳远类型所有可能的值。
二、无符号类型的运算对象
如果一个运算对象是无符号类型、另外一个运算对象是带符号类型,而且其中的无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的。
剩下的一种情况是带符号类型大于无符号类型,此时转换的结果依赖于机器。如果无符号类型的所有值都能存在该带符号类型中,则无符号类型的运算对象转换成带符号类型。如果不能,那么带符号类型的运算对象转换成无符号类型。例如,如果两个运算对象的类型分别是long和unsigned int,并且int和long的大小相同,则long类型的运算对象转换成unsigned int类型;如果long类型占用的空间比int更多,则unsigned int类型的运算对象转换成long类型。
三、理解算术转换
4.11.2 其他隐式类型转换
(1)数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针。当数组被用作decltype关键字的参数,或者作为取地址符、sizeof及typeid等运算符的运算对象时,上述转换不会发生。
(2)指针的转换:常量整数值0或字面值nullptr能转换成任意指针类型;指向任意非常量的指针能转换成void*;指向对象的指针能转换成const void*。
(3)转换成布尔类型
(4)转换成常量:如果T是一种类型,能将指向T的指针或引用分别转换成指向const T的指针或引用
(5)类类型定义的转换
string s, t = "a value"; // 字符串字面值转换成string类型
while (cin >> s) // while的条件部分把cin转换成布尔值
4.11.3 显式转换
一、命名的强制类型转换
cast-name<type>(expression)
如果type是引用类型,则结果是左值
cast-name是static_cast、dynamic_cast、const_cast、reinterpret_cast
(1)static_cast:任何具有明确定义的类型转换,只要不包含底层const,均可使用
void *p = &d;
double *dp = static_cast<double*>(p);
(2)const_cast:只能改变运算对象的底层const,常用于有函数重载的上下文中
const char *pc;
char *p = const_cast<char*>(pc); // 正确,但是通过p写值是未定义的行为
const char *cp;
char *q = static_cast<char*>(cp); // 错误:static_cast不能转换掉const性质
static_cast<string>(cp); // 正确:字符串字面值转换成string类型
const_cast<string>(cp); // 错误:const_cast只改变常量属性
const int a = 10;
int b = const_cast<int>(a); // 错误,只能改变指针或引用的底层const属性
#include <iostream>
using namespace std;
int main()
{
const int a = 10;
int *pt = const_cast<int *>(&a);
int &r = const_cast<int &>(a);
*pt = 20;
cout << "*pt = " << *pt << endl; // 20
cout << "r = " << r << endl; // 20
cout << "a = " << a << endl; // 10:编译器优化,通过pt修改的是内存的值,而a保存在寄存器中
return 0;
}
(3)reinterpret_cast
二、旧式的强制类型转换
type (expr); // 函数形式的强制类型转换
(type) expr; // C语言风格的强制类型转换
4.12 运算符优先级表
小结
C++语言提供了一套丰富的运算符,并定义了这些运算符作用域内置类型的运算对象时所执行的操作。此外,C++语言还支持运算符重载的机制,允许我们自己定义运算符作用于类类型时的含义。
对于含有超过一个运算符的表达式,要想理解其含义关键要理解优先级、结合律和求值顺序。每个运算符都有其对应的优先级和结合律,优先级规定了复合表达式中运算符组合的方式,结合律则说明当运算符的优先级一样时应该如何组合。
大多数运算符并不明确规定运算对象的求值顺序:编译器有权自由选择先对左侧运算对象求值还是先对右侧运算对象求值。一般来说,运算对象的求值顺序对表达式的最终结果没有影响。但是,如果两个运算对象指向同一个对象而且其中一个改变了对象的值,就会导致程序出现不易发现的严重缺陷。
最后一点,运算对象经常从原始类型自动转换成某种关联的类型。例如,表达式中的小整型会自动提升成大整型。不论内置类型还是类类型都涉及类型转换的问题。如果需要,我们还可以显式地进行强制类型转换。