C中的移位运算符(<<,>>)是算术还是逻辑?

lit*_*yte 120 c binary bit-manipulation bit-shift

在C中,移位运算符(<<,>>)是算术还是逻辑?

Gre*_*ill 126

向左移位时,算术和逻辑移位之间没有区别.向右移位时,移位类型取决于移位值的类型.

(作为那些不熟悉差异的读者的背景,1位的"逻辑"右移将所有位向右移位,并用0填充最左边的位."算术"移位将原始值保留在最左边的位当处理负数时,差异变得很重要.)

当移位无符号值时,C中的>>运算符是逻辑移位.当移位有符号值时,>>运算符是算术移位.

例如,假设一台32位机器:

signed int x1 = 5;
assert((x1 >> 1) == 2);
signed int x2 = -5;
assert((x2 >> 1) == -3);
unsigned int x3 = (unsigned int)-5;
assert((x3 >> 1) == 0x7FFFFFFD);
Run Code Online (Sandbox Code Playgroud)

  • 如此接近,格雷格.您的解释几乎是完美的,但转换已签名类型和负值的表达式是实现定义的.见ISO/IEC 9899:1999第6.5.7节. (48认同)
  • @Rob:实际上,对于左移和带符号的负数,行为是未定义的. (12认同)
  • 实际上,如果得到的数学值(不受位大小限制)无法在该有符号类型中表示为正值,则左移也会导致*正*有符号值的未定义行为.最重要的是,在右移有符号值时必须小心行事. (4认同)
  • @supercat:我真的不知道.但是,我知道有文档记录的情况,其中具有未定义行为的代码会导致编译器执行非常直观的操作(通常是由于积极的优化 - 例如,请参阅旧的Linux TUN/TAP驱动程序空指针错误:http:/ /lwn.net/Articles/342330/).除非我需要右移的符号填充(我意识到是实现定义的行为),我通常尝试使用无符号值执行我的位移位,即使它意味着使用强制转换到达那里. (3认同)
  • @MichaelBurr:我知道超现代编译器使用 C 标准未定义的行为这一事实(即使它已在 99% 的*实现* 中定义)作为将其行为完全定义的程序的理由在它们可以运行的所有平台上,变成毫无价值的机器指令,没有有用的行为。我承认,虽然(讽刺)我很困惑为什么编译器作者错过了最大的优化可能性:省略程序的任何部分,如果到达,将导致函数被嵌套...... (2认同)
  • ...超过三个深.有现实世界的平台,甚至四个深度嵌套的方法与大量的局部变量可以轰炸(例如在极低内存条件下运行MS-DOS程序),标准并没有说一个程序可以合法地轰炸一个低于2K的可用内存的系统不应该在有两次演出的系统上炸弹. (2认同)

Ron*_*nie 91

根据K&R第二版,结果依赖于实现,用于签名值的右移.

维基百科说,C/C++"通常"对签名值实施算术转换.

基本上你需要测试你的编译器或不依赖它.我对当前MS C++编译器的VS2008帮助表明他们的编译器进行了算术移位.

  • @stephan:在某些情况下,编译器的选择可能是由处理器体系结构驱动的,但当今大多数编译器都会将带符号值的“&gt;&gt;”处理为算术右移*即使需要添加符号扩展代码*。 (3认同)

leg*_*s2k 49

TL; DR

考虑in分别成为移位运算符的左右操作数; i整数提升后的类型T.假设n[0, sizeof(i) * CHAR_BIT)- 否则未定义 - 我们遇到以下情况:

| Direction  |   Type   | Value (i) | Result                   |
| ---------- | -------- | --------- | ------------------------ |
| Right (>>) | unsigned |    ? 0    | ?? ? (i ÷ 2?)            |
| Right      | signed   |    ? 0    | ?? ? (i ÷ 2?)            |
| Right      | signed   |    < 0    | Implementation-defined†  |
| Left  (<<) | unsigned |    ? 0    | (i * 2?) % (T_MAX + 1)   |
| Left       | signed   |    ? 0    | (i * 2?) ‡               |
| Left       | signed   |    < 0    | Undefined                |
Run Code Online (Sandbox Code Playgroud)

†大多数编译器将其实现为算术移位
‡undefined如果值溢出结果类型T; 推广类型的我


首先是从数学角度看逻辑和算术转换之间的差异,而不必担心数据类型的大小.逻辑移位总是用零填充丢弃的位,而算术移位仅用左移位用零填充它,但是对于右移,它复制MSB从而保留操作数的符号(假设负值的二进制补码编码).

换句话说,逻辑移位将移位的操作数视为一个比特流并移动它们,而不必担心结果值的符号.算术移位将其视为(带符号)数字,并在移位时保留符号.

X乘以n的左算术移位相当于将X乘以2 n,因此相当于逻辑左移; 逻辑转换也会产生相同的结果,因为MSB无论如何都会失败,没有什么可以保留的.

如果X是非负的,则X 乘以n的右算术移位相当于X的整数除以2 n!整数除法不过是数学除法,而是向0(截断)舍入.

对于由二进制补码编码表示的负数,右移n位具有数学上将其除以2 n并向-∞(floor)舍入的效果; 因此,对于非负值和负值,右移是不同的.

对于X≥0,X >> n = X/2 n = trunc(X÷2 n)

对于X <0,X >> n = floor(X÷2 n)

÷数学除法在哪里,/是整数除法.我们来看一个例子:

37)10 = 100101)2

37÷2 = 18.5

37/2 = 18(向18.5舍入为0)= 10010)2 [算术右移的结果]

-37)10 = 11011011)2(考虑二进制补码,8位表示)

-37÷2 = -18.5

-37/2 = -18(向18.5舍入为0)= 11101110)2 [不是算术右移的结果]

-37 >> 1 = -19(舍入18.5朝向-∞)= 11101101)2 [算术右移的结果]

正如Guy Steele指出的那样,这种差异导致了多个编译器中的错误.这里非负(数学)可以映射到无符号和有符号的非负值(C); 两者都是相同的,右移它们是通过整数除法完成的.

所以逻辑和算术在左移和右移的非负值中是等价的; 它正在改变它们不同的负值.

操作数和结果类型

标准C99§6.5.7:

每个操作数应具有整数类型.

对每个操作数执行整数提升.结果的类型是提升的左操作数的类型.如果右操作数的值为负或大于或等于提升的左操作数的宽度,则行为未定义.

short E1 = 1, E2 = 3;
int R = E1 << E2;
Run Code Online (Sandbox Code Playgroud)

在上面的代码片段中,两个操作数都变为int(由于整数提升); 如果E2是否定的,E2 ? sizeof(int) * CHAR_BIT那么操作是不确定的.这是因为超过可用位的移位肯定会溢出.如果R被声明为short,则int移位操作的结果将被隐式转换为short; 缩小转换,如果值在目标类型中无法表示,则可能导致实现定义的行为.

左移

E1 << E2的结果是E1左移E2位位置; 腾出的位用零填充.如果E1具有无符号类型,则结果的值为E1×2 E2,比结果类型中可表示的最大值减少一个模数.如果E1具有带符号类型和非负值,并且E1×2 E2在结果类型中可表示,那么这就是结果值; 否则,行为未定义.

由于左移对于两者都是相同的,所以空出的位简单地用零填充.然后它声明对于无符号和有符号类型,它是一个算术移位.我将其解释为算术移位,因为逻辑移位不会对比特所代表的值感到烦恼,它只是将其视为比特流; 但是标准不是根据比特来说,而是根据E1与2 E2的乘积得到的值来定义.

这里需要注意的是,对于有符号的类型,该值应该是非负的,并且结果值应该在结果类型中可表示.否则操作未定义.结果类型将是应用整数提升后的E1类型,而不是目标(将保留结果的变量)类型.结果值隐式转换为目标类型; 如果它在该类型中不可表示,则转换是实现定义的(C99§6.3.1.3/ 3).

如果E1是具有负值的带符号类型,则左移位的行为是未定义的.这是一个容易被忽略的未定义行为的简单途径.

右转

E1 >> E2的结果是E1右移E2位位置.如果E1具有无符号类型或者E1具有带符号类型和非负值,则结果的值是E1/2 E2的商的整数部分.如果E1具有带符号类型和负值,则结果值是实现定义的.

无符号和有符号非负值的右移非常简单; 空位用零填充.对于有符号的负值,右移的结果是实现定义的.也就是说,像GCC和Visual C++这样的大多数实现都通过保留符号位来实现右移作为算术移位.

结论

与Java不同,Java有一个特殊>>>的逻辑移位运算符,>><<C和C++只有算术移位,一些区域未定义和实现定义.我认为它们是算术的原因是由于数学运算的标准措辞而不是将移位的操作数视为比特流; 这也许就是为什么它将这些区域保留为/实现定义而不是仅仅将所有情况定义为逻辑转换的原因.


Nic*_*ick 16

就你所获得的转变类型而言,重要的是你正在转变的价值类型.一个典型的错误来源是当你将文字移动到比如掩盖位时.例如,如果要删除无符号整数的最左边的位,那么可以尝试将其作为掩码:

~0 >> 1
Run Code Online (Sandbox Code Playgroud)

不幸的是,这会让你陷入麻烦,因为掩码将设置它的所有位,因为被移位的值(~0)被签名,因此执行算术移位.相反,您希望通过明确地将值声明为无符号来强制进行逻辑移位,即通过执行以下操作:

~0U >> 1;
Run Code Online (Sandbox Code Playgroud)


Joh*_*one 16

以下是保证C中int的逻辑右移和算术右移的函数:

int logicalRightShift(int x, int n) {
    return (unsigned)x >> n;
}
int arithmeticRightShift(int x, int n) {
    if (x < 0 && n > 0)
        return x >> n | ~(~0U >> n);
    else
        return x >> n;
}
Run Code Online (Sandbox Code Playgroud)


小智 6

当你这样做时 - 左移1,你乘以2 - 右移1,你除以2

 x = 5
 x >> 1
 x = 2 ( x=5/2)

 x = 5
 x << 1
 x = 10 (x=5*2)
Run Code Online (Sandbox Code Playgroud)


Mik*_*one 5

好吧,我在维基百科上查了一下,他们是这么说的:

然而,C 语言只有一个右移运算符 >>。许多 C 编译器根据要移位的整数类型来选择执行哪种右移;通常,有符号整数使用算术移位进行移位,无符号整数使用逻辑移位进行移位。

所以听起来这取决于你的编译器。另外,在那篇文章中,请注意左移对于算术和逻辑是相同的。我建议在边界情况下使用一些带符号和无符号的数字(当然是高位集)进行简单的测试,然后看看编译器上的结果是什么。我还建议避免依赖其中之一,因为 C 似乎没有标准,至少在合理且可能避免这种依赖的情况下。