二进制位最终直接对应到 CPU 或内存中的晶体管,以及每个晶体管是“开”还是“关”。
底层操作,俗称“bit-twiddling”,在系统级语言中尤为重要。
像 Julia 这样的高级语言通常会抽象掉大部分这类细节。不过,基础语言中提供了完整的位级运算。
注意: 要在 REPL 中看到人类可读的二进制输出,下面几乎所有的示例都需要用bitstring()函数包裹起来。这在视觉上很干扰,因此这个函数的多数出现都被省略了。
整数类型,无论有符号还是无符号,都可以表示为由 1 和 0 组成的字符串。
julia> bitstring(UInt8(5))
"00000101"
位移只是把所有内容向左或向右移动指定的位数。对于UInt类型,一些位会从一端掉出去,另一端则用 0 填充:
julia> ux::UInt8 = 5
5
julia> bitstring(ux)
"00000101"
julia> ux << 2 # left by 2
"00010100"
julia> ux >> 1 # right by 1
"00000010"
每次左移都会使值翻倍,每次右移都会使其减半(会有截断)。用十进制表示会更明显:
julia> 3 << 2
12
julia> 24 >> 3
3
这种位移比“正规”的算术运算快得多,因此这种技巧在底层编程中非常流行。
对于有符号整数,我们需要更小心一些。
左移相对简单:
julia> sx = Int8(5)
5
julia> sx # positive integer
"00000101"
julia> sx << 2
"00010100"
julia> -sx # negative integer
"11111011"
julia> -sx << 2
"11101100"
因此,正的有符号整数左移与无符号整数相同。
负值以补码形式存储,也就是说最左边的位是 1。左移没有问题,但右移时,最左边的位该如何填充?
julia> sx >> 2 # simple for positive values!
"00000001"
julia> -sx # negative integer
"11111011"
julia> -sx >> 2 # pad with repeated sign bit
"11111110"
julia> -sx >>> 2 # pad with 0
"00111110"
>>运算符执行算术移位,保留符号位。
>>>运算符执行逻辑移位,用 0 填充,就好像这个数是无符号的。
在之前的概念中我们了解到,运算符&&(与)、||(或)和!(非)用于布尔值。
还有对应的运算符&(按位与)、|(按位或)和~(波浪号,按位非),用于比较两个整数中的位。
julia> 0b1011 & 0b0010 # bit is 1 in both numbers
"00000010"
julia> 0b1011 | 0b0010 # bit is 1 in at least one number
"00001011"
julia> ~0b1011 # flip all bits
"11110100"
julia> xor(0b1011, 0b0010) # bit is 1 in exactly one number, not both
"00001001"
这里,xor()是异或,以函数的形式使用。
你的朋友刚刚给你发来一条带有重要秘密的消息。 为了不让别人轻易读到它,这条消息经过一系列位操作进行了加密。 你需要编写一些函数来帮助解密这条消息。
解密这条消息的第一步,是把加密过程中的移位还原,也就是把位向右移回去。 解密过程中后续的一些步骤会假定 0 是从左侧插入的。
实现shift_back()函数,它接收一个值和要移位的位数,并执行移位。
shift_back(0b1001, 2) # => 0b0010
接下来,有一些位需要设为 1。
实现set_bits()函数,它接收一个值和一个掩码,返回把值中的相应位设为 1 之后的结果。
如果mask中的某一位是 1,那么value中对应的位也应设为 1。
其余所有位保持不变。
set_bits(0b0110, 0b0101) # => 0b0111
加密过程中有一些位被翻转了。 要解密这条消息,就需要把它们翻转回来。
实现flip_bits()函数,它接收一个值和掩码。
掩码指明值中哪些位需要翻转。
如果mask中的某一位是 1,value中对应的位就会被翻转。
其余所有位保持不变。
flip_bits(0b1100, 0b0101) # => 0b1001
最后,还有一些位解密后总是 0。
实现clear_bits()函数,它接收一个值和掩码。
掩码中为 1 的位,在value中对应的位应设为 0。
其余所有位保持不变。
clear_bits(0b0110, 0b0101) # => 0b0010