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周期。
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 知识体系总览
下面这张图把本章涉及的运算符类型和它们之间的关系梳理清楚了,你可以把它当作一个快速参考。
cout 打印结果,看看是不是和你预想的一致。遇到不一致的地方,就去查优先级表,这样记得最牢。
好了,运算符和表达式的内容就讲到这里。这些东西看着简单,但用好了能写出非常优雅高效的代码。记住我反复强调的那句话:拿不准就加括号,别嫌麻烦。
公众号:蓝海资料掘金营,微信deep3321