运算符与表达式:C++ 的运算基石
运算符和表达式,说白了就是 C++ 这门语言的「动词」和「动作」。你定义变量(名词),然后用运算符(动词)把它们组合成表达式(句子),最终让程序产生行为。我个人觉得,理解运算符是区分「会写 C++」和「写好 C++」的第一道分水岭。
今天我们就来聊聊 C++ 中的六大类运算符:算术、关系、逻辑、位、赋值,以及那个让无数新手头疼的——运算符优先级。嗯,这里要注意,优先级不是靠背的,是靠理解的。
一、算术运算符:最熟悉的陌生人
加减乘除,谁都会。但 C++ 里有些细节,我当年踩过坑。
int a = 10;
int b = 3;
int c = a / b; // c = 3,不是 3.333
double d = a / b; // d = 3.0,还是整数除法!
double e = a / 3.0; // e ≈ 3.333,这才对
为什么会这样?因为两个整数相除,结果还是整数,小数部分直接截断。我在项目中遇到过一位同事,用整数除法算平均帧率,结果永远偏小,排查了半天才发现是类型问题。
核心要点:只要操作数中有一个是浮点类型,结果就是浮点除法。否则就是整数除法。
取模运算符 % 也值得一说。它只适用于整数类型。我曾经用 % 2 判断奇偶性,结果负数取模的行为在不同 C++ 标准下有差异——C++11 之前,-5 % 3 可能是 -2 或 1,C++11 之后统一为 -2(结果符号与被除数相同)。
二、关系运算符:真假之间
>、<、>=、<=、==、!=,这六个家伙返回 bool 类型:true 或 false。
我建议你养成一个习惯:把常量写在左边。比如 if (5 == x) 而不是 if (x == 5)。为什么?因为如果你手滑写成 if (x = 5),编译器不会报错(赋值表达式返回 5,非零即为 true),但 if (5 = x) 编译直接报错。这个习惯救过我一次,那次我连续加班到凌晨三点,眼睛都快睁不开了。
避坑指南:浮点数不要直接用 == 比较!因为浮点精度问题,0.1 + 0.2 == 0.3 在 C++ 中可能是 false。我曾经因为这个 bug 被测试同事追着问了一下午。正确做法是用绝对值差小于某个极小值来判断。
三、逻辑运算符:短路求值
&&(逻辑与)、||(逻辑或)、!(逻辑非)。
这里有个重要特性:短路求值。你想想看,a && b 如果 a 已经是 false,那 b 还会被求值吗?不会。同样,a || b 如果 a 是 true,b 也不会被求值。
int* ptr = nullptr;
if (ptr != nullptr && *ptr == 42) {
// 安全:ptr 为 nullptr 时,不会执行 *ptr
}
这个特性非常实用。我在写网络库时,经常用短路求值先检查指针是否有效,再访问其成员。如果顺序写反了,程序直接崩溃。
四、位运算符:底层操作的艺术
&(按位与)、|(按位或)、^(按位异或)、~(按位取反)、<<(左移)、>>(右移)。
位运算在嵌入式、图形学、加密算法中非常常见。我个人习惯用位运算来管理状态标志:
// 定义标志位
const int FLAG_VISIBLE = 1 << 0; // 0001
const int FLAG_ENABLED = 1 << 1; // 0010
const int FLAG_SELECTED = 1 << 2; // 0100
int state = 0;
state |= FLAG_VISIBLE; // 设置可见
state &= ~FLAG_ENABLED; // 清除启用
bool selected = state & FLAG_SELECTED; // 检查选中
左移 << 相当于乘以 2 的幂,右移 >> 相当于除以 2 的幂(对于无符号整数)。但要注意:对有符号负整数右移,行为是 implementation-defined,不同编译器可能不同。我建议非必要不用有符号数做右移。
小技巧:用 x & (x - 1) 可以清除 x 最低位的 1。这个技巧在统计二进制中 1 的个数时非常高效。我曾经用它优化过一个图像处理算法,速度提升了 3 倍。
五、赋值运算符:不只是等号
= 是简单赋值,+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>= 是复合赋值。
赋值表达式本身也有值,就是赋完后的结果。所以你可以写出 a = b = c = 5 这样的链式赋值,从右向左执行。
但我不建议在复杂表达式中嵌入赋值操作。你想想看,if (x = func()) 这种写法虽然合法,但可读性差。我见过一个同事在条件判断里写 if (a = b && c = d),结果优先级搞混,调试了一整天。嗯,代码是写给人类读的,不是写给编译器读的。
六、运算符优先级:谁先谁后?
这是本章的重头戏。C++ 有几十个运算符,优先级分 18 个等级。我不建议你死记硬背,但有几个关键点必须记住:
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1(最高) | ::(作用域解析) |
左到右 |
| 2 | () [] . -> |
左到右 |
| 3 | ++ --(后缀) |
左到右 |
| 4 | ++ --(前缀) ! ~ + -(一元) * &(解引用/取地址) |
右到左 |
| 5 | * / % |
左到右 |
| 6 | + - |
左到右 |
| 7 | << >> |
左到右 |
| 8-12 | 关系运算符 < > == != 等 |
左到右 |
| 13 | &(按位与) |
左到右 |
| 14 | ^(按位异或) |
左到右 |
| 15 | |(按位或) |
左到右 |
| 16 | && |
左到右 |
| 17 | || |
左到右 |
| 18(最低) | = += 等赋值运算符 |
右到左 |
我个人有个原则:拿不准优先级,就用括号。括号不仅明确语义,还能让读你代码的人少死脑细胞。我在 code review 时,看到 a + b << c 这种写法,一定会要求加括号——因为 + 优先级高于 <<,但很多人会误以为 << 先执行。
黄金法则:如果你需要查表才能确定优先级,那就加括号。代码的可维护性比少打两个字符重要得多。
七、表达式求值:顺序点与未定义行为
表达式求值有两个关键概念:顺序点和未定义行为。
在 C++17 之前,i = i++ + ++i 这样的表达式是未定义行为。为什么?因为 i++ 和 ++i 都在修改 i,而且它们之间没有顺序点,编译器可以任意安排求值顺序。不同编译器、不同优化级别,结果可能完全不同。
C++17 之后,规则更严格了:后缀 ++ 的副作用在下一个顺序点之前必须完成,但前缀 ++ 的求值顺序也有了更明确的约束。不过,我建议你——永远不要在一个表达式中多次修改同一个变量。这不是技术问题,是职业素养问题。
int i = 0;
// 不要这样写:
int x = i++ + i++; // 未定义行为!
// 应该这样写:
int x = i;
i++;
int y = i;
i++;
我在面试中经常问这个问题,能答对的人不到三成。但真正优秀的工程师,根本不会写出这种代码。
知识体系总览
下面这张图概括了本章的核心知识结构,你可以把它当作学习路线图:
这张图把六大运算符和两个核心概念串在了一起。你从上往下看:先了解每种运算符是什么,再理解短路求值和未定义行为这两个「坑」,最后记住那个黄金原则。嗯,这样学起来就清晰多了。
我的个人建议:学运算符最好的方式不是看书,而是写代码验证。打开你的 IDE,写几个表达式,用 std::cout 打印结果,看看是不是和你预期一致。不一致的地方,就是你需要深入理解的地方。我当年就是这么过来的。
好了,运算符与表达式就聊到这里。记住:运算符是工具,不是玩具。用得好,代码简洁高效;用得不好,bug 缠身。下次写代码时,多想想「这个表达式真的清晰吗?」——你的同事会感谢你的。