重载运算符时,其包括运算对象的类型和返回值的类型,都是由该运算符定义的;但是运算对象的个数、运算符的优先级和结合律都是无法改变的。
4.1 左值和右值
当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
一个重要的原则:在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。当一个左值被当成右值使用时,实际使用的是它的内容(值)。
要用到左值的运算符:
- 赋值运算符需要一个左值作为其左侧运算对象,得到的结果也仍然是一个左值
- 取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值
- 内置解引用运算符、下标运算符、迭代器解引用运算符、
string
和vector
的下标运算符的求值结果都是左值 - 内置类型和迭代器的递增递减运算符作用于左值运算对象
4.2 求值顺序
优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值。对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。
例如,下面的输出表达式是未定义的,我们无法推测编译器是先求 ++i 还是先输出:
int i = 0;
cout << i << " " << ++i << endl; //未定义的
有4种运算符明确规定了运算对象的求值顺序。&&、||、?:、和逗号(,)运算符。
对于f()+g()*h()+j()的表达式:
- 优先级规定,g() 的返回值和 h() 的返回值相乘
- 结合律规定,f() 的返回值先与 g() 和 h() 的乘积相加,所得结果再与 j() 的返回值相加
- 对于这些函数的调用顺序没有明确规定
- 如果 f、g、h 和 j 是无关函数,它们既不会改变同一对象的状态也不执行 IO 任务,那么函数的调用顺序不受限制。反之,如果其中某几个函数影响同一对象,则它是一条错误的表达式,将产生未定义的行为
4.3 算术运算符
+(正号)、-(负号)、*(乘)、/(除),%(求余),+(加)、-(减)
算术运算符的运算对象和求值结果都是右值。
一元正号负号运算符对运算对象作用后,返回一个(提升后的)副本:
int i = 1024;
int k = -i;
bool b = true;
bool b2 = -b; //b2 是 true
对大多数运算符来说,布尔类型的运算对象将被提升为int
类型。如上,b 参与运算时被提升成整数值1,对它求负-1,转换为布尔值将其作为 b2 的初始值。
在除法运算中,如果两个运算对象的符号相同则商为正,否则商为负。C++ 语言的早期版本允许结果为负值的商向上或向下取整,C++11 新标准则规定商一律向0取整(即直接切除小数部分)。
4.4 逻辑和关系运算符
运算对象和求值结果都是右值;短路求值;进行比较运算符时,除非比较的对象是布尔类型,否则不要使用布尔字面值true
和false
作为运算对象
4.5 赋值运算符
- 赋值运算符的左侧运算对象必须是一个可修改的左值
- 赋值运算的结果是它的左侧运算对象,并且是一个左值,结果的类型就是左侧运算对象的类型
- 如果赋值运算符的左右两个运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型
- 赋值运算满足右结合律:ival = jval = 0;
4.6 ++ 和 -- 运算符
- 前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回
- 建议:除非必须,否则不用递增递减算符的后置版本
- 后置递增运算符的优先级高于解引用运算符,因此 *pbeg++ 等价于 *(pbeg++),pbeg++ 把 pbeg 的值加1,然后返回 pbeg 的初始值的副本作为其求值结果
4.7 成员访问运算符
- 由于解引用运算符的优先级低于逗号运算符,因此要对解引用运算符加括号
- 箭头运算符作用于一个指针类型的运算对象,结果是一个左值
- 点运算符分成两种情况:如果成员所属的对象是左值,那么结果就是左值;反之,如果成员所属的对象是右值,那么结果是右值
4.8 条件运算符
- 当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值;否则运算的结果是右值
- 条件运算符满足右结合律
4.9 位运算符
- 位运算符作用于整数类型的运算对象
- 关于符号位如何处理没有明确的规定,所以强烈建议仅将位运算符作用于处理无符号类型
- 移位运算符
- 首先令左侧运算对象的内容按照右侧运算对象的要求移动指定位数,然后将经过移动的(可能还进行了提升)左侧运算对象的拷贝作为求值结果。其中,右侧的运算对象一定不能为负,而且值必须严格小于结果的位数,否则就会产生未定义的行为。移出边界之外的位就被舍弃掉了
- 移位运算符满足左结合律
- 移位运算符的优先级比算术运算符低,比关系运算符、赋值运算符和条件运算符高
4.10 sizeof运算符
sizeof
运算符返回一条表达式或一个类型名字所占的字节数。sizeof
运算符满足右结合律,其所得的值是一个size_t
类型的常量表达式
两种形式
sizeof (type)
sizeof expr
sizeof
并不直接计算其运算对象的值。
对于sizeof *p
:
- 因为
sizeof
满足右结合律并且与 * 的运算符的优先级一样,所以表达式按照从右到左的顺序结合,等价于:sizeof (*p)
- 其次,因为
sizeof
不实际求运算对象的值,所以即使 p 是一个无效(即未初始化)的指针也不会有什么影响 - 在
sizeof
的运算对象中解引用一个无效的指针仍然是一种安全的行为,因为指针实际上并没有被真正使用 sizeof
运算符的结果部分地依赖于其作用的类型:- 对
char
或者类型为char
的表达式执行sizeof
运算,结果得1 - 对引用类型执行
sizeof
运算得到被引用对象所占空间的大小 - 对指针执行
sizeof
运算得到指针本身所占空间的大小 - 对解引用指针执行
sizeof
运算得到指针指向的对象所占空间的大小,指针不需有效 - 对数值执行
sizeof
运算符得到整个数组所占空间的大小。sizeof
运算不会把数组转换成指针来处理 - 对
string
对象或者vector
对象执行sizeof
运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。即只取决于里面存放的数据类型,与元素的个数无关,是个编译器相关的值
- 对
4.11 逗号运算符
首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号表达式真正的结果是右侧表达式的值。如果右侧运算对象是左值,那么最终的求值结果也是左值。
4.12 类型转换
4.12.1 何时发生隐式类型转换
- 在大多数表达式中,比
int
类型小的整数值首先提升为较大的整数类型 - 在条件中,非布尔值转换成布尔类型
- 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象装换成左侧运算对象的类型
- 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型
- 函数调用时也会发生类型转换
- 数组转换成指针(当数组被用作
decltype
关键字的参数,或者作为取地址符、sizeof
即typeid
等运算符的运算对象时,上述转换不会发生,如果用一个引用来初始化数组,上述转换也不会发生)
4.12.2 指针的转换
- 常量整数值0或者字面值
nullptr
能转换成任意指针类型 - 指向任意非常量的指针能转换成
void *
- 指向任意对象的指针能转换成
const void *
- 转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。相反的转换并不存在,因为它试图删掉底层
const
4.12.3 类类型定义的转换
- 类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换,如果同时提出多个转换请求,这些请求将被拒绝
- 之前遇到过的类类型转换
string s, t = "a value"; //字符串字面值转换成 string 类型
wile( cin >> s) //while 的条件部分把 cin 转换成布尔值
4.12.4 显示转换
虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的。
4.12.5 命名的强制类型转换
一个命名的强制类型转换具有如下格式:cast-name<type>(expression);
type 是转换的目标类型。如果 type 是引用类型,则结果是左值;expression 是要转换的值;cast-name 是static_cast
、dynamic_cast
、const_cast
和reinterpret_cast
中的一种。dynamic_cast
支持运行时类型识别。
4.12.6 static_cast
任何具有明确定义的类型转换,只要不包含底层const
,都可以使用static_cast
。当需要把一个较大的算术类型赋给较小的类型时,static_cast
非常有用。此时,强制类型转换告诉程序的读者和编译器:我们知道并且不在乎潜在的精度损失。
static_cast
对于编译器无法自动执行的类型转换也非常有用。例如,我们可以使用它找回void *
指针中的值:
void *p = &d;
double *dp = static_cast<double *>(p);
4.12.7 const_cast
const_cast
只能改变运算对象的底层const
:
const char *pc;
char *p = const_cast<char*>(pc);//正确,但通过p写值是未定义的行为
只有const_cast
能改变表达式的常量属性。
4.12.8 reinterpret_cast
einterpret_cast
通常为运算对象的位模式提供较低层次上的重新解释。
假如有以下转换:
int *ip;
char *pc = reinterpret_cast<char*>(ip);
我们必须牢记 pc 所指的真实对象是一个 int 而非字符,如果把 pc 当成普通的字符指针使用就可能在运行时发生错误。
string str(pc); //可能导致异常的运行时行为
reinterpret_cast
本质上依赖于机器。要想安全地使用reinterpret_cast
必须对涉及的类型和编译器实现转换的过程都非常了解。
强制类型转换干扰了正常的类型检查,因此强烈建议避免使用强制类型转换。
4.12.9 旧式的强制类型转换
在早期版本的 C++ 语言中,显示地进行强制类型转换包含两种形式:
type (expr); //函数形式的强制类型转换
(type) expr; //C 语言风格的强制类型转换
根据所设计的类型不同,旧式的强制类型转换分别具有与const_cast
,static_cast
或reinterpret_cast
相似的行为。当我们在某处执行旧式的强制类型转换时,如果换成const_cast
和static_cast
也合法,则其行为与相应的命名转换一致。如果替换后不合法,则旧式强制类型转换执行与reinterpret_cast
类似的功能。
char *pc = (char*) ip; //ip是指向整数的指针
上述代码的效果与reinterpret_cast
一样。