在 C 中,移位运算符 ( <<
, >>
) 是算术还是逻辑?
11 回答
左移时,算术移位和逻辑移位没有区别。右移时,移位的类型取决于要移位的值的类型。
(作为不熟悉差异的读者的背景,“逻辑”右移 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);
TL;博士
考虑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 则未定义;我的提升类型
换档
首先是从数学角度来看逻辑和算术移位之间的区别,无需担心数据类型大小。逻辑移位总是用零填充丢弃的位,而算术移位仅在左移时用零填充,但对于右移,它复制 MSB 从而保留操作数的符号(假设负值的二进制补码编码)。
换句话说,逻辑移位将移位的操作数视为只是一个位流并移动它们,而不用担心结果值的符号。算术移位将其视为(有符号)数字,并在进行移位时保留符号。
一个数 X 乘以 n 的算术左移相当于将 X 乘以 2 n,因此相当于逻辑左移;逻辑转换也会给出相同的结果,因为 MSB 无论如何都会落到最后,并且没有什么可保留的。
仅当 X 为非负数时,数字 X 乘以 n 的算术右移等效于 X 除以 2 n !整数除法只不过是数学除法并向 0 ( trunc )舍入。
对于由二进制补码编码表示的负数,右移 n 位具有数学上将其除以 2 n并向 -∞ ( floor ) 舍入的效果;因此,对于非负值和负值,右移是不同的。
对于 X ≥ 0,X >> n = X / 2 n = trunc(X ÷ 2 n )
对于 X < 0, X >> n = floor(X ÷ 2 n )
其中÷
是数学除法,/
是整数除法。让我们看一个例子:
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(向 0 舍入 18.5)= 11101110) 2 [不是算术右移的结果]
-37 >> 1 = -19(向 -∞ 舍入 18.5)= 11101101)2 [算术右移的结果]
正如Guy Steele 所指出的,这种差异导致了不止一个编译器出现错误。这里非负(数学)可以映射为无符号和有符号的非负值(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×2 E2,比结果类型中可表示的最大值多模一减少。如果 E1 有带符号类型且非负值,并且 E1×2 E2在结果类型中是可表示的,那么这就是结果值;否则,行为未定义。
由于两者的左移相同,因此空出的位只需用零填充。然后它指出,对于无符号和有符号类型,它都是算术移位。我将其解释为算术移位,因为逻辑移位不关心位表示的值,它只是将其视为位流;但是该标准不是根据位来讨论的,而是根据 E1 与 2 E2的乘积所获得的值来定义它。
这里需要注意的是,对于有符号类型,该值应该是非负的,并且结果值应该可以在结果类型中表示。否则操作是未定义的。结果类型将是应用积分提升后的 E1 类型,而不是目标(将保存结果的变量)类型。结果值被隐式转换为目标类型;如果它在该类型中不可表示,则转换是实现定义的(C99 §6.3.1.3/3)。
如果 E1 是带负值的有符号类型,则左移行为未定义。这是一个容易被忽视的未定义行为的简单途径。
右移
E1 >> E2 的结果是 E1 右移 E2 位位置。如果 E1 具有无符号类型或 E1 具有带符号类型和非负值,则结果的值是 E1/2 E2的商的整数部分。如果 E1 具有带符号类型和负值,则结果值是实现定义的。
无符号和有符号非负值的右移非常简单;空位用零填充。对于有符号负值,右移的结果是实现定义的。也就是说,像 GCC 和Visual C++这样的大多数实现都通过保留符号位将右移实现为算术移位。
结论
>>>
与 Java 不同,Java除了通常的>>
and之外还有一个用于逻辑移位的特殊运算符<<
,C 和 C++ 仅具有算术移位,其中一些区域未定义和实现定义。我认为它们是算术的原因是由于标准措辞数学上的操作,而不是将移位的操作数视为位流;这也许就是为什么它将这些区域保留为未定义/实现定义而不是仅仅将所有情况定义为逻辑转变的原因。
就您获得的转变类型而言,重要的是您正在转变的价值的类型。一个典型的错误来源是当您将文字转换为掩码位时。例如,如果您想删除无符号整数的最左侧位,则可以尝试将其作为掩码:
~0 >> 1
不幸的是,这会给你带来麻烦,因为掩码的所有位都会被设置,因为被移位的值 (~0) 是有符号的,因此会执行算术移位。相反,您希望通过将值显式声明为无符号来强制进行逻辑转换,即通过执行以下操作:
~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)
好吧,我在维基百科上查了一下,他们有这样的说法:
然而,C 只有一个右移运算符 >>。许多 C 编译器根据要移位的整数类型来选择执行哪个右移;通常有符号整数使用算术移位进行移位,无符号整数使用逻辑移位进行移位。
所以听起来这取决于你的编译器。同样在那篇文章中,请注意左移对于算术和逻辑是相同的。我建议在边界情况下使用一些有符号和无符号数字(当然是高位集)做一个简单的测试,看看你的编译器上的结果是什么。我还建议避免依赖它是一个或另一个,因为 C 似乎没有标准,至少如果它是合理的并且可以避免这种依赖。
左移<<
这在某种程度上很容易,并且每当您使用移位运算符时,它始终是按位运算,因此我们不能将它与双精度和浮点运算一起使用。每当我们左移一位零时,它总是被添加到最低有效位 ( LSB
)。
但是在右移中,>>
我们必须遵循一个附加规则,该规则称为“符号位复制”。“符号位复制”的含义是,如果最高有效位MSB
(MSB
如果前一位为 1,则该位为零,然后在移位后再次为 1。此规则不适用于左移。
右移最重要的例子,如果你将任何负数右移,然后在一些移位之后,值最终达到零,然后在此之后,如果将这个 -1 移位任意次数,值将保持不变。请检查。
海合会
for -ve -> 算术移位
对于 +ve -> 逻辑移位
根据许多c编译器:
<<
是算术左移或按位左移。>>
是算术右移或按位右移。