位运算符 &、|、^ 分别用于检测、置位、翻转特定位;常见错误是忽略运算符优先级导致误判,如 flags & 0x04 == 0x04 应写为 (flags & 0x04) == 0x04;置第 n 位用 flags |= (1U << n)。

怎么用 & | ^ 做开关位和翻转位
位运算不是炫技,是直接改内存里某个 bit 的唯一办法。比如控制硬件寄存器、压缩标志位、实现布隆过滤器——& 用来“查”某一位是否为 1,| 用来“开”某一位,^ 用来“翻”某一位。
常见错误:写成 flags & 0x04 == 0x04,结果因为优先级问题变成 (flags & 0x04) == 0x04 是对的,但很多人漏括号,实际执行的是 flags & (0x04 == 0x04) → flags & 1,全错。
- 开第 n 位(从 0 开始):
flags |= (1U —— 一定要用 <code>U后缀防符号扩展 - 关第 n 位:
flags &= ~(1U —— <code>~是按位取反,不是逻辑非! - 翻第 n 位:
flags ^= (1U - 查第 n 位:
(flags & (1U —— 别直接用作 bool,某些编译器会警告
std::bitset 什么时候该用,什么时候别碰
std::bitset 适合编译期确定大小、需要可读性高、且不频繁修改的场景,比如状态机标志、协议字段解析。但它不是万能替代品:底层是静态数组,不能动态 resize;拷贝开销比裸 uint32_t 大;部分老编译器(如 GCC 4.8)对 bitset::operator[] 生成的代码不够优化。
- 用它:定义固定 16 个标志位,要支持
.set(3)、.test(7)这种语义清晰的操作 - 别用它:循环里高频读写单个 bit(不如移位+掩码快),或位数超 128(栈上占空间大,
bitset<1024>就 128 字节) - 注意:
bitset::to_ulong()在位数 >sizeof(unsigned long)*8时抛std::overflow_error,to_ullong()同理
为什么 __builtin_popcount 比手写循环快十倍
因为它是编译器内建函数,直接翻译成 CPU 的 POPCNT 指令(Intel/AMD x86-64 支持),一个指令周期搞定 64 位计数。手写 while(n) { count += n&1; n>>=1; } 要至少 64 次分支+移位,现代 CPU 流水线全卡住。
立即学习“C++免费学习笔记(深入)”;
- GCC/Clang 专属:
__builtin_popcount(x)(unsigned int)、__builtin_popcountll(x)(unsigned long long) - MSVC 用
__popcnt/__popcnt64,需包含<intrin.h>,且要确认 CPU 支持(可用__cpuid查) - 跨平台封装建议:宏判断编译器,fallback 到查表法(256 字节表 + 四次查表)比循环快得多
- 注意:输入为 0 时返回 0,没问题;但传负数给
__builtin_popcount是未定义行为(它只认无符号语义)
左移右移陷阱:为什么 1 在 32 位 <code>int 上是未定义行为
C++ 标准规定:对有符号整数做移位,若结果无法用该类型表示,就是未定义行为(UB)。int 通常是 32 位补码,最大正数是 231−1,1 等于 2<sup>31</sup>,溢出,编译器可以生成任意代码——包括看似正常但实际在某些优化等级下崩溃。
- 永远用无符号类型做移位:
1U 、<code>1ULL - 右移有符号数是算术右移(符号位填充),但标准只要求“实现定义”,不同平台可能不同;无符号右移
>>一定是逻辑右移(高位补 0) - 移位位数不能 ≥ 类型宽度,
1U 对 <code>uint32_t是 UB,哪怕你用的是 64 位系统 - Clang/GCC 加
-fsanitize=undefined能捕获这类问题,开发期务必开
位运算真正难的不是写法,是记住哪些操作在什么条件下会垮掉——尤其是跨平台、跨编译器、开 O2 之后。别信“我本地跑得通”,得看标准怎么写的,再看生成的汇编是不是真用了 POPCNT 或 BTS。









