什么是位运算与位掩码?
在上一课“位运算符”(O.2 – Bitwise operators) 中,我们讨论了各类位运算符如何在操作数的每一位上执行逻辑运算。既然已掌握其工作机制,本节将介绍它们在实际中的常见用法。
位掩码的定义与作用
为了操控单个位(例如将其置 1 或清 0),必须首先指明要操作的具体位。遗憾的是,位运算符无法直接识别“位位置”,它们只能与“位掩码”协同工作。
位掩码是一组预先定义的位模式,用于选取后续运算将要修改的特定位。
举现实生活中的例子:若要给窗框刷漆而不慎,可能连玻璃也会一并刷上。解决之道是购买遮蔽胶带,将其贴在玻璃及其他不希望被漆到之处;随后刷漆时,遮蔽胶带会阻挡油漆,从而只有未遮蔽区域(我们想要刷漆之处)被着色。
位掩码对位的作用与此类似——它阻止位运算符触及我们不想修改的位,同时允许访问需要修改的位。
首先探讨如何定义简单的位掩码,随后演示其用法。
如何在C++14中定义位掩码
最简单的方法是为每个位位置定义一个位掩码。用 0 屏蔽无关位,用 1 标记需要操作的位。
尽管位掩码可以是字面量,但通常定义为符号常量,以便赋予有意义的名字并方便复用。
由于 C++14 支持二进制字面量,定义如下:
#include <cstdint>
constexpr std::uint8_t mask0{ 0b0000'0001 }; // 位 0
constexpr std::uint8_t mask1{ 0b0000'0010 }; // 位 1
constexpr std::uint8_t mask2{ 0b0000'0100 }; // 位 2
constexpr std::uint8_t mask3{ 0b0000'1000 }; // 位 3
constexpr std::uint8_t mask4{ 0b0001'0000 }; // 位 4
constexpr std::uint8_t mask5{ 0b0010'0000 }; // 位 5
constexpr std::uint8_t mask6{ 0b0100'0000 }; // 位 6
constexpr std::uint8_t mask7{ 0b1000'0000 }; // 位 7
如此便获得了一组表示各 bit 位置的符号常量,可用于位操作(稍后演示)。
如何在C++11及更早版本中定义位掩码
C++11 不支持二进制字面量,因此需采用其他方式设定符号常量。常用两种方法:
方法一:使用十六进制字面量
(相关说明见 5.2 课 Literals。)
十六进制与二进制对应关系:
十六进制 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F
二进制 | 0000 | 0001 | 0010 | 0011 | 0100 | 0101 | 0110 | 0111 | 1000 | 1001 | 1010 | 1011 | 1100 | 1101 | 1110 | 1111
因此可用十六进制定义位掩码:
constexpr std::uint8_t mask0{ 0x01 }; // 对应 0000 0001
constexpr std::uint8_t mask1{ 0x02 }; // 对应 0000 0010
constexpr std::uint8_t mask2{ 0x04 }; // 对应 0000 0100
constexpr std::uint8_t mask3{ 0x08 }; // 对应 0000 1000
constexpr std::uint8_t mask4{ 0x10 }; // 对应 0001 0000
constexpr std::uint8_t mask5{ 0x20 }; // 对应 0010 0000
constexpr std::uint8_t mask6{ 0x40 }; // 对应 0100 0000
constexpr std::uint8_t mask7{ 0x80 }; // 对应 1000 0000
若对十六进制转二进制不熟,可读性稍差。
方法二:使用左移运算符将单个 1 移动到对应位置
constexpr std::uint8_t mask0{ 1 << 0 }; // 0000 0001
constexpr std::uint8_t mask1{ 1 << 1 }; // 0000 0010
constexpr std::uint8_t mask2{ 1 << 2 }; // 0000 0100
constexpr std::uint8_t mask3{ 1 << 3 }; // 0000 1000
constexpr std::uint8_t mask4{ 1 << 4 }; // 0001 0000
constexpr std::uint8_t mask5{ 1 << 5 }; // 0010 0000
constexpr std::uint8_t mask6{ 1 << 6 }; // 0100 0000
constexpr std::uint8_t mask7{ 1 << 7 }; // 1000 0000
检测某位状态(开/关)
拥有位掩码后,可与位标志变量配合操控位标志。
判断某一位是否置位,使用按位与运算符并配合对应掩码:
#include <cstdint>
#include <iostream>
int main()
{
[[maybe_unused]] constexpr std::uint8_t mask0{ 0b0000'0001 };
[[maybe_unused]] constexpr std::uint8_t mask1{ 0b0000'0010 };
[[maybe_unused]] constexpr std::uint8_t mask2{ 0b0000'0100 };
[[maybe_unused]] constexpr std::uint8_t mask3{ 0b0000'1000 };
[[maybe_unused]] constexpr std::uint8_t mask4{ 0b0001'0000 };
[[maybe_unused]] constexpr std::uint8_t mask5{ 0b0010'0000 };
[[maybe_unused]] constexpr std::uint8_t mask6{ 0b0100'0000 };
[[maybe_unused]] constexpr std::uint8_t mask7{ 0b1000'0000 };
std::uint8_t flags{ 0b0000'0101 }; // 8 位,可容纳 8 个标志
std::cout << "bit 0 is " << (static_cast<bool>(flags & mask0) ? "on\n" : "off\n");
std::cout << "bit 1 is " << (static_cast<bool>(flags & mask1) ? "on\n" : "off\n");
return 0;
}
输出:
bit 0 is on
bit 1 is off
以 flags & mask0
为例:
0000'0101 &
0000'0001
---------
0000'0001
将结果 0000'0001
转换为 bool,由于非零即 true,故为真。
同理 flags & mask1
结果为 0000'0000
,转为 bool 为假。
置位(打开某一位)
要将某一位置 1,使用按位或赋值运算符:
flags |= mask1; // 打开 bit 1
亦可同时打开多位:
flags |= (mask4 | mask5); // 同时打开 bit 4 和 bit 5
复位(关闭某一位)
要将某一位清 0,需结合按位与与按位非:
flags &= ~mask2; // 关闭 bit 2
同样可同时关闭多位:
flags &= ~(mask4 | mask5); // 同时关闭 bit 4 和 bit 5
关键提示
某些编译器可能因整型提升而提示符号转换警告:
flags &= ~mask2;
因为 mask2
类型小于 int
,operator~
会将其提升至 int
,从而右操作数变为有符号,而左操作数为无符号,导致警告。
解决方式:
flags &= static_cast<std::uint8_t>(~mask2);
此问题在 O.2 课已讨论。
翻转位状态
要翻转位(0↔1),使用按位异或:
flags ^= mask2; // 翻转 bit 2
亦可同时翻转多位:
flags ^= (mask4 | mask5); // 翻转 bit 4 和 bit 5
位掩码与 std::bitset
std::bitset
支持全部位运算符。尽管使用其成员函数(test、set、reset、flip)修改单个位更便利,但若想一次性修改多位,仍可使用位运算符与位掩码。
示例:
#include <bitset>
#include <iostream>
int main()
{
[[maybe_unused]] constexpr std::bitset<8> mask0{ 0b0000'0001 };
[[maybe_unused]] constexpr std::bitset<8> mask1{ 0b0000'0010 };
[[maybe_unused]] constexpr std::bitset<8> mask2{ 0b0000'0100 };
[[maybe_unused]] constexpr std::bitset<8> mask3{ 0b0000'1000 };
[[maybe_unused]] constexpr std::bitset<8> mask4{ 0b0001'0000 };
[[maybe_unused]] constexpr std::bitset<8> mask5{ 0b0010'0000 };
[[maybe_unused]] constexpr std::bitset<8> mask6{ 0b0100'0000 };
[[maybe_unused]] constexpr std::bitset<8> mask7{ 0b1000'0000 };
std::bitset<8> flags{ 0b0000'0101 };
std::cout << "bit 1 is " << (flags.test(1) ? "on\n" : "off\n");
std::cout << "bit 2 is " << (flags.test(2) ? "on\n" : "off\n");
flags ^= (mask1 | mask2); // 翻转 bit 1 和 2
// ...
}
注意
std::bitset
无直接通过位掩码查询成员的函数,需使用按位与。- 使用
any()
判断结果是否非零。
赋予位掩码语义
将位掩码命名为 mask1
、mask2
仅能指示所操作位,却无法说明其实际含义。
最佳实践是为位掩码赋予有意义的名字,以文档化位标志用途。以下示例源于某游戏:
#include <cstdint>
#include <iostream>
int main()
{
[[maybe_unused]] constexpr std::uint8_t isHungry { 1 << 0 }; // 0000 0001
[[maybe_unused]] constexpr std::uint8_t isSad { 1 << 1 }; // 0000 0010
[[maybe_unused]] constexpr std::uint8_t isMad { 1 << 2 }; // 0000 0100
[[maybe_unused]] constexpr std::uint8_t isHappy { 1 << 3 }; // 0000 1000
[[maybe_unused]] constexpr std::uint8_t isLaughing { 1 << 4 }; // 0001 0000
[[maybe_unused]] constexpr std::uint8_t isAsleep { 1 << 5 }; // 0010 0000
[[maybe_unused]] constexpr std::uint8_t isDead { 1 << 6 }; // 0100 0000
[[maybe_unused]] constexpr std::uint8_t isCrying { 1 << 7 }; // 1000 0000
std::uint8_t me{}; // 所有标志初始为关
me |= (isHappy | isLaughing); // 开心且大笑
me &= ~isLaughing; // 不再大笑
std::cout << std::boolalpha;
std::cout << "I am happy? " << static_cast<bool>(me & isHappy) << '\n';
std::cout << "I am laughing? " << static_cast<bool>(me & isLaughing) << '\n';
}
使用 std::bitset
的等效实现:
#include <bitset>
#include <iostream>
int main()
{
[[maybe_unused]] constexpr std::bitset<8> isHungry { 0b0000'0001 };
[[maybe_unused]] constexpr std::bitset<8> isSad { 0b0000'0010 };
[[maybe_unused]] constexpr std::bitset<8> isMad { 0b0000'0100 };
[[maybe_unused]] constexpr std::bitset<8> isHappy { 0b0000'1000 };
[[maybe_unused]] constexpr std::bitset<8> isLaughing { 0b0001'0000 };
[[maybe_unused]] constexpr std::bitset<8> isAsleep { 0b0010'0000 };
[[maybe_unused]] constexpr std::bitset<8> isDead { 0b0100'0000 };
[[maybe_unused]] constexpr std::bitset<8> isCrying { 0b1000'0000 };
std::bitset<8> me{};
me |= (isHappy | isLaughing);
me &= ~isLaughing;
std::cout << std::boolalpha;
std::cout << "I am happy? " << (me & isHappy).any() << '\n';
std::cout << "I am laughing? " << (me & isLaughing).any() << '\n';
}
何时使用位标志最有效?
敏锐的读者可能注意到,上述示例并未节省内存:8 个 bool
变量通常占 8 字节,而示例(使用 std::uint8_t
)共占 9 字节——8 字节用于位掩码,1 字节用于标志变量!
位标志的真正优势在于存在大量相同的标志变量时。例如,若上例中“我”的数量增至 100 人,使用 8 个 bool
每人需 800 字节;位标志仅需 8 字节掩码 + 100 字节标志变量,共 108 字节,节省约 8 倍。
对大多数程序而言,节省的内存不足以抵消复杂度;但若对象数以万计乃至百万计,位标志能显著减少内存占用,是值得掌握的优化手段。
另一场景是函数需接受 32 种选项的任意组合。若用 32 个 bool
形参:
void someFunction(bool option1, ..., bool option32);
调用时:
someFunction(false, ..., true, ..., true); // 难以阅读
而使用位标志:
void someFunction(std::bitset<32> options);
someFunction(option10 | option32); // 清晰易读
这正是 OpenGL 等知名 3D 图形库采用位标志参数的原因之一:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
其中:
#define GL_DEPTH_BUFFER_BIT 0x00000100
#define GL_STENCIL_BUFFER_BIT 0x00000400
#define GL_COLOR_BUFFER_BIT 0x00004000
多位的位掩码示例
尽管位掩码常用于选择单个位,也可选择多位。以显示器像素颜色为例:
像素颜色通常由红、绿、蓝三色光强度(各 8 位)及 Alpha 透明度(8 位)构成,共 32 位,格式如下:
RRRRRRRR GGGGGGGG BBBBBBBB AAAAAAAA
以下程序读取 32 位十六进制 RGBA 值,并提取各分量:
#include <cstdint>
#include <iostream>
int main()
{
constexpr std::uint32_t redBits { 0xFF000000 };
constexpr std::uint32_t greenBits { 0x00FF0000 };
constexpr std::uint32_t blueBits { 0x0000FF00 };
constexpr std::uint32_t alphaBits { 0x000000FF };
std::cout << "Enter a 32-bit RGBA color value in hexadecimal (e.g. FF7F3300): ";
std::uint32_t pixel{};
std::cin >> std::hex >> pixel;
const std::uint8_t red { static_cast<std::uint8_t>((pixel & redBits) >> 24) };
const std::uint8_t green { static_cast<std::uint8_t>((pixel & greenBits) >> 16) };
const std::uint8_t blue { static_cast<std::uint8_t>((pixel & blueBits) >> 8) };
const std::uint8_t alpha { static_cast<std::uint8_t>(pixel & alphaBits) };
std::cout << "Your color contains:\n";
std::cout << std::hex;
std::cout << static_cast<int>(red) << " red\n";
std::cout << static_cast<int>(green) << " green\n";
std::cout << static_cast<int>(blue) << " blue\n";
std::cout << static_cast<int>(alpha) << " alpha\n";
}
运行示例:
Enter a 32-bit RGBA color value in hexadecimal (e.g. FF7F3300): FF7F3300
Your color contains:
ff red
7f green
33 blue
0 alpha
总结:位标志置位、清零、翻转与查询的简明总结
- 查询:使用按位与
if (flags & option4) ...
- 置位:使用按位或赋值
flags |= option4;
flags |= (option4 | option5);
- 清零:使用按位与与按位非
flags &= ~option4;
flags &= ~(option4 | option5);
- 翻转:使用按位异或
flags ^= option4;
flags ^= (option4 | option5);