当一条表达式里同时出现多个运算符号时,比如 2 + 3 * 4,程序到底会从左往右算出 20,还是先乘后加算出 14 呢?
实际结果是 14。这和我们平时数学里的四则运算是一样的:乘号(*)的级别比加号(+)高,所以要先算乘法、后算加法。在 C++ 中,这种决定各个运算符谁先算、谁后算的规则,就叫做 “运算符优先级”。
常见运算符优先级
对于前面我们已经学过的常用运算符,它们的优先级从高到低如下表所示。
| 优先级 | 运算符 | 说明 | 结合方向 |
|---|---|---|---|
| 1(最高) | () | 小括号分组 | 从左到右 |
| 2 | !、++、--、+、- | 逻辑非、自增自减、正负号 | 从右到左 |
| 3 | *、/、% | 乘、除、求余 | 从左到右 |
| 4 | +、- | 加、减 | 从左到右 |
| 5 | >、>=、<、<= | 大于、大于等于、小于、小于等于 | 从左到右 |
| 6 | ==、!= | 等于、不等于 | 从左到右 |
| 7 | && | 逻辑与 | 从左到右 |
| 8 | || | 逻辑或 | 从左到右 |
| 9(最低) | =、+=、-=、*=、/=、%= | 赋值与复合赋值 | 从右到左 |
需要注意的是,表中第 2 级的 + 和 - 指的是写在单个数字或变量前面的 “正负号”,比如 -5、-a,只作用于一个操作数,而不是 a - b 里的减法。
C++ 的运算符很多,这张表只是方便小伙伴们平时查询,并不是叫大家死记硬背。在实际写代码时,我们只需要记住以下 4 条基本规律就够用了:
- 小括号最高:小括号
()的优先级最高,想让哪一部分先算,直接用小括号括起来即可。 - 算术高于比较:乘、除、求余的优先级相同,且都高于加和减。所有算术运算都会在比较大小之前先算完。
- 比较高于逻辑:先通过
>、==等比较出真假,再用逻辑运算符组合。在 3 个逻辑运算符中,非(!)高于与(&&),与(&&)高于或(||)。 - 赋值运算最低:赋值运算符(
=、+=等)的优先级最低,总是等号右边的表达式全部算完之后,最后才把结果赋给左边的变量。
示例 1:算术运算与小括号
#include <iostream>
int main() {
int result1 = 2 + 3 * 4;
int result2 = (2 + 3) * 4;
std::cout << result1 << '\n';
std::cout << result2 << '\n';
return 0;
}运行结果如下。
14
20分析:
计算 2 + 3 * 4 时,* 的优先级高于 +,程序会先算 3 * 4 得到 12,再算 2 + 12 得到 14,最后赋给 result1。而在 (2 + 3) * 4 中,程序会优先计算小括号内的 2 + 3 得到 5,再乘以 4 得到 20。
如果一条表达式里同时混用了比较和逻辑运算符,计算顺序又是怎样的呢?下面我们再来看一个例子。
示例 2:比较与逻辑运算的优先级
#include <iostream>
int main() {
int age = 16;
bool hasTicket = false;
bool isStaff = true;
bool result1 = age >= 18 && hasTicket || isStaff;
bool result2 = age >= 18 && (hasTicket || isStaff);
std::cout << result1 << '\n';
std::cout << result2 << '\n';
return 0;
}运行结果如下。
1
0分析:
按照 “比较高于 &&,&& 高于 ||” 的规则,计算 result1 时相当于执行 ((age >= 18) && hasTicket) || isStaff。由于 age 是 16,前半部分为 false,而右边的 isStaff 为 true,两者做 || 运算的结果为 true,因此输出 1。
计算 result2 时,小括号强制让 (hasTicket || isStaff) 先算出 true,而左边的 age >= 18 为 false,两者做 && 运算的结果为 false,因此输出 0。编写 if...else 语句的条件判断时,合理加上小括号能让判断逻辑清晰很多。
运算符的结合方向
到这里小伙伴可能会问:如果一条表达式里出现的两个运算符优先级一模一样,比如求余(%)和除号(/)同属第 3 级,此时又应该先算哪一个呢?
当同一优先级的运算符放在一起时,计算顺序由 “结合方向”(也叫结合性)来决定,主要分为以下 2 种情况:
- 从左到右结合:大多数常用的双目运算符,比如加减乘除、比较运算以及
&&和||,都是从左往右依次计算的。 - 从右到左结合:主要是赋值运算符(
=、+=、-=等)以及单目运算符(!、正负号等),它们是从右往左依次计算的。
示例 3:运算符的结合方向
#include <iostream>
int main() {
// 算术运算:从左到右结合
int num = 20 % 11 / 3;
// 赋值运算:从右到左结合
int a = 0;
int b = 0;
a = b = 10;
std::cout << num << '\n';
std::cout << a << ' ' << b << '\n';
return 0;
}运行结果如下。
3
10 10分析:
在计算 20 % 11 / 3 时,% 和 / 的优先级相同,且结合方向是 “从左到右”。因此程序会先算左边的 20 % 11 得到余数 9,再算 9 / 3 得到 3。
再看下面的连续赋值 a = b = 10;:赋值运算符(=)的结合方向是 “从右到左”,因此这行代码等价于先执行右边的 b = 10;,再把 b 的值赋给左边的 a:
b = 10; // 先将 10 赋给 b
a = b; // 再将 b 的值赋给 a编写复杂表达式的建议
在实际开发中,如果写代码时过度依赖默认优先级,写出来的长表达式往往让人看得很累,还容易埋下隐患。建议大家平时养成以下 2 个习惯。
1. 多用小括号明确分组
当一条表达式中包含多个比较或逻辑运算符时,哪怕默认优先级本身是对的,我们也建议手动加上小括号 () 来明确分组。比如对比下面这两种等价写法:
// 方式 1
bool pass = !isBlocked && score >= 60 || isVip;
// 方式 2(推荐)
bool pass = ((!isBlocked) && (score >= 60)) || isVip;不仅仅是初学者,就算是工作多年的开发工程师,看到方式 1 这种不加括号的长串条件,有时也得停下来反应一会儿。而在方式 2 中加上必要的括号后,哪两个条件是一组一目了然,后期维护起来也不容易出错。
提示 小伙伴们遇到拿不准优先级的表达式时,直接加上小括号 () 就可以了。小括号是在编译阶段确定计算顺序的,完全不会降低程序的运行效率。
2. 自增与自减单独成行
对于自增(++)、自减(--)以及复合赋值(+=、-=),我们建议把它们单独拆成一行语句,不要塞进复杂的算术表达式里。请看下面例子。
示例 4:拆分包含自减的表达式
#include <iostream>
int main() {
int a = 10;
int b = --a + 20;
std::cout << b << '\n';
return 0;
}运行结果如下。
29分析:
由于前置自减(第 2 级)的优先级高于加法(第 4 级),程序会先算 --a 让 a 变成 9,然后再与 20 相加得到 29。虽然合在一行也能算出结果,但像下面方式 2 这样拆成两行写会直观得多:
// 方式 1
int b = --a + 20;
// 方式 2(推荐)
--a;
int b = a + 20;更需要特别注意的是:千万不要在同一条表达式里对同一个变量多次自增或自减,比如写出 int x = ++a + a++;。这在 C++ 中属于 “未定义行为”,不同编译器算出的结果可能完全不同。
不少初学者喜欢写看起来很绕的语法来 “炫技”。其实在多人协作的项目开发中,非常忌讳这种难读懂的写法,把代码写得直观清晰、方便维护才是最重要的。
