C++ 位运算从入门到精通(全知识点+面试题+实战应用)
一、位运算基础概念
位运算是直接对二进制位(bit)进行操作的运算,是计算机底层最基础、最高效的运算方式。在嵌入式开发、高性能算法、网络协议、加密解密、面试高频考点中都有极其广泛的应用。C++ 提供了 6 种基础位运算符,所有运算均以二进制补码形式进行,不涉及浮点数运算,仅针对整数类型(char、short、int、long、long long 等)生效。
1.1 二进制与补码基础
计算机中所有整数均以补码形式存储和运算,这是因为补码能统一正数和负数的运算规则,避免出现"正零"和"负零"的歧义,同时简化减法运算(减法可转化为加法)。
核心规则
- 正数:原码 = 反码 = 补码。例如:int 类型的 5(32位),原码、反码、补码均为 00000000 00000000 00000000 00000101。
- 负数:反码 = 原码符号位不变,其余各位按位取反;补码 = 反码 + 1。例如:int 类型的 -5,原码为 10000000 00000000 00000000 00000101,反码为 11111111 11111111 11111111 11111010,补码为 11111111 11111111 11111111 11111011。
- 符号位:二进制最高位为符号位,0 表示正数,1 表示负数;32位 int 类型的取值范围是 -2³¹ ~ 2³¹ - 1,其中 -2³¹ 是唯一无法用原码和反码表示的数,只能用补码表示。
位运算的核心优势的是效率极高------CPU 执行位运算指令无需复杂的算术运算(如乘除、取模),直接操作内存中的二进制位,速度比普通算术运算快 10~100 倍,尤其适合对性能要求极高的场景。
1.2 六种基础位运算符(重点)
C++ 提供 6 种基础位运算符,优先级从高到低依次为:~(按位取反)> <<、>>(移位)> &(按位与)> ^(按位异或)> |(按位或)。所有运算符均针对二进制补码进行操作,具体规则如下:
| 运算符 | 名称 | 作用 | 运算规则 | 示例(x=5=0101,y=3=0011) |
|--------|------|------|----------|----------------------------|
| & | 按位与 | 对两个数的二进制位逐位进行"与"操作 | 两位均为 1,结果为 1;否则为 0 | x & y = 0001(1) |
| | | 按位或 | 对两个数的二进制位逐位进行"或"操作 | 任意一位为 1,结果为 1;否则为 0 | x | y = 0111(7) |
| ^ | 按位异或 | 对两个数的二进制位逐位进行"异或"操作 | 两位相同为 0,不同为 1 | x ^ y = 0110(6) |
| ~ | 按位取反 | 对一个数的二进制位逐位取反(包括符号位) | 0 变 1,1 变 0 | ~x = 1010(补码,对应-6) |
| << | 左移 | 将二进制位整体向左移动 n 位 | 高位丢弃,低位补 0 | x << 1 = 1010(10) |
| >> | 右移 | 将二进制位整体向右移动 n 位 | 正数:高位补 0;负数:高位补 1(算术右移) | x >> 1 = 0010(2) |
注意事项
- 移位运算中,移位的位数不能为负数,也不能超出当前数据类型的宽度(如 32 位 int 类型,移位位数不能 ≥32),否则行为未定义(不同编译器表现不同)。
- 按位取反
~是单目运算符,对正数取反后结果为负数,对负数取反后结果为正数,公式:~x = -x - 1。 - 位运算仅针对整数类型,若对浮点数使用位运算,编译器会报错(需强制转换为整数后再操作)。
二、六种基础位运算符详解(附实战代码)
2.1 按位与 &(最常用)
核心性质
- 任何数 & 0 = 0(清零);
- 任何数 & 自身 = 自身(保留原值);
- 按位与具有"掩码"作用,可用于提取某几位、判断某一位是否为 1。
高频应用场景
- 判断一个整数的奇偶性(最经典用法)
原理:整数的二进制最低位为 1 时是奇数,为 0 时是偶数。用x & 1可快速判断------结果为 1 则是奇数,为 0 则是偶数。
代码示例:
cpp
#include <iostream>
using namespace std;
// 判断奇偶性
bool isOdd(int x) {
return x & 1; // 等价于 x % 2 == 1,但效率更高
}
int main() {
cout << isOdd(5) << endl; // 1(奇数)
cout << isOdd(6) << endl; // 0(偶数)
return 0;
}
- 清零二进制中最低位的 1
原理:x & (x - 1)会将 x 二进制中最低位的 1 变为 0,其余位保持不变。这是位运算中最核心的技巧之一,广泛用于统计 1 的个数、判断 2 的幂等场景。
代码示例:
cpp
#include <iostream>
using namespace std;
// 清零最低位的 1
int clearLowestOne(int x) {
return x & (x - 1);
}
int main() {
int x = 6; // 二进制 0110
cout << clearLowestOne(x) << endl; // 4(0100,最低位1被清零)
x = 5; // 0101
cout << clearLowestOne(x) << endl; // 4(0100)
return 0;
}
- 提取二进制中的特定位
原理:用一个"掩码"(mask)与目标数进行按位与,掩码中需要保留的位设为 1,其余位设为 0,即可提取出目标位。
示例:提取 int 类型 x 的第 3 位(从 0 开始计数,最低位为第 0 位)。
代码示例:
cpp
#include <iostream>
using namespace std;
// 提取第 k 位(0-based)
int getKthBit(int x, int k) {
return (x & (1 << k)) != 0; // 1<<k 生成掩码,只有第k位为1
}
int main() {
int x = 10; // 二进制 1010
cout << getKthBit(x, 1) << endl; // 1(第1位是1)
cout << getKthBit(x, 2) << endl; // 0(第2位是0)
return 0;
}
2.2 按位或 |
核心性质
- 任何数 | 0 = 自身;
- 任何数 | 自身 = 自身;
- 按位或可用于"置位",即将某一位强制设为 1,其余位保持不变。
高频应用场景
- 将二进制中某一位设为 1
原理:用1 << k生成掩码(第 k 位为 1),与目标数进行按位或,即可将第 k 位设为 1,其余位不变。
代码示例:
cpp
#include <iostream>
using namespace std;
// 将第 k 位设为 1(0-based)
int setKthBit(int x, int k) {
return x | (1 << k);
}
int main() {
int x = 5; // 0101
cout << setKthBit(x, 2) << endl; // 9(1001,第2位设为1)
cout << setKthBit(x, 3) << endl; // 13(1101,第3位设为1)
return 0;
}
- 合并多个标志位
在实际开发中,常用位来表示多个独立的状态(标志位),用按位或合并多个标志位,实现多状态共存。
代码示例(权限控制):
cpp
#include <iostream>
using namespace std;
// 定义权限标志位(每一位代表一种权限)
const int READ = 1 << 0; // 第0位:读权限(1)
const int WRITE = 1 << 1; // 第1位:写权限(2)
const int EXEC = 1 << 2; // 第2位:执行权限(4)
// 合并权限
int addPerm(int perm, int p) {
return perm | p;
}
int main() {
int perm = 0; // 初始无权限
perm = addPerm(perm, READ); // 增加读权限:001
perm = addPerm(perm, WRITE); // 增加写权限:011(3)
cout << perm << endl; // 3
return 0;
}
2.3 按位异或 ^(最灵活)
按位异或是位运算中最灵活的运算符,核心特性是"相同为 0,不同为 1",且满足交换律、结合律,具有可逆性。
核心性质
- x ^ x = 0(任何数与自身异或,结果为 0);
- x ^ 0 = x(任何数与 0 异或,结果为自身);
- 交换律:a ^ b = b ^ a;
- 结合律:(a ^ b) ^ c = a ^ (b ^ c);
- 可逆性:若 a ^ b = c,则 a = c ^ b,b = c ^ a。
高频应用场景
- 交换两个整数(无需临时变量)
原理:利用异或的可逆性,无需额外临时变量即可交换两个数的值,效率极高,是面试中常见的写法。
代码示例:
cpp
#include <iostream>
using namespace std;
// 异或交换两个整数(无临时变量)
void swap(int& a, int& b) {
if (a == b) return; // 避免 a == b 时,异或后变为 0
a ^= b; // a = a ^ b
b ^= a; // b = b ^ (a ^ b) = a
a ^= b; // a = (a ^ b) ^ a = b
}
int main() {
int a = 5, b = 10;
swap(a, b);
cout << "a = " << a <<, "b = " << b << endl; // a=10, b=5
return 0;
}
注意:当 a 和 b 指向同一块内存(如 swap(x, x))时,会导致 x 变为 0,因此需添加 a == b 的判断。
- 翻转二进制中的某一位
原理:用 1 << k 生成掩码,与目标数异或------若第 k 位为 0,则变为 1;若为 1,则变为 0,实现翻转。
代码示例:
cpp
#include <iostream>
using namespace std;
// 翻转第 k 位(0-based)
int flipKthBit(int x, int k) {
return x ^ (1 << k);
}
int main() {
int x = 5; // 0101
cout << flipKthBit(x, 0) << endl; // 4(0100,第0位翻转)
cout << flipKthBit(x, 1) << endl; // 7(0111,第1位翻转)
return 0;
}
- 找出数组中唯一出现一次的数字
题目:数组中只有一个数字出现一次,其余数字均出现两次,找出这个唯一的数字(面试高频题)。
原理:利用 x ^ x = 0 和 x ^ 0 = x 的性质,将数组中所有元素异或,出现两次的数字会相互抵消(结果为 0),最终结果就是唯一出现一次的数字。
代码示例:
cpp
#include <iostream>
#include <vector>
using namespace std;
int singleNumber(vector<int>& nums) {
int res = 0;
for (int x : nums) {
res ^= x; // 所有元素异或,抵消出现两次的数字
}
return res;
}
int main() {
vector<int> nums = {2, 3, 2, 4, 4};
cout << singleNumber(nums) << endl; // 3(唯一出现一次的数字)
return 0;
}
2.4 按位取反 ~
按位取反是单目运算符,对二进制的每一位(包括符号位)进行取反,0 变 1,1 变 0。由于计算机存储的是补码,因此取反后的结果需要结合补码规则解读。
核心规律
对于整数 x,~x = -x - 1(无论 x 是正数还是负数,均成立)。
示例:
- x = 5(补码 00000101),~x = 11111010(补码),对应十进制 -6,满足 -5 -1 = -6;
- x = -5(补码 11111011),~x = 00000100(补码),对应十进制 4,满足 -(-5) -1 = 4。
应用场景
- 生成全 1 的掩码
例如,生成 32 位全 1 的掩码(用于提取所有位),可使用~0(0 的补码是全 0,取反后是全 1)。
代码示例:
cpp
#include <iostream>
using namespace std;
int main() {
int mask = ~0; // 32位全1,对应十进制 -1
cout << mask << endl; // -1
// 生成低 k 位全 1 的掩码(如 k=3,掩码为 00000111)
int k = 3;
int lowMask = (1 << k) - 1; // 等价于 ~(~0 << k)
cout << lowMask << endl; // 7(00000111)
return 0;
}
- 快速计算负数的绝对值(辅助作用)
结合取反和加法,可快速计算负数的绝对值,公式:abs(x) = ~x + 1(仅对负数有效)。
代码示例:
cpp
#include <iostream>
using namespace std;
int absNeg(int x) {
if (x < 0) {
return ~x + 1; // 负数取反加1,得到绝对值
}
return x;
}
int main() {
cout << absNeg(-5) << endl; // 5
cout << absNeg(5) << endl; // 5
return 0;
}
2.5 左移 <<
左移运算将二进制位整体向左移动 n 位,高位丢弃,低位补 0。左移 n 位等价于乘以 2ⁿ(前提是不发生溢出),效率远高于乘法运算。
核心规律
x << n = x * 2ⁿ(x 为正数,且不溢出)。
示例:
- 5 << 1 = 10(5 * 2¹);
- 5 << 2 = 20(5 * 2²);
- 5 << 3 = 40(5 * 2³)。
注意事项
- 溢出风险:左移可能导致数值超出当前数据类型的取值范围,溢出后行为未定义。例如,32 位 int 类型的最大值是 2147483647(0x7FFFFFFF),左移 1 位后变为 0xFFFFFFFE,对应十进制 -2,发生溢出。
- 左移位数不能为负数,也不能超出数据类型宽度(如 32 位 int 不能左移 ≥32 位)。
应用场景
- 快速乘法(乘以 2 的幂)
在高性能算法中,常用左移替代乘以 2 的幂,提升运算速度。
代码示例:
cpp
#include <iostream>
using namespace std;
// 快速乘以 2^n
int fastMultiply(int x, int n) {
return x << n;
}
int main() {
cout << fastMultiply(5, 1) << endl; // 10(5*2)
cout << fastMultiply(5, 3) << endl; // 40(5*8)
return 0;
}
- 生成掩码
左移可快速生成"某一位为 1,其余位为 0"的掩码,用于置位、提取位等操作(前面已多次用到)。
2.6 右移 >>
右移运算将二进制位整体向右移动 n 位,分为两种类型:
- 算术右移(主流编译器默认):正数高位补 0,负数高位补 1(保持符号不变);
- 逻辑右移:无论正数还是负数,高位均补 0(仅部分编译器支持,如 GCC 需加
-funsigned-bitfields选项)。
核心规律
x >> n = x / 2ⁿ(向下取整,适用于正数和负数)。
示例:
- 10 >> 1 = 5(10 / 2 = 5,向下取整);
- 9 >> 1 = 4(9 / 2 = 4.5,向下取整);
- -10 >> 1 = -5(-10 / 2 = -5);
- -9 >> 1 = -5(-9 / 2 = -4.5,向下取整为 -5)。
应用场景
- 快速除法(除以 2 的幂)
与左移对应,右移可快速实现除以 2 的幂,效率高于除法运算。
代码示例:
cpp
#include <iostream>
using namespace std;
// 快速除以 2^n(向下取整)
int fastDivide(int x, int n) {
return x >> n;
}
int main() {
cout << fastDivide(10, 1) << endl; // 5
cout << fastDivide(9, 1) << endl; // 4
cout << fastDivide(-9, 1) << endl; // -5
return 0;
}
- 二进制位遍历
通过右移,可逐位遍历二进制的每一位,用于统计 1 的个数、判断某一位是否为 1 等操作。
代码示例(遍历二进制每一位):
cpp
#include <iostream>
using namespace std;
// 遍历 x 的每一位(32位)
void printBits(int x) {
for (int i = 31; i >= 0; i--) {
// 右移 i 位,提取第 i 位
cout << ((x >> i) & 1);
if (i % 4 == 0) cout << " ";
}
cout << endl;
}
int main() {
int x = 10; // 00000000 00000000 00000000 00001010
printBits(x);
return 0;
}
三、位运算经典技巧(面试必备)
位运算的核心价值在于"高效"和"简洁",以下是面试中高频出现的经典技巧,涵盖判断、统计、计算等场景,全部附带可直接运行的代码。
3.1 判断一个数是否是 2 的幂
核心原理
2 的幂的二进制只有一个 1(如 2=10、4=100、8=1000),因此满足:x > 0 且 x & (x - 1) == 0。
注意:x 必须大于 0,因为 0 和负数不是 2 的幂。
代码示例:
cpp
#include <iostream>
using namespace std;
// 判断 x 是否是 2 的幂
bool isPowerOfTwo(int x) {
return x > 0 && (x & (x - 1)) == 0;
}
int main() {
cout << isPowerOfTwo(8) << endl; // 1(是)
cout << isPowerOfTwo(6) << endl; // 0(不是)
cout << isPowerOfTwo(0) << endl; // 0(不是)
cout << isPowerOfTwo(-4) << endl; // 0(不是)
return 0;
}
3.2 统计二进制中 1 的个数(汉明重量)
方法一:循环清零最低位 1(最优解)
原理:利用 x & (x - 1) 清零最低位的 1,每执行一次,计数器加 1,直到 x 变为 0,计数器的值就是 1 的个数。
代码示例:
cpp
#include <iostream>
using namespace std;
// 统计 x 二进制中 1 的个数
int countOneBits(int x) {
int cnt = 0;
while (x) {
x &= x - 1; // 清零最低位的 1
cnt++;
}
return cnt;
}
int main() {
cout << countOneBits(5) << endl; // 2(0101)
cout << countOneBits(7) << endl; // 3(0111)
cout << countOneBits(-1) << endl; // 32(32位全1)
return 0;
}
方法二:逐位遍历(兼容所有场景)
原理:通过右移逐位判断每一位是否为 1,适合对负数也需要统计所有位(包括符号位)的场景。
代码示例:
cpp
#include <iostream>
using namespace std;
int countOneBits(int x) {
int cnt = 0;
for (int i = 0; i < 32; i++) {
if ((x >> i) & 1) {
cnt++;
}
}
return cnt;
}
int main() {
cout << countOneBits(5) << endl; // 2
cout << countOneBits(-1) << endl; // 32
return 0;
}
3.3 获取二进制中最低位的 1(树状数组核心)
核心原理
x & -x(利用补码特性),可快速获取最低位的 1 所在的位置,返回值是"只有最低位 1"的数。
示例:
- x = 6(0110),-x = 11111010(补码),x & -x = 0010(2);
- x = 5(0101),-x = 11111011(补码),x & -x = 0001(1);
- x = 8(1000),-x = 11111000(补码),x & -x = 1000(8)。
代码示例:
cpp
#include <iostream>
using namespace std;
// 获取最低位的 1
int lowBit(int x) {
return x & -x;
}
int main() {
cout << lowBit(6) << endl; // 2
cout << lowBit(5) << endl; // 1
cout << lowBit(8) << endl; // 8
return 0;
}
应用场景
lowBit 是树状数组(Fenwick Tree)的核心操作,用于快速更新和查询前缀和,在算法竞赛中广泛应用。
3.4 反转二进制位(面试高频题)
题目:将一个 32 位无符号整数的二进制位反转,例如:输入 00000010100101000001111010011100,输出 00111001011110000010100101000000。
核心思路
逐位提取原数的每一位,依次放到结果的对应位置,通过左移和或运算拼接结果。
代码示例:
cpp
#include <iostream>
using namespace std;
// 反转 32 位无符号整数的二进制位
unsigned int reverseBits(unsigned int x) {
unsigned int res = 0;
for (int i = 0; i < 32; i++) {
res = (res << 1) | (x & 1); // 提取x的最低位,放到res的最低位
x >>= 1; // x右移,处理下一位
}
return res;
}
int main() {
unsigned int x = 0b00000010100101000001111010011100;
cout << reverseBits(x) << endl; // 输出 0b00111001011110000010100101000000 对应的十进制
return 0;
}