在 C 语言中,移位运算符(<<,>>)是算术运算还是逻辑运算?
<<
>>
当向左移位时,算术移位和逻辑移位没有区别。当向右移动时,移动的类型取决于被移动的值的类型。
(对于那些不熟悉这种差异的读者来说,作为背景,“逻辑”右移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;
Gcc 通常对无符号变量使用逻辑移位,对有符号变量使用左移位。算术右移是真正重要的一个,因为它将符号扩展变量。
Gcc 将在适用的时候使用它,就像其他编译器可能会做的那样。
下面是 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)
根据许多 C编译器的说法:
考虑将 i和 n分别作为移位操作符的左操作数和右操作数; 经过整数提升后,i的类型为 T。假设 n在 [0, sizeof(i) * CHAR_BIT)中ーー除此之外没有定义ーー我们有这样的情况:
i
n
T
[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)
对于 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[算术右移的结果]
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; 这是一种收缩转换,如果该值在目标类型中不能表示,则可能导致实现定义的行为。
int
E2
E2 ≥ sizeof(int) * CHAR_BIT
R
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)。
LSB
但是在右移 >>时,我们必须遵循另外一条规则,这条规则被称为“符号位复制”。“符号位复制”的意思是,如果最重要的位(MSB)被设置,然后在右移之后,如果它被重置,那么 MSB将被设置,然后再次重置,意味着如果先前的值为零,然后再次移动,位是零,如果先前的位是一,然后在移动之后,它再次是一。此规则不适用于左移。
MSB
最重要的例子就是右移如果你把任何一个负数移到右移,那么在一些移动之后,值最终达到零,然后在这之后,如果移动这个 -1,任何次数的值将保持不变。请检查一下。
海湾合作委员会知道
For-ve-> 算术移位
For + ve-> Logical Shift