3. 运算符与表达式:算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、运算符优先级

运算符和表达式,说白了就是C++这门语言的「动作指令」。你定义变量是准备好了材料,而运算符就是告诉计算机怎么去加工这些材料。我刚开始学的时候,觉得这东西太简单了,不就是加减乘除嘛。结果后来在项目里踩了不少坑,才发现这里面的门道比想象中多。

这一章,我会把C++里最常用的几类运算符掰开揉碎了讲清楚。你跟着我走一遍,保证以后写代码心里有底。

3.1 算术运算符:最基础的数学工具

算术运算符就是我们从小用到大的加减乘除,但在C++里,它们有一些小脾气。

运算符 名称 示例 结果
+ 加法 5 + 3 8
- 减法 5 - 3 2
* 乘法 5 * 3 15
/ 除法 5 / 3 1(整数除法)
% 取模(求余) 5 % 3 2

这里有个坑,我必须要提醒你。整数除法会直接截断小数部分,不会四舍五入。比如 5 / 3 的结果是1,而不是1.666。我曾经在计算游戏里的伤害数值时,忘了这个细节,结果算出来的伤害总是少一截,排查了半天才发现是整数除法搞的鬼。

注意: 取模运算符 % 只能用于整数类型。如果你对浮点数取模,编译器会报错。另外,除数和取模的右操作数都不能为0,否则程序会崩溃。

自增自减运算符 ++-- 也是算术运算符里的常客。它们分前置和后置两种写法:

int a = 5;
int b = a++;  // b = 5, a = 6(先赋值,后自增)
int c = ++a;  // c = 7, a = 7(先自增,后赋值)

我个人习惯在单独一行使用自增自减,比如 i++;,这样不会搞混。但在复杂表达式里混用,很容易写出让人看不懂的代码。嗯,这里要注意,面试题特别喜欢考这个。

3.2 关系运算符:比较大小与判断相等

关系运算符用来比较两个值之间的关系,结果是一个布尔值:true(真)或 false(假)。

运算符 名称 示例 结果
== 等于 5 == 3 false
!= 不等于 5 != 3 true
> 大于 5 > 3 true
< 小于 5 < 3 false
>= 大于等于 5 >= 3 true
<= 小于等于 5 <= 3 false

新手最容易犯的错误,是把 == 写成 =。你想想看,一个是判断相等,一个是赋值,完全是两码事。我曾经在代码审查时看到有人写了 if (x = 5),这个表达式永远为真,因为赋值操作返回的是被赋的值(5),而5是非零值,被当作 true 处理了。这种bug极难排查,因为编译器不会报错。

小技巧: 我习惯把常量写在左边,比如 if (5 == x)。这样万一写成 if (5 = x),编译器会直接报错,因为不能给常量赋值。这个习惯帮我避免了好几次低级错误。

3.3 逻辑运算符:组合多个条件

逻辑运算符用来组合多个布尔表达式,实现更复杂的判断逻辑。

运算符 名称 示例 结果
&& 逻辑与(AND) (5 > 3) && (2 < 4) true
|| 逻辑或(OR) (5 > 3) || (2 > 4) true
! 逻辑非(NOT) !(5 > 3) false

这里有个非常重要的特性叫「短路求值」。对于 &&,如果左边是 false,右边就不会执行了。对于 ||,如果左边是 true,右边也不会执行。

为什么会这样?因为结果已经确定了,没必要再浪费CPU时间去算右边。我曾经利用这个特性写过一段代码:

if (ptr != nullptr && ptr->value > 10) {
    // 安全地使用指针
}

如果 ptr 是空指针,左边为 false,右边根本不会执行,也就不会发生空指针解引用的崩溃。这就是短路求值的妙用。

3.4 位运算符:直接操作二进制位

位运算符直接操作整数的二进制位,在底层开发、嵌入式系统、性能优化中非常有用。说实话,做应用层开发可能用得不多,但理解了它,你对计算机的理解会上一个台阶。

运算符 名称 示例 二进制解释
& 按位与 5 & 3 0101 & 0011 = 0001 (1)
| 按位或 5 | 3 0101 | 0011 = 0111 (7)
^ 按位异或 5 ^ 3 0101 ^ 0011 = 0110 (6)
~ 按位取反 ~5 ~0101 = 1010 (-6,补码表示)
<< 左移 5 << 1 0101 << 1 = 1010 (10)
>> 右移 5 >> 1 0101 >> 1 = 0010 (2)

左移一位相当于乘以2,右移一位相当于除以2(整数除法)。这个特性在性能敏感的场景下特别有用,因为位运算比乘除法快得多。我在做图像处理项目时,经常用移位运算来代替乘除,能省下不少CPU周期。

经典用法: 用位运算实现权限管理。比如用一个整数的不同位表示不同权限:读权限(1 << 0)、写权限(1 << 1)、执行权限(1 << 2)。通过按位与、按位或来快速判断和设置权限。

3.5 赋值运算符:给变量喂数据

最基本的赋值运算符就是 =,但C++还提供了一组复合赋值运算符,把运算和赋值合二为一。

运算符 示例 等价于
+= a += 5 a = a + 5
-= a -= 5 a = a - 5
*= a *= 5 a = a * 5
/= a /= 5 a = a / 5
%= a %= 5 a = a % 5
&= a &= 5 a = a & 5
|= a |= 5 a = a | 5
<<= a <<= 1 a = a << 1
>>= a >>= 1 a = a >> 1

我个人非常推荐使用复合赋值运算符。为什么?因为代码更简洁,而且变量只写一次,减少了出错的可能。比如 a += 5;a = a + 5; 更不容易写错。

3.6 运算符优先级:谁先谁后?

当一个表达式里有多个运算符时,谁先计算?这就是优先级和结合性要解决的问题。我见过太多因为优先级搞错而引发的bug了。

下面这张表列出了常用运算符的优先级,数字越小优先级越高:

优先级 运算符 结合性
1(最高) ::(作用域解析) 左到右
2 () [] -> . ++(后缀) --(后缀) 左到右
3 ++(前缀) --(前缀) +(正号) -(负号) ! ~ &(取地址) *(解引用) 右到左
4 ->* .* 左到右
5 * / % 左到右
6 + - 左到右
7 << >> 左到右
8 < <= > >= 左到右
9 == != 左到右
10 &(按位与) 左到右
11 ^(按位异或) 左到右
12 |(按位或) 左到右
13 && 左到右
14 || 左到右
15 ?:(三目运算符) 右到左
16 = += -= *= /= %= &= |= ^= <<= >>= 右到左
17 ,(逗号运算符) 左到右

你不需要死记硬背这张表。我的建议是:拿不准的地方就加括号。括号的优先级最高,而且能让代码的意图一目了然。

比如:

// 这样写,别人可能看不懂
int result = a + b * c >> 1 & 0xFF;

// 加括号后,意图清晰
int result = ((a + (b * c)) >> 1) & 0xFF;

我曾经在维护一个老项目时,看到一段没有括号的复杂表达式,花了整整一个下午才理清它的计算顺序。从那以后,我给自己定了个规矩:只要表达式里用了超过两种运算符,就加括号。

3.7 知识体系总览

下面这张图把本章涉及的运算符类型和它们之间的关系梳理清楚了,你可以把它当作一个快速参考。

C++ 运算符 算术运算符 + - * / % ++ -- 关系运算符 == != < > <= >= 逻辑运算符 && || ! 位运算符 & | ^ ~ << >> 赋值运算符 = += -= *= /= %= 运算符优先级 括号 > 单目 > 双目 核心原则 拿不准优先级?加括号! 代码是写给人看的,可读性比炫技更重要
我的经验: 学运算符最好的方法就是多写多练。你可以自己写几个小程序,把各种运算符组合起来,然后用 cout 打印结果,看看是不是和你预想的一致。遇到不一致的地方,就去查优先级表,这样记得最牢。

好了,运算符和表达式的内容就讲到这里。这些东西看着简单,但用好了能写出非常优雅高效的代码。记住我反复强调的那句话:拿不准就加括号,别嫌麻烦。


公众号:蓝海资料掘金营,微信deep3321