C语言还提供了一组移位运算,以便向左或者向右移动位模式。对于一个位表示为[xn-1,xn-2,…,x0]的操作数x,C表达式x<<k会生成一个值,其位表示为[xn-k-1,xn-k-2,…,x0,0,…,0]。也就是说,x向左移动k位,丢弃最高的k位,并在右端补k个0。移位量应该是一个0~n-1之间的值。移位运算是从左至右可结合的,所以x<<j<<k等价于(x<<j)<<k。
有一个相应的右移运算x>>k,但是它的行为有点微妙。一般而言,机器支持两种形式的右移:逻辑右移和算术右移。逻辑右移在左端补k个0,得到的结果是[0,…,0,xn-1,xn-2,…,xk]。算术右移是在左端补k个最高有效位的值,得到的结果是[xn-1,…,xn-1,xn-1,xn-2,…,xk]。这种做法看上去可能有点奇特,但是我们会发现它对有符号整数数据的运算非常有用。
让我们来看一个例子,下面的表给出了对某些实例8位数据做不同的移位操作得到的结果。
操作 值 参数X [0110 0011] [1001 0101] X<<4 [0011 0000] [0101 0000] X>>4(逻辑右移) [0000 0110] [0000 1001] X>>4(算术右移) [0000 0110] [1111 1001]
斜体的数字表示的是最右端(左移)或最左端(右移)填充的值。可以看到除了一个条目之外,其他的都涉及填充0。唯一的例外是算术右移[10010101]的情况。因为操作数的最高位是1,填充的值就是1。
C语言标准并没有明确定义应该使用哪种类型的右移。对于无符号数据(也就是以限定词unsigned声明的整型对象),右移必须是逻辑的。而对于有符号数据(默认的声明的整型对象),算术的或者逻辑的右移都可以。不幸的是,这就意味着任何假设一种或者另一种右移形式的代码都潜在着可移植性问题。然而,实际上,几乎所有的编译器/机器组合都对有符号数据使用算术右移,且许多程序员也都假设机器会使用这种右移。
另一方面,Java对于如何进行右移有明确的定义。表达式x>>k会将x算术右移k个位置,而x>>>k会对x做逻辑右移。
当移动k位,这里k很大时对于一个由w位组成的数据类型,如果要移动k≥w位会得到什么结果呢?例如,在一个32位机器上计算下面的表达式会得到什么结果:
Int lval = 0xFEDCBA98 << 32; int lval = 0xFEDCBA98 >> 36 unsigned lval = 0xFEDCBA98u >> 40;
C语言标准很小心地规避了说明在这种情况下该如何做。在许多机器上,当移动一个w位的值时,移位指令只考虑位移量的低log2w位,因此实际上位移量就是通过计算k mod w得到的。例如,在一台采用这个规则的32位机器上,上面三个移位运算分别是移动0、4和8位,得到结果:
Lval 0xFEDCBA98 Aval 0xFEDCBA9 Uval 0x00FEDCBA
不过这种行为对于C程序来说是没有保证的,所以移位数量应该保持小于字长。另一方面,Java特别要求位移数量应该按照我们前面所讲的求模的方法来计算。
与移位运算有关的操作符优先级问题常常有人会写这样的表达式1<<2+3<<4,其本意是(1<<2)+(3<<4)。但是在C语言中,前面的表达式等价于1<<(2+3)<<4,这是由于加法(和减法)的优先级比移位运算要高。然后,按照从左至右结合性规则,括号应该是这样打的(1<<(2+3))<<4,因此得到的结果是512,而不是期望的52。在C表达式中搞错优先级是一种常见的程序错误,而且常常很难检查出来。所以当你拿不准的时候,请加上括号!
C++ Primer Plus 第6版 中文版 清晰有书签PDF+源代码
将C语言梳理一下,分布在以下10个章节中:
Linux-C成长之路(十):其他高级议题