算术运算符

运算符 功能 用法
+ 一元正号 + expr
- 一元负号 - expr
* 乘法 expr * expr
/ 除法 expr / expr
% 求余 expr % expr
+ 加法 expr + expr
- 减法 expr - expr

1. 优先级和结合律

一元运算符的优先级最高,然后是乘法和除法,优先级最低的是加法和减法。上面所有的运算符都满足左结合律,意味着优先级相同时满足从左到右的顺序进行组合。

2. 溢出

当计算结果超出该类型所能表示的范围时可能产生溢出,比如最大的short型数值为32767,这时候+1可能输出-32768(这是因为符号位从0变为1,从而变成负值)。当然在别的系统程序的行为可能不同甚至崩溃。

3. 除法与负号

Tips:C语言的早期版本允许结果为负值的商向上或向下取整,C11新标准规定商一律向0取整(即直接切除小数部分)。如果两个运算对象的符号相同则商为正,否则商为负。

  1. 21 / 6; // 3
  2. -21 / -6; // 3
  3. 21 / -6; // -3
  4. -21 / 6; // -3

4. 取余与负号

Tips:如果m%n不等于0,那么运算结果的符号和m相同。

  1. 21 % 6; // 3
  2. 21 % 7; // 0
  3. -21 % -8; // -5
  4. 21 % -5; // 1

逻辑运算符

结合律 运算符 功能 用法
! 逻辑非 !expr
< 小于 expr < expr
<= 小于等于 expr <= expr
> 大于 expr > expr
>= 大于等于 expr >= expr
== 相等 expr == expr
!= 不等 expr != expr
&& 逻辑与 expr && expr
|| 逻辑或 expr || expr

1. 逻辑与和逻辑或的短路求值

逻辑与&&和逻辑或||都是先求左侧对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果才会计算右侧运算对象的值,这种策略被称为短路求值。基于短路求值的特点,我们可以通过左侧运算对象来确保右侧运算对象求值的正确性和安全性:

  1. // 只能左侧运算对象为真则右侧运算对象才安全
  2. index != s.size() && !isspace(s[index])

2. 不要连写关系运算符

因为关系运算符的求值结果是布尔值,所以将几个关系运算符连写在一起会产生意想不到的结果:

  1. // 错误写法: 用i < j的布尔值结果与k比较
  2. if (i < j < k)
  3. // 正确写法: 使用&&或者||连接
  4. if (i < j && j < k)

赋值运算符

1. 运算对象与返回结果

赋值运算符的左侧运算对象必须是一个可修改的左值,返回的结果是它的左侧运算对象(仍然是左值)。

Tips:注意赋值不等于初始化,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦去,并用一个新值替代它。

  1. int i = 0, j = 0, k = 0; // 初始化而非赋值
  2. const int ci = i; // 初始化而非赋值, 因此左侧运算对象可以是常量
  3. 1024 = k; // 错误: 字面值是右值
  4. i + j = k; // 错误: 算数表达式是右值
  5. ci = k; // 错误: ci是常量, 是不可修改的左值

2. 初始化列表赋值

C++11新标准允许使用初始化列表赋值:

  1. // 1) 编译器warning提示窄化转换: narrowing conversion of ‘3.1499999999999999e+0’ from ‘double’ to ‘int’ inside { }
  2. int k;
  3. k = {3.14};
  4. // 2) 无论左侧运算对象类型是什么, 初始值列表都可以为空, 此时编译器创造一个值初始化的临时量并将其赋给左侧运算对象
  5. int i = {}; // i值为0

3. 赋值运算符满足右结合律

在下面的例子中,先执行j = 0,返回左侧运算对象,再执行i = j,因此执行结束后两个变量都被赋值为0。

  1. int i, j;
  2. i = j = 0;

4. 赋值运算符优先级较低

由于赋值运算符的优先级低于关系运算符的优先级,因此在条件语句中,赋值部分通常应该加上括号:

  1. int i;
  2. // 如果i = get_value()左右两侧不加括号的话, 含义就截然不同
  3. while ((i = get_value()) != 10 ) {
  4. // do something...
  5. }

递增和递减运算符

编码规范:对于迭代器和其他模板对象使用前缀形式的自增(自减)运算符。不考虑返回值的情况下,前置自增++i通常比后置自增i++效率高,因为后置自增需要对表达式的值i进行一次拷贝,如果i是迭代器或其他非数值类型,拷贝的代价是比较大的。

1. 前置版本和后置版本

前置版本会将运算对象加1(或减1),然后将改变后的对象作为求值结果。后置版本也会将运算对象加1(或减1),但是求值结果是运算对象改变之前值的副本。这两种运算符必须作用于左侧运算对象,其中前置版本将对象本身作为左值返回,后置版本将对象原始值的副本的作为右值返回。

Tips:除非必须,否则不用递增递减运算符的后置版本。前置版本的递增运算将值加1之后直接返回该运算对象,但是后置版本需要将原始值存储下来以便于返回这个未修改的内容,如果我们不需要修改前的值的话就是一种性能上的浪费。对于整数和指针类型而言,编译器可能对这种额外的工作进行优化,但是如果是对于相对复杂的迭代器类型,这种额外的工作就消耗巨大了。建议养成使用前置版本习惯,这样不仅不需要担心性能问题,而且不会引入非预期的错误。

  1. int i = 0, j;
  2. j = ++i; // j = 1, i = 1: 前置版本得到递增之后的值
  3. j = i++; // j = 1, i = 2:后置版本得到递增之前的值

2. 后置版本的可能使用场景

后置版本最常用的场景就是在一条语句中混用解引用和递增运算符的后置版本:

  1. auto pbeg = v.begin();
  2. // 输出元素直到遇到第一个负值
  3. while (pbeg != v.end() && *pbeg >= 0)
  4. cout << *pebg++ << endl; // 输出当前值并将pbeg向前移动一个元素

*pbeg++这种写法非常普遍,会先把pbeg的值加1,然后返回pbeg的初始值的副本作为其求解结果,此时解引用的运算对象是pbeg未增加之前的值。

成员访问运算符

点运算符和箭头运算符都可用于访问成员,ptr->mem等价于(*ptr).mem。需要注意的是解引用运算符优先级低于点运算符,所以必须加上括号。

条件运算符

条件运算符满足右结合律,意味着运算对象一般按照从右往左的顺序组合,因此我们使用嵌套条件运算符:

  1. finalgrade = (grade > 90) ? "high pass"
  2. : (grade < 60) ? "fail" : "pass"

注意条件运算符的优先级非常低,所以一条长表达式中嵌套了条件运算子表达式时,通常需要在两端加上括号:

  1. cout << ((grade < 60) ? "fail" : "pass"); // 输出pass或者fail

位运算符(左结合律)

Tips:如果运算对象是带符号的且它的值为负,那么位运算符如何处理运算对象的“符号位”依赖于机器,而且此时的左移操作可能会改变符号位的值,因此是一种未定义的行为。关于符号位如何处理没有明确的规定,所以强烈建议仅将位运算符用于处理无符号类型。

运算符 功能 用法
~ 位求反 ~ expr
<< 左移 expr1 << expr2
>> 右移 expr1 >> expr2
& 位与 expr & expr
^ 位异或 expr ^ expr
| 位或 expr | expr

1. 移位运算符

左移运算符<<在右侧插入值为0的二进制位,右移运算符>>的行为则依赖其左侧运算对象的类型,如果该运算对象是无符号类型,在左侧插入值为0的二进制位。

  1. // 假定char占8位, int占32位
  2. // 0233是八进制的字面值
  3. // 二进制: 100111011
  4. unsigned char bits = 0233;
  5. // bits被提升为int类型, 然后向左移动8位
  6. // 二进制: 00000000 00000000 10011011 00000000
  7. bits << 8
  8. // 向左移动31位, 左边超出边界的位丢弃掉了
  9. // 二进制: 10000000 00000000 00000000 00000000
  10. bits << 31
  11. // 向右移动3位, 右边超出边界的位丢弃掉了
  12. // 二进制: 00000000 00000000 00000000 00010011
  13. bits >> 3

2. 位求反运算符

对于char类型的运算对象首先提升为int类型,提升时运算对象原来的位保持不变,往高位添加0即可。接下来将提升后的值逐位求反。

  1. // 假定char占8位, int占32位
  2. // 0227是八进制的字面值
  3. // 二进制: 10010111
  4. unsigned char bits = 0227;
  5. // char被提升为int型, 往高位添加0
  6. // 二进制: 00000000 00000000 00000000 10010111
  7. // 逐位求反
  8. // 二进制: 11111111 11111111 11111111 01101000
  9. ~bits

3. 位与、位或和位异或

  • 位与:两个都是1则返回1,否则为0
  • 位或:两个至少有一个为1则返回1,否则为0
  • 位异或:两个有且只有一个为1则返回1

sizeof运算符

sizeof运算符返回一条表达式或者一个类型名字所占的字节数,所得的值是一个size_t类型(一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小)。当传入一条表达式时,sizeof运算符并不实际计算其运算对象的值。

1. 不同类型的sizeof运算结果

  • char或者类型为char的表达式执行sizeof,返回1
  • 对引用类型执行sizeof运算得到被引用对象所占空间的大小
  • 对指针执行sizeof得到指针本身所占空间的大小
  • 对解引用指针执行sizeof运算得到指针你指向的对象所占空间的大小,指针本身不需要有效
  • 对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次sizeof运算并将所得结果求和
  • string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间

2. sizeof返回常量表达式

因为sizeof的返回值是一个常量表达式,因此我们可以用sizeof的结果声明数组的维度。

3. sizeof中解引用指针

由于sizeof满足右结合律并且与*运算符的优先级一样,因此sizeof *p等价于sizeof (*p)。另外由于sizeof不会实际求运算对象的值,所以在sizeof的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正地使用。

逗号运算符

逗号运算符含有两个运算对象,首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符真正的结果是右侧表达式的值,如果右侧运算对象是左值,那么最终的求值结果也是左值。

逗号运算符通常被用在for循环中:

  1. vector<int>::size_type cnt = ivec.size();
  2. // 把从size到1的值依次赋给ivec的元素
  3. for (vector<int>::size_type ix = 0; ix != ivec.size(); ++ix, --cnt) {
  4. ivec[ix] = cnt;
  5. }