表达式、语句与运算符
理解表达式与语句的区别、运算符优先级、求值顺序、短路求值,以及 C++ 特有的运算符特性。
表达式 vs 语句
在 C++ 中,表达式产生一个值:x + 1、f()、a = b(赋值是一个表达式!)。语句执行一个动作:int x = 5;(声明)、if (...) {}、return val;。
一个关键区别:a = b(赋值,返回 a 的值)vs a == b(比较,返回 bool)。在条件语句中混淆它们是典型的错误 ── if (x = 0) 会把 0 赋给 x 然后始终判断为 false,而不是测试 x 是否为零。现代编译器对此会发出警告。
运算符优先级与结合性
C++ 有超过 50 个运算符,分为约 17 个优先级层次。记住全部既不切实际也没有必要。关键规则是:
1. 算术运算符遵循数学规则:* 和 / 优先于 + 和 -
2. 比较运算符(<, ==)比逻辑运算符(&&, ||)优先级高
3. 赋值(=, +=)具有最低优先级之一
4. 有疑问时加括号 ── 不要让读者查优先级表
一个具体的陷阱:位运算符(&, |, ^)比比较运算符优先级低。所以 x & 0xFF == 0 被解析为 x & (0xFF == 0) ── 几乎肯定不是你想要的。写成 (x & 0xFF) == 0。
短路求值
逻辑运算符 && 和 || 使用短路求值:如果结果可以仅从左操作数确定,则右操作数不会被求值。这不仅是优化 ── 它是你可以依赖的保证。
逗号运算符与求值顺序
逗号运算符 (a, b) 先求值 a,丢弃其结果,然后求值 b。它很少有意使用 ── 通常它出现在 for 循环头中以推进多个变量。
重要的是:大多数运算符不保证它们的操作数的求值顺序。在 f() + g() 中,f() 或 g() 可能先被调用。如果两者都有副作用(例如读取同一个文件),你的代码可能表现出不确定的行为。C++17 保证了一些顺序(例如 << 链),但安全的规则是:不要在同一个表达式中多次修改同一个变量。
C++ 特有的运算符特性
C++ 中有几个运算符需要注意的特殊行为:
整数除法截断:7 / 2 是 3,不是 3.5。如果至少有一个操作数是 double,你才能得到浮点除法。
取模运算符 (%) 只对整数有效。结果的符号在 C++11 及之后版本中被定义为与被除数相同。
递增/递减:前缀 ++x 递增并返回新值;后缀 x++ 递增但返回旧值。对于非基本类型(迭代器),前缀更高效因为后缀必须创建临时副本。
三元运算符 (? :) 是一个简洁的 if/else,但不要嵌套它们 ── 嵌套的三元运算符几乎不可读。
使用命名转换来表达意图并获得适当的编译器检查:
- static_cast ── 用于定义明确的转换:数值类型转换、类层次中的向上转换,以及 void* 到具体类型指针的转换。这是你最常用的转换。
- dynamic_cast ── 用于多态层次中的安全向下转换。要求基类至少有一个虚函数。失败时返回 nullptr(指针)或抛出 std::bad_cast(引用)。有运行时开销。
- const_cast ── 仅用于与非 const 正确的遗留 API 交互。绝不要用它来修改真正的 const 对象。
- reinterpret_cast ── 用于底层位重新解释。几乎总是表示平台相关的代码。在应用代码中很少需要。
在 C++ 中永远不要使用 C 风格的转换 (Type)expr ── 它们隐藏了正在执行的转换类型并绕过了安全检查。
- 赋值(
=)是表达式 ──if (x = 0)是合法但几乎总是一个错误 - 位运算符比比较运算符优先级低 ── 混合使用时始终加括号
- 短路求值保证了
&&/||的安全性 ── 右操作数可能不被求值 - 整数除法截断 ──
7/2 == 3,不是 3.5
- 运算符优先级决定了分组方式 ── 不确定时使用括号来明确意图
- 短路求值在 && 和 || 中保证从左到右的顺序,并跳过不必要的求值
- 整数提升将小类型(char、short、bool)在算术运算前转换为 int
- 混合有符号和无符号类型会将有符号值转换为无符号 ── 负的 int 变成一个巨大的正数
- 使用四种命名转换(static_cast、dynamic_cast、const_cast、reinterpret_cast)代替 C 风格转换
- C 风格转换是危险的,因为它们会静默尝试多种转换类型,而不告诉你应用了哪一种
测验 — 检验你的知识
(15 经验值)1. 为什么 `x & 0xFF == 0` 不能按预期工作?
2. `&&` 中的短路求值是什么意思?
3. 为什么前缀 `++x` 比后缀 `x++` 更推荐用于非基本类型?