O-3 使用位运算符和位掩码进行位操作

在上一节关于位运算符的课程(O.2 -- 位运算符)中,我们探讨了各种位运算符如何对操作数中的每个位应用逻辑运算符。既然我们已经理解了它们的工作原理,接下来让我们看看它们更常见的用法。


位掩码

为了操作单个比特(例如将其打开或关闭),我们需要某种方法来标识想要操作的特定比特。遗憾的是,位运算符无法直接处理比特位置,而是通过位掩码来实现。

位掩码是一组预先定义的比特集合,用于选择后续操作将修改的具体比特。

设想一个现实场景:你需要粉刷窗框。若操作不当,不仅窗框会被涂上油漆,玻璃本身也可能被误涂。此时可购买遮蔽胶带,将其覆盖在玻璃及其他不需要涂漆的部位。这样在粉刷时,胶带就能阻隔油漆接触到不需涂刷的区域。最终只有未遮盖的区域(即需要上色的部分)会被涂刷。

位掩码本质上对位元执行相同功能——它阻止位运算符触及不需要修改的位元,同时允许操作需要修改的位元。

接下来我们将先探讨如何定义简单的位掩码,然后展示具体应用方法。


在C++14中定义位掩码

最简单的位掩码集是为每个位位置定义一个掩码。我们用0掩盖不需要的位,用1标记需要修改的位。

虽然位掩码可以是字面量,但通常会定义为符号常量,以便赋予其有意义的名称并轻松复用。

由于C++14支持二进制字面量,定义这些位掩码变得非常简单:

#include <cstdint>

constexpr std::uint8_t mask0{ 0b0000'0001 }; // represents bit 0
constexpr std::uint8_t mask1{ 0b0000'0010 }; // represents bit 1
constexpr std::uint8_t mask2{ 0b0000'0100 }; // represents bit 2
constexpr std::uint8_t mask3{ 0b0000'1000 }; // represents bit 3
constexpr std::uint8_t mask4{ 0b0001'0000 }; // represents bit 4
constexpr std::uint8_t mask5{ 0b0010'0000 }; // represents bit 5
constexpr std::uint8_t mask6{ 0b0100'0000 }; // represents bit 6
constexpr std::uint8_t mask7{ 0b1000'0000 }; // represents bit 7

现在我们有一组符号常量,它们分别代表每个位的位置。我们可以利用这些常量来操作位(具体方法稍后将展示)。


在C++11及更早版本中定义位掩码

由于C++11不支持二进制字面量,我们需要采用其他方法设置符号常量。实现此目的有两种有效方法。

第一种方法是使用十六进制字面量。

相关内容
我们在第5.2课——字面量中讨论过十六进制。

以下是十六进制转换为二进制的过程:

Hexadecimal 0 1 2 3 4 5 6 7 8 9 A B C D E F
Binary 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111

因此,我们可以使用十六进制来定义位掩码,如下所示:

constexpr std::uint8_t mask0{ 0x01 }; // hex for 0000 0001
constexpr std::uint8_t mask1{ 0x02 }; // hex for 0000 0010
constexpr std::uint8_t mask2{ 0x04 }; // hex for 0000 0100
constexpr std::uint8_t mask3{ 0x08 }; // hex for 0000 1000
constexpr std::uint8_t mask4{ 0x10 }; // hex for 0001 0000
constexpr std::uint8_t mask5{ 0x20 }; // hex for 0010 0000
constexpr std::uint8_t mask6{ 0x40 }; // hex for 0100 0000
constexpr std::uint8_t mask7{ 0x80 }; // hex for 1000 0000

有时会省略十六进制前导零(例如 0x01 可能简写为 0x1)。无论哪种情况,若不熟悉十六进制转二进制转换,读取起来都可能稍显困难。

更简便的方法是使用左移运算符将单比特移入正确位置:

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

测试位(判断其是否为1或0)

现在我们拥有一组位掩码,可将其与位标志变量配合使用来操作位标志。

要判断位是否为1或0,需使用位与运算配合对应位的位掩码:

#include <cstdint>
#include <iostream>

int main()
{
	[[maybe_unused]] constexpr std::uint8_t mask0{ 0b0000'0001 }; // represents bit 0
	[[maybe_unused]] constexpr std::uint8_t mask1{ 0b0000'0010 }; // represents bit 1
	[[maybe_unused]] constexpr std::uint8_t mask2{ 0b0000'0100 }; // represents bit 2
	[[maybe_unused]] constexpr std::uint8_t mask3{ 0b0000'1000 }; // represents bit 3
	[[maybe_unused]] constexpr std::uint8_t mask4{ 0b0001'0000 }; // represents bit 4
	[[maybe_unused]] constexpr std::uint8_t mask5{ 0b0010'0000 }; // represents bit 5
	[[maybe_unused]] constexpr std::uint8_t mask6{ 0b0100'0000 }; // represents bit 6
	[[maybe_unused]] constexpr std::uint8_t mask7{ 0b1000'0000 }; // represents bit 7

	std::uint8_t flags{ 0b0000'0101 }; // 8 bits in size means room for 8 flags

	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;
}

这将输出

image

我们还可以使用位或运算同时开启多个位:

flags |= (mask4 | mask5); // turn bits 4 and 5 on at the same time

重置位

要重置(清零)一位(使其值为0),我们同时使用位与运算和位非运算:

#include <cstdint>
#include <iostream>

int main()
{
    [[maybe_unused]] constexpr std::uint8_t mask0{ 0b0000'0001 }; // represents bit 0
    [[maybe_unused]] constexpr std::uint8_t mask1{ 0b0000'0010 }; // represents bit 1
    [[maybe_unused]] constexpr std::uint8_t mask2{ 0b0000'0100 }; // represents bit 2
    [[maybe_unused]] constexpr std::uint8_t mask3{ 0b0000'1000 }; // represents bit 3
    [[maybe_unused]] constexpr std::uint8_t mask4{ 0b0001'0000 }; // represents bit 4
    [[maybe_unused]] constexpr std::uint8_t mask5{ 0b0010'0000 }; // represents bit 5
    [[maybe_unused]] constexpr std::uint8_t mask6{ 0b0100'0000 }; // represents bit 6
    [[maybe_unused]] constexpr std::uint8_t mask7{ 0b1000'0000 }; // represents bit 7

    std::uint8_t flags{ 0b0000'0101 }; // 8 bits in size means room for 8 flags

    std::cout << "bit 2 is " << (static_cast<bool>(flags & mask2) ? "on\n" : "off\n");

    flags &= ~mask2; // turn off bit 2

    std::cout << "bit 2 is " << (static_cast<bool>(flags & mask2) ? "on\n" : "off\n");

    return 0;
}

这将输出

image

我们可以同时关闭多个位:

flags &= ~(mask4 | mask5); // turn bits 4 and 5 off at the same time

关键洞察

某些编译器可能会对这行代码中的符号转换提出警告:

flags &= ~mask2;

由于mask2的类型小于int,~运算符会导致操作数mask2进行整数提升至int类型。随后编译器报错指出:我们试图使用&=运算符时,左操作数为无符号类型而右操作数为有符号类型。

若出现此情况,请尝试以下方法:

flags &= static_cast<std::uint8_t>(~mask2);

我们在第O.2课——位运算符中讨论这个问题。


翻转位

要切换(翻转)位状态(从0到1或从1到0),我们使用按位异或运算:

#include <cstdint>
#include <iostream>

int main()
{
    [[maybe_unused]] constexpr std::uint8_t mask0{ 0b0000'0001 }; // represents bit 0
    [[maybe_unused]] constexpr std::uint8_t mask1{ 0b0000'0010 }; // represents bit 1
    [[maybe_unused]] constexpr std::uint8_t mask2{ 0b0000'0100 }; // represents bit 2
    [[maybe_unused]] constexpr std::uint8_t mask3{ 0b0000'1000 }; // represents bit 3
    [[maybe_unused]] constexpr std::uint8_t mask4{ 0b0001'0000 }; // represents bit 4
    [[maybe_unused]] constexpr std::uint8_t mask5{ 0b0010'0000 }; // represents bit 5
    [[maybe_unused]] constexpr std::uint8_t mask6{ 0b0100'0000 }; // represents bit 6
    [[maybe_unused]] constexpr std::uint8_t mask7{ 0b1000'0000 }; // represents bit 7

    std::uint8_t flags{ 0b0000'0101 }; // 8 bits in size means room for 8 flags

    std::cout << "bit 2 is " << (static_cast<bool>(flags & mask2) ? "on\n" : "off\n");
    flags ^= mask2; // flip bit 2
    std::cout << "bit 2 is " << (static_cast<bool>(flags & mask2) ? "on\n" : "off\n");
    flags ^= mask2; // flip bit 2
    std::cout << "bit 2 is " << (static_cast<bool>(flags & mask2) ? "on\n" : "off\n");

    return 0;
}

这将输出

image

我们可以同时翻转多个位:

flags ^= (mask4 | mask5); // flip bits 4 and 5 at the same time

位掩码与std::bitset

std::bitset支持完整的位运算符集。因此,尽管使用函数(test、set、reset和flip)修改单个位更为便捷,但若需要,您仍可使用位运算符和位掩码。

为何需要这样做?函数仅允许修改单个位,而位运算符可同时修改多个位。

#include <bitset>
#include <iostream>

int main()
{
	[[maybe_unused]] constexpr std::bitset<8> mask0{ 0b0000'0001 }; // represents bit 0
	[[maybe_unused]] constexpr std::bitset<8> mask1{ 0b0000'0010 }; // represents bit 1
	[[maybe_unused]] constexpr std::bitset<8> mask2{ 0b0000'0100 }; // represents bit 2
	[[maybe_unused]] constexpr std::bitset<8> mask3{ 0b0000'1000 }; // represents bit 3
	[[maybe_unused]] constexpr std::bitset<8> mask4{ 0b0001'0000 }; // represents bit 4
	[[maybe_unused]] constexpr std::bitset<8> mask5{ 0b0010'0000 }; // represents bit 5
	[[maybe_unused]] constexpr std::bitset<8> mask6{ 0b0100'0000 }; // represents bit 6
	[[maybe_unused]] constexpr std::bitset<8> mask7{ 0b1000'0000 }; // represents bit 7

	std::bitset<8> flags{ 0b0000'0101 }; // 8 bits in size means room for 8 flags
	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); // flip bits 1 and 2
	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); // turn bits 1 and 2 on
	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); // turn bits 1 and 2 off
	std::cout << "bit 1 is " << (flags.test(1) ? "on\n" : "off\n");
	std::cout << "bit 2 is " << (flags.test(2) ? "on\n" : "off\n");

	return 0;
}

这将输出

image


赋予位掩码实际意义

将位掩码命名为“mask1”或“mask2”虽能表明操作的位,却无法说明该位标志的实际用途。

最佳实践是为位掩码赋予有意义的名称,以此记录位标志的含义。以下是我们可能编写的游戏代码示例:

#include <cstdint>
#include <iostream>

int main()
{
        // Define a bunch of physical/emotional states
	[[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{}; // all flags/options turned off to start
	me |= (isHappy | isLaughing); // I am happy and laughing
	me &= ~isLaughing; // I am no longer laughing

	// Query a few states
	// (we'll use static_cast<bool> to interpret the results as a boolean value)
	std::cout << std::boolalpha; // print true or false instead of 1 or 0
	std::cout << "I am happy? " << static_cast<bool>(me & isHappy) << '\n';
	std::cout << "I am laughing? " << static_cast<bool>(me & isLaughing) << '\n';

	return 0;
}

以下是使用 std::bitset 实现的相同示例:

#include <bitset>
#include <iostream>

int main()
{
        // Define a bunch of physical/emotional states
	[[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{}; // all flags/options turned off to start
	me |= (isHappy | isLaughing); // I am happy and laughing
	me &= ~isLaughing; // I am no longer laughing

	// Query a few states (we use the any() function to see if any bits remain set)
	std::cout << std::boolalpha; // print true or false instead of 1 or 0
	std::cout << "I am happy? " << (me & isHappy).any() << '\n';
	std::cout << "I am laughing? " << (me & isLaughing).any() << '\n';

	return 0;
}

这里有两点说明:首先,std::bitset 没有提供方便的函数来使用位掩码查询位。因此,若想使用位掩码而非位置索引,就必须通过位与运算来查询位。其次,我们使用了 any() 函数——当存在任何设置的位时返回 true,否则返回 false——以此判断查询到的位是否仍处于开启或关闭状态。


位标志何时最有用?

敏锐的读者可能注意到,上述示例实际上并未节省内存。8个独立的布尔值通常占用8字节,但上述示例(使用std::uint8_t)却占用了9字节——8字节用于定义位掩码,1字节用于标志变量!

当存在大量相同的标志变量时,位标志才真正体现价值。例如,假设上述示例中人物数量从1人(即我本人)增加到100人。若每人使用8个布尔变量(对应每种可能状态),则需消耗800字节内存。而采用位标志时,仅需8字节存储位掩码,加上100个位标志变量共108字节——内存消耗约减少8倍。

对多数程序而言,位标志节省的内存空间并不值得增加的复杂性。但在涉及数万甚至数百万相似对象的程序中,位标志能显著降低内存占用。若需此类优化,它值得纳入你的工具箱。

还有一种场景适合使用位标志与位掩码。假设某个函数需处理32个选项的任意组合,传统实现方式是使用32个独立布尔参数:

void someFunction(bool option1, bool option2, bool option3, bool option4, bool option5, bool option6, bool option7, bool option8, bool option9, bool option10, bool option11, bool option12, bool option13, bool option14, bool option15, bool option16, bool option17, bool option18, bool option19, bool option20, bool option21, bool option22, bool option23, bool option24, bool option25, bool option26, bool option27, bool option28, bool option29, bool option30, bool option31, bool option32);

希望你能给形参取更具描述性的名称,但重点在于展示形参列表有多么冗长难看。

当你想调用该函数并将选项10和32设为true时,就必须这样操作:

someFunction(false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true);

这简直难读得离谱(到底是选项9、10还是11被设为true?),还意味着你必须记住哪个参数对应哪个选项(设置“编辑标志”是第9、10还是第11个参数?)。

相反,如果你像这样使用位标志定义函数:

void someFunction(std::bitset<32> options);

那么你可以使用位标志来仅传递所需的选项:

someFunction(option10 | option32);

这样更易于阅读。

这正是广受好评的3D图形库OpenGL选择使用位标志参数而非多个连续布尔参数的原因之一。

以下是OpenGL中一个函数调用的示例:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // clear the color and the depth buffer

GL_COLOR_BUFFER_BIT 和 GL_DEPTH_BUFFER_BIT 是如下定义的位掩码(在 gl2.h 中):

#define GL_DEPTH_BUFFER_BIT               0x00000100
#define GL_STENCIL_BUFFER_BIT             0x00000400
#define GL_COLOR_BUFFER_BIT               0x00004000

涉及多个位的位掩码

虽然位掩码通常用于选择单个位,但也可用于选择多个位。让我们通过一个稍复杂的示例来了解这种用法。

电视和显示器等彩色显示设备由数百万像素构成,每个像素可呈现一个彩色点。每个彩色点由三束光线组合而成:红光、绿光和蓝光(RGB)。通过调节这三束光线的强度来生成不同颜色。

通常,特定像素的R、G、B强度由8位无符号整数表示。例如:红色像素为R=255, G=0, B=0;紫色像素为R=255, G=0, B=255;中灰色像素为R=127, G=127, B=127。

为像素赋值时,除R、G、B外常使用第四个值A。“A”代表“alpha”(透明度),用于控制颜色的透明程度。当A=0时颜色完全透明,当A=255时颜色完全不透明。

R、G、B和A通常存储为单个32位整数,每个分量占用8位:

32-bit RGBA value

bits 31-24 bits 23-16 bits 15-8 bits 7-0
RRRRRRRR GGGGGGGG BBBBBBBB AAAAAAAA
red green blue alpha

以下程序要求用户输入一个32位十六进制数值,然后提取其中R、G、B和A的8位颜色值。

#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; // std::hex allows us to read in a hex value

	// use Bitwise AND to isolate the pixels for our given color,
	// then right shift the value into the lower 8 bits
	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; // print the following values in hex

        // reminder: std::uint8_t will likely print as a char
        // we static_cast to int to ensure it prints as an integer
	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";

	return 0;
}

这将产生以下输出:

image

在上面的程序中,我们使用位与运算查询感兴趣的8位数据集,然后将其右移转换为8位值,以便以十六进制形式输出。


摘要

总结如何设置、清除、切换和查询位标志:

要查询位状态,我们使用位与运算:

if (flags & option4) ... // if option4 is set, do something

要设置位(开启),我们使用按位或运算:

flags |= option4; // turn option 4 on.
flags |= (option4 | option5); // turn options 4 and 5 on.

要清除位(关闭),我们使用位与运算配合位非运算:

flags &= ~option4; // turn option 4 off
flags &= ~(option4 | option5); // turn options 4 and 5 off

要翻转位状态,我们使用按位异或运算:

flags ^= option4; // flip option4 from on to off, or vice versa
flags ^= (option4 | option5); // flip options 4 and 5

测验时间

问题 #1

本测验中请勿使用 std::bitset。我们仅将 std::bitset 用于打印输出。

给定以下程序:

#include <bitset>
#include <cstdint>
#include <iostream>

int main()
{
    [[maybe_unused]] constexpr std::uint8_t option_viewed{ 0x01 };
    [[maybe_unused]] constexpr std::uint8_t option_edited{ 0x02 };
    [[maybe_unused]] constexpr std::uint8_t option_favorited{ 0x04 };
    [[maybe_unused]] constexpr std::uint8_t option_shared{ 0x08 };
    [[maybe_unused]] constexpr std::uint8_t option_deleted{ 0x10 };

    std::uint8_t myArticleFlags{ option_favorited };

    // Place all lines of code for the following quiz here

    std::cout << std::bitset<8>{ myArticleFlags } << '\n';

    return 0;
}

a) 添加一行代码将文章标记为已查看。

预期输出:

00000101

显示答案

myArticleFlags |= option_viewed;

b) 添加一行代码来检查文章是否已被删除。

显示答案

if (myArticleFlags & option_deleted) ...

c) 添加一行代码来清除文章的收藏标记。
预期输出(假设你已完成(a)部分):

00000001

显示答案

myArticleFlags &= static_cast<std::uint8_t>(~option_favorited);
若您获得00000000,请检查两点:

  • 确保测验(a)的答案尚未被移除。
  • 确认您清空的是option_favorited而非option_viewed。

1d) 附加题:为什么以下两行相同?

myflags &= ~(option4 | option5); // turn options 4 and 5 off
myflags &= ~option4 & ~option5; // turn options 4 and 5 off

显示答案

德摩根定律指出,当分布非运算符时,需将或运算与与运算的符号互换。因此~(选项4 | 选项5)变为~选项4 & ~选项5。
posted @ 2026-02-21 09:37  游翔  阅读(0)  评论(0)    收藏  举报