4、运算符与表达式:算术、关系、逻辑、位、赋值、三目运算符,运算符优先级与结合性

C++ 的运算符体系是表达式求值的核心工具,也是初学者从“写变量”迈向“写逻辑”的关键一步。本章将系统梳理六大类运算符的语义与使用边界,并重点剖析运算符优先级与结合性对表达式结果的影响。理解这些规则,不仅是为了通过编译,更是为了写出可读、可维护、无歧义的代码。

C++ 运算符体系与表达式求值路径 表达式求值 算术运算符 关系运算符 逻辑运算符 位运算符 赋值运算符 三目运算符 优先级 + 结合性 → 唯一求值顺序 结果值 / 副作用 / 类型 短路求值、隐式类型转换、左值/右值语义贯穿其中

4.1 算术运算符与表达式基础

核心原理

算术运算符包括 +、-、*、/、%(取模),以及自增 ++ 和自减 --。它们作用于整型、浮点型和字符型数据。整数除法会截断小数部分,而取模运算要求两个操作数均为整型。自增/自减分为前置和后置两种形式:前置版本先修改值再返回新值,后置版本返回旧值再修改。

关键点:整数除法与浮点除法的差异是初学者最常见的错误来源。7 / 2 的结果是 3 而非 3.5,因为两个操作数都是 int。若希望得到浮点结果,至少一个操作数需为浮点类型,例如 7.0 / 2 或 static_cast<double>(7) / 2。

判断方法

在编写算术表达式时,先确认操作数类型,再判断结果类型。若涉及混合类型,C++ 会执行隐式类型转换(通常向精度更高的类型转换)。例如 int + double 会将 int 提升为 double。取模运算 % 的符号与被除数一致:-7 % 3 的结果是 -1。

案例拆解

假设需要计算一个三位数的各位数字之和,例如 n = 382:

int n = 382;
int hundreds = n / 100;      // 3
int tens = (n / 10) % 10;    // 8
int ones = n % 10;           // 2
int sum = hundreds + tens + ones; // 13

这里利用整数除法截断和取模运算,将数字逐位拆解。若将 n / 10 误写为 n % 10,则 tens 会得到 2,逻辑完全错误。

常见误区

  • 自增运算的副作用:在同一表达式中多次使用 i++ 或 ++i 会导致未定义行为(C++11 之前),即使现代标准也强烈不建议。例如 int x = i++ + i++; 的结果不可预测。
  • 浮点数取模:% 不能用于浮点数,需使用 std::fmod。
  • 除以零:整数除以零会导致程序崩溃(未定义行为),浮点除以零会得到 inf 或 nan,但应主动避免。

4.2 关系运算符与逻辑运算符

核心原理

关系运算符(>、<、>=、<=、==、!=)比较两个操作数,返回 bool 值(true 或 false)。逻辑运算符包括 &&(逻辑与)、||(逻辑或)和 !(逻辑非)。&& 和 || 具有短路求值特性:若左侧操作数已能确定整个表达式的结果,右侧操作数将不再求值。

短路求值的实际意义:在 if (ptr != nullptr && ptr->value > 10) 中,若 ptr 为空指针,右侧的 ptr->value 不会执行,从而避免空指针解引用错误。这是 C++ 中非常实用的安全模式。

判断方法

关系运算符的优先级低于算术运算符,但高于赋值运算符。逻辑非 ! 的优先级高于关系运算符,而 && 的优先级高于 ||。当表达式混合多种运算符时,建议使用括号明确意图,避免依赖优先级记忆。

案例拆解

判断某年份是否为闰年:

int year = 2024;
bool isLeap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);

该表达式利用短路求值:若 year % 4 != 0,则第一个括号内直接为 false,无需计算 year % 100。若第一个括号为 true,则整个表达式为 true,第二个括号不再求值。这种写法既简洁又高效。

常见误区

  • 误用 = 代替 ==:在条件判断中写 if (x = 5) 会将 5 赋值给 x,且表达式值恒为 true。编译器通常给出警告,但初学者容易忽略。
  • 连续比较的误解:a < b < c 在 C++ 中合法但语义错误,它先计算 a < b 得到 bool,再与 c 比较。正确写法是 a < b && b < c。
  • 短路求值被破坏:若在 && 右侧写入具有副作用的表达式(如 i++),当左侧为 false 时副作用不会发生,可能导致逻辑错误。

4.3 位运算符与移位运算

核心原理

位运算符直接操作整型数据的二进制位,包括 &(按位与)、|(按位或)、^(按位异或)、~(按位取反)、<<(左移)和 >>(右移)。位运算常用于底层系统编程、嵌入式开发、状态标志管理、数据压缩和加密算法。

运算符 名称 示例 结果 典型用途
& 按位与 0b1100 & 0b1010 0b1000 掩码提取、清零特定位
| 按位或 0b1100 | 0b1010 0b1110 置位特定位
^ 按位异或 0b1100 ^ 0b1010 0b0110 翻转特定位、无临时变量交换
~ 按位取反 ~0b1100 ...0011 生成补码、掩码取反
<< 左移 0b0001 << 3 0b1000 乘以 2 的幂次
>> 右移 0b1000 >> 2 0b0010 除以 2 的幂次(无符号数)

判断方法

使用位运算前,先明确操作数的位宽(如 int 通常为 32 位)和有符号性。右移操作对有符号负数的行为是实现定义的(通常为算术右移,保留符号位),而对无符号数总是逻辑右移。左移操作可能溢出,但无符号数的左移是明确定义的(模 2^n)。

案例拆解

使用位运算管理权限标志:

// 定义权限位
const int READ = 1 << 0;   // 0b001
const int WRITE = 1 << 1;  // 0b010
const int EXEC = 1 << 2;   // 0b100

int permissions = 0;
permissions |= READ | EXEC;  // 赋予读和执行权限
bool canWrite = (permissions & WRITE) != 0;  // false
permissions ^= WRITE;        // 翻转写权限
bool canWrite2 = (permissions & WRITE) != 0; // true

这种位掩码方式在资源受限的嵌入式系统中极为常见,一个 int 可以存储 32 个独立布尔标志,节省内存且操作高效。

常见误区

  • 混淆逻辑与位运算:&& 与 & 完全不同,前者短路且返回 bool,后者按位操作且不短路。
  • 有符号数右移:对负数进行 >> 操作,结果依赖编译器实现,应避免依赖。
  • 移位位数越界:移位位数大于等于类型位宽时是未定义行为,例如 int x = 1 << 32; 在 32 位 int 下非法。

4.4 赋值运算符与复合赋值

核心原理

赋值运算符 = 将右侧表达式的值赋给左侧的左值(lvalue)。复合赋值运算符(+=、-=、*=、/=、%=、<<=、>>=、&=、|=、^=)等价于先执行对应运算再赋值,但左侧表达式只求值一次。赋值表达式本身具有值(即被赋的值),且结合性为从右到左。

判断方法

赋值运算符的优先级非常低,仅高于逗号运算符。因此 int x = a + b; 中先计算 a + b 再赋值。链式赋值 a = b = c 从右向左执行,先将 c 赋给 b,再将 b 的值赋给 a。

风险提示:复合赋值与普通赋值的区别在于左侧表达式只求值一次。对于简单变量没有区别,但对于数组下标或指针解引用,例如 arr[i++] += 5,i++ 只执行一次,而 arr[i++] = arr[i++] + 5 会导致 i 被修改两次,产生未定义行为。

案例拆解

统计一个整数二进制表示中 1 的个数(经典位操作):

int countOnes(int n) {
    int count = 0;
    while (n) {
        n &= (n - 1);  // 清除最低位的 1
        count++;
    }
    return count;
}

这里 n &= (n - 1) 是复合赋值,等价于 n = n & (n - 1)。每次循环清除一个 1,循环次数即为 1 的个数。该算法比逐位检查更高效,时间复杂度为 O(k),k 为 1 的个数。

常见误区

  • 赋值与相等混淆:在条件中误用 = 导致逻辑错误,应使用 ==。
  • 复合赋值的类型转换:int x = 5; x += 2.7; 等价于 x = (int)(x + 2.7),结果为 7,而不是 7.7。复合赋值会隐式转换为左侧类型。
  • 链式赋值中的类型不一致:double a; int b; a = b = 3.14; 先将 3.14 赋给 b(截断为 3),再将 3 赋给 a,最终 a 为 3.0。

4.5 三目运算符与优先级综合

核心原理

三目运算符 cond ? expr1 : expr2 是 C++ 中唯一的三元运算符。它先计算条件 cond,若为 true 则求值 expr1,否则求值 expr2。整个表达式的类型是 expr1 和 expr2 的公共类型。三目运算符可以嵌套使用,但过度嵌套会严重损害可读性。

判断方法

三目运算符的优先级高于赋值运算符,但低于逻辑或 ||。结合性为从右到左,因此 a ? b : c ? d : e 等价于 a ? b : (c ? d : e)。在实际编码中,建议使用括号明确嵌套关系。

案例拆解

使用三目运算符简化条件赋值:

int score = 85;
char grade = (score >= 90) ? 'A' :
             (score >= 80) ? 'B' :
             (score >= 70) ? 'C' : 'D';

这段代码将 if-else 链压缩为一行,逻辑清晰。但若条件复杂或分支超过三层,建议改用 switch 或函数,避免可读性下降。

优先级与结合性综合表

优先级 运算符 结合性 说明
1(最高) () [] -> . 左到右 括号、下标、成员访问
2 ! ~ ++ -- + - * & 右到左 单目运算符、取地址、解引用
3 * / % 左到右 乘除取模
4 + - 左到右 加减
5 << >> 左到右 移位
6 < <= > >= 左到右 关系比较
7 == != 左到右 相等性比较
8 & 左到右 按位与
9 ^ 左到右 按位异或
10 | 左到右 按位或
11 && 左到右 逻辑与
12 || 左到右 逻辑或
13 ?: 右到左 三目运算符
14 = += -= 等 右到左 赋值与复合赋值
15(最低) , 左到右 逗号运算符

常见误区与总结

  • 过度依赖优先级:即使熟悉优先级表,也应使用括号明确表达意图。代码是写给人读的,编译器不关心括号数量。
  • 三目运算符的类型陷阱:int x = 5; double y = 3.7; auto z = true ? x : y; 中 z 的类型为 double,因为 int 会转换为 double。
  • 逗号运算符的误用:int x = (1, 2, 3); 的结果是 3,因为逗号表达式依次求值并返回最后一个值。在 for 循环中 for (int i=0, j=10; i<j; ++i, --j) 是合法用法,但应避免在其他场景使用。

实战建议:在编写复杂表达式时,遵循“一表达式一目的”原则。若表达式超过 3 个运算符,建议拆分为多个中间变量。例如 int avg = (a + b + c) / 3; 比 int avg = a / 3 + b / 3 + c / 3; 更准确,后者因整数除法截断会产生误差。

运算符与表达式是 C++ 语法体系的基石。掌握各类运算符的语义、优先级和结合性,能够帮助开发者写出高效且无歧义的代码。在实际工程中,优先保证代码可读性,使用括号明确逻辑,避免依赖晦涩的优先级规则。下一阶段的学习中,这些基础将直接应用于控制流、函数调用和类设计,建议读者通过大量编码练习巩固本章内容。