4. 运算符与表达式:C++ 的“计算引擎”
运算符和表达式,说白了就是 C++ 里做“计算”的那一套工具。你写代码,本质上就是在处理数据——要么算数、要么比较、要么做逻辑判断。我刚开始学 C++ 时,觉得运算符就是些符号,没啥好学的。后来踩了不少坑,才发现这里面的门道比想象中多得多。
今天我们就来把这套“计算引擎”拆开看看。我会结合自己项目里的实际经验,帮你避开那些我曾经掉进去的坑。
核心观点:运算符的优先级和结合性,是 C++ 里最容易出 bug 的地方之一。很多老手都会在这里翻车。
4.1 算术运算符:最基础的加减乘除
算术运算符就是 +、-、*、/、%。嗯,小学就学过的东西。但在 C++ 里,有几个细节你一定要注意。
整数除法会截断! 我记得刚入行时,写了个计算平均分的代码:
int total = 95;
int count = 4;
double avg = total / count; // 结果是 23.0,不是 23.75!
为什么会这样?因为两个整数相除,结果还是整数,小数部分直接丢掉。我当时调试了半天,最后才发现是类型问题。正确的写法是:
double avg = static_cast<double>(total) / count; // 23.75
取模运算符 % 只适用于整数。我在做循环队列时经常用它来绕圈:
int index = (current + 1) % BUFFER_SIZE; // 循环前进
小技巧:判断一个数是不是偶数,用 n % 2 == 0 比用位运算更直观。虽然位运算更快,但可读性更重要。
4.2 赋值运算符:不只是等号
很多人以为 = 就是赋值。其实 C++ 里有一整套复合赋值运算符:+=、-=、*=、/=、%= 等等。
我个人习惯用复合赋值,因为代码更简洁,而且编译器优化起来也更容易。比如:
int score = 100;
score += 10; // 等价于 score = score + 10;
score *= 2; // 等价于 score = score * 2;
注意:赋值运算符有返回值!a = b = c = 5; 这种链式赋值是合法的,因为 c = 5 返回 5,然后赋给 b,再赋给 a。但我不建议写这种代码,可读性太差。
4.3 关系运算符:比较大小
关系运算符就是 >、<、>=、<=、==、!=。它们返回 bool 类型,要么 true,要么 false。
这里有个经典坑:不要用 == 比较浮点数。我曾经在金融项目里吃过这个亏:
double a = 0.1 + 0.2; // 实际是 0.30000000000000004
if (a == 0.3) { // 永远不成立!
// 这段代码永远不会执行
}
正确的做法是判断差值是否小于一个很小的数:
const double EPSILON = 1e-9;
if (fabs(a - 0.3) < EPSILON) {
// 可以认为相等
}
4.4 逻辑运算符:与或非
逻辑运算符有三个:&&(与)、||(或)、!(非)。它们用来组合多个条件。
短路求值 是这里最重要的特性。什么意思?就是如果第一个条件已经能决定结果,后面的就不执行了。比如:
if (ptr != nullptr && ptr->value > 0) {
// 如果 ptr 是 nullptr,第二个条件根本不会执行
// 这就避免了空指针访问
}
我建议你利用这个特性来写更安全的代码。比如先检查指针是否有效,再访问它的成员。
避坑指南:我曾经看到有人写 if (x > 5 & x < 10),少写了一个 &。单个 & 是位运算,不是逻辑运算!结果就是 bug 找了一下午。
4.5 位运算符:底层操作的利器
位运算符包括 &(按位与)、|(按位或)、^(按位异或)、~(按位取反)、<<(左移)、>>(右移)。
说实话,日常业务代码里用得不多。但在嵌入式、网络协议、性能优化场景下,位运算是神器。我在做网络协议解析时,经常用位运算来提取标志位:
// 假设一个字节的 flags 里,第3位表示是否加密
uint8_t flags = packet[0];
bool isEncrypted = (flags & 0x04) != 0; // 0x04 = 0000 0100
左移右移 可以用来做快速的乘除 2 的幂:
int x = 8;
int y = x << 2; // 相当于 x * 4 = 32
int z = x >> 1; // 相当于 x / 2 = 4
注意:右移有符号数时,行为是 implementation-defined 的。有的编译器做算术右移(补符号位),有的做逻辑右移(补0)。我建议只对无符号数做右移操作。
4.6 三目运算符:简洁的条件表达式
三目运算符 ? : 是 C++ 里唯一的三元运算符。格式是 条件 ? 表达式1 : 表达式2。
我个人觉得它很适合用来写简单的条件赋值:
int maxVal = (a > b) ? a : b; // 取较大值
但别滥用。我曾经见过有人嵌套三层三目运算符,那代码简直没法看。我的原则是:如果一行写不下,就用 if-else。
4.7 运算符优先级与结合性:最容易翻车的地方
好了,这是今天的重头戏。运算符优先级决定了谁先算,结合性决定了同优先级时从左还是从右。
我直接给你一张常用优先级表(从高到低):
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1(最高) | ::(作用域解析) | 左到右 |
| 2 | () [] -> . | 左到右 |
| 3 | ++ -- + - ! ~ & *(单目) | 右到左 |
| 4 | * / % | 左到右 |
| 5 | + - | 左到右 |
| 6 | << >> | 左到右 |
| 7 | < <= > >= | 左到右 |
| 8 | == != | 左到右 |
| 9 | &(按位与) | 左到右 |
| 10 | ^(按位异或) | 左到右 |
| 11 | |(按位或) | 左到右 |
| 12 | && | 左到右 |
| 13 | || | 左到右 |
| 14 | ? :(三目) | 右到左 |
| 15 | = += -= 等赋值 | 右到左 |
| 16(最低) | ,(逗号) | 左到右 |
你想想看,如果我不给你这张表,你能一眼看出下面这个表达式的结果吗?
int result = 2 + 3 * 4 >> 1 & 0x0F;
嗯,我建议你直接加括号。真的,拿不准优先级时,加括号永远是最安全的做法。代码是写给人看的,不是写给编译器看的。
我的经验:在团队代码规范里,我明确要求:除了简单的加减乘除,其他运算符组合必须加括号。这看起来有点啰嗦,但能避免 90% 的优先级相关 bug。
总结一下
运算符是 C++ 的基础工具,每个都很简单,但组合起来就容易出问题。记住几点:
- 整数除法会截断,记得转型
- 浮点数不要直接用 == 比较
- 利用短路求值写更安全的代码
- 位运算是底层利器,但注意右移有符号数的问题
- 优先级拿不准就加括号,不丢人
好了,运算符这块就聊到这儿。代码写多了,这些自然就熟练了。