C 中的移位运算符(<<、>>)是算术运算符还是逻辑运算符?
在 C 中,移位运算符(<<
、>>
)是算术运算符还是逻辑运算符?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
在 C 中,移位运算符(<<
、>>
)是算术运算符还是逻辑运算符?
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
接受
或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
发布评论
评论(11)
TL;DR
将
i
和n
分别视为移位运算符的左操作数和右操作数;i
的类型,经过整数提升后,为T
。 假设n
位于[0, sizeof(i) * CHAR_BIT)
中(否则未定义),我们有以下情况:† 大多数编译器将其实现为算术移位
‡ 如果值溢出结果类型 T(i 的提升类型),则未定义
移位
首先是从数学角度来看逻辑移位和算术移位之间的区别,无需担心数据类型大小。 逻辑移位总是用零填充丢弃的位,而算术移位仅在左移时用零填充,但对于右移,它复制 MSB 从而保留操作数的符号(假设 负值的补码编码)。
换句话说,逻辑移位将移位的操作数视为位流并移动它们,而不关心结果值的符号。 算术移位将其视为(有符号)数字,并在进行移位时保留符号。
将 X 左移 n 相当于将 X 乘以 2n。 逻辑左移和算术左移的结果是相同的。
X 的算术右移 n 相当于将 X 除以 2 并向 -∞ 舍入,即
因此逻辑和算术在左移中是等效的,而对于非负值在右移中是等效的; 它们的不同之处在于负值的右移。 这是因为 MSB 仅针对二进制补码表示中的负值设置,因此算术右移的 MSB 复制行为很重要。
旁白:与整数除法的关系
算术右移有时会与整数除以 2 的幂相混淆,但它们是不同的。 X 除以 n 的整数除以 X 除以 n 并向 0 舍入,即 trunc,与右移不同,floor 。
floor
和trunc
在正空间中给出相同的结果,而在负空间中则不然,例如floor(-3.9)
=-4,<代码>trunc(-3.9) = <代码>-3而<代码>地板(3.9) = <代码>3,<代码>截断(3.9)=
3
。 它们在正空间中的相似性导致右移和整数除以 2 相似的假设无效。 正如 Guy Steele 指出的,这种差异导致了多个编译器中的错误。 负空间中的操作有所不同。我们来看一个例子(
÷
是数学除法,/
是整数除法):操作数和结果类型
标准 C99 §6.5.7:
在上面的代码片段中,两个操作数都变成了
int
(由于整数提升); 如果E2
为负数或E2 ≥ sizeof(int) * CHAR_BIT
则操作未定义。 这是因为移位超过可用位肯定会溢出。 如果将R
声明为short
,则移位操作的int
结果将隐式转换为short
; 缩小转换,如果该值无法在目标类型中表示,则可能会导致实现定义的行为。左移
由于两者的左移相同,因此空出的位只需用零填充。 然后它指出对于无符号和有符号类型来说,它都是算术移位。 我将其解释为算术移位,因为逻辑移位并不关心位表示的值,它只是将其视为位流; 但该标准不是以比特为单位进行讨论,而是以 E1 与 2E2 的乘积所获得的值来定义它。
这里需要注意的是,对于有符号类型,该值应该是非负的,并且结果值应该可以在结果类型中表示。 否则,操作未定义。 结果类型将是应用整数提升后 E1 的类型,而不是目标(将保存结果的变量)类型。 结果值隐式转换为目标类型; 如果它不能用该类型表示,则转换是实现定义的(C99 §6.3.1.3/3)。
如果 E1 是具有负值的有符号类型,则左移行为未定义。这是导致未定义行为的简单途径,很容易被忽视。
右移
无符号和有符号非负值的右移非常简单; 空位用零填充。 对于带符号的负值,右移的结果是实现定义的。也就是说,大多数实现(例如 GCC 和Visual C++ 通过保留符号位将右移实现为算术移位。
结论
与 Java 不同,Java 有一个特殊的运算符
>>>
,用于逻辑移位,与通常的>>
和<<
不同。 code>、C 和 C++ 仅具有算术移位,某些区域未定义且由实现定义。 我将它们视为算术的原因是由于数学运算的标准措辞,而不是将移位的操作数视为位流; 这也许就是为什么它没有/实现定义这些区域,而不是仅仅将所有情况定义为逻辑转变的原因。C++20 明确定义(早期定义不明确的情况)右移为算术(不考虑符号),带符号左移为乘以 2 的幂并环绕(模)。
TL;DR
Consider
i
andn
to be the left and right operands respectively of a shift operator; the type ofi
, after integer promotion, beT
. Assumingn
to be in[0, sizeof(i) * CHAR_BIT)
— undefined otherwise — we've these cases:† most compilers implement this as arithmetic shift
‡ undefined if value overflows the result type T (the promoted type of i)
Shifting
First is the difference between logical and arithmetic shifts from a mathematical viewpoint, without worrying about data type size. Logical shifts always fills discarded bits with zeros while arithmetic shift fills it with zeros only for left shift, but for right shift it copies the MSB thereby preserving the sign of the operand (assuming a two's complement encoding for negative values).
In other words, logical shift looks at the shifted operand as just a stream of bits and move them, without bothering about the sign of the resulting value. Arithmetic shift looks at it as a (signed) number and preserves the sign as shifts are made.
Left shifting X by n is equivalent to multiplying X by 2n. The result would be the same for both logical and arithmetic left shifts.
Arithmetic right shift of X by n is equivalent to dividing X by 2 and rounding towards -∞ i.e. floor. Results would differ from logical right shift for negative values.
So logical and arithmetic are equivalent in left-shifting and for non-negative values in right shifting; it's in right shifting of negative values that they differ. This is because MSB is set only for negative values in a two’s complement representation, and thereby arithmetic right shift’s MSB copying behaviour matters.
Aside: Relationship with integer divide
A right arithmetic shift is sometimes confused with integer division by powers of 2 but they’re different. Integer division of X by n is equivalent to dividing X by n and rounding towards 0 i.e. trunc, unlike right shift which floors.
floor
andtrunc
gives the same results in positive space while not in negative space e.g.floor(-3.9)
=-4
,trunc(-3.9)
=-3
whilefloor(3.9)
=3
,trunc(3.9)
=3
. Their similarity in positive space leads to the invalid assumption that right shift and integer division by 2 are similar. As Guy Steele pointed out, this discrepancy has led to bugs in more than one compiler. The operations are different in negative space.Let's look at an example (
÷
is mathematical division,/
is integer division):Operand and Result Types
Standard C99 §6.5.7:
In the above snippet, both operands become
int
(due to integer promotion); ifE2
was negative orE2 ≥ sizeof(int) * CHAR_BIT
then the operation is undefined. This is because shifting more than the available bits is surely going to overflow. HadR
been declared asshort
, theint
result of the shift operation would be implicitly converted toshort
; a narrowing conversion, which may lead to implementation-defined behaviour if the value is not representable in the destination type.Left Shift
As left shifts are the same for both, the vacated bits are simply filled with zeros. It then states that for both unsigned and signed types it's an arithmetic shift. I'm interpreting it as arithmetic shift since logical shifts don't bother about the value represented by the bits, it just looks at it as a stream of bits; but the standard talks not in terms of bits, but by defining it in terms of the value obtained by the product of E1 with 2E2.
The caveat here is that for signed types the value should be non-negative and the resulting value should be representable in the result type. Otherwise the operation is undefined. The result type would be the type of the E1 after applying integral promotion and not the destination (the variable which is going to hold the result) type. The resulting value is implicitly converted to the destination type; if it is not representable in that type, then the conversion is implementation-defined (C99 §6.3.1.3/3).
If E1 is a signed type with a negative value then the behaviour of left shifting is undefined. This is an easy route to undefined behaviour which may easily get overlooked.
Right Shift
Right shift for unsigned and signed non-negative values are pretty straight forward; the vacant bits are filled with zeros. For signed negative values the result of right shifting is implementation-defined. That said, most implementations like GCC and Visual C++ implement right-shifting as arithmetic shifting by preserving the sign bit.
Conclusion
Unlike Java, which has a special operator
>>>
for logical shifting apart from the usual>>
and<<
, C and C++ have only arithmetic shifting with some areas left undefined and implementation-defined. The reason I deem them as arithmetic is due to the standard wording the operation mathematically rather than treating the shifted operand as a stream of bits; this is perhaps the reason why it leaves those areas un/implementation-defined instead of just defining all cases as logical shifts.C++20 clearly defines (the earlier not so well-defined cases) right shift as arithmetic (irrespective of sign) and signed left shift as multiplying by power of 2 with wrap around (modulo).
根据 K&R 第二版,结果取决于实现的右移有符号的值。
Wikipedia 表示 C/C++“通常”对有符号值实现算术移位。
基本上,您需要测试您的编译器或不依赖它。 我对当前 MS C++ 编译器的 VS2008 帮助说他们的编译器会进行算术移位。
According to K&R 2nd edition the results are implementation-dependent for right shifts of signed values.
Wikipedia says that C/C++ 'usually' implements an arithmetic shift on signed values.
Basically you need to either test your compiler or not rely on it. My VS2008 help for the current MS C++ compiler says that their compiler does an arithmetic shift.
左移时,算术移位和逻辑移位没有区别。 右移时,移位类型取决于被移位的值的类型。
(作为那些不熟悉差异的读者的背景知识,“逻辑”右移 1 位会将所有位向右移动,并用 0 填充最左边的位。“算术”移位将原始值保留在最左边的位当处理负数时,差异变得很重要。)
当移位无符号值时,>>。 C 中的运算符是逻辑移位。 当移位有符号值时,>> 运算符是算术移位。
例如,假设一台 32 位机器:
When shifting left, there is no difference between arithmetic and logical shift. When shifting right, the type of shift depends on the type of the value being shifted.
(As background for those readers unfamiliar with the difference, a "logical" right shift by 1 bit shifts all the bits to the right and fills in the leftmost bit with a 0. An "arithmetic" shift leaves the original value in the leftmost bit. The difference becomes important when dealing with negative numbers.)
When shifting an unsigned value, the >> operator in C is a logical shift. When shifting a signed value, the >> operator is an arithmetic shift.
For example, assuming a 32 bit machine:
当你这样做时
- 左移 1 乘以 2
- 右移1除以2
When you do
- left shift by 1 you multiply by 2
- right shift by 1 you divide by 2
好吧,我在维基百科上查了一下,他们是这么说的:
所以听起来这取决于你的编译器。 另外,在那篇文章中,请注意左移对于算术和逻辑是相同的。 我建议在边界情况下使用一些带符号和无符号的数字(当然是高位集)进行简单的测试,然后看看编译器上的结果是什么。 我还建议避免依赖其中之一,因为 C 似乎没有标准,至少在合理且可能避免这种依赖的情况下。
Well, I looked it up on wikipedia, and they have this to say:
So it sounds like it depends on your compiler. Also in that article, note that left shift is the same for arithmetic and logical. I would recommend doing a simple test with some signed and unsigned numbers on the border case (high bit set of course) and see what the result is on your compiler. I would also recommend avoiding depending on it being one or the other since it seems C has no standard, at least if it is reasonable and possible to avoid such dependence.
左移
<<
这在某种程度上很简单,每当您使用移位运算符时,它始终是按位运算,因此我们不能将其与双精度和浮点运算一起使用。 每当我们左移一个零时,它总是添加到最低有效位(
LSB
)。但在右移
>>
中,我们必须遵循一项附加规则,该规则称为“符号位复制”。 “符号位复制”的含义是,如果最高有效位 (MSB
) 被设置,则在再次右移后,如果重置,则MSB
将被设置再次重置,意味着如果先前的值为零,则再次移位后,该位为零;如果先前的位为 1,则移位后再次为 1。 此规则不适用于左移。关于右移的最重要的例子,如果将任何负数右移,那么经过一些移位后,该值最终达到零,然后如果将此 -1 移动任意次数,该值将保持不变。 请检查。
Left shift
<<
This is somehow easy and whenever you use the shift operator, it is always a bit-wise operation, so we can't use it with a double and float operation. Whenever we left shift one zero, it is always added to the least significant bit (
LSB
).But in right shift
>>
we have to follow one additional rule and that rule is called "sign bit copy". Meaning of "sign bit copy" is if the most significant bit (MSB
) is set then after a right shift again theMSB
will be set if it was reset then it is again reset, means if the previous value was zero then after shifting again, the bit is zero if the previous bit was one then after the shift it is again one. This rule is not applicable for a left shift.The most important example on right shift if you shift any negative number to right shift, then after some shifting the value finally reach to zero and then after this if shift this -1 any number of times the value will remain same. Please check.
gcc 通常会对无符号变量使用逻辑移位,并且有符号变量左移。 算术右移是真正重要的一项,因为它会对变量进行符号扩展。
gcc 将在适用时使用它,就像其他编译器一样很可能会这样做。
gcc will typically use logical shifts on unsigned variables and for left-shifts on signed variables. The arithmetic right shift is the truly important one because it will sign extend the variable.
gcc will will use this when applicable, as other compilers are likely to do.
GCC 执行
for -ve - > 算术移位
For +ve -> 逻辑移位
GCC does
for -ve - > Arithmetic Shift
For +ve -> Logical Shift
根据许多 c 编译器
>>
是算术右移或按位右移。According to many c compilers:
<<
is an arithmetic left shift or bitwise left shift.>>
is an arithmetic right shiftor bitwise right shift.就您获得的转变类型而言,重要的是您要转变的值的类型。 错误的一个典型来源是当您将文字转换为屏蔽位时。 例如,如果您想删除无符号整数的最左边的位,那么您可以尝试将其作为掩码:
不幸的是,这会给您带来麻烦,因为掩码将设置其所有位,因为值被移动(~0) 带符号,因此执行算术移位。 相反,您希望通过显式地将值声明为无符号来强制逻辑移位,即通过执行如下操作:
In terms of the type of shift you get, the important thing is the type of the value that you're shifting. A classic source of bugs is when you shift a literal to, say, mask off bits. For example, if you wanted to drop the left-most bit of an unsigned integer, then you might try this as your mask:
Unfortunately, this will get you into trouble because the mask will have all of its bits set because the value being shifted (~0) is signed, thus an arithmetic shift is performed. Instead, you'd want to force a logical shift by explicitly declaring the value as unsigned, i.e. by doing something like this:
以下是 C 语言中保证 int 逻辑右移和算术右移的函数:
Here are functions to guarantee logical right shift and arithmetic right shift of an int in C: