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


Answers:


97

根据K&R第二版,结果的实现取决于符号值的右移。

维基百科说,C / C ++通常会对有符号值进行算术转换。

基本上,您需要测试或不依赖编译器。我对当前MS C ++编译器的VS2008帮助说,他们的编译器进行了算术转换。


141

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

(作为不熟悉该差异的读者的背景知识,“逻辑”右移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);

57
太近了,格雷格。您的解释几乎是完美的,但移位带符号类型和负值的表达式是实现定义的。参见ISO / IEC 9899:1999第6.5.7节。
罗伯(Robᵩ)

12
@Rob:实际上,对于左移和带负号的正负号,行为是不确定的。
JeremyP 2012年

5
实际上,如果不能将结果数学值(不受位大小的限制)表示为该带正负号类型的正值,则左移还会导致正带正负号的行为未定义。底线是,当右移一个有符号的值时,您必须小心行事。
2013年

3
@supercat:我真的不知道。但是,我确实知道,在某些情况下,有未定义行为的代码会导致编译器执行非常不直观的操作(通常是由于进行了积极的优化-例如,请参见旧的Linux TUN / TAP驱动程序空指针错误:lwn.net) / Articles / 342330)。除非我需要在右移上进行符号填充(我意识到这是实现定义的行为),否则我通常会尝试使用无符号值执行位移位,即使这意味着使用强制类型转换也可以。
迈克尔·伯

2
@MichaelBurr:我知道超现代的编译器使用C标准未定义的行为(即使在99%的实现中定义了该行为)作为将所有行为都已完全定义的程序转为正当性的理由可能期望它们运行的​​平台,变成毫无价值的无用机器指令。我会承认,尽管(讽刺)我对为什么编译器作者错过了最大的优化可能性感到困惑:忽略程序的任何部分,如果达到该部分,则会导致函数嵌套……
supercat

51

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                |

†大多数编译器将其实现为算术移位
•如果value溢出结果类型T,则为undefined;我的晋升类型


换档

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

换句话说,逻辑移位将移位后的操作数仅视为比特流,并对其进行移动,而不必担心结果值的符号。算术移位将其视为一个(有符号的)数字,并在进行移位时保留该符号。

X左移n等于将X乘以2 n,因此等于逻辑左移。逻辑上的转换也将产生相同的结果,因为无论如何MSB最终都将失败,并且没有任何可保留的东西。

X的右算术移位n等于X除以2 为负才等于X除以2 n的整数!整数除法不过是数学除法,并且朝0(trunc舍入

对于以二进制补码表示的负数,右移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(向1取整18.5)= 10010)2 [算术右移的结果]

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

-37÷2 = -18.5

-37 / 2 = -18(向0舍入18.5)= 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;

在以上代码段中,两个操作数都变为int(由于整数提升);如果E2为负,E2 ≥ sizeof(int) * CHAR_BIT则操作不确定。这是因为移位多于可用位肯定会溢出。有R被宣布为short,该int移位操作的结果将被隐式转换为short; 缩小的转换,如果该值无法在目标类型中表示,则可能导致实现定义的行为。

左移

E1 << E2的结果是E1左移E2位位置;空位用零填充。如果E1具有无符号类型,则结果的值为E1×2 E2,比结果类型中可表示的最大值模减少1。如果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,它有一个特殊的操作者>>>为除了通常的逻辑移位>><<,C和C与一些区域未定义和实现定义++只有算术移位。我之所以将它们视为算术运算,是因为标准的数学运算方式不是将移位的操作数视为位流;这也许就是为什么将这些区域保留为未定义/未实现定义的原因,而不仅仅是将所有情况都定义为逻辑转移的原因。


1
好答案。关于四舍五入(在标题为Shifting的部分中)-Inf,负数和正数都向右舍入。舍入为正数的0是舍入为的私人情况-Inf。截断时,您始终会丢弃正加权值,因此,您会从其他精确结果中减去。
ysap

1
@ysap是的,很好的观察。基本上,对正数向0取整是对-∞的更一般取整的特例;在表中可以看到,正负数我都指出它朝着-∞取整。
legends2k

17

就获得的移位类型而言,重要的是要移位的值的类型。错误的经典来源是将文字转换为例如掩盖位。例如,如果您想删除无符号整数的最左位,则可以尝试使用此掩码:

~0 >> 1

不幸的是,这会给您带来麻烦,因为掩码将设置所有位,因为要移位的值(〜0)是带符号的,因此将执行算术移位。相反,您希望通过将值显式声明为unsigned来强制进行逻辑转换,即通过执行以下操作:

~0U >> 1;

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;
}

7

执行操作时-左移1乘以2-右移1乘以2

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

 x = 5
 x << 1
 x = 10 (x=5*2)

在x >> a和x << a中,如果条件为a> 0,则答案分别为x = x / 2 ^ a,x = x * 2 ^ a,那么如果a <0,答案是什么?
JAVA

@sunny:一个必须不小于0正是在C.未定义的行为
杰里米

4

好吧,我在Wikipedia上查询了一下,他们说:

但是,C仅具有一个右移运算符>>。许多C编译器根据要移位的整数类型选择要执行的右移。通常,使用算术移位对有符号整数进行移位,而使用逻辑移位对无符号整数进行移位。

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


尽管大多数C编译器以前对有符号的值进行算术左移,但似乎已经弃用了这种有用的行为。当前的编译器原理似乎是假设对变量执行左移操作使编译器有资格假定变量必须为非负值,因此,如果变量为负,则省略了正确行为所必需的其他代码。 。
2015年

0

左移 <<

这很容易,每当您使用shift运算符时,它总是按位操作,因此我们不能将其与double和float操作一起使用。每当我们左移一位零时,它总是被加到最低有效位(LSB)。

但是在右移中,>>我们必须遵循另一条规则,该规则称为“符号位复制”。“符号位复制”的含义是,如果最高有效位(MSB)被置位,则在再次右移MSB后将被置位,如果被复位,则将被再次复位,这意味着如果先前的值为零,则在再次移位后,如果前一位为1,则该位为零,然后在移位后再次为1。此规则不适用于左移。

在右移上最重要的示例是,如果将任何负数移至右移,则在将值进行一些移位后最终达到零,然后在此之后将该值移位-1任意次数,则该值将保持不变。请检查。




-7

据许多 编译器:

  1. << 是算术左移或按位左移。
  2. >> 是算术右移或按位右移。

3
“算术右移”和“按位右移”是不同的。这就是问题的重点。问的问题是“ >>算术还是按位(逻辑)?” 您回答“ >>是算术运算还是按位运算”。那不能回答问题。
wchargin 2014年

不,<<>>运营商的逻辑,不是算术
shjeff
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.