位标志与通过std::bitset进行位操作:C++高效位操作指南

在现代计算机体系结构中,内存的最小可寻址单元是字节。由于所有对象都需要具有唯一的内存地址,这意味着对象必须至少占用一个字节大小。对于大多数变量类型而言这没有问题,但对于布尔值来说就有些浪费了(双关语 intended)。布尔类型只有两种状态:真(1)或假(0),这种状态集合仅需1个二进制位即可存储。然而,由于变量必须至少占用1个字节(8位),这意味着布尔值实际只使用了1位,其余7位都被闲置。

大多数情况下这无关紧要——我们通常不会拮据到需要计较这7个闲置位的程度(代码的可理解性和可维护性更重要)。但在某些存储密集型场景中,将8个独立布尔值"打包"到单个字节中可以显著提升存储效率。

实现这种操作需要我们在位级别上操控对象。幸运的是,C++提供了专门工具。这种对对象内部单个位进行修改的操作称为位操作。

作者注: 位操作在图形编程、加密、压缩和优化等特定领域应用广泛,但在通用编程中并不常见。因此本章为选读内容,建议略读或后期回看。

什么是位标志?

此前我们使用变量存储单个值:

int foo { 5 }; // 赋值为5(可能占用32位存储空间)
std::cout << foo; // 输出值5

但我们可以将对象中的每个位视为独立的布尔值。当对象的各个位被用作布尔值时,这些位被称为位标志。

术语说明

  • 值为0的位称为"假"、“关闭"或"未设置”
  • 值为1的位称为"真"、“开启"或"设置”
  • 当位从0变为1或1变为0时,称为"翻转"或"反转"

类比说明

在计算机中,标志(flag)是表示程序某种状态的信号值。对于位标志,true值表示该条件成立。例如美国邮箱侧面的红色小旗,当有待发邮件时升起标志表示需要取件。

如何定义位标志集合

定义位标志集合时,通常使用适当大小的无符号整数(8位、16位、32位等,取决于标志数量)或std::bitset:

#include <bitset> // 引入std::bitset

std::bitset<8> mybitset{}; // 8位大小可容纳8个标志

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

本文将展示通过std::bitset进行位操作的简易方法,后续课程将探讨更灵活但复杂的方法。

位编号与位位置

给定位序列时,我们通常从右向左编号,从0开始(非1)。每个编号表示一个位位置:

76543210  位位置
00000101  位序列

示例中,位置0和2的位值为1,其余位值为0。

std::bitset的基本用法与位操作

在第5.3章《数字系统》中我们已展示使用std::bitset输出二进制值的方法,但其功能远不止于此。

std::bitset的常用成员函数

std::bitset提供4个关键成员函数:

  • test():查询位是0还是1
  • set():开启位(若已开启则无操作)
  • reset():关闭位(若已关闭则无操作)
  • flip():翻转位值(0变1或1变0)

每个函数都以要操作的位位置作为唯一参数。

示例:基本位操作

#include <bitset>
#include <iostream>

int main()
{
    std::bitset<8> bits{ 0b0000'0101 }; // 初始模式0000 0101
    bits.set(3);    // 位置3置1(现为0000 1101)
    bits.flip(4);   // 翻转位4(现为0001 1101)
    bits.reset(4);  // 位4复位(现为0000 1101)

    std::cout << "所有位:" << bits << '\n';
    std::cout << "位3值:" << bits.test(3) << '\n';
    std::cout << "位4值:" << bits.test(4) << '\n';
    return 0;
}

输出:

所有位:00001101
位3值:1
位4值:0

成员函数说明

普通函数调用形式为function(object),成员函数调用形式为object.function()。

为位命名提升可读性

为位命名可增强代码可读性:

#include <bitset>
#include <iostream>

int main()
{
    [[maybe_unused]] constexpr int isHungry{0};
    [[maybe_unused]] constexpr int isSad{1};
    // ...其他状态定义...

    std::bitset<8> me{ 0b0000'0101 };
    me.set(isHappy);      // 设置高兴状态
    me.flip(isLaughing);  // 翻转大笑状态
    me.reset(isLaughing); // 关闭大笑状态

    std::cout << "所有位:" << me << '\n';
    std::cout << "高兴状态:" << me.test(isHappy) << '\n';
    std::cout << "大笑状态:" << me.test(isLaughing) << '\n';
    return 0;
}

进阶:多比特位操作与std::bitset内存占用

多比特位操作

std::bitset对此支持有限,需使用传统方法实现,这将在后续课程中讲解。

std::bitset的内存占用

需注意的是std::bitset为速度而非内存优化设计。其大小通常是存储所需字节数向上取整到sizeof(size_t)的倍数(32位机器为4字节,64位机器为8字节)。因此std::bitset<8>通常占用4或8字节,尽管实际只需1字节存储8位。故std::bitset更适合追求便利性而非节省内存的场景。

std::bitset的查询功能

其他实用成员函数

  • size():返回位数
  • count():返回置1的位数
  • all():检查是否所有位都为1
  • any():检查是否有位为1
  • none():检查是否无位为1

示例:查询功能

#include <bitset>
#include <iostream>

int main()
{
    std::bitset<8> bits{ 0b0000'1101 };
    std::cout << "总位数:" << bits.size() << '\n';
    std::cout << "置1位数:" << bits.count() << '\n';

    std::cout << std::boolalpha;
    std::cout << "全为真:" << bits.all() << '\n';
    std::cout << "存在真:" << bits.any() << '\n';
    std::cout << "全为假:" << bits.none() << '\n';
    return 0;
}

输出:

总位数:8
置1位数:3
全为真:false
存在真:true
全为假:false

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"