1.基础
表达式有一个或多个运算对象组成,对表达式求值将得到一个结果。
字面值和变量是最简单的表达式,运算符和一个或多个运算对象组合成复杂的表达式。
1.1 基本概念
C++定义了一元运算符和二元运算符,例如:取地址符&、解引用符* 作用于一个运算对象的运算符称为一元运算符;相等运算符(==)、乘法运算符(*)作用于两个运算对象的运算符是二元运算符。
三元运算符是唯一的,其格式为 (a>b?a:b);
一些符号既能作为一元运算符也能作为二元运算符,主要根据它的上下文来判断一元还是二元。
在表达式求值的过程中,运算对象常常由一种类型转换成另一种类型,例如,整数能转换成浮点数,这种情况称为类型转换。
当运算符作用于类类型的运算对象时,用户可以自行定义其含义,这相当于给已定义的运算符赋予另一种含义,称之为重载运算符;例如,IO库中的>>和<<运算符以及string对象,vector对象等。
我们在使用重载运算符时,其包括运算对象的类型和返回值的类型,都是由该运算符定义的;但是运算对象的个数,运算符的优先级和结合律都是无法改变的。
C++的表达式不是左值,就是右值。左值可以位于赋值语句的左侧,右值则不可以。
当一个对象被用作右值时,用的是对象的值(内容);当对象被用作左值时,用的是对象的身份(在内存中的位置)。
int i,j;
i = 42; //i是左值,使用的是i的位置
j = i; //i是右值,使用的是i的内容
在需要右值的地方可以用左值来代替,但是不能把右值当成左值来使用;
当一个左值被当成右值来使用时,实际使用的时它的内容(值)。
使用关键字decltype时,如果表达式的求职结果是左值,那么decltype将得到一个引用类型。
取地址符作用于一个左值,返回一个指向该运算对象的指针,这个指针是一个右值。
1.2 优先级与结合律
复合表达式是指含有两个或多个运算符的表达式,优先级与结合律决定了运算对象组合的方式。
表达式中的括号无视优先级和结合律,可以使用括号将表达式的某个局部括起来使其得到优先运算。
逻辑与运算符(&&),逻辑或运算符(||),条件运算符(?:),逗号运算符(,)能明确规定了运算对象的求值顺序。
如果在表达式中,某几个函数影响着同一对象,则它是一条错误的表达式,将产生未定义的行为。
2.算术运算符
运算符 | 功能 | 用法 |
+ | 一元正号 | +expr |
- | 一元负号 | -expr |
* | 乘法 | expr * expr |
/ | 除法 | expr / expr |
% | 求余 | expr % expr |
+ | 加法 | expr + expr |
- | 减法 | expr - expr |
上面的运算符优先级由上往下递减,满足左结合律。
一元正号运算符、加法运算符和减法运算符都能作用于指针。
对于大多数运算符来说,布尔类型的运算对象将被提升为int类型。布尔变量的值为真,参与运算时将被提升成整数值1。
运算符%负责计算两个整数相除所得的余数,参与取余运算的运算对象必须是整数类型。
在C++新标准中,(-m)/n 和 m/(-n) 等于-(m)/n;m%(-n) 等于 m%n; (-m)%n 等于 -(m%n);
3.逻辑和关系运算符
结合律 | 运算符 | 功能 | 用法 |
右 | ! | 逻辑非 | !expr |
左 | < | 小于 | expr < expr |
左 | <= | 小于等于 | expr <= expr |
左 | > | 大于 | expr > expr |
左 | >= | 大于等于 | expr >= expr |
左 | == | 相等 | expr == expr |
左 | != | 不相等 | expr != expr |
左 | && | 逻辑与 | expr && expr |
左 | || | 逻辑或 | expr || expr |
逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果才会计算右侧运算对象的值。
关系运算符比较运算对象的大小关系并返回布尔值,关系运算符都满足左结合律。
进行比较运算时除非比较的对象是布尔类型,否则不要使用布尔字面值作为运算对象。
4.赋值运算符
赋值运算符的左侧运算对象必须是一个可修改的左值。
赋值运算的结果是它的左侧运算对象,并且是一个左值。
对于类类型来说,赋值运算的细节由类本身决定。
赋值运算符满足右结合律。
对于多重赋值语句中的每一个对象,它的类型或者与右边对象的类型相同、或者可由右边对象的类型转换得到。
string s1,s2;
s1 = s2 = "ok"; //字符串字面值“ok”转换成string对象
因为赋值运算符的优先级相对较低,所以通常需要给赋值部分加上括号使其符合我们的原意。
5.递增和递减运算符
递增运算符(++)和递减运算符(--)保持代码书写简洁并且能应用于迭代器中。
递增和递减运算符有前置版本和后置版本,必须作用于左值运算对象。
后置版本需要将原始值存储下来以便于返回这个未修改的内容,会消耗更多空间;如果我们不需要修改前的值,那么推荐使用前置版本。。
int i = 0;
int j;
j = ++i; // j = 1, i = 1 先加完再赋值
j = i++; // j = 1, i = 2 先输出完再++
在一条语句中混用解引用和递增运算符,要记住递增运算符的优先级高于解引用运算符。
cout << *pbeg++ << endl;
// *pbeg++ 等价于 *(pbeg++)
由于后置递增运算符是先输出再++,那么程序会先返回pbeg的初始值的副本作为其求值结果,然后pbeg++把pbeg的值加1,此时解引用运算符的运算对象是pbeg未增加之前的值。
同一表达式多次修改一个对象时,应注意运算对象的求值顺序。
6.成员访问运算符
点运算符和箭头运算符都可用于访问成员,点运算符用来获取类对象的一个成员。
string s1 = "hello", *p = &s1;
auto n = s1.size();
n = (*P).size(); //对指针进行解引用,得到类对象(解引用运算符优先级低于点运算符)
n = p->size(); //等价于 (*p).size()
箭头运算符作用于一个指针类型的运算对象,结果是一个左值;点运算符结果由成员所属的对象决定。
7.条件运算符
条件运算符允许我们把简单的if-else逻辑嵌入到单个表达式当中。
cond ? expr1 : expr2;
//cond是判断条件的表达式,expr1是当条件为真是返回该值,expr2则相反。
允许在条件运算符的内部嵌套另外一个条件运算符,条件表达式可以作为另一个条件运算符的cond或expr。
arr = (grade > 90) ? "high":(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的值时true还是false产生对应的字面值
8.位运算符
位运算符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合,提供检查和设置二进制位的功能。
运算符 | 功能 | 用法 |
~ | 位求反(1变0,0变1) | ~expr |
<< | 左移 | expr1 << expr2 |
>> | 右移 | expr1 >> expr2 |
& | 位与(同1为1) | expr1 & expr2 |
^ | 位异或(有一个1为1) | expr1 ^ expr2 |
| | 位或(有1为1) | expr1 | expr2 |
一般来说,如果运算对象是“小整型”,则它的值会被自动提升成较大的整数类型。
关于符号位如何处理没有明确的规定,建议使用位运算符处理无符号类型。
移位运算符的内置含义是对其运算对象执行机遇二进制位的移动操作,移出边界之外的位就被舍弃掉。
位求反运算符逐位地将1置为0,0置为1,生成一个新值。
char类型地运算对象先提升成int类型,提升时运算对象原来的位保持不变,往高位添加0即可。
移位运算符地优先级比算术运算符低,但比关系运算符、赋值运算符和条件运算符的优先级高。
9.sizeof运算符
sizeof运算符返回一条表达式或一个类型名字所占的字节数;sizeof运算符满足右结合律,返回size_t类型的常量表达式。
sizeof不会实际计算其运算对象的值,所以即使运算对象是一个无效的指针,也不会有什么影响。
对引用类型执行sizeof运算得到被引用对象所占空间的大小。
对解引用指针执行sizeof运算得到指针指向的对象所占空间的大小,指针无需有效。
对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行依次sizeof运算并将所得结果求和。(sizeof运算不会把数组转换成指针来处理)
sizeof运算的返回值是一个常量表达式,所以我们可以用sizeof的结果声明数组的维度。
10.逗号运算符
逗号运算符含有两个运算对象,按照从左向右的顺序依次求值。
对于逗号运算符来说,先对左侧的表达式求值,然后将结果丢弃掉。逗号运算符真正的结果是右侧表达式的值。
11.类型转换
在C++中,如果两种类型可以相互转换,那么它们之间有关联。
C++语言会根据类型转换规则将运算对象的类型统一,再进行操作,上述的类型转换是自动执行的,该过程称为隐式转换。
11.1 算术转换
算术转换的含义是把一种算术类型转换成另一种算术类型。
在算术转换过程中,运算符的运算对象将转换成最宽的类型。
整数提升负责把小整数类型转换成较大的整数类型;例如bool、char、short、unsigned char、unsigned short等类型都会被提升成int类型。
整数提升的前提是转换后的类型要能容纳原类型所有可能的值。
如果一个运算对象是无符号类型,另一个运算对象是带符号类型,而且其中的无符号类型不小于带符号类型,那么带符号的运算对象会转换成无符号的。
若带符号类型大于无符号类型,且带符号类型能容纳无符号类型的所有值,则无符号类型的运算对象转换成带符号类型。
11.2 其他隐式类型转换
数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针。
当数组被用作decltype关键字的参数,或者作为取地址符、sizeof及typeid等运算符的运算对象时,上述转换不会发生。
指针的转换:常量整数值0或者字面值nullptr能转换成任意指针类型;指向任意非常量的指针都能转换成void*;指向任意对象的指针能转换成const void*;
转换成布尔类型:算术类型或指针类型转换成布尔类型,如果指针或算术类型的值为0,则转换结果是false;反之则相反。
非常量转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,引用也适用该规则。
int i;
const int &j = i; //非常量转换成const int 的引用
const int *p = &i; //非常量的地址转换成const的地址
11.3 显式转换
有时不得不使用强制类型转换,但这种方法本质上是非常危险的。
命名的强制类型转换的格式: cast-name<type>(expression)
type是想转换成的目标类型;expression是要转换的对象。
cast-name 是static_cast、dynamic_cast、const_cast、reinterpret_cast 其中的一种。
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast.
当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用。
double slope = static_cast<double>(j)/i;
//将j转换成double类型,并除以i,赋给slope
static_cast能够执行编译器无法自动执行的类型转换,例如将void指针强制转换回原来的类型。
强制转换的结果必须与原始的地址值相等,因此应该确保转换后所得的类型与指针所指类型一致。
void* p = &d;
double *dp = static_cast<double*>(p);
//将void*转换成double指针类型
const_cast只能改变运算对象的底层const,改变表达式的常量属性,不改变表达式的类型。
const char *p;
const_cast<string>(p); //错误:const_cast 只改变常量属性,不改变类型
如果对象本身不是一个常量,使用强制类型转换获得修改权限是合法行为,但是对象是一个常量,就会产生未定义的结果。
const char *pc;
char *P = const_cast<char*>(pc);
//pc的底层const被去除,但是pc所指的对象没有改变,因此通过p去修改值是未定义的行为
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。
int *P;
char *pc = reinterpret_cast<char*>(p);
//牢记pc所指的真实对象是一个int而非字符。
在正常情况下,强制类型转换干扰了正常的类型检查,因此我们强烈建议避免使用类型转换,尤其是reinterpret_cast。