基础
基本概念
一元运算符作用于一个运算对象,二元运算符作用于两个运算对象,三元对运算对象没有限制。
C++语言定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。当运算符作用于类类型的运算对象时,用户可以自行定义其含义。因为这种自定义的过程事实上是为已存在的运算符赋予了另外一层含义,所以称之为**重载运算符( overloadedoperator)**。IO库的>>和<<运算符以及string对象、vector对象和迭代器使用的运算
左值和右值
- 当一个对象被用作右值的时候,用的是对象的值(内容);
- 当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
1 | int *p = NULL; |
求值顺序
1 | int i = 0; |
这里可能先++i再求值,结果为1 1,也可能求值,再++i,结果为0 1,所以避免出现此种代码。
只有&&、||、?:、,四种运算符有明确的求值顺序。
形如f() + g() * h() + j()
,因为求值的顺序与优先级和结合律无关,所以如果函数内部改变了一些关联的参数,则无法预计
算数运算符
- 一元运算符大于二元运算符
- %不允许运算对象为浮点类型
- C++11规定无论正负一律向0取整
逻辑和关系运算符
运算符 | 含义 |
---|---|
== | 等于 |
< | 小于 |
!= | 不等于 |
>= | 大于等于 |
> | 大于 |
<= | 小于等于 |
运算符 | 含义 |
---|---|
& | 与 |
| | 或 |
^ | 异或 |
|| | 短路或 |
&& | 短路与 |
! | 非 |
逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略称为短路求值(short-circuit evaluation)。
逻辑非运算符
在对象前加!表示取反,如!s.empty()
。
关系运算符
1 | //哎哟!这个条件居然拿i<j的布尔值结果和k比较! |
赋值运算符
赋值运算的结果是它的左侧运算对象,并且是一个左值。相应的,结果的类型就是左侧运算对象的类型。如果赋值运算符的左右两个运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型。
C++11允许使用花括号赋值
如vector<int> v = {0,5,3,4,8};
赋值运算满足右结合律
1 | //赋值运算符满足右结合律,这一点与其他二元运算符不太一样: |
1 | //这是一种形式烦琐、容易出错的写法 |
递增和递减
++和–是加一减一一种简介的书写,若非必须,建议养成写前置版本的习惯。因为后置版本会造成性能浪费。
1 | auto pbeg = v.begin (); |
*pbeg++
等价于*(pbeg++)
1 | //该循环的行为是未定义的! |
如果一条子表达式改变了某个运算对象的值,另一条子表达式又要使用该值的话,运算对象的求值顺序就很关键了。因为递增运算符和递减运算符会改变运算对象的值,所以要提防在复合表达式中错用这两个运算符。
成员访问运算符
ptr->mem等价于(*ptr).mem;
因为解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号。如果没加括号,代码的含义就大不相同了。
条件运算符
cond ? expr1 : expr2;
此运算符只对expr1和expr2中的一个求值。
嵌套运算符
1 | finalgrade = (grade > 90) ? "high pass" |
条件运算符是满足右结合性质,意味着从右向左顺序组合,但嵌套最好不要超过两层。
解释一下左(右)结合律,举个例子,假设是一个运算符,又有表达式abc,如果是左结合的,那么该表达式被解析为(ab)c,如果是右结合的,那么该表达式将被解析为a(b~c)。比如上表中三目运算符?:是从右向左结合的
优先级
该运算符优先级非常低,所以长表达式嵌套时最好加上括号。
1 | cout << ( (grade < 60) ? "fail" : "pass" ); // 输出pass或者fail |
位运算符
1 | & 位与 |
关于符号位如何处理没有明确的规定,所以强烈建议仅将位运算符用于处理无符号类型,小整型在使用位运算符时会自动提升至大整型。
移位运算符
左移运算符(<<)在右侧插入值为0的二进制位。右移运算符(>>)的行为则依赖于其左侧运算对象的类型:如果该运算对象是无符号类型,在左侧插入值为0的二进制位;如果该运算对象是带符号类型,在左侧插入符号位的副本或值为0的二进制位,如何选择要视具体环境而定。
位求反运算符
位求反运算符(~)将运算对象逐位求反后生成一个新值,将1置为0、将0置为1。char类型的运算对象首先提升成int类型,提升时运算对象原来的位保持不变,往高位添加0即可。因此在本例中,首先将bits提升成int类型,增加24个高位0,随后将提升后的值逐位求反。
使用位运算符
1 | unsigned long quiz1 =0; //我们把这个值当成是位的集合来使用 |
设quiz1当成位的集合,每一个位标识该位学生是否及格,则使用该代码可以表示第27位学生及格了。
若第27位学生没有及格使用一个第27位是0,其他位都是1的数,使用&位于运算即可。
1 | quizl &= ^(1UL << 27); |
最后代码可判断第27位是否通过检测。
位移运算符(IO预算符)满足左结合律
1 | cout<< "hi" << " there" <<endl; |
移位运算符的优先级不高不低,介于中间:比算术运算符的优先级低,但比关系运算符、赋值运算符和条件运算符的优先级高。因此在一次使用多个运算符时,有必要在适当的地方加上括号使其满足我们的要求。
sizeof运算符
sizeof运算符返回一条表达式或一个类型名字所占的字节数。sizeof运算符满足右结合律,其所得的值是一个size_t类型的常量表达式。运算符的运算对象有两种形式:sizeof (type);sizeof expr;
1 | Sales_data data, *p; |
这些例子中最有趣的一个是sizeof *p
。首先,因为 sizeof满足右结合律并且与*
运算符的优先级一样,所以表达式按照从右向左的顺序组合。也就是说,它等价于sizeof (*p)
。其次,因为sizeof不会实际求运算对象的值,所以即使p是一个无效(即未初始化)的指针也不会有什么影响。在sizeof的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用。sizeof不需要真的解引用指针也能知道它所指对象的类型。
- 对char或者类型为char的表达式执行sizeof运算,结果得1。
- 对引用类型执行sizeof运算得到被引用对象所占空间的大小。·对指针执行sizeof运算得到指针本身所占空间的大小。
- 对解引用指针执行sizeof运算得到指针指向的对象所占空间的大小,指针不需有效。
- 对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次sizeof运算并将所得结果求和。注意,sizeof运算不会把数组转换成指针来处理。
- 对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。
逗号运算符
对于逗号运算符来说,首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符真正的结果是右侧表达式的值。如果右侧运算对象是左值,那么最终的求值结果也是左值。
类型转换
如果两种类型可以**相互转换( conversion)**,那么它们就是关联的。
举个例子,考虑下面这条表达式,它的目的是将ival初始化为6:
int ival = 3.541 + 3;//编译器可能会警告该运算损失了精度
代码中,首先为了不损失精度,3转为double类型,与3.541相加,得到double类型结果,但无法复制给int类型,最后再次转会int进行初始化。
上述的类型转换是自动执行的,无须程序员的介入,有时甚至不需要程序员了解。因此,它们被称作隐式转换(implicit conversion)。
何时发生隐式转换:
- 在大多数表达式中,比int类型小的整型值首先提升为较大的整数类型。
- 在条件中,非布尔值转换成布尔类型。
初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。 - 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
- 如第6章将**要介绍的,函数调用时也会发生类型转换。
算术转换
其中运算符的运算对象将转换成最宽的类型。例如,如果一个运算对象的类型是 longdouble,那么不论另外一个运算对象的类型是什么都会转换成long double。还有一种更普遍的情况,当表达式中既有浮点类型也有整数类型时,整数值将转换成相应的浮点类型。
整型提升
对于bool、char、signed char、unsigned char、short和unsigned short等类型来说,只要它们所有可能的值都能存在 int 里,它们就会提升成int 类型;否则,提升成unsigned int类型。就如我们所熟知的,布尔值false提升成0、true提升成1。
较大的char类型( wchar_t.char16_t、char32_t)提升成int.unsigned int、long、unsigned long、long long和 unsigned long long中最小的一种类型,前提是转换后的类型要能容纳原类型所有可能的值。
无符号与带符号转换(*****)
若类型分别位无符号和带符号,则
- 无符号>=带符号,带符号转为无符号。
- 相反,如果无符号类型所有值都能存在带符号类型中,则无符号转带符号,如不行带符号转无符号
其他隐式类型转换
数组转指针
在大多数组表达式中,数组自动的转为指针,如:int ia[10]; int* ip = ia;
但是当数组被用作 decltype关键字的参数,或者作为取地址符( &)、sizeof 及typeid等运算符的运算对象时,上述转换不会发生。同样的,如果用一个引用来初始化数组int (&arrRef)[10] = arr
;,上述转换也不会发生。
指针的转换
指针的转换:C++还规定了几种其他的指针转换方式,包括常量整数值О或者字面值nullptr能转换成任意指针类型;指向任意非常量的指针能转换成void*
;指向任意对象的指针能转换成const void*
。15.2.2节(第530页)将要介绍,在有继承关系的类型间还有另外一种指针转换的方式。
转换布尔类型
1 | char *cp = get_string (); |
转换成常量
1 | int i; |
类类型定义的转换:
类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。在7.5.4节(第263页)中我们将看到一个例子,如果同时提出多个转换请求,这些请求将被拒绝。
1 | string s, t = "a value" ; //字符串字面值转换成string类型 |
显示转换
1 | int i,j; |
就要使用某种方法将i和/或j显式地转换成double,这种方法称作**强制类型转换(cast)**。
命名的强制类型转换
cast-name<type>(expression);
其中,type是转换的目标类型而expression是要转换的值。如果 type是引用类型,则结果是左值。cast-name是static_cast . dynamic_cast . const_cast和reinterpret_cast中的一种。dynamic_cast支持运行时类型识别。
static_cast
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。例如,通过将一个运算对象强制转换成double类型就能使表达式执行浮点数除法:
1 | //进行强制类型转换以便执行浮点数除法 |
1 | void* p = &d; //正确:任何非常量对象的地址都能存入void* |
const_cast
const_cast只能改变运算对象的底层const,称其为去掉const性质:
1 | const char *pc; |
它能且只能改变常量属性,如对象是常量,再使用它执行写操作会产生未定义后果。
1 | const char *cp; |
reinterpret_cast(*****)
通常为运算对象的位模式提供较低层次上的重新解释。
旧式强制类型转换
1 | type (expr ) ; //函数形式的强制类型转换 |
根据所涉及的类型不同,旧式的强制类型转换分别具有与const_cast、static_cast或reinterpret_cast相似的行为。当我们在某处执行旧式的强制类型转换时,如果换成const_cast和static_cast也合法,则其行为与对应的命名转换一致。如果替换后不合法,则旧式强制类型转换执行与reinterpret_cast类似的功能,与reinterpret_cast效果一样。
小结:
C++语言提供了一套丰富的运算符,并定义了这些运算符作用于内置类型的运算对象时所执行的操作。此外,C++语言还支持运算符重载的机制,允许我们自己定义运算符作用于类类型时的含义。第14章将介绍如何定义作用于用户类型的运算符。
对于含有超过一个运算符的表达式,要想理解其含义关键要理解优先级、结合律和求值顺序。每个运算符都有其对应的优先级和结合律,优先级规定了复合表达式中运算符组合的方式,结合律则说明当运算符的优先级一样时应该如何组合。
大多数运算符并不明确规定运算对象的求值顺序:编译器有权自由选择先对左侧运算对象求值还是先对右侧运算对象求值。一般来说,运算对象的求值顺序对表达式的最终结果没有影响。但是,如果两个运算对象指向同一个对象而且其中一个改变了对象的值,就会导致程序出现不易发现的严重缺陷。
最后一点,运算对象经常从原始类型自动转换成某种关联的类型。例如,表达式中的小整型会自动提升成大整型。不论内置类型还是类类型都涉及类型转换的问题。如果需要,我们还可以显式地进行强制类型转换。