关于二进制:C中的移位运算符(>>,<<)是算术还是逻辑?

Are the shift operators (<<, >>) arithmetic or logical in C?

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


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

(对于那些不熟悉差异的读者来说,一个"逻辑"右移1位会将所有的位右移,并用0填充最左边的位。"算术"移位将原始值保留在最左边的位中。当处理负数时,差异变得很重要。)

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

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

1
2
3
4
5
6
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);


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

维基百科说C/C++通常在签名值上实现算术移位。

基本上,你要么测试你的编译器,要么不依赖它。我的VS2008帮助当前的MS C++编译器说他们的编译器做算术移位。


DR

in分别看作移位运算符的左、右操作数;在整数提升后,i的类型为T。假设n[0, sizeof(i) * CHAR_BIT)中,否则没有定义,我们有以下情况:好的。

1
2
3
4
5
6
7
8
| 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乘n的右算术移位等于x乘2n的整数除,前提是x不是负数!整数除法只不过是数学除法,向0取整(trunc)。好的。

对于用两个补码编码表示的负数,右移n位会产生数学上除以2n并四舍五入的效果。∞(地板);因此,对于非负值和负值,右移是不同的。好的。

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

Ok.

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

Ok.

其中÷为数学除法,/为整数除法。让我们来看一个例子:好的。< Buff行情>

37)10=100101)2好的。

37乘2=18.5好的。

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

-37)10=11011011)2(考虑到一个2的补码,8位表示法)好的。

-37÷2=-18.5好的。

-37/2=-18(向0舍入18.5)=11101110)2<[Not the result of算术右移]好的。

-37>>1=-19(向?舍入18.5)∞)=11101101)2[算术右移结果]好的。< /块引用>

正如盖伊·斯蒂尔指出的,这种差异导致了不止一个编译器中的错误。在这里,非负(数学)可以映射到无符号和有符号非负值(C);两者都被处理为相同的,右移位通过整数除法完成。好的。

所以逻辑和算术在左移位和右移位的非负值上是等价的;正是在负值的右移位上它们不同。好的。操作数和结果类型

标准C99第6.5.7节:好的。

Each of the operands shall have integer types.

Ok.

The integer promotions are performed on each of the operands. The type of the result is that of the promoted left operand. If the value of the right operand is negative or is greater than or equal to the width of the promoted left operand, the behaviour is undefined.

Ok.

1
2
short E1 = 1, E2 = 3;
int R = E1 << E2;

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

The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is E1×2E2, reduced modulo one more than the maximum value representable in the result type. If E1 has a signed type and non-negative value, and E1×2E2 is representable in the result type, then that is the resulting value; otherwise, the behaviour is undefined.

Ok.

由于两种方法的左移相同,空出来的位只不过是用零填充。然后它声明,对于无符号类型和有符号类型,它都是算术移位。我把它解释为算术移位,因为逻辑移位不需要考虑由位表示的值,它只是把它看作一个位流;但是标准并不是用位来表示的,而是用e1和2e2的乘积得到的值来定义它。好的。

这里的警告是,对于有符号类型,值应该是非负的,结果值应该在结果类型中表示。否则操作未定义。结果类型将是应用整数提升后的e1类型,而不是目标(将保存结果的变量)类型。结果值被隐式转换为目标类型;如果在该类型中不可表示,则转换被定义为实现(C99§6.3.1.3/3)。好的。

如果e1是带负号的有符号类型,那么左移位的行为是未定义的。这是一条通往不明确行为的简单途径,很容易被忽视。好的。右移

The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a non-negative value, the value of the result is the integral part of the quotient of E1/2E2. If E1 has a signed type and a negative value, the resulting value is implementation-defined.

Ok.

无符号和有符号非负值的右移位非常直接;空位用零填充。对于有符号负值,右移位的结果是实现定义的。也就是说,大多数实现如GCC和VisualC++都通过保留符号位实现算术移位。好的。结论

与Java不同,Java有一个特殊的运算符EDOCX1,0,用于逻辑移位,除了通常的EDCOX1、1和EDCX1,2,C和C++只有算术移位,一些区域未定义和实现定义。我将它们视为算术的原因是,操作的标准措词是数学上的,而不是将移位的操作数视为位流;这可能是它不将这些区域/实现定义为逻辑移位,而将所有情况定义为逻辑移位的原因。好的。好啊。


就你得到的移位类型而言,最重要的是你要移位的值的类型。一个典型的bug源是当你把一个文字转换成,比如,屏蔽位。例如,如果要删除无符号整数的最左边的位,可以尝试将其作为掩码:

1
~0 >> 1

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

1
~0U >> 1;

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

1
2
3
4
5
6
7
8
9
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;
}

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

1
2
3
4
5
6
7
 x = 5
 x >> 1
 x = 2 ( x=5/2)

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


嗯,我在维基百科上查过,他们有这样的说法:

C, however, has only one right shift
operator, >>. Many C compilers choose
which right shift to perform depending
on what type of integer is being
shifted; often signed integers are
shifted using the arithmetic shift,
and unsigned integers are shifted
using the logical shift.

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


海湾合作委员会

  • 对于-ve->算术移位

  • for+ve->逻辑移位


  • 左移<<

    这在某种程度上很简单,无论何时使用移位运算符,它总是一个有点明智的操作,因此我们不能将它与双浮点运算结合使用。每当我们左移一个零,它总是加到最低有效位(LSB)。

    但在右移>>中,我们必须遵循一条附加规则,该规则称为"符号位拷贝"。"符号位拷贝"的含义是,如果设置了最高有效位(MSB,那么在再次右移之后,如果重置了MSB,那么它将被设置,然后再次重置,意味着如果先前的值是零,那么在再次移动之后,如果先前的位是一,那么在移动之后,它将是零。此规则不适用于左移位。

    最重要的例子是右移位,如果将任何负数移位到右移位,然后在某些移位之后,值最终达到零,然后在此之后,如果将此移位-1,值将保持不变的任何次数。请核对一下。


    GCC通常在无符号变量上使用逻辑移位,在有符号变量上使用左移位。算术右移是非常重要的,因为它将符号扩展变量。

    GCC将在适用的情况下使用它,因为其他编译器可能会这样做。


    根据许多C编译器的说法:

  • <<是算术左移位或按位左移位。
  • >>是算术右移或按位右移。