C++ 运算符完全指南:从算术、位操作到优先级总表
【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. (某大型游戏线上攻略,内含炫酷算术魔法)项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki
本文基于 OI-wiki 语言基础章节(docs/lang/op.md)整理成文。C++ 运算符是程序表达一切计算与逻辑的基础设施,也是 OI / ICPC 参赛者最容易因细节踩坑的地方——从取模与除法的类型陷阱、移位操作的未定义行为,到
if (op=1)这类赋值与比较混淆的经典 bug。读完本文,你将系统掌握 C++ 全部运算符的分类语义、运算中的类型转换规则、自增自减的前后缀差异、短路求值特性,并能熟练查阅并使用文末完整的 C++ 运算符优先级总表,在比赛中写出更少歧义、更快更稳的代码。
算术运算符
C++ 的算术运算符由两个单目运算符(一元运算符)和五个双目运算符(二元运算符)组成:
| 运算符 | 功能 |
|---|---|
+(单目) | 正 |
-(单目) | 负 |
*(双目) | 乘法 |
/ | 除法 |
% | 取模 |
+(双目) | 加法 |
-(双目) | 减法 |
单目运算符指被操作对象只有一个的运算符,双目运算符的被操作对象有两个。例如1 + 2中的加号就是双目运算符,它有1和2两个被操作数;此外 C++ 中还有唯一的一个三目运算符?:。在算术运算符内部,单目运算符的优先级最高。
- 取模运算符
%计算两个整数相除得到的余数,即求余数,参与运算的两个操作数必须是整数类型; -为双目运算符时做减法运算,如2-1;为单目运算符时做负值运算,如-1。
表达式遵循数学中加减乘除的优先规律:先进行优先级高的运算,同优先级按运算的结合性运算,括号可提高优先级。例如op=x-y*z先计算y*z,再与x相减。
算术运算中的类型转换
对于双目算术运算符,当参与运算的两个变量类型相同时,不发生类型转换,运算结果用参与运算的变量的类型容纳;否则会发生类型转换,以使两个变量的类型一致,具体规则参见类型转换。
例如,对于整型(int)变量x和双精度浮点型(double)变量y:
x/3的结果是整型(整数除法,结果截断为整数);x/3.0的结果是双精度浮点型(字面量3.0为double类型);x/y的结果是双精度浮点型(x被转换为double);x*1/3的结果是整型(x*1仍是整型,整型除法舍去小数);x*1.0/3的结果是双精度浮点型(1.0触发整型到浮点型的转换)。
从变量与类型一章可以进一步理解其底层机制:类型转换分为数值提升(值本身保持不变,如float提升为double、char/short提升为int)和数值转换(值可能发生改变,如浮点转整数舍弃小数部分、宽类型转窄类型发生截断/取模)。算术运算符不接受小于int的类型作为实参,运算前会先实施整数提升,这正是char参与算术运算时行为常出人意料的原因。特别注意浮点数转换为整数时全部小数部分被舍弃(向零截断),例如3.99转为int得到3;而int转float/double则会舍入到目标类型下最接近的值,若该值不能适应目标类型则行为未定义。
位操作符
另请参阅:位运算。
| 运算符 | 功能 |
|---|---|
~ | 逐位非 |
&(双目) | 逐位与 |
\| | 逐位或 |
^ | 逐位异或 |
<< | 逐位左移 |
>> | 逐位右移 |
位运算是对整数的二进制位序列逐位应用布尔函数(取反、与、或、异或)的运算,各位的运算相互独立。位操作在 OI 中有三种典型用途(详见位运算):高效代替某些低效运算(即编译优化中的强度削减,如x*2在开启优化后与x<<1完全等价)、表示集合(常用于状压 DP)、以及题目本身要求进行位操作。仓库中的示例代码 docs/math/code/bit/bit_2.cpp 给出了大量可直接复用的位操作实现:用n << m计算n*(2^m)、用n >> m计算n/(2^m)、用(a >> b) & 1取第b位、用a & ~(1 << b)将某位清零、用a | (1 << b)将某位置 1、用a ^ (1 << b)翻转某位,以及用x & -x提取最低位 1(lowbit,常用于树状数组与 popcount 计数)。
需要注意,位操作的优先级低于算术运算符(单目取反~除外),而按位与&、按位或|和异或^又低于比较运算符(详见下文优先级总表),所以使用位操作时需多加注意,必要时添加括号。例如x & 1 == 0会被解析为x & (1 == 0)而非(x & 1) == 0。
移位运算的未定义行为
移位运算中如果出现如下情况,其行为未定义:
- 右操作数(即移位数)为负值;
- 右操作数大于等于左操作数的位数。
例如对于int32_t类型的变量a,a<<-1和a<<32都是未定义的(int32_t为 32 位,移位数32等于其位宽)。
此外还有两条与符号性相关的规则:
- 对带符号非负数的左移操作,需确保移位后的结果能被原数的类型容纳,否则行为未定义;对一个负数执行左移操作同样未定义。
- 对于右移操作,右侧多余的位将被舍弃,左侧的填充则取决于类型:无符号数在左侧补
0(即逻辑右移);有符号数用最高位(符号位,非负数为0、负数为1)补齐(即算术右移)。
关于标准版本差异(仓库原文注释)需要注意:C++20 前,带符号类型的右移是依实现定义的,大多数实现采用算术右移;C++20 起标准规定a >> b为 $\lfloor a/2^b\rfloor$,带符号数右移由此被固定为算术右移。左移方面,C++20 前若原值为带符号类型且移位结果能被原类型的无符号版本容纳,则将该结果转换为相应的带符号值,否则行为未定义;C++20 起规定a << b为 $a\cdot 2^b$ 在模 $2^N$ 下的值($N$ 为结果类型的位宽),即带符号数与无符号数的左移均直接舍弃移出结果类型的位。此外,右移对负数是向下取整,与 C/C++ 除法向零取整并不一致——例如-1/2的值为0,而-1>>1的值为-1。
自增/自减运算符
有时我们需要让变量自增 1 或自减 1,这时自增运算符++和自减运算符--就派上用场了。
自增/自减运算符可放在变量前或变量后:放在变量前称为前缀,放在变量后称为后缀。单独使用时前后缀无需特别区别;如果需要用到表达式的值则需注意求值顺序,具体可看下面的例子,详细情况可参考引用中介绍的例子部分。
i = 100; op1 = i++; // op1 = 100,先 op1 = i,然后 i = i + 1 i = 100; op2 = ++i; // op2 = 101,先 i = i + 1,然后赋值 op2 i = 100; op3 = i--; // op3 = 100,先赋值 op3,然后 i = i - 1 i = 100; op4 = --i; // op4 = 99,先 i = i - 1,然后赋值 op4复合赋值运算符
复合赋值运算符是表达式的缩写形式,分为两类:
- 复合算术运算符:
+=、-=、*=、/=、%=; - 复合位操作符:
&=、|=、^=、<<=、>>=。
例如op = op + 2可写为op += 2,op = op - 2可写为op -= 2,op = op * 2可写为op *= 2,位运算的复合形式同理(如flags <<= 2;、flags &= new_flags;)。
条件运算符
条件运算符?:是 C++ 中唯一的三目运算符,可以看作if语句的简写:在a ? b : c中,如果表达式a成立(值为真),整个条件表达式的结果是b,否则结果是c。例如:
int i = a > b ? a : b; // 取 a、b 中的较大值条件运算符不可重载,且是从右向左结合。
比较运算符
| 运算符 | 功能 |
|---|---|
> | 大于 |
>= | 大于等于 |
< | 小于 |
<= | 小于等于 |
== | 等于 |
!= | 不等于 |
其中特别需要注意的是要将等于运算符==和赋值运算符=区分开来,这在判断语句中尤为重要。
if (op=1)与if (op==1)看起来类似,但实际功能相差甚远:第一条语句是对op进行赋值,若赋值为非 0 则表达式为真值,条件始终满足,完全无法达到判断的作用;第二条语句才是对op的值进行判断。这种"赋值误写为比较"是新手最常见的隐蔽 bug 之一,建议在条件中把常量写在左侧(如if (1 == op))以便编译器在误写为=时直接报错。
逻辑运算符
| 运算符 | 功能 |
|---|---|
&& | 逻辑与 |
\|\| | 逻辑或 |
! | 逻辑非 |
Result = op1 && op2; // 当 op1 与 op2 都为真时则 Result 为真 Result = op1 || op2; // 当 op1 或 op2 其中一个为真时则 Result 为真 Result = !op1; // 当 op1 为假时则 Result 为真内建的&&和||运算符进行短路求值(short-circuit evaluation):若在求值第一个操作数后结果已经确定,则不再求值第二个操作数。这在比赛中是常用技巧,例如if (i < n && a[i] > 0)可以安全地先检查下标再访问数组。但要注意,重载的&&与||无此特性,会始终对两个操作数都进行求值。
逗号运算符
逗号运算符可将多个表达式分隔开来,被分隔开的表达式按从左至右的顺序依次计算,整个表达式的值是最后一个表达式的值。逗号表达式的优先级在所有运算符中最低。
exp1, exp2, exp3; // 最后的值为 exp3 的运算结果 Result = 1 + 2, 3 + 4, 5 + 6; // 得到 Result 的值为 3 而不是 11,因为赋值运算符 "=" // 的优先级比逗号运算符高,先进行了赋值运算才进行逗号运算 Result = (1 + 2, 3 + 4, 5 + 6); // 若要让 Result 的值得到逗号运算的结果则应将整个表达式用括号提高优先级,此时 // Result 的值才为 11逗号运算符最常见的实用场景是在for循环的初始化与迭代表达式中同时处理多个变量,如for (i = 0, j = 0; i < 10; i++, j++)。
成员访问运算符
| 运算符 | 功能 |
|---|---|
[] | 数组下标 |
. | 对象成员 |
&(单目) | 取地址/获取引用 |
*(单目) | 间接寻址/解引用 |
-> | 指针成员 |
这些运算符用来访问对象的成员或者内存,除->外上述运算符都可被重载。与&、*和->相关的内容请阅读指针和引用教程。这里还省略了两个很少用到的运算符.*和->*(类成员指针访问运算符)。
auto result1 = v[1]; // 获取 v 中下标为 1 的对象(注意 C++ 下标从 0 开始) auto result2 = p.q; // 获取 p 对象的 q 成员 auto result3 = p -> q; // 获取 p 指针指向的对象的 q 成员,等价于 (*p).q auto result4 = &v; // 获取指向 v 的指针 auto result5 = *v; // 获取 v 指针指向的对象从指针教程可以了解到,&取地址与*解引用互为逆运算:&得到一个变量的内存地址,*则沿指针找到其所指对象,而->就是"解引用 + 取成员"的语法糖。
C++ 运算符优先级总表
下表整理自 C++ 运算符优先级规范(有修改)。掌握这张表,是理解一切表达式求值顺序的前提。C++20 起新增的三路比较运算符<=>(太空船运算符)也收录于第八级别。
| 运算符 | 描述 | 例子 | 可重载性 |
|---|---|---|---|
| 第一级别 | |||
:: | 作用域解析符 | Class::age = 2; | 不可重载 |
| 第二级别 | |||
++ | 后自增运算符 | for (int i = 0; i < 10; i++) cout << i; | 可重载 |
-- | 后自减运算符 | for (int i = 10; i > 0; i--) cout << i; | 可重载 |
type() type{} | 强制类型转换 | unsigned int a = unsigned(3.14); | 可重载 |
() | 函数调用 | isdigit('1') | 可重载 |
[] | 数组数据获取 | array[4] = 2; | 可重载 |
. | 对象型成员调用 | obj.age = 34; | 不可重载 |
-> | 指针型成员调用 | ptr->age = 34; | 可重载 |
| 第三级别(从右向左结合) | |||
++ | 前自增运算符 | for (i = 0; i < 10; ++i) cout << i; | 可重载 |
-- | 前自减运算符 | for (i = 10; i > 0; --i) cout << i; | 可重载 |
+ | 正号 | int i = +1; | 可重载 |
- | 负号 | int i = -1; | 可重载 |
! | 逻辑取反 | if (!done) … | 可重载 |
~ | 按位取反 | flags = ~flags; | 可重载 |
(type) | C 风格强制类型转换 | int i = (int) floatNum; | 可重载 |
* | 指针取值 | int data = *intPtr; | 可重载 |
& | 值取指针 | int *intPtr = &data; | 可重载 |
sizeof | 返回类型内存 | int size = sizeof floatNum; int size = sizeof(float); | 不可重载 |
new | 动态元素内存分配 | long *pVar = new long; MyClass *ptr = new MyClass(args); | 可重载 |
new [] | 动态数组内存分配 | long *array = new long[n]; | 可重载 |
delete | 动态析构元素内存 | delete pVar; | 可重载 |
delete [] | 动态析构数组内存 | delete [] array; | 可重载 |
| 第四级别 | |||
.* | 类对象成员引用 | obj.*var = 24; | 不可重载 |
->* | 类指针成员引用 | ptr->*var = 24; | 可重载 |
| 第五级别 | |||
* | 乘法 | int i = 2 * 4; | 可重载 |
/ | 除法 | float f = 10.0 / 3.0; | 可重载 |
% | 取余数(模运算) | int rem = 4 % 3; | 可重载 |
| 第六级别 | |||
+ | 加法 | int i = 2 + 3; | 可重载 |
- | 减法 | int i = 5 - 1; | 可重载 |
| 第七级别 | |||
<< | 位左移 | int flags = 33 << 1; | 可重载 |
>> | 位右移 | int flags = 33 >> 1; | 可重载 |
| 第八级别 | |||
<=> | 三路比较运算符 | if ((i <=> 42) < 0) ... | 可重载 |
| 第九级别 | |||
< | 小于 | if (i < 42) ... | 可重载 |
<= | 小于等于 | if (i <= 42) ... | 可重载 |
> | 大于 | if (i > 42) ... | 可重载 |
>= | 大于等于 | if (i >= 42) ... | 可重载 |
| 第十级别 | |||
== | 等于 | if (i == 42) ... | 可重载 |
!= | 不等于 | if (i != 42) ... | 可重载 |
| 第十一级别 | |||
& | 位与运算 | flags = flags & 42; | 可重载 |
| 第十二级别 | |||
^ | 位异或运算 | flags = flags ^ 42; | 可重载 |
| 第十三级别 | |||
\| | 位或运算 | flags = flags \| 42; | 可重载 |
| 第十四级别 | |||
&& | 逻辑与运算 | if (conditionA && conditionB) ... | 可重载 |
| 第十五级别 | |||
\|\| | 逻辑或运算 | if (conditionA \|\| conditionB) ... | 可重载 |
| 第十六级别(从右向左结合) | |||
? : | 条件运算符 | int i = a > b ? a : b; | 不可重载 |
throw | 异常抛出 | throw EClass("Message"); | 不可重载 |
= | 赋值 | int a = b; | 可重载 |
+= | 加赋值运算 | a += 3; | 可重载 |
-= | 减赋值运算 | b -= 4; | 可重载 |
*= | 乘赋值运算 | a *= 5; | 可重载 |
/= | 除赋值运算 | a /= 2; | 可重载 |
%= | 模赋值运算 | a %= 3; | 可重载 |
<<= | 位左移赋值运算 | flags <<= 2; | 可重载 |
>>= | 位右移赋值运算 | flags >>= 2; | 可重载 |
&= | 位与赋值运算 | flags &= new_flags; | 可重载 |
^= | 位异或赋值运算 | flags ^= new_flags; | 可重载 |
\|= | 位或赋值运算 | flags \|= new_flags; | 可重载 |
| 第十七级别 | |||
, | 逗号分隔符 | for (i = 0, j = 0; i < 10; i++, j++) ... | 可重载 |
需要注意的是,表中并未列出const_cast、static_cast、dynamic_cast、reinterpret_cast、typeid、sizeof...、noexcept及alignof等运算符,因为它们的使用形式与函数调用相同,不会出现歧义。
实战要点速记
结合上文与仓库中相关的变量与类型、位运算、指针、引用等章节,可提炼出几条在 OI 实战中最容易踩坑、也最值得记忆的要点:
- 整数除法与类型转换:
int/int结果为整数(向零截断),至少一个操作数为浮点类型结果才为浮点;%只适用于整数操作数。 - 位运算优先级低:
&、^、|低于比较运算符,混用位运算与比较/算术运算时务必加括号。 - 移位安全:移位数必须小于左操作数的位宽且非负;带符号负数的左移、结果溢出类型的左移均为未定义行为;右移对负数向下取整,与除法向零取整不同。
==与=不可混用:判断语句中写=会造成条件恒真,属于隐蔽 bug。- 短路求值:内建
&&、||短路,可安全地组合边界检查与数组访问;重载版本不短路。 - 前后缀自增减:单独使用时无区别,参与表达式时后缀"先用后增"、前缀"先增后用"。
- 逗号表达式优先级最低:若要整体取值需用括号包住。
以上全部内容来自 OI-wiki 的 docs/lang/op.md 及其关联章节,可作为日常查阅 C++ 运算符语义与优先级的速查手册。
【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. (某大型游戏线上攻略,内含炫酷算术魔法)项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考