位域和你
我将使用一个简单的示例来解释基础知识。假设您有一个四位无符号整数:
[0][0][0][0] = 0
您可以通过将其转换为基数 2 来表示 0 到 15 之间的任何数字。假设我们的右端是最小的:
[0][1][0][1] = 5
所以第一位加 1,第二位加 2,第三位加 4,第四位加 8。例如,这里是 8:
[1][0][0][0] = 8
所以呢?
假设您想在应用程序中表示二进制状态——如果启用了某个选项,是否应该绘制某个元素,等等。您可能不想对其中的每一个都使用一个完整的整数——它会使用一个 32 位整数来存储一位信息。或者,以四位继续我们的示例:
[0][0][0][1] = 1 = ON
[0][0][0][0] = 0 = OFF //what a huge waste of space!
(当然,这个问题在现实生活中更为明显,因为 32 位整数看起来像这样:
[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0] = 0
答案是使用位域。我们有一组属性(通常是相关的),我们将使用位操作打开和关闭这些属性。所以,比如说,你可能在一个硬件上有 4 个不同的灯,你想打开或关闭它们。
3 2 1 0
[0][0][0][0] = 0
(为什么我们从光 0 开始?我稍后会解释。)请注意,这是一个整数,并且存储为整数,但用于表示多个对象的多个状态。疯狂的!假设我们打开灯 2 和 1:
3 2 1 0
[0][1][1][0] = 6
您应该在这里注意的重要一点:灯 2 和 1 亮起应该等于 6 可能没有明显的原因,而且我们将如何使用这种信息存储方案做任何事情可能并不明显。如果添加更多位,它看起来并不明显:
3 2 1 0
[1][1][1][0] = 0xE \\what?
为什么我们关心这个?对于 0 到 15 之间的每个数字,我们是否只有一个状态?如果没有一些疯狂的 switch 语句系列,我们将如何管理它?啊...
尽头的光
因此,如果您之前使用过二进制算术,您可能会意识到左边的数字和右边的数字之间的关系当然是以 2 为底的。即:
1*(2 3 ) + 1*(2 2 ) + 1*(2 1 ) +0 *(2 0 ) = 0xE
所以每束光都存在于方程每一项的指数中。如果灯亮,则其术语旁边有一个 1 - 如果灯熄灭,则有一个 0。花点时间说服自己,在 0 到 15 之间恰好有一个整数对应于这个编号方案中的每个状态。
位运算符
现在我们已经完成了,让我们花点时间看看在这个设置中移位对整数有什么作用。
[0][0][0][1] = 1
当您在整数中向左或向右移动位时,它实际上是向左和向右移动位。(注意:我 100% 否认这种对负数的解释!有龙!)
1<<2 = 4
[0][1][0][0] = 4
4>>1 = 2
[0][0][1][0] = 2
当移动不止一位表示的数字时,您会遇到类似的行为。此外,让自己相信 x>>0 或 x<<0 只是 x 应该不难。不会在任何地方转移。
这可能向不熟悉 Shift 运算符的任何人解释了它们的命名方案。
位运算
这种以二进制表示的数字也可用于阐明按位运算符对整数的操作。第一个数字中的每个位与其同伴编号进行异或运算、与运算或或运算。花点时间浏览一下维基百科,熟悉一下这些布尔运算符的功能——我将解释它们如何在数字上起作用,但我不想详细地重复这个总体概念。
...
欢迎回来!让我们首先检查 OR (|) 运算符对存储在四位中的两个整数的影响。
OR OPERATOR ON:
[1][0][0][1] = 0x9
[1][1][0][0] = 0xC
________________
[1][1][0][1] = 0xD
艰难的!这与布尔 OR 运算符的真值表非常相似。请注意,每一列都忽略了相邻的列,只是用第一位和第二位 OR'd 的结果填充结果列。另请注意,在该特定列中,任何或 1 的值都是 1。任何带零的或'd 都保持不变。
AND (&) 的表格很有趣,尽管有些倒置:
AND OPERATOR ON:
[1][0][0][1] = 0x9
[1][1][0][0] = 0xC
________________
[1][0][0][0] = 0x8
在这种情况下,我们做同样的事情——我们对列中的每个位执行 AND 操作,并将结果放入该位。没有列关心任何其他列。
关于这一点的重要教训,我邀请您使用上图进行验证:任何与零相加的东西都是零。此外,同样重要的是 - 与 1 进行与运算的数字不会发生任何事情。他们保持不变。
最终表 XOR 具有我希望你们现在都可以预测的行为。
XOR OPERATOR ON:
[1][0][0][1] = 0x9
[1][1][0][0] = 0xC
________________
[0][1][0][1] = 0x5
每个位都与其列 yadda yadda 等进行异或运算。但是仔细看第一排和第二排。哪些位改变了?(其中一半。)哪些位保持不变?(回答这个没有积分。)
当(且仅当)第二行中的位为 1 时,结果中的第一行中的位正在更改!
一个灯泡的例子!
所以现在我们有了一组有趣的工具,可以用来翻转单个位。让我们回到灯泡示例,只关注第一个灯泡。
0
[?] \\We don't know if it's one or zero while coding
我们知道我们有一个操作可以始终使该位等于一个 - OR 1 运算符。
0|1 = 1
1|1 = 1
所以,忽略其余的灯泡,我们可以这样做
4_bit_lightbulb_integer |= 1;
并且确定我们除了将第一个灯泡设置为 ON 之外什么也没做。
3 2 1 0
[0][0][0][?] = 0 or 1? \\4_bit_lightbulb_integer
[0][0][0][1] = 1
________________
[0][0][0][1] = 0x1
同样,我们可以将数字与零相加。嗯 - 不完全为零 - 我们不想影响其他位的状态,所以我们将用 1 填充它们。
我将使用一元(一个参数)运算符进行位否定。~ (NOT) 位运算符翻转其参数中的所有位。〜(0X1):
[0][0][0][1] = 0x1
________________
[1][1][1][0] = 0xE
我们将把它与下面的 AND 位结合使用。
让我们做 4_bit_lightbulb_integer & 0xE
3 2 1 0
[0][1][0][?] = 4 or 5? \\4_bit_lightbulb_integer
[1][1][1][0] = 0xE
________________
[0][1][0][0] = 0x4
我们在右侧看到很多整数,它们没有任何直接相关性。如果您经常处理位字段,您应该习惯这一点。看左边。右边的位始终为零,其他位不变。我们可以关掉灯 0 并忽略其他一切!
最后,您可以使用 XOR 位选择性地翻转第一位!
3 2 1 0
[0][1][0][?] = 4 or 5? \\4_bit_lightbulb_integer
[0][0][0][1] = 0x1
________________
[0][1][0][*] = 4 or 5?
我们现在实际上不知道 * 的值是什么——只是从什么翻转过来的?曾是。
结合位移和按位运算
关于这两个操作的有趣事实是,当它们一起使用时,它们允许您操作选择性位。
[0][0][0][1] = 1 = 1<<0
[0][0][1][0] = 2 = 1<<1
[0][1][0][0] = 4 = 1<<2
[1][0][0][0] = 8 = 1<<3
唔。有趣的。我将在这里提到否定运算符 (~),因为它以类似的方式用于为位字段中的内容生成所需的位值。
[1][1][1][0] = 0xE = ~(1<<0)
[1][1][0][1] = 0xD = ~(1<<1)
[1][0][1][1] = 0xB = ~(1<<2)
[0][1][1][1] = 0X7 = ~(1<<3)
您是否看到移位值与移位位的相应灯泡位置之间存在有趣的关系?
规范的位移运算符
正如上面提到的,我们有一个有趣的通用方法,可以使用上面的位移器打开和关闭特定的灯。
要打开灯泡,我们使用位移在正确位置生成 1,然后将其与当前灯泡位置进行或运算。假设我们要打开灯 3,而忽略其他所有内容。我们需要进行 OR 的位移操作
3 2 1 0
[?][?][?][?] \\all we know about these values at compile time is where they are!
和 0x8
[1][0][0][0] = 0x8
多亏了位移位,这很容易!我们将选择灯的编号并切换值:
1<<3 = 0x8
接着:
4_bit_lightbulb_integer |= 0x8;
3 2 1 0
[1][?][?][?] \\the ? marks have not changed!
我们可以保证第三个灯泡的位设置为 1,并且没有其他任何变化。
清除位的工作方式类似——我们将使用上面的否定位表来清除灯 2。
~(1<<2) = 0xB = [1][0][1][1]
4_bit_lightbulb_integer & 0xB:
3 2 1 0
[?][?][?][?]
[1][0][1][1]
____________
[?][0][?][?]
翻转位的 XOR 方法与 OR 方法相同。
所以位切换的规范方法是这样的:
打开灯 i:
4_bit_lightbulb_integer|=(1<<i)
关灯 i:
4_bit_lightbulb_integer&=~(1<<i)
翻转灯 i:
4_bit_lightbulb_integer^=(1<<i)
等等,我怎么读这些?
为了检查位,我们可以简单地将所有位清零,除了我们关心的位。然后我们将检查结果值是否大于零——因为这是唯一可能为非零的值,所以当且仅当它非零时,它将使整个整数非零。例如,要检查位 2:
1<<2:
[0][1][0][0]
4_bit_lightbulb_integer:
[?][?][?][?]
1<<2 & 4_bit_lightbulb_integer:
[0][?][0][0]
还记得前面的例子中 的值吗?没有改变。还要记住,任何 AND 0 都是 0。因此,我们可以肯定地说,如果该值大于零,则位置 2 处的开关为真,灯泡为零。同样,如果该值关闭,则整个事物的值将为零。
(您可以将 4_bit_lightbulb_integer 的整个值交替移动 i 位,然后将其与 1 相加。我不记得如果一个比另一个快,但我对此表示怀疑。)
所以规范检查功能:
检查位 i 是否打开:
if (4_bit_lightbulb_integer & 1<<i) {
\\do whatever
}
具体情况
现在我们已经有了一套完整的按位运算的工具,我们可以看这里的具体例子。这基本上是相同的想法 - 除了执行它的更简洁和强大的方式。我们来看看这个函数:
void set(int i) { x[i>>SHIFT] |= (1<<(i & MASK)); }
从规范实现中,我将猜测这是试图将一些位设置为 1!让我们取一个整数,看看如果我将值 0x32(十进制的 50)输入i时会发生什么:
x[0x32>>5] |= (1<<(0x32 & 0x1f))
好吧,那是一团糟。让我们在右边剖析这个操作。为方便起见,假设还有 24 个不相关的零,因为它们都是 32 位整数。
...[0][0][0][1][1][1][1][1] = 0x1F
...[0][0][1][1][0][0][1][0] = 0x32
________________________
...[0][0][0][1][0][0][1][0] = 0x12
看起来一切都在顶部的边界处被切断,1s 变成了零。这种技术称为位掩码。有趣的是,这里的边界将结果值限制在 0 到 31 之间……这正是 32 位整数的位数!
x[0x32>>5] |= (1<<(0x12)) 让我们看看另一半。
...[0][0][1][1][0][0][1][0] = 0x32
右移五位:
...[0][0][0][0][0][0][0][1] = 0x01
请注意,这种转换完全破坏了函数第一部分的所有信息——我们有 32-5 = 27 个剩余的位,这些位可能是非零的。这表明选择了整数数组中的 2 27个整数中的哪一个。所以现在简化的方程是:
x[1] |= (1<<0x12)
这看起来就像规范的位设置操作!我们刚刚选择了
所以这个想法是使用前 27 位来选择一个要移位的整数,最后 5 位指示该整数中 32 位中的哪一位要移位。