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

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

1
算术和逻辑的含义是什么?有关带符号整数的相关问题:https://dev59.com/a2865IYBdhLWcg3wEKOZ - Ciro Santilli OurBigBook.com
11个回答

190

左移时,算术移位和逻辑移位没有区别。右移的类型取决于被移位的值的类型。

(对于那些不熟悉差异的读者,"逻辑"右移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);

71
非常接近,Greg。你的解释几乎完美,但移动带有符号类型和负值的表达式是实现定义的。请参见ISO/IEC 9899:1999第6.5.7节。 - Robᵩ
19
@Rob:实际上,对于左移和带符号的负数,行为是未定义的。 - JeremyP
6
如果对于一个正有符号数来说,左移导致的数学值(它的位数没有限制)不能在该有符号类型下表示为正值,那么左移也会导致未定义行为。总之,在对有符号值进行右移时要小心谨慎。 - Michael Burr
4
@supercat:我真的不知道。但我知道有一些记录了的案例,其中具有未定义行为的代码会导致编译器执行非常不符合直觉的操作(通常是由于激进的优化 - 例如参见旧Linux TUN / TAP驱动程序空指针错误:http://lwn.net/Articles/342330/)。除非我需要在右移时进行符号填充(我意识到这是实现定义的行为),否则我通常尝试使用无符号值执行位移操作,即使这意味着需要使用强制转换。 - Michael Burr
2
@MichaelBurr:我知道超现代编译器利用C标准未定义的行为(即使在99%的*实现中已经定义)作为将程序转换为一堆毫无用处的机器指令的理由,而这些程序在它们可以运行的所有平台上的行为都是完全定义的。尽管如此,我承认(讽刺地说),我对编译器作者为什么错过了最大的优化可能性感到困惑:省略程序的任何部分,如果到达该部分,则会导致函数嵌套... - supercat
显示剩余8条评论

119
根据K&R第二版,对有符号值进行右移操作的结果取决于具体实现。 维基百科称C/C++通常对有符号值进行算术右移操作。
基本上你需要测试你的编译器或不依赖它。我的VS2008帮助文档中提到,他们的编译器执行算术右移操作。

1
关于这个答案,行为不仅取决于编译器,还取决于编译器和(处理器)架构的组合。 - stephan
3
@stephan:编译器的选择在某些情况下可能受处理器架构的影响,但是今天大多数编译器将使用带符号值的>>作为算术右移进行处理,即使需要添加符号扩展代码。 - supercat

60

简介

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                |

† 大多数编译器将其实现为算术移位
‡ 如果值溢出结果类型 T,则未定义;i 的提升类型


移位操作

首先从数学角度区分逻辑移位和算术移位的差异,不考虑数据类型的大小。逻辑移位总是用零填充被丢弃的位,而算术移位仅在左移时用零填充它,但对于右移,则复制 MSB 以保留操作数的符号(假设使用二进制补码编码表示负值)。

换句话说,逻辑移位只将被移位的操作数视为一串位,并将它们移动,而不需要担心所得到值的符号。算术移位则将其视为(有符号)数字,并随着移位保留符号。

将数字 X 左移 n 次的算术移位等同于将 X 乘以 2n,因此与逻辑左移相当;逻辑移位也会给出相同结果,因为 MSB 无论如何都会掉落,没有什么可保存的。

如果X是非负数,那么将X向右移动n位的算术右移,等同于X除以2n后进行整数除法运算并round到0 (trunc)。

对于由二进制补码编码表示的负数,向右移动n位的效果为通过数学除以2n并向−∞(floor)舍入来进行数学除法;因此向右移位对于非负和负值不同。

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

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

其中÷表示数学除法,/表示整数除法。让我们看一个例子:

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 [算术右移的结果]

盖伊·斯蒂尔指出,这种差异导致了一个以上编译器的错误。在这里,非负数(数学)可以映射为无符号和有符号的非负值(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×2E2,对结果类型可表示的最大值加一进行取模。如果E1具有带符号类型且非负值,并且E1×2E2可以在结果类型中表示,则该值为结果;否则,行为未定义。

作为左移对于两种类型来说是相同的,因此空出的位将简单地填充为零。然后它指出对于无符号和有符号类型,这都是算术移位。我将其解释为算术移位,因为逻辑移位不关心位所表示的值,它只将其视为一串位;但标准不是以位为单位定义它,而是通过定义为E1与2E2的乘积获得的值。 这里的警告是对于有符号类型,值应为非负数,并且结果值应可表示为结果类型。否则,该操作是未定义的。结果类型将是应用整数提升后的E1的类型,而不是目标(将保存结果的变量)类型。生成的值会隐式转换为目标类型;如果在该类型中不能表示,则转换是实现定义的(C99 §6.3.1.3/3)。 如果E1是带有负值的有符号类型,则左移的行为是未定义的。这是一条通向未定义行为的简单路线,可能很容易被忽略。

右移

E1 >> E2 的结果是将 E1 右移 E2 位。如果 E1 具有无符号类型,或者 E1 具有有符号类型且为非负值,则结果的值是 E1/2E2 商的整数部分。如果 E1 具有有符号类型且为负值,则结果的值是实现定义的。对于无符号和有符号非负值的右移操作很直接,空缺的位用零填充。对于有符号负值的右移操作的结果是实现定义的。大多数实现(如 GCC 和 Visual C++)通过保留符号位来实现算术右移。最后,总结一下。
与Java不同,C和C++只有算术移位运算,而没有像普通的>><<之外的逻辑移位运算符>>>。这些区域的一些部分未定义或实现定义。我认为它们是算术移位的原因是标准用数学语言描述了操作,而不是将移位操作数视为一串位;这可能是它留下那些未/实现定义的区域而不是将所有情况定义为逻辑移位的原因。

1
不错的回答。关于舍入(在标题为Shifting的部分中)- 右移对于负数和正数都向-Inf舍入。正数向0舍入是向-Inf舍入的一个特例。当截断时,您总是会丢弃具有正权值的值,因此您需要从本来精确的结果中减去这些值。 - ysap
1
@ysap 是的,观察得很好。基本上,正数向0舍入是更一般的向负无穷舍入的特例;这可以在表格中看到,在那里我已经将正数和负数都标注为向负无穷舍入。 - legends2k

19

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

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

18

就移位类型而言,重要的是你所移位的值的类型。一个常见的bug来源是当你将一个字面量进行移位来掩盖某些位时。例如,如果你想要去掉无符号整数的最左边一位,那么你可能会使用以下掩码:

~0 >> 1

不幸的是,这样做会导致问题,因为掩码中的所有位都将被设置,原因是被移位的值(~0)是有符号的,因此执行算术移位。相反,您需要通过明确声明值为无符号来强制进行逻辑移位,例如执行以下操作:

~0U >> 1;

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:a 必须不小于0。在C语言中,这是未定义的行为。 - Jeremy

5

我在维基百科上查到了,他们说:

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

所以这似乎取决于你的编译器。另外,在那篇文章中,请注意左移对于算术和逻辑是相同的。我建议您在边界情况下(当然设置高位)使用一些有符号和无符号数字进行简单测试,并查看在您的编译器上的结果。我还建议避免依赖它是一种或另一种方式,因为似乎C没有标准,至少如果可以合理地避免这种依赖。


尽管大多数C编译器曾经为有符号值提供算术左移支持,但这种有用的行为似乎已被弃用。当前编译器的哲学似乎是假定变量的左移性能使编译器可以假定变量必须是非负的,并因此省略任何其他代码,如果变量为负,则需要正确行为。 - supercat

0

左移位 <<

这个操作有点简单,每当使用移位运算符时,它总是一个按位操作,因此我们不能将其与双精度和浮点数操作一起使用。每当我们左移一位,就会将一个零添加到最低有效位(LSB)。

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

右移位上最重要的例子是如果您将任何负数右移,那么经过一些移位后,值最终会达到零,然后在此之后,如果将这个-1移位任意次数,值将保持不变。请检查。


0

通常会在无符号变量上使用逻辑移位,在有符号变量的左移位操作上也是如此。算术右移是最重要的一个,因为它会进行符号扩展。

会在适用时使用这种方法,其他编译器也可能会这样做。


-1

GCC 做如下操作:

  1. 对于负数 -> 算术移位

  2. 对于正数 -> 逻辑移位


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接