在计算机中,数字在内部以二进制表示。这意味着,当你为变量使用整数类型时,它在内部会被表示为 0 和 1 的求和或拼接。

你也可以这样看:

布尔值也是如此!如果你有一个等于 1 的位,或者一个等于 true 的布尔值,那么这两个变量的值之间不会有任何区别。
虽然就值输出而言确实如此,但一个布尔值会占用整整一个字节。 这是因为你不能只借用 1 位,所以要定义一个布尔值(bool),你需要占用 1 个字节或 8 位。
你可能知道,一个单独的位代表一个 0 或一个 1。八个这样的位拼接在一起代表一个字节。一个字节的最大值是 255,即所有位的总和。每个位都有一个“值”,而所有位的总和就是我们所说的掩码(mask)。例如,第一个位的值是 1,第二个位的值是其右边位的值的 2 倍。
// 这相当于说 2^N = x,例如
2^0=1
2^1=2
2^2=4
2^3=8
2^4=16
2^5=32
2^6=64
2^7=128
/*
* 你可以相应地将一个字节表示为 0 和 1 的序列。
* 按照这种方式阅读,位的顺序是从右到左
* <-----
*/
1 = 0 0 0 0 0 0 0 1
2 = 0 0 0 0 0 0 1 0
4 = 0 0 0 0 0 1 0 0
8 = 0 0 0 0 1 0 0 0
16 = 0 0 0 1 0 0 0 0
32 = 0 0 1 0 0 0 0 0
64 = 0 1 0 0 0 0 0 0
128 = 1 0 0 0 0 0 0 0
当一个数字是 4 字节长时,这意味着你的数字实际上使用了内部存储中的 32 位:
00000000 00000000 00000000 00000000
32 位整数数据类型可以保存 −2,147,483,648 到 2,147,483,647 范围内的整数值。你也可以将这种数据类型称为有符号整数(signed int)或无符号整数。无符号整数(unsigned int)意味着 32 位无符号整数数据类型可以保存 0 到 4,294,967,295 范围内的整数值。 无符号整数之所以总能比有符号整数存储更多的正值,是因为我们将有符号整数的负值用作正值,这样你可以在变量中存储的正值数量就翻倍了。
现在想象一下,你想创建一个基于多个值保存状态的程序:
// 存储该程序的当前状态
bool firstValue = true;
bool secondValue = false;
或者,使用单个整数变量,用其内部 8 位中的每一位来表示不同的真假值。
// 00000101
int maskValue = 5;
在上面,第一位为 true,代表第一个变量。第二位为 false,代表第二个变量。第三位为 true,依此类推……
这是一种非常紧凑的数据存储方式,用途广泛。
这就是位掩码(bit masking)发挥作用的地方。听起来很复杂,但实际上非常简单。
位掩码允许用户:
对位进行操作与在两个变量之间设置条件非常相似。例如,假设我们有:
if (1 && !0)
printf('This condition is true');
任何不为 0 的值都等同于 true。这意味着:
这样我们就可以比较两个位的值,如果它们都为 true,我们就可以保留该位的值,并得到相应的最终结果:
255 &= 1
// 由于以下原因,255 将变成 1:
// 用 F - False(假)和 T - True(真)来比较 255 和 1 的位
(255) -> 1 1 1 1 1 1 1 1
(1) -> 0 0 0 0 0 0 0 1
(1) -> F F F F F F F T
0 0 0 0 0 0 0 1
// 另一个示例:
值: 00000101
掩码: 00000100
---- AND ---------
结果: 00000100
// 在这个例子中,我们的值是 5
int MyState = 5;
// 我们想从该值中移除 1 个状态
MyState &= 4;
/* 这会将我们的 00000101 (5)
* 变成 00000100 (4)
*/
上面的例子确实是使用位运算的糟糕示例,可能让你感到疑惑:为什么不直接从数字中减去这些值呢?
5-1 = 4
这比绕来绕去地比较字节或位更容易理解,也更符合人类语言的直觉。
在位运算中使用 & 的意义,往往伴随着另一个位运算字符,即 '~'。
~ 允许你反转位掩码,例如:
255 &= ~1; // 这会使 255 变成 254,因为:
(255) -> 1 1 1 1 1 1 1 1
(1) -> 0 0 0 0 0 0 0 1 // 第一位是 true
(~1) -> 1 1 1 1 1 1 1 0 // 反转 1 中的位 -> 254
(254) -> 1 1 1 1 1 1 1 0
想象这样一个位掩码: 1 0 0 0 0 0 0 0 (128)
如果这是一次整数运算,那么 128 - 64 = 64
然而,如果你试图从 128 中移除 64:
1 0 0 0 0 0 0 0 &= ~64
(128) 1 0 0 0 0 0 0 0
(64) 0 1 0 0 0 0 0 0
(~64) 1 0 1 1 1 1 1 1
---------------------
(128) 1 0 0 0 0 0 0 0
所以 128 不会改变,因为 64 已经是关闭(off)状态。
这就是为什么你经常会在文件中看到变量添加新标志、移除其他标志。
向位掩码添加新标志比移除它们更容易。
这个概念的运用方式与条件中的 OR 完全相同:
if (true || false)
printf("This condition is true");
else if (true || true)
printf("This condition is true");
else if (false || true)
printf("This condition is true");
else if (false || false)
printf("This condition is false");
就像两个条件一样,你可以比较两个位并得到一个新的位掩码,因为只要其中任意一个位为 true,该位的值就会变成 true。换句话说,如果你有 1 或 0,新的位掩码值将是 1,例如:
// 这里我们用 5 初始化 MyState,用位的语言来说就是 00000101
int MyState = 5;
// 然后我们可以用以下语法向它添加一个掩码
MyState |= 128;
// 由于以下逻辑,我们的最终结果会将 5 变成 133:
5 - 0 0 0 0 0 1 0 1
128 - 1 0 0 0 0 0 0 0
------- 结果 --------
133 - 1 0 0 0 0 1 0 1
这个位运算符的工作方式与预期不同,可能很难找到它的用法。
想象有以下位掩码:
| 位掩码 | 值 |
|---|---|
0 1 0 0 1 1 1 0 |
78 |
0 0 1 1 0 1 0 0 |
52 |
F T T T T F T F |
XOR 结果 |
0 1 1 1 1 0 1 0 |
122 |
你可能对刚才发生的事情感到困惑。
其实,我们只是将第一个掩码中的位与第二个掩码中的位逐一比较。如果它们不同,那么最终值为 true。用程序表示就是这样:
// 比较掩码 1 和掩码 2 的值
// 如果它们不同,则 XOR 条件为 true
for (int i = 0; i < 8; ++i)
{
if (bitmask_1[i] != bitmask_2[i])
printf("This condition is true")
}
该位运算会将所有二进制值按指定次数 N 进行移位。让我们深入了解一下:
如果我们在二进制中有如下值 X:
0 0 0 0 0 0 0 1(即 1)
然后我们执行 x << 1,x 将变成
0 0 0 0 0 0 1 0
根据这个规则,它将所有位向左移动一位,并在右侧补一个 0。 x << 2 会将所有位移动 2 位,因此 x 将变成
0 0 0 0 0 1 0 0
// 将值 "1" 向左移动 "8-1" 次
1<<(8-1)
// (8-1) 等同于 7
1<<(7)
// 这会将该值 000000001 中的 1 向左移动 7 位。
// 所以结果是 128 或
10000000
| 字符 | 运算 | 描述 |
|---|---|---|
| & | 与(AND) | 以两个数字作为操作数,对两个数字的每一位进行 AND 运算。仅当两个位都为 1 时,AND 的结果才为 1 |
| | | 或(OR) | 以两个数字作为操作数,对两个数字的每一位进行 OR 运算。只要两个位中任意一个为 1,OR 的结果就为 1 |
| ^ | 异或(XOR) | 以两个数字作为操作数,对两个数字的每一位进行 XOR 运算。如果两个位不同,XOR 的结果为 1。 |
| << | 左移 | 以两个数字为操作数,将第一个操作数的位向左移动,第二个操作数决定移动的位数。 |
| >> | 右移 | 以两个数字为操作数,将第一个操作数的位向右移动,第二个操作数决定移动的位数。 |
| ~ | 非(NOT) | 取一个数字并反转它的所有位 |
本文档参考了以下文章编写:
https://www.geeksforgeeks.org/bitwise-operators-in-c-cpp/
https://stackoverflow.com/questions/31575691/what-is-a-bitmask-and-a-mask