三、运算符与表达式:算术、关系、逻辑、位、赋值、自增自减运算符,优先级与结合性,表达式求值
运算符是C++语言中构建表达式的核心工具,它们决定了数据如何被计算、比较与变换。对于初学者而言,运算符种类繁多、优先级复杂,常常是编写正确代码的第一道门槛。本章将系统梳理C++中六大类运算符的用法与陷阱,深入解析优先级与结合性的底层规则,并通过实际案例演示表达式求值的完整过程。掌握本章内容,你将能够准确阅读和编写复杂的C++表达式,避免因运算符误用而导致的逻辑错误。
3.1 核心原理:运算符的分类与本质
运算符本质上是对操作数进行特定运算的函数式封装。C++中的运算符可以按照操作数个数分为一元、二元和三元运算符,也可以按照功能分为算术、关系、逻辑、位、赋值和自增自减六大类。理解运算符的本质,需要把握三个关键维度:操作数类型、运算结果类型以及是否产生副作用。
算术运算符处理数值类型(int、float、double等),其中%取模运算仅适用于整数类型。关系运算符比较两个值的大小或相等性,返回bool类型(true/false)。逻辑运算符对布尔值进行与、或、非运算,支持短路求值。位运算符直接操作整数的二进制位,包括按位与(&)、按位或(|)、按位异或(^)、按位取反(~)以及左移(<<)和右移(>>)。赋值运算符将右侧表达式的值赋给左侧的左值,复合赋值运算符(如+=)则先执行算术运算再赋值。自增自减运算符(++/--)是C++中特有的快捷操作,分为前缀和后缀两种形式。
| 运算符类别 | 包含运算符 | 操作数类型 | 结果类型 | 副作用 |
|---|---|---|---|---|
| 算术 | + - * / % | 数值类型 | 数值类型 | 无 |
| 关系 | < > <= >= == != | 可比较类型 | bool | 无 |
| 逻辑 | && || ! | bool或可转换类型 | bool | 短路求值 |
| 位 | & | ^ ~ << >> | 整数类型 | 整数类型 | 无 |
| 赋值 | = += -= *= /= %= <<= >>= &= |= ^= | 左值 = 右值 | 左值引用 | 修改左值 |
| 自增自减 | ++i i++ --i i-- | 可修改左值 | 前缀返回引用,后缀返回值 | 修改操作数 |
3.2 判断方法:优先级与结合性的判定规则
C++定义了严格的运算符优先级和结合性规则。优先级决定了哪个运算符先执行,结合性决定了同优先级运算符的执行顺序(从左到右或从右到左)。掌握这些规则,是正确分析复杂表达式的关键。
优先级从高到低的核心顺序可以概括为:一元运算符 > 算术 > 移位 > 关系 > 相等性 > 位与 > 位异或 > 位或 > 逻辑与 > 逻辑或 > 条件 > 赋值 > 逗号。特别需要注意的是,赋值运算符的优先级非常低,仅高于逗号运算符,且结合性为从右到左。自增自减运算符中,前缀形式(++i)优先级高于后缀形式(i++),但后缀形式在表达式中先返回原值再自增。
优先级速查口诀:“单算移关与,异或逻条赋逗”——一元、算术、移位、关系、位与、位异或、位或、逻辑与、逻辑或、条件、赋值、逗号。结合性口诀:“赋值条件右结合,其余大多左结合”。
判断表达式求值顺序的实操步骤:
- 划分操作数:识别表达式中的所有操作数和运算符。
- 标注优先级:为每个运算符标注优先级数值,从高到低排列。
- 确定结合性:对同优先级的运算符,根据结合性确定执行顺序。
- 逐步化简:从最高优先级开始,逐步将子表达式替换为其结果。
- 检查副作用:确认自增自减、赋值等操作对变量值的影响时机。
3.3 案例拆解:复杂表达式求值实战
下面通过一个综合案例,完整演示表达式求值的全过程。假设有以下代码片段:
int a = 5, b = 3, c = 2;
int result = a++ + (b > 2 ? b * 2 : c) && --c > 0 ? a + b : a - b;
这个表达式混合了自增、条件、逻辑、算术、关系和赋值运算符,是检验优先级理解的绝佳案例。我们逐步分析:
| 步骤 | 子表达式 | 优先级 | 求值过程 | 结果 |
|---|---|---|---|---|
| 1 | a++ | 后缀自增(最高) | 先返回a的原值5,然后a变为6 | 5(a=6) |
| 2 | b > 2 | 关系(高于条件) | 3 > 2 为真 | true |
| 3 | b * 2 | 算术(高于关系) | 3 * 2 = 6 | 6 |
| 4 | b > 2 ? b*2 : c | 条件(低于关系) | 条件为真,取6 | 6 |
| 5 | --c | 前缀自增 | c先减为1,返回1 | 1(c=1) |
| 6 | --c > 0 | 关系 | 1 > 0 为真 | true |
| 7 | 5 + 6 && true | 算术高于逻辑与 | 11 && true,非零为真 | true |
| 8 | true ? a+b : a-b | 条件 | 条件为真,取a+b=6+3=9 | 9 |
| 9 | result = 9 | 赋值(最低) | 将9赋给result | result=9 |
最终result的值为9,同时a变为6,c变为1。这个案例展示了多个关键点:后缀自增先使用后修改、条件运算符的优先级低于逻辑与、赋值运算符最后执行。
风险提示:在同一个表达式中多次修改同一变量(如i++ + i++)属于未定义行为(Undefined Behavior),不同编译器可能产生不同结果。务必避免在复杂表达式中对同一变量进行多次自增或赋值操作。
3.4 常见误区与总结复盘
在实际编程中,运算符相关的错误往往隐蔽且难以排查。以下是初学者最常犯的几类错误:
- 混淆
=与==:在条件判断中误用赋值运算符,导致条件恒为真或恒为假。例如if (x = 5)会将5赋给x,条件永远为真。 - 忽视短路求值:逻辑与(
&&)和逻辑或(||)具有短路特性,即左侧操作数已能确定结果时,右侧不再求值。例如if (ptr != nullptr && ptr->value > 0),当ptr为空时不会访问其成员。 - 位运算符与逻辑运算符混淆:
&与&&、|与||虽然符号相似,但语义完全不同。位运算符按位操作,逻辑运算符返回布尔值。 - 自增自减的副作用误判:在函数参数传递或复杂表达式中使用自增自减,容易造成难以预测的结果。
- 整数除法截断:两个整数相除结果仍为整数,小数部分被截断。例如
5/2结果为2而非2.5,需要至少一个操作数为浮点类型。
经验建议:编写复杂表达式时,优先使用括号明确运算顺序,不要依赖记忆优先级。括号不仅提高可读性,还能避免因优先级误判导致的逻辑错误。同时,尽量将复杂表达式拆分为多个简单语句,便于调试和阅读。