(四) 运算符与位运算
把“是偶数且位于范围内”写成代码,需要先得到两个真假结果,再合并。计算、赋值和判断有不同含义,下面每次只追踪一个表达式怎样得到结果。
1. 算术、赋值与自增
| 写法 | 含义 | 例子 |
|---|---|---|
+ - * / % |
加减乘除、整数取余 | 5/2 为 2,5%2 为 1 |
= |
给已有对象赋值 | x=3 修改 x,不是判断 x 是否等于 3 |
+= -= *= /= %= |
复合赋值 | sum+=x 把 x 加入 sum |
++x、x++ |
都令 x 增加一 | 前置表达式得到新值,后置表达式得到旧值 |
--x、x-- |
都令 x 减少一 | 同样区别表达式的结果值 |
题目原文(自拟演示): x 初值为 3,先用 x++ 初始化 before,再用 ++x 初始化 after,输出 before、after 和 x。
#include <iostream>
using namespace std;
int main() {
int x = 3;
int before = x++; // before 得到旧值 3;语句结束后 x 为 4。
int after = ++x; // 先把 x 改成 5,after 得到 5。
cout << before << ' ' << after << ' ' << x << '\n'; // 输出 3 5 5。
}只写一条 ++x; 或 x++; 时,增加的效果相同;区别在于是否还使用表达式返回的值。避免在同一复杂表达式中多次修改同一个变量;拆成独立语句更容易确定执行顺序。
整数除法向 0 截断,商与余数满足 \(a=(a/b)b+a\%b\)。例如 -5/2 为 -2,余数为 -1,因为 \((-2)\times2+(-1)=-5\)。在正模数 m 下,若需要非负余数,可先令 r=x%m,再在 r 为负时加 m;下一节的 if 会写出这个判断。
2. 比较与逻辑:从两个条件得到一个结论
| 写法 | 结果或含义 | 示例 |
|---|---|---|
== != < <= > >= |
比较,得到 bool | a==b 判断相等 |
&& |
两条件都真 | x>=0 && x<n |
|| |
至少一个条件真 | x<0 || x>=n |
! |
真假取反 | !found 表示未找到 |
condition ? a : b |
条件真取 a,否则取 b | a>b ? a:b 取较大值 |
题目原文(自拟): 输入 0<=x<=1000。判断 x 是否为偶数、是否满足 \(4<x\le12\),再输出“两个条件都成立、至少一个成立、恰好一个成立、都不成立”的结果,以空格分隔。
推导: 先把两个性质各保存为 bool。都成立用逻辑与,至少一个用逻辑或,恰好一个表示两个 bool 不同,都不成立表示分别取反后再做与。不要写 4<x<=12:它会先算前一个比较得到 0 或 1,再把这个结果与 12 比较。
#include <iostream>
using namespace std;
int main() {
int x;
cin >> x;
bool even = x % 2 == 0; // 先求余数,再比较它是否为 0。
bool inRange = x > 4 && x <= 12; // 把数学连不等式拆成两个比较。
cout << (even && inRange) << ' ' << (even || inRange) << ' ' << (even != inRange) << ' '
<< (!even && !inRange) << '\n';
}输入 8,输出 1 1 0 0。再分别代入 4、5、12、13:4 不满足严格的“大于 4”,5 满足;12 满足“不超过 12”,13 不满足。这样就能看出 > 与 >=、< 与 <= 的区别。
3. 逻辑短路与表达式求值顺序
逻辑与 && 的左侧为假时,整个结果已经确定为假,右侧就不再计算;逻辑或的左侧为真时,也不再计算右侧。比如 b!=0 && a/b>2,先检查 b:b 为 0 就直接得到假,只有 b 非零才计算除法。这个“得到结论后停止”的过程称为短路。
算术通常先乘除再加减;比较之后才合并逻辑结果。优先级决定如何分组,不等于所有子表达式都按书写顺序执行。(a+b)*c 用括号明确先求和,(mask & bit)!=0 用括号明确先按位与。逻辑判断中少写副作用,读到哪里就能知道检查了什么。
4. 用二进制位记录选中位置
按位运算处理每一个二进制位,和逻辑运算处理真假不同。& 是按位与,| 是按位或,^ 是异或,~ 是按位取反;^ 不表示乘方。对本例的 unsigned int,右移一位相当于整除 2;左移一位把各位向高位移动,并在最低位补 0。移出类型位宽的高位会丢失。
题目原文(自拟演示): mask 从 0 开始,将从 0 编号的第 1、3 位设为 1,检查第 3 位是否选中,再清除第 3 位,依次输出初次设置结果、判断结果和清除结果。
#include <iostream>
using namespace std;
int main() {
unsigned int mask = 0;
mask |= 1u << 1; // 1u 左移一位得到 2,将第 1 位置为 1。
mask |= 1u << 3; // 得到 2+8=10;按位或不影响已经选中的第 1 位。
cout << mask << '\n';
bool selected = (mask & (1u << 3)) != 0; // 只取出目标位,再转成真假。
cout << selected << '\n';
mask &= ~(1u << 3); // 目标位的掩码取反后,该位为 0,其余位为 1。
cout << mask << '\n';
}输出为 10、1、2,各占一行。图展示低四位,其余高位仍参与真实的无符号运算。mask ^= bit 会切换目标位,再切一次恢复;mask |= bit 重复置位不会切回 0。
位号从 0 开始。在常见的 32 位 unsigned int 中,可以标记第 0 到 31 位;需要标记第 40 位时,写 1ULL<<40,先用 unsigned long long 的 1 再移位。左侧字面量决定运算类型,接收结果的变量不会倒过来改变它。
练习题: 洛谷 P5710 — 数的性质;洛谷 P1100 — 高低位交换(位运算进阶,按题目的 32 位数理解拆分与组合)。
表达式能给出一个值,下一节再让这个值决定执行哪条路径、重复哪些步骤。