在现代计算机体系结构中,内存的最小可寻址单元是字节。由于所有对象都需要具有唯一的内存地址,这意味着对象必须至少占用一个字节大小。对于大多数变量类型而言这没有问题,但对于布尔值来说就有些浪费了(双关语 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还是1set()
:开启位(若已开启则无操作)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()
:检查是否所有位都为1any()
:检查是否有位为1none()
:检查是否无位为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