位域和你
我将使用一个简单的示例来解释基础知识。假设您有一个四位无符号整数:
[0][0][0][0] = 0
您可以通过将其转换为基数 2 来表示从 0 到 15 的任何数字。假设我们让右端最小:
[0][1][0][1] = 5
因此第一位加 1,第二位加 2,第三位加 4,第四位加 8。例如,这里是 8:
[1][0][0][0] = 8
So What?假设您想在应用程序中表示二进制状态 - 如果启用了某个选项,是否应该绘制某个元素等等。您可能不想为其中每一个使用整个整数 - 它会使用 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
(为什么我们从 light 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*(23) + 1*(22) + 1*(21) +0 *(20) = 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
这可能向任何不熟悉 Shift 运算符的人解释了它们的命名方案。
按位运算
这种二进制数字表示也可用于阐明整数上的按位运算符的运算。第一个数字中的每一位都与其同伴数字进行异或、与或或运算。花点时间浏览维基百科并熟悉这些布尔运算符的功能 - 我将解释它们如何在数字上起作用,但我不想详细地重复一般概念。
...
欢迎回来!让我们首先检查 OR (|) 运算符对存储在四位中的两个整数的影响。
OR OPERATOR ON:
[1][0][0][1] = 0x9
[1][1][0][0] = 0xC
________________
[1][1][0][1] = 0xD
艰难的!这与布尔 OR 运算符的真值表非常相似。请注意,每一列都会忽略相邻的列,而只是将第一位和第二位进行或运算的结果填充到结果列中。笔记also与 1 进行或运算的任何值在该特定列中均为 1。任何与零进行或运算的值都保持不变。
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
我们知道有一个操作可以使该位始终等于 1——OR 1 运算符。
0|1 = 1
1|1 = 1
所以,忽略其余的灯泡,我们可以这样做
4_bit_lightbulb_integer |= 1;
并且确信我们除了将第一个灯泡打开之外什么也没做。
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
唔。有趣的。我将在这里提到否定运算符 (~),因为它以类似的方式使用来生成位字段中的 AND 运算所需的位值。
[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,忽略其他一切。我们需要进行位移位运算,或
3 2 1 0
[?][?][?][?] \\all we know about these values at compile time is where they are!
and 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][?][?]
翻转位的异或方法与或方法的思想相同。
所以位交换的规范方法是这样的:
打开灯我:
4_bit_lightbulb_integer|=(1<<i)
关掉灯我:
4_bit_lightbulb_integer&=~(1<<i)
翻转灯 i:
4_bit_lightbulb_integer^=(1<<i)
等等,我该如何阅读这些内容?
为了检查一位,我们可以简单地将除我们关心的位之外的所有位清零。然后我们将检查结果值是否大于零,因为这是唯一可能非零的值,当且仅当它非零时,它才会使整个整数非零。例如,要检查位 2:
1
[0][1][0][0]
4_bit_lightbulb_integer:
[?][?][?][?]
1
[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
看起来一切都在顶部边界处被切断,1 变成了 0。这种技术称为位掩码。有趣的是,这里的边界将结果值限制在 0 到 31 之间……这正是 32 位整数的位数!
x[0x32>>5] |= (1
...[0][0][1][1][0][0][1][0] = 0x32
右移五位:
...[0][0][0][0][0][0][0][1] = 0x01
Note that this transformation exactly destroyed all information from the first part of the function- we have 32-5 = 27 remaining bits which could be nonzero. This indicates which of 227 integers in the array of integers are selected. So the simplified equation is now:
x[1] |= (1<<0x12)
这看起来就像规范的位设置操作!我们刚刚选择了
因此,我们的想法是使用前 27 位来选择要移位的整数,最后 5 位指示要移位该整数中 32 位中的哪一位。