<<
,>>
)是算术运算符还是逻辑运算符?<<
,>>
)是算术运算符还是逻辑运算符?左移时,算术移位和逻辑移位没有区别。右移的类型取决于被移位的值的类型。
(对于那些不熟悉差异的读者,"逻辑"右移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);
将i
和n
视为移位运算符的左右操作数;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)
其中÷
表示数学除法,/
表示整数除法。让我们看一个例子:
如盖伊·斯蒂尔指出,这种差异导致了一个以上编译器的错误。在这里,非负数(数学)可以映射为无符号和有符号的非负值(C); 两者被视为相同,并且对它们进行右移位是通过整数除法完成的。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 [算术右移的结果]
short E1 = 1, E2 = 3;
int R = E1 << E2;
int
;如果E2
是负数或E2 ≥ sizeof(int) * CHAR_BIT
,则该操作未定义。这是因为移位超过可用位肯定会溢出。如果R
被声明为short
,则移位操作的int
结果将被隐式转换为short
;这是一种缩小转换,如果该值在目标类型中不可表示,则可能导致实现定义的行为。
作为左移对于两种类型来说是相同的,因此空出的位将简单地填充为零。然后它指出对于无符号和有符号类型,这都是算术移位。我将其解释为算术移位,因为逻辑移位不关心位所表示的值,它只将其视为一串位;但标准不是以位为单位定义它,而是通过定义为E1与2E2的乘积获得的值。 这里的警告是对于有符号类型,值应为非负数,并且结果值应可表示为结果类型。否则,该操作是未定义的。结果类型将是应用整数提升后的E1的类型,而不是目标(将保存结果的变量)类型。生成的值会隐式转换为目标类型;如果在该类型中不能表示,则转换是实现定义的(C99 §6.3.1.3/3)。 如果E1是带有负值的有符号类型,则左移的行为是未定义的。这是一条通向未定义行为的简单路线,可能很容易被忽略。E1 << E2的结果是E1向左移动E2位;空出的位填充为零。如果E1具有无符号类型,则结果的值为E1×2E2,对结果类型可表示的最大值加一进行取模。如果E1具有带符号类型且非负值,并且E1×2E2可以在结果类型中表示,则该值为结果;否则,行为未定义。
>>
和<<
之外的逻辑移位运算符>>>
。这些区域的一些部分未定义或实现定义。我认为它们是算术移位的原因是标准用数学语言描述了操作,而不是将移位操作数视为一串位;这可能是它留下那些未/实现定义的区域而不是将所有情况定义为逻辑移位的原因。-Inf
舍入。正数向0舍入是向-Inf
舍入的一个特例。当截断时,您总是会丢弃具有正权值的值,因此您需要从本来精确的结果中减去这些值。 - ysap以下是在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;
}
就移位类型而言,重要的是你所移位的值的类型。一个常见的bug来源是当你将一个字面量进行移位来掩盖某些位时。例如,如果你想要去掉无符号整数的最左边一位,那么你可能会使用以下掩码:
~0 >> 1
不幸的是,这样做会导致问题,因为掩码中的所有位都将被设置,原因是被移位的值(~0)是有符号的,因此执行算术移位。相反,您需要通过明确声明值为无符号来强制进行逻辑移位,例如执行以下操作:
~0U >> 1;
当您进行以下操作时: - 左移1位相当于乘以2 - 右移1位相当于除以2
x = 5
x >> 1
x = 2 ( x=5/2)
x = 5
x << 1
x = 10 (x=5*2)
我在维基百科上查到了它,他们说:
C语言只有一个右移运算符 >>。许多C编译器选择根据整数类型执行哪个右移操作;通常使用算术右移来移位有符号整数,使用逻辑右移来移位无符号整数。
所以这似乎取决于你的编译器。另外,在那篇文章中,请注意左移对于算术和逻辑是相同的。我建议您在边界情况下(当然设置高位)使用一些有符号和无符号数字进行简单测试,并查看在您的编译器上的结果。我还建议避免依赖它是一种或另一种方式,因为似乎C没有标准,至少如果可以合理地避免这种依赖。
左移位 <<
这个操作有点简单,每当使用移位运算符时,它总是一个按位操作,因此我们不能将其与双精度和浮点数操作一起使用。每当我们左移一位,就会将一个零添加到最低有效位(LSB
)。
但在右移位 >>
中,我们必须遵循一个附加规则,该规则称为“符号位复制”。 “符号位复制”的含义是,如果最高有效位(MSB
)被设置,则右移后再次将MSB
设置为1,如果它被重置,则再次重置。这意味着,如果先前的值为零,则移位后的位为零,如果先前的位为1,则移位后仍为1。这个规则对于左移不适用。
右移位上最重要的例子是如果您将任何负数右移,那么经过一些移位后,值最终会达到零,然后在此之后,如果将这个-1移位任意次数,值将保持不变。请检查。
GCC 做如下操作:
对于负数 -> 算术移位
对于正数 -> 逻辑移位