我遇到了一个奇怪的问题。为了演示,让我们取我机器上最大的无符号数(printf "%X \n" -1
给我FFFFFFFFFFFFFFFF
),并尝试移动一些位。?首先,向左移动:
printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF<<4 ))
FFFFFFFFFFFFFFF0
printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF<<8 ))
FFFFFFFFFFFFFF00
printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF<<16 ))
FFFFFFFFFFFF0000
Run Code Online (Sandbox Code Playgroud)
到现在为止还挺好。正如预期的那样。现在让我们尝试右移:
printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF>>4 ))
FFFFFFFFFFFFFFFF
printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF>>8 ))
FFFFFFFFFFFFFFFF
printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF>>16 ))
FFFFFFFFFFFFFFFF
Run Code Online (Sandbox Code Playgroud)
等等,什么??为什么这不起作用?这是一个错误吗?
编辑:
我害怕有人会建议与提出的符号位有某种联系。但我们不是在谈论算术,所以符号的概念在这里没有立足之地。其他工具如*
和/
用于算术。拥有一个可以操作位的工具的全部意义在于能够操作位——无论我以后如何选择显示这些位,有符号还是无符号。对?喜欢:
printf "%u \n" -1
18446744073709551615
Run Code Online (Sandbox Code Playgroud)
有人有什么想法吗?
编辑:
由于这里的答案直接涉及乘法或除法,让我尝试更清楚地解释我的担忧。乘法/除法和位移位是两种不同的东西,尽管我可以在长期程序员的脑海中看到它们之间的联系。做算术的时候,必须要有符号的概念;对于位移,你没有。Bash 为我们提供了两种截然不同的工具集来处理这两种不同的事情。当我想将一个数字乘以 2 时,我会使用该*
工具。Bash 在底层可以使用位移进行算术这一事实超出了重点。
引用其中一个答案...
如果没有复制符号位,结果将变成无符号数。例如,将 8 位值
1111 0000
向右移动一次将给出0111 1000
但1111 0000
变成0111 1000
正是我想要的。如果我想做一个除法,那么我会改用算术运算符。
无论如何,是否至少有某种方法可以明确指定在移位时应该填充什么样的位?
ilk*_*chu 13
“逻辑右移”在左边插入零位,因此右移一位的结果对应于将无符号二进制数除以二。echo $(( 16 >> 1 ))
给8
.
并且,“算术右移”在左边插入了符号位的副本,所以右移一位的结果对应于有符号二进制数除以二。echo $(( 16 >> 1 ))
给8
,并echo $(( -16 >> 1 ))
给-8
。除了在二进制补码上,它与实际除法的舍入不匹配:-15 >> 1
给出-8
;而-15 / 2
给-7
.
如果符号位没有被复制,而是被清零,结果将是一个正数。例如,将 8 位值1111 0000
(0xf0, -16) 向右移动一次将得到0111 1000
(0x78, +120)。
现在,使用其中的哪一个是更棘手的问题。
在实践中,许多实现会对有符号数使用算术移位,而 shell 算术主要是在有符号 long 上完成的。
但这并不能完全保证。shell 算术的 POSIX 定义是指大多数行为的 C 标准,例如运算符表没有说明>>
应该是哪种类型的移位。(请参阅:Shell 命令语言、2.6.4 算术扩展和Shell 与实用程序、1.1.2 源自 ISO C 标准的概念:算术精度和运算)
整数变量和常量,包括操作数和选项参数的值,[...] 应实现为等效于 ISO C 标准有符号长数据类型 [...]
算术运算符和控制流关键字的实现应与引用的 ISO C 标准部分 [...]
<<
中的相同>>
:第 6.5.7 节,按位移位运算符
对于 negative
a
, 的值a >> b
是实现定义的(在大多数实现中,这执行算术右移,因此结果保持为负)。
(这可能是一个并非所有事物都是二进制补码的世界的残余。一个补码或符号幅度数的右侧移位与二进制补码数的右侧移位不同。但结果是一样的:它是实现定义的。)
其他一些编程语言,如 Javascript,有不同的算术右移运算符>>
和逻辑右移运算符>>>
。但是 C 没有,我试过的任何 shell 也没有。
顺便说一句,如果您要以大于字宽的偏移量进行移位,您还会看到奇怪的事情发生。在 x86 上,1 << 64
只是1
,因为处理器只查看移位值的最低 6 位,所以它与1 << 0
. (1 << 32) << 32
是0
,但是,其结果可能是在另一个处理器不同。
你说,
但是符号的概念在这里没有立足之地。我的意思是,一个数字就是一个数字,不管你以后会选择把它显示为有符号还是无符号,对吧?
对于二进制补码机上的加法、减法和乘法的低部分(例如 32x32 -> 32)来说也是如此。
但对于乘法或除法的高部分,通常情况并非如此。8 位值0xff
可以表示无符号数 255 或有符号数 -1。例如,8x8 -> 16 的乘法0xff * 0xff
是0x0001
或0xfe01
,具体取决于它是有符号 (-1 * -1) 还是无符号 (255 * 255)。还例如0xff / 3
是任一0
或0x55
,如果它的签名取决于(-1 / 3 == 0),或无符号(255/3 = = 85)。
Qua*_*odo 10
逻辑右移用零填充,
[01010110] >> 2 becomes [00010101]
[11010110] >> 2 becomes [00110101]
Run Code Online (Sandbox Code Playgroud)
算术右移填充最高有效位,
[01010110] >> 2 becomes [00010101]
[11010110] >> 2 becomes [11110101]
Run Code Online (Sandbox Code Playgroud)
您期望逻辑移位,但 Bash 执行算术移位。这确实与签名有关。
该手册说
评估以固定宽度的整数完成。运算符及其优先级、结合性和值与 C 语言中的相同。
整数常量遵循 C 语言定义,没有后缀或字符常量。带有前导 0 的常量被解释为八进制数。前导“0x”或“0X”表示十六进制。
并引用计算机系统,Randal Bryant 和 David O'Hallaron 的话:
C 标准没有精确定义应该使用哪种类型的右移。对于无符号数据,右移必须符合逻辑。对于有符号数据(默认),可以使用算术或逻辑移位。(...) 然而,在实践中,几乎所有编译器/机器组合都对带符号的数据使用算术右移,许多程序员认为情况就是这样。
Stack Overflow 中也有人问过这个问题:C 中的移位运算符 (<<, >>) 是算术还是逻辑?