运算符与表达式: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 类型:truefalse

我建议你养成一个习惯:把常量写在左边。比如 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 如果 atrueb 也不会被求值。

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++;

我在面试中经常问这个问题,能答对的人不到三成。但真正优秀的工程师,根本不会写出这种代码。

知识体系总览

下面这张图概括了本章的核心知识结构,你可以把它当作学习路线图:

运算符与表达式 算术运算符 关系运算符 逻辑运算符 位运算符 赋值运算符 优先级 + - * / % > < == != && || ! & | ^ ~ << >> = += -= *= 18级优先级 短路求值 未定义行为 核心原则:拿不准就加括号! 代码可读性 > 炫技

这张图把六大运算符和两个核心概念串在了一起。你从上往下看:先了解每种运算符是什么,再理解短路求值和未定义行为这两个「坑」,最后记住那个黄金原则。嗯,这样学起来就清晰多了。

我的个人建议:学运算符最好的方式不是看书,而是写代码验证。打开你的 IDE,写几个表达式,用 std::cout 打印结果,看看是不是和你预期一致。不一致的地方,就是你需要深入理解的地方。我当年就是这么过来的。

好了,运算符与表达式就聊到这里。记住:运算符是工具,不是玩具。用得好,代码简洁高效;用得不好,bug 缠身。下次写代码时,多想想「这个表达式真的清晰吗?」——你的同事会感谢你的。