C++ STL之bitset位操作详解:从使用到底层,再到面试八股

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 的工程应用。

五、常见坑

  1. 下标顺序b[0] 是最低位(LSB),输出时从左到右是高位到低位,cout << b 打印 高位...低位
  2. 越界不抛b[i] 不检查边界,b.test(i) 才抛异常。
  3. 栈空间bitset<100000000> 约 12MB 会爆栈,大位图请用 vector<bool> 或堆分配。
  4. 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 元组详解。