C++ STL之bitset位操作详解:从使用到底层,再到面试八股
本文面向面试和日常开发,先讲调用,再讲原理,最后给口语化面试答案。
一、bitset 是什么
bitset 是 C++ 标准库提供的编译期定长位集容器。它在栈上存储,完全内联,没有堆分配,也没有迭代器。核心区别一句话:
| 维度 | bitset<N> |
vector<bool> |
|---|---|---|
| 长度 | 编译期常量 | 运行时动态 |
| 存储 | 栈上紧凑位存储 | 动态堆分配(专业版位压缩) |
| 迭代器 | 无 | 有(代理迭代器) |
| 性能 | 零运行时开销,直接位运算 | 间接访问,有代理对象开销 |
vector<bool> 虽然也做了位压缩,但它不是标准容器------标准明确规定 vector<bool> 不是 vector,它的 operator[] 返回的是代理对象而非引用。
二、用法速查
2.1 初始化与基本操作
cpp
#include <bitset>
#include <iostream>
#include <string>
using namespace std;
int main() {
bitset<8> b1; // 00000000,全 0
bitset<8> b2(42); // 00101010,从无符号 long 构造
bitset<8> b3("10101010"); // 10101010,从字符串构造
bitset<8> b4("1010", 4); // 00001010,取前 4 位
bitset<8> b5("10101010", 2, 4); // 00001010,从 pos=2 取 4 位
cout << b2 << "\n"; // 00101010
cout << b3 << "\n"; // 10101010
}
2.2 访问与修改
cpp
#include <bitset>
#include <iostream>
using namespace std;
int main() {
bitset<8> b(0b01101001);
cout << b[0] << "\n"; // 1(最低位,从 0 开始)
cout << b.test(3) << "\n"; // 0,带越界检查,越界抛 out_of_range
cout << b.count() << "\n"; // 4,置位(值为1)的位数
cout << b.size() << "\n"; // 8,总位数
cout << b.any() << "\n"; // 1,是否存在 1
cout << b.none() << "\n"; // 0,是否全 0
cout << b.all() << "\n"; // 0,是否全 1
b.set(); // 11111111
b.set(3, false); // 11110111,将位 3 置 0
b.set(2); // 11110111 | 00000100 = 11111111
b.reset(); // 00000000
b.reset(5); // 对全 0 的位 5 置 0,无影响
b.flip(); // 11111111
b.flip(2); // 11111011
}
2.3 位运算
cpp
#include <bitset>
#include <iostream>
using namespace std;
int main() {
bitset<4> a("1100");
bitset<4> b("1010");
cout << (a & b) << "\n"; // 1000 与
cout << (a | b) << "\n"; // 1110 或
cout << (a ^ b) << "\n"; // 0110 异或
cout << (~a) << "\n"; // 0011 取反(对全部 N 位)
cout << (a << 1) << "\n"; // 1000 左移,低位补 0
cout << (b >> 1) << "\n"; // 0101 右移,高位补 0
}
2.4 类型转换
cpp
#include <bitset>
#include <iostream>
#include <string>
using namespace std;
int main() {
bitset<8> b(170); // 10101010
unsigned long ul = b.to_ulong(); // 170
unsigned long long ull = b.to_ullong(); // C++11,170
string s = b.to_string(); // "10101010"
// 可指定字符
string t = b.to_string('Y', 'N'); // "YNYNYNYN"
}
三、核心特性
3.1 编译期定长
bitset<N> 的 N 必须是编译期常量表达式。编译器将整个位集展开为一个整数数组(或一个超大整数),所有操作无虚函数、无堆分配,可以极致内联。
cpp
constexpr size_t N = 1024;
bitset<N> b; // OK,N 是 constexpr
size_t n = 1024;
// bitset<n> b2; // 编译错误!n 不是编译期常量
3.2 bitset vs 手写位操作 vs vector
| 场景 | 推荐 | 原因 |
|---|---|---|
| 定长标记、掩码 | bitset |
安全、可读、零开销 |
| 动态长度位图 | vector<bool> |
运行时才知道大小 |
| 高性能计算 | 手写 uint64_t[] |
可 SIMD 向量化 |
| 海量数据去重 | std::vector<bool> 或自定义位图 |
bitset 栈上限约 1MB |
关于栈溢出 :bitset<1000000> 约 125KB,通常在栈上没问题。bitset<10000000> 约 1.19MB,可能爆栈。
四、面试高频题
题 1:判断一个数是否是 2 的幂
cpp
bool isPowerOfTwo(int n) {
return n > 0 && (n & (n - 1)) == 0;
}
用 bitset 也能写,但位运算本身就够了。bitset 的考点不在这里,继续看。
题 2:统计二进制中 1 的个数
cpp
size_t countOnes(unsigned int n) {
return bitset<32>(n).count();
}
bitset::count() 内部用 __builtin_popcount 或硬件指令(POPCNT),O(1) 或 O(log位数),远快于手写循环。
题 3:二进制翻转(镜像反转)
cpp
#include <bitset>
#include <iostream>
using namespace std;
unsigned int reverseBits(unsigned int n) {
bitset<32> b(n);
bitset<32> rev;
for (size_t i = 0; i < 32; ++i)
rev[i] = b[31 - i];
return rev.to_ulong();
}
题 4:用 bitset 实现埃拉托色尼质数筛
cpp
#include <bitset>
#include <iostream>
#include <cmath>
using namespace std;
const int N = 1000000;
bitset<N + 1> isPrime;
void sieve() {
isPrime.set(); // 先假设全是质数
isPrime[0] = isPrime[1] = false; // 0 和 1 不是
for (int i = 2; i * i <= N; ++i) {
if (isPrime[i]) {
for (int j = i * i; j <= N; j += i)
isPrime[j] = false;
}
}
}
int countPrimes(int n) {
sieve();
int cnt = 0;
for (int i = 2; i <= n; ++i)
if (isPrime[i]) ++cnt;
return cnt;
}
bitset 筛的优势 :每个标记只占 1 位,内存 = N/8 字节(1M 的筛仅 125KB),set/reset 直接对应位操作,性能优于 vector<bool> 和 vector<char>。
#mermaid-svg-X9J2FonvKcuFHxlP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-X9J2FonvKcuFHxlP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-X9J2FonvKcuFHxlP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-X9J2FonvKcuFHxlP .error-icon{fill:#552222;}#mermaid-svg-X9J2FonvKcuFHxlP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-X9J2FonvKcuFHxlP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-X9J2FonvKcuFHxlP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-X9J2FonvKcuFHxlP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-X9J2FonvKcuFHxlP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-X9J2FonvKcuFHxlP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-X9J2FonvKcuFHxlP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-X9J2FonvKcuFHxlP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-X9J2FonvKcuFHxlP .marker.cross{stroke:#333333;}#mermaid-svg-X9J2FonvKcuFHxlP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-X9J2FonvKcuFHxlP p{margin:0;}#mermaid-svg-X9J2FonvKcuFHxlP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-X9J2FonvKcuFHxlP .cluster-label text{fill:#333;}#mermaid-svg-X9J2FonvKcuFHxlP .cluster-label span{color:#333;}#mermaid-svg-X9J2FonvKcuFHxlP .cluster-label span p{background-color:transparent;}#mermaid-svg-X9J2FonvKcuFHxlP .label text,#mermaid-svg-X9J2FonvKcuFHxlP span{fill:#333;color:#333;}#mermaid-svg-X9J2FonvKcuFHxlP .node rect,#mermaid-svg-X9J2FonvKcuFHxlP .node circle,#mermaid-svg-X9J2FonvKcuFHxlP .node ellipse,#mermaid-svg-X9J2FonvKcuFHxlP .node polygon,#mermaid-svg-X9J2FonvKcuFHxlP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-X9J2FonvKcuFHxlP .rough-node .label text,#mermaid-svg-X9J2FonvKcuFHxlP .node .label text,#mermaid-svg-X9J2FonvKcuFHxlP .image-shape .label,#mermaid-svg-X9J2FonvKcuFHxlP .icon-shape .label{text-anchor:middle;}#mermaid-svg-X9J2FonvKcuFHxlP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-X9J2FonvKcuFHxlP .rough-node .label,#mermaid-svg-X9J2FonvKcuFHxlP .node .label,#mermaid-svg-X9J2FonvKcuFHxlP .image-shape .label,#mermaid-svg-X9J2FonvKcuFHxlP .icon-shape .label{text-align:center;}#mermaid-svg-X9J2FonvKcuFHxlP .node.clickable{cursor:pointer;}#mermaid-svg-X9J2FonvKcuFHxlP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-X9J2FonvKcuFHxlP .arrowheadPath{fill:#333333;}#mermaid-svg-X9J2FonvKcuFHxlP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-X9J2FonvKcuFHxlP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-X9J2FonvKcuFHxlP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-X9J2FonvKcuFHxlP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-X9J2FonvKcuFHxlP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-X9J2FonvKcuFHxlP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-X9J2FonvKcuFHxlP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-X9J2FonvKcuFHxlP .cluster text{fill:#333;}#mermaid-svg-X9J2FonvKcuFHxlP .cluster span{color:#333;}#mermaid-svg-X9J2FonvKcuFHxlP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-X9J2FonvKcuFHxlP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-X9J2FonvKcuFHxlP rect.text{fill:none;stroke-width:0;}#mermaid-svg-X9J2FonvKcuFHxlP .icon-shape,#mermaid-svg-X9J2FonvKcuFHxlP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-X9J2FonvKcuFHxlP .icon-shape p,#mermaid-svg-X9J2FonvKcuFHxlP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-X9J2FonvKcuFHxlP .icon-shape .label rect,#mermaid-svg-X9J2FonvKcuFHxlP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-X9J2FonvKcuFHxlP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-X9J2FonvKcuFHxlP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-X9J2FonvKcuFHxlP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
循环结束
初始:isPrime 全部置 1
i 从 2 到 sqrt(N)
isPrimei 为真?
将 i², i²+i, i²+2i... 全部置 0
i 自增
筛选完成
isPrime 中仍为 1 的下标
即为质数
题 5:检测两个整数是否有相同位模式的奇偶性
cpp
bool sameParityOfBits(unsigned int a, unsigned int b) {
return bitset<32>(a).count() % 2 == bitset<32>(b).count() % 2;
}
题 6:用 bitset 做子集枚举
cpp
#include <bitset>
#include <iostream>
using namespace std;
void enumerateSubsets(int arr[], int n) {
for (int mask = 0; mask < (1 << n); ++mask) {
bitset<32> b(mask);
cout << "子集: ";
for (int i = 0; i < n; ++i)
if (b[i]) cout << arr[i] << " ";
cout << "\n";
}
}
这是状态压缩 DP 的经典用法,bitset 让掩码的位操作语义更清晰。
题 7:两个大 bitset 的汉明距离
cpp
size_t hammingDistance(bitset<1024>& a, bitset<1024>& b) {
return (a ^ b).count(); // 异或后数 1 的个数
}
一行搞定。手写大位图异或 + popcount 至少 5 行循环。
题 8:bitset 实现有限状态机
cpp
enum State { S0, S1, S2, S3, STATE_COUNT };
bitset<STATE_COUNT> cur;
void transit() {
bitset<4> next;
// S0 -> S1
if (cur[S0]) next.set(S1);
// ... 其他转移规则
cur = next;
}
适合状态数少、转移规则的组合逻辑较复杂的场景。面试时提到这一点会加分,说明你理解 bitset 的工程应用。
五、常见坑
- 下标顺序 :
b[0]是最低位(LSB),输出时从左到右是高位到低位,cout << b打印高位...低位。 - 越界不抛 :
b[i]不检查边界,b.test(i)才抛异常。 - 栈空间 :
bitset<100000000>约 12MB 会爆栈,大位图请用vector<bool>或堆分配。 - to_ulong 溢出 :如果 bitset 超过
unsigned long位数(通常是 32 或 64),调用to_ulong()会抛overflow_error。
六、面试速记
Q:bitset 和 vector<bool> 的区别?
bitset 长度编译期固定,栈上分配,没有动态开销,支持完整的位运算操作符。vector 运行时动态,堆上分配,特化版本在位压缩的同时保留容器接口(push_back 等),但它的 operator\[\] 返回代理对象,不能取引用,严格来说不是标准容器。
Q:什么场景用 bitset?
固定长度的标记位、有限状态机、状态压缩 DP、小规模质数筛(几百万以内),以及任何需要紧凑位存储+高效位运算的地方。超过运行时才能确定长度、需要动态扩容的场景,老老实实用 vector。
下一期预告:C++ STL 之 tuple 元组详解。