二进制基础与位权
计算机内部的所有数据都以 二进制 (Binary) 形式存储和处理,即只包含 0 和 1。
二进制数的每一位都有一个 位权 (Weight),表示该位代表的数值大小。位权是 2 的幂次方,从右到左(从低位到高位)依次为 20, 21, 22, ...
位置 (Index) | ... 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 ← 最低位 (LSB) |
---|---|---|---|---|---|---|---|---|
位权 (2n) | ... 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
示例 (00110102) | ... 0 | 0 | 1 | 1 | 0 | 1 | 0 | 0 |
计算 | ... | 0×64 | 1×32 | 1×16 | 0×8 | 1×4 | 0×2 | 0×1 |
示例:001101002 = (0×128) + (0×64) + (1×32) + (1×16) + (0×8) + (1×4) + (0×2) + (0×1) = 32 + 16 + 4 = 5210
关键点: 位运算直接操作这些二进制位,理解位权是进行位运算的基础。
常用位运算符
位运算符直接对整数的二进制位进行操作。以下是 C++ 中常用的位运算符:
&
位与 (AND): 两个操作数的对应位都为 1 时,结果位才为 1,否则为 0。
|
位或 (OR): 两个操作数的对应位只要有一个为 1,结果位就为 1,否则为 0。
^
位异或 (XOR): 两个操作数的对应位不同时,结果位为 1,相同时结果位为 0。
~
位取反 (NOT): 对操作数的每一位取反,0 变 1,1 变 0。(这是一个一元运算符)
<<
左移 (Left Shift): 将操作数的所有位向左移动指定的位数,右侧空出的位补 0。
>>
右移 (Right Shift): 将操作数的所有位向右移动指定的位数。对于无符号数,左侧补 0;对于有符号数,通常补符号位(算术右移)。
运算规则示例 (假设 4 位):
A | B | A & B | A | B | A ^ B | ~A (结果可能不止4位) | A << 1 | A >> 1 |
---|---|---|---|---|---|---|---|
1010 (10) | 1100 (12) | 1000 (8) | 1110 (14) | 0110 (6) | ...0101 (?) | 0100 (4, 高位溢出) | 0101 (5) |
记忆技巧: 与(&&) -> 全1才1;或(||) -> 有1就1;异或(^) -> 不同为1;左移(<<) -> 乘2n;右移(>>) -> 除2n (近似)。
位运算的实际应用
位运算在底层编程、性能优化、算法设计等方面有广泛应用:
1. 状态压缩与掩码操作
用一个整数的不同位来表示多个独立的布尔状态(开关),非常节省空间且高效。
- 设置状态:
flags |= STATUS_A;
(将 STATUS_A 对应的位置 1) - 清除状态:
flags &= ~STATUS_A;
(将 STATUS_A 对应的位置 0) - 切换状态:
flags ^= STATUS_A;
(翻转 STATUS_A 对应的位) - 检查状态:
if (flags & STATUS_A) { ... }
(检查 STATUS_A 对应的位是否为 1) - 示例: 文件权限 (读/写/执行)、图形界面选项、游戏角色 Buff
// 假设定义了掩码
#define READ_PERMISSION 0b001 // 1
#define WRITE_PERMISSION 0b010 // 2
#define EXEC_PERMISSION 0b100 // 4
int file_flags = 0; // 初始无权限
file_flags |= READ_PERMISSION; // 添加读权限 (flags = 1)
file_flags |= WRITE_PERMISSION; // 添加写权限 (flags = 3)
if (file_flags & READ_PERMISSION) { /* 有读权限 */ }
file_flags &= ~WRITE_PERMISSION; // 移除写权限 (flags = 1)
2. 性能优化
位运算通常比对应的算术运算(乘、除、模)速度更快,因为它们更接近硬件指令。
- 乘以 2n:
x << n
- 除以 2n (取整):
x >> n
(注意有符号数的右移) - 判断奇偶:
x & 1
(结果为 1 是奇数, 0 是偶数) - 取模 2n:
x & ( (1 << n) - 1 )
(例如x % 8
等价于x & 7
)
3. 算法技巧
- 交换两个数 (不使用临时变量):
a ^= b; b ^= a; a ^= b;
- 快速计算二进制中 1 的个数 (Hamming Weight)
- 集合运算 (位图)
关键点: 位运算是底层优化的利器,但也可能降低代码可读性,需权衡使用。
注意事项与陷阱
使用位运算时需要注意以下几点:
1. 符号位 (Signed vs Unsigned)
- 对 有符号整数 (signed int) 进行位运算,特别是右移 (
>>
) 和取反 (~
),行为可能依赖于具体的实现(例如,右移是算术右移还是逻辑右移)。 - 负数的二进制表示通常是 补码 (Two's Complement)。
- 为了避免不确定性,进行位掩码、状态管理等操作时,推荐使用 无符号整数 (unsigned int)。
// 示例:有符号数右移 (行为可能依赖编译器)
int signed_num = -8; // 补码: ...11111000 (假设8位)
int shifted_signed = signed_num >> 1; // 可能结果: -4 (...11111100), 算术右移
unsigned int unsigned_num = 248; // ...11111000 (假设8位)
unsigned int shifted_unsigned = unsigned_num >> 1; // 结果: 124 (...01111100), 逻辑右移
2. 运算符优先级
- 位运算符的优先级通常 低于 算术运算符,且
&
,^
,|
的优先级不同。 &
>^
>|
- 位运算符的优先级也 低于 关系运算符 (
==
,!=
,<
,>
)。 - 强烈建议: 在复杂的表达式中,总是使用 括号 () 来明确运算顺序,避免混淆。
// 错误示例: 优先级导致意外结果
if (flags & MASK == MASK) { ... } // 实际计算: flags & (MASK == MASK)
// 正确示例: 使用括号
if ((flags & MASK) == MASK) { ... }
3. 整数溢出
- 左移 (
<<
) 操作可能导致数据溢出,高位会被丢弃。 - 对有符号整数的溢出行为是未定义的。
关键点: 优先使用无符号整数,多用括号明确优先级,警惕有符号数的位运算陷阱。