位串还有一组逻辑运算:移位和旋转。这些运算可以进一步细分为左移、循环左移、右移和循环右移。在许多程序中都会使用到这些运算。
如图3-3所示,左移运算会把位串中的每一位都向左移一位。原先的第0位移至第1位,第1位移至第2位,依此类推。
图3-3 (字节的)左移运算
读者可能会提出两个疑问:第0位从哪里来?高位去哪了?我们在第0位移入一个0,而原先高位中的值将作为运算的进位。
一些高级语言(例如C/C++/C#、Swift、Java及Free Pascal/Delphi)提供了左移运算符。在C语言及其衍生语言中左移运算符为<<,而Free Pascal/Delphi使用shl运算符。下面是一些例子:
一个数值的二进制表示左移一位等同于该值乘以2。如果编程语言不提供专门的左移运算符,则可以通过将二进制值乘以2来模拟左移。虽然乘法运算通常要比左移运算慢,但是大多数编译器都可以智能地把乘数为2的幂的乘法运算转换成左移运算。于是,在Visual Basic中可以用下面这段代码来进行左移:
除了移动的方向相反,右移运算和左移运算是一样的。将第7位移至第6位,第6位移至第5位,第5位移至第4位,依此类推。在右移时,在第7位移入0,而将第0位作为运算的进位(如图3-4所示)。C、C++、C#、Swift和Java使用>>运算符表示右移运算。Free Pascal/Delphi则使用shr运算符。大多数汇编语言也提供了右移指令(比如80x86的shr)。
图3-4 (字节的)右移运算
无符号二进制值右移一位等于将该值除以2。例如,将无符号的254($FE)右移一位得到127($7F),这是我们期望的结果。但是,如果将-2($FE)的8位二进制补码表示形式($FE)右移一位则得到127($7F),结果是错误的。我们使用第三种移位运算,通过移位得到有符号数除以2的结果。这种运算就是算术右移(Arithmetic Shift Right),它不会改变高位的值。图3-5展示了8位操作数的算术右移运算。
图3-5 (字节的)算术右移运算
有符号操作数的二进制补码的算术右移的结果通常符合我们的期望。例如,将-2($FE)算术右移一位得到的是-1($FF)。但是请注意,算术右移始终通过四舍五入得到最接近实际值的整数结果,要么比实际结果小,要么和实际结果相等。例如
-1($FF)算术右移的结果是-1而不是0。因为-1小于0,所以算术右移运算四舍五入的结果为-1。这并不是算术右移运算的“错误”,只是它采用的是不同(但可行)的整数除法定义。最坏的结果就是在不支持算术右移的语言中,无法使用有符号数的除法运算代替算术右移,因为大多数整数的除法运算都会舍入到0。
同时,支持逻辑右移和算术右移的高级语言非常少见。更糟糕的是,某些语言把使用算术右移运算还是逻辑右移运算的决定权交给编译器的实现者。因此,只有在能保证高位的值进行两种右移运算产生相同的结果的情况下,才可以放心地使用右移运算符。确保右移一定是逻辑右移或算术右移,要么使用汇编语言,要么手动处理高位。高级代码很快就会变得难看,因此,如果程序不需要跨CPU移植,则快速的内联汇编语句是更好的解决方案。下面的代码展示了如何在右移运算类型不确定的语言中模拟32位逻辑右移和算术右移:
许多汇编语言还提供了各种各样的旋转指令。旋转指令将从操作数一端移出的位移入另一端,以实现操作数的位循环。高级语言很少支持这类运算。但是这些运算也不常用到。如果确实需要循环移位,则可以使用高级语言提供的移位运算进行组合:
更多关于移位和旋转运算的信息,请参考 The Art of Assembly Language (No Starch Press)。