O-1 通过 std::bitset 进行位标志与位操作

在现代计算机架构中,内存的最小寻址单位是字节。由于所有对象都需要拥有唯一的内存地址,这意味着对象的大小至少需达到一个字节。对于大多数变量类型而言,这完全可行。但对于布尔值而言,这种设计略显浪费(双关语)。布尔类型仅有两种状态:真(1)或假(0)。这套状态仅需1位存储空间。但若变量必须至少占用1字节(8位),则意味着布尔值占用1位,其余7位闲置。

在多数情况下这无伤大雅——我们通常不必为节省7位存储空间而纠结(优先优化可读性和可维护性更为明智)。但在存储资源紧张的场景中,将8个独立布尔值“打包”至单字节存储便显得尤为实用。

实现这些操作需要具备在位元层级处理对象的能力。值得庆幸的是,C++ 恰好提供了这样的工具。对对象内部单个位元进行修改的行为,被称为位操作bit manipulation

作者注

位操作在某些编程场景中应用广泛(例如图形处理、加密、压缩、优化),但在常规编程中并不常见。

因此本章内容均为可选阅读。读者可自由跳过或略读,日后再行补读。


位标志

到目前为止,我们一直使用变量来存储单个值:

int foo { 5 }; // assign foo the value 5 (probably uses 32 bits of storage)
std::cout << foo; // print the value 5

然而,与其将对象视为持有单一值,我们不妨将对象中的每个位视为独立的布尔值。当对象的单个位被用作布尔值时,这些位就被称为位标志bit flags

命名法

存储0值的位被称为“假”、“关”或“未设置”。

存储1值的位称为“真”、“开”或“已设置”。

当位从0变为1或从1变为0时,我们称其为“翻转flipped”或“反转inverted”。

顺带一提……

在计算机领域,标志(flag)是一种值,用于指示程序中某条件是否成立。对于位标志而言,真值表示该条件成立。

类似地,在美国许多邮箱侧面都装有小型(通常为红色)的实体标志。当有待投递的邮件等待邮递员收取时,标志会被竖起以示有待寄出的邮件。

要定义一组位标志,通常会使用适当大小的无符号整型(根据标志数量选择8位、16位、32位等)或std::bitset。

#include <bitset> // for std::bitset

std::bitset<8> mybitset {}; // 8 bits in size means room for 8 flags

最佳实践
位操作是少数几个你应该明确使用无符号整数(或std::bitset)的情况之一。

在本节课中,我们将展示如何通过 std::bitset 以简便的方式进行位操作。在接下来的系列课程中,我们将探讨如何采用更复杂但功能更强大的方法实现位操作。


位编号与位位置

对于给定的一组比特序列,我们通常从右向左为比特编号,从0开始(而非1)。每个编号代表一个比特位置bit position

76543210  Bit position
00000101  Bit sequence

给定位序列 0000 0101,位于第 0 位和第 2 位的比特值为 1,其余比特值为 0。


通过 std::bitset 操作位

第 5.3 课——数制(十进制、二进制、十六进制和八进制)中,我们已经展示了如何使用 std::bitset 以二进制形式打印值。然而,这并非 std::bitset 的唯一实用功能。

std::bitset提供了4个关键成员函数用于位操作:

  • test()用于查询位值为0或1。
  • set()用于设置位值为1(若该位已为1则无操作)。
  • reset()用于将位值设为0(若该位已为0则无操作)。
  • flip() 用于将位值从 0 翻转为 1 或反之。

这些函数均以操作位的位置作为唯一参数。

示例如下:

#include <bitset>
#include <iostream>

int main()
{
    std::bitset<8> bits{ 0b0000'0101 }; // we need 8 bits, start with bit pattern 0000 0101
    bits.set(3);   // set bit position 3 to 1 (now we have 0000 1101)
    bits.flip(4);  // flip bit 4 (now we have 0001 1101)
    bits.reset(4); // set bit 4 back to 0 (now we have 0000 1101)

    std::cout << "All the bits: " << bits<< '\n';
    std::cout << "Bit 3 has value: " << bits.test(3) << '\n';
    std::cout << "Bit 4 has value: " << bits.test(4) << '\n';

    return 0;
}

这将输出:

image

提醒

我们在第5.7节——std::string介绍中介绍了成员函数。普通函数的调用方式是函数(对象),而成员函数的调用方式是对象.函数()。

我们在第5.3节——数制(十进制、二进制、十六进制和八进制)中讲解了二进制字面量前缀0b和数字分隔符'。

为代码片段命名有助于提高代码的可读性:

#include <bitset>
#include <iostream>

int main()
{
    [[maybe_unused]] constexpr int  isHungry   { 0 };
    [[maybe_unused]] constexpr int  isSad      { 1 };
    [[maybe_unused]] constexpr int  isMad      { 2 };
    [[maybe_unused]] constexpr int  isHappy    { 3 };
    [[maybe_unused]] constexpr int  isLaughing { 4 };
    [[maybe_unused]] constexpr int  isAsleep   { 5 };
    [[maybe_unused]] constexpr int  isDead     { 6 };
    [[maybe_unused]] constexpr int  isCrying   { 7 };

    std::bitset<8> me{ 0b0000'0101 }; // we need 8 bits, start with bit pattern 0000 0101
    me.set(isHappy);      // set bit position 3 to 1 (now we have 0000 1101)
    me.flip(isLaughing);  // flip bit 4 (now we have 0001 1101)
    me.reset(isLaughing); // set bit 4 back to 0 (now we have 0000 1101)

    std::cout << "All the bits: " << me << '\n';
    std::cout << "I am happy: " << me.test(isHappy) << '\n';
    std::cout << "I am laughing: " << me.test(isLaughing) << '\n';

    return 0;
}

image

相关内容
我们在第1.4课——变量赋值与初始化中介绍了[maybe_unused]。
第13.2课——无作用域枚举中,我们将展示枚举器如何构建更优的命名位集合。


如果我们想同时获取或设置多个位呢?

std::bitset 并不支持这种操作。要实现此功能,或者当我们希望使用无符号整数位标志代替 std::bitset 时,就需要采用更传统的方法。我们将在接下来的几节课中探讨这些方法。


std::bitset的大小

一个可能令人意外的点是,std::bitset的优化目标是速度而非节省内存。其大小通常取为存储位所需字节数的向上取整值,即最接近的sizeof(size_t)大小——在32位机器上为4字节,64位机器上为8字节。

因此,std::bitset<8> 通常会占用 4 或 8 字节内存,尽管从技术上讲存储 8 位仅需 1 字节。由此可见,std::bitset 的价值主要体现在便捷性而非节省内存方面。


查询 std::bitset

还有几个常用的成员函数:

  • size() 返回位集中的位数。
  • count() 返回位集中设置为 true 的位数。
  • all() 返回一个布尔值,指示是否所有位都设置为 true。
  • any() 返回布尔值,指示是否存在任何位被设置为真。
  • none() 返回布尔值,指示是否不存在任何位被设置为真。
#include <bitset>
#include <iostream>

int main()
{
    std::bitset<8> bits{ 0b0000'1101 };
    std::cout << bits.size() << " bits are in the bitset\n";
    std::cout << bits.count() << " bits are set to true\n";

    std::cout << std::boolalpha;
    std::cout << "All bits are true: " << bits.all() << '\n';
    std::cout << "Some bits are true: " << bits.any() << '\n';
    std::cout << "No bits are true: " << bits.none() << '\n';

    return 0;
}

这将输出:

image

posted @ 2026-02-20 16:56  游翔  阅读(0)  评论(0)    收藏  举报