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