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

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

286019 次浏览

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

(对于那些不熟悉这种差异的读者来说,作为背景,“逻辑”右移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);

根据 K & R 第二版,结果依赖于符号值的右移。

Wikipedia 说 C/C + + “通常”对有符号的值实现算术移位。

基本上,您需要或者测试您的编译器,或者不依赖它。我的 VS2008帮助目前的 MS C + + 编译器说,他们的编译器做一个算术转移。

我看了 在维基百科上找到的他们说:

然而,C 只有一个正确的转变 运算符 > > 。许多 C 编译器选择 哪个右转执行取决于 关于整数的类型 经常有符号整数是 使用算术移位, 和无符号整数移位 使用逻辑转换。

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

就你得到的转移类型而言,重要的是你正在转移的价值的类型。一个典型的 bug 来源是当你把一个字面值转换为,比如说,掩码。例如,如果您想删除无符号整数的最左边位,那么您可以尝试使用下面的代码作为掩码:

~0 >> 1

不幸的是,这将给您带来麻烦,因为掩码将设置它的所有位,因为被移位的值(~ 0)是有符号的,因此执行算术移位。相反,您需要通过显式地将值声明为 unsigned 来强制进行逻辑转换,例如,可以这样做:

~0U >> 1;

通常对无符号变量使用逻辑移位,对有符号变量使用左移位。算术右移是真正重要的一个,因为它将符号扩展变量。

将在适用的时候使用它,就像其他编译器可能会做的那样。

下面是 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;
}

等你找到的时候 左移1乘以2 右移1除以2

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


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

根据许多 编译器的说法:

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

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                |

? 大多数编译器将其实现为算术移位
如果值溢出结果类型 T,± 未定义; 升级类型 i


移动

首先,逻辑和算术之间的区别是从数学的角度出发,而不用担心数据类型的大小。逻辑偏移总是用零填充丢弃的位,而算术偏移只在左偏移时用零填充,但对于右偏移,它复制 MSB,从而保留操作数的符号(假设负值为 两个人的互补编码)。

换句话说,逻辑移位将移位的操作数看作一个位流并移动它们,而不用考虑结果值的符号。算术移位将其视为一个(有符号的)数字,并在移位时保留符号。

数字 X 乘以 n 的左算术移位等于 X 乘以2N,因此等价于逻辑左移位; 逻辑移位也会得到相同的结果,因为 MSB 无论如何都会从结尾掉下来,没有什么可保留的。

一个数 X 乘以 n 的右算术移位等于 X 除以2N的整数只有当 X 是非负的!整数除法只不过是数学除法和 圆的向0(Trunc)。

对于由两个补码表示的负数,n 位右移的数学效果是将其除以2N并向 -∞(地板)舍入,因此对于非负值和负值右移是不同的。

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

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

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

37) 10 = 100101) 2

372 = 18.5

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

-37) 10 = 11011011) 2(考虑2的补数,8位表示)

-372 = -18.5

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

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

作为 Guy Steele 指出,这种差异导致了 多个编译器中的 bug。在这里,非负(数学)可以映射到无符号和有符号的非负值(C) ; 两者处理相同,并通过整数除法实现右移。

所以逻辑和算术在左移和非负值在右移是等价的,它们在负值的右移中不同。

操作数和结果类型

标准 C996.5.7 :

每个操作数都应该具有整数类型。

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

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

在上面的代码片段中,两个操作数都变成了 int(由于整数提升) ; 如果 E2为负或者 E2 ≥ sizeof(int) * CHAR_BIT,那么操作就是未定义的。这是因为移动的数据比可用的数据更多,肯定会溢出。如果将 R声明为 short,则 shift 操作的 int结果将被隐式转换为 short; 这是一种收缩转换,如果该值在目标类型中不能表示,则可能导致实现定义的行为。

左转

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

由于两者的左移是相同的,所以空位只是被零填充。然后它指出,对于无符号类型和有符号类型,它是一个算术移位。我将其解释为算术移位,因为逻辑移位并不关心由比特表示的值,它只是把它看作一个比特流; 但是标准不是用比特来表示的,而是用 E1与2E2的乘积得到的值来定义的。

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

如果 E1是一个负值的有符号类型,那么左移位的行为是未定义的。这是一条容易导致不明行为的路径,很容易被忽视。

右转

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

无符号和有符号非负值的右移非常直接; 空位用零填充。也就是说,大多数像 GCC 和 Visual C + + 这样的实现通过保留符号位来实现右移作为算术移位。

结论

Java 有一个特殊的操作符 >>>,用于逻辑移位,而不是通常的 >><<,C 和 C + + 只有算术移位,其中一些区域没有定义和实现定义。我之所以认为它们是算术,是因为标准的用数学的方式来表述操作,而不是把移位操作数当作一串位; 这也许就是为什么它没有定义这些区域/实现,而不是仅仅把所有的情况定义为逻辑移位。

左转 <<

这在某种程度上很容易,而且无论何时使用 shift 操作符,它总是一个位操作,所以我们不能将它用于 double 和 float 操作。每当我们左移一个零,它总是添加到最低有效位(LSB)。

但是在右移 >>时,我们必须遵循另外一条规则,这条规则被称为“符号位复制”。“符号位复制”的意思是,如果最重要的位(MSB)被设置,然后在右移之后,如果它被重置,那么 MSB将被设置,然后再次重置,意味着如果先前的值为零,然后再次移动,位是零,如果先前的位是一,然后在移动之后,它再次是一。此规则不适用于左移。

最重要的例子就是右移如果你把任何一个负数移到右移,那么在一些移动之后,值最终达到零,然后在这之后,如果移动这个 -1,任何次数的值将保持不变。请检查一下。

海湾合作委员会知道

  1. For-ve-> 算术移位

  2. For + ve-> Logical Shift