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

发布于 2024-07-04 17:59:12 字数 77 浏览 10 评论 0 原文

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

In C, are the shift operators (<<, >>) arithmetic or logical?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

玩世 2024-07-11 17:59:12

TL;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。 逻辑左移和算术左移的结果是相同的。

X 的算术右移 n 相当于将 X 除以 2 并向 -∞ 舍入,即

因此逻辑和算术在左移中是等效的,而对于非负值在右移中是等效的; 它们的不同之处在于负值的右移。 这是因为 MSB 仅针对二进制补码表示中的负值设置,因此算术右移的 MSB 复制行为很重要。

旁白:与整数除法的关系

算术右移有时会与整数除以 2 的幂相混淆,但它们是不同的。 X 除以 n 的整数除以 X 除以 n 并向 0 舍入,即 trunc,与右移不同,floor

floortrunc 在正空间中给出相同的结果,而在负空间中则不然,例如 floor(-3.9) = -4,<代码>trunc(-3.9) = <代码>-3而<代码>地板(3.9) = <代码>3,<代码>截断(3.9)= 3。 它们在正空间中的相似性导致右移和整数除以 2 相似的假设无效。 正如 Guy Steele 指出的,这种差异导致了多个编译器中的错误。 负空间中的操作有所不同。

我们来看一个例子(÷是数学除法,/是整数除法):

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 [算术右移结果]

操作数和结果类型

标准 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×2E2,比结果类型中可表示的最大值减少模一。 如果 E1 具有有符号类型和非负值,并且 E1×2E2 可以在结果类型中表示,那么这就是结果值; 否则,行为未定义。

由于两者的左移相同,因此空出的位只需用零填充。 然后它指出对于无符号和有符号类型来说,它都是算术移位。 我将其解释为算术移位,因为逻辑移位并不关心位表示的值,它只是将其视为位流; 但该标准不是以比特为单位进行讨论,而是以 E1 与 2E2 的乘积所获得的值来定义它。

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

如果 E1 是具有负值的有符号类型,则左移行为未定义。这是导致未定义行为的简单途径,很容易被忽视。

右移

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

无符号和有符号非负值的右移非常简单; 空位用零填充。 对于带符号的负值,右移的结果是实现定义的。也就是说,大多数实现(例如 GCC 和Visual C++ 通过保留符号位将右移实现为算术移位。

结论

与 Java 不同,Java 有一个特殊的运算符 >>>,用于逻辑移位,与通常的 >><< 不同。 code>、C 和 C++ 仅具有算术移位,某些区域未定义且由实现定义。 我将它们视为算术的原因是由于数学运算的标准措辞,而不是将移位的操作数视为位流; 这也许就是为什么它没有/实现定义这些区域,而不是仅仅将所有情况定义为逻辑转变的原因。

C++20 明确定义(早期定义不明确的情况)右移为算术(不考虑符号),带符号左移为乘以 2 的幂并环绕(模)。

TL;DR

Consider i and n to be the left and right operands respectively of a shift operator; the type of i, after integer promotion, be T. Assuming n to be in [0, sizeof(i) * CHAR_BIT) — undefined otherwise — we've these cases:

| 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                |

† 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 and trunc gives the same results in positive space while not in negative space e.g. floor(-3.9) = -4, trunc(-3.9) = -3 while floor(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):

37)10 = 100101)2

37 ÷ 2 = 18.5

37 / 2 = 18 (rounding 18.5 towards 0) = 10010)2 [result of arithmetic right shift]

-37)10 = 11011011)2 (considering a two's complement, 8-bit representation)

-37 ÷ 2 = -18.5

-37 / 2 = -18 (rounding 18.5 towards 0) = 11101110)2 [NOT the result of arithmetic right shift]

-37 >> 1 = -19 (rounding 18.5 towards −∞) = 11101101)2 [result of arithmetic right shift]

Operand and Result Types

Standard C99 §6.5.7:

Each of the operands shall have integer types.

The integer promotions are performed on each of the operands. The type of the result is that of the promoted left operand. If the value of the right operand is negative or is greater than or equal to the width of the promoted left operand, the behaviour is undefined.

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

In the above snippet, both operands become int (due to integer promotion); if E2 was negative or E2 ≥ sizeof(int) * CHAR_BIT then the operation is undefined. This is because shifting more than the available bits is surely going to overflow. Had R been declared as short, the int result of the shift operation would be implicitly converted to short; a narrowing conversion, which may lead to implementation-defined behaviour if the value is not representable in the destination type.

Left Shift

The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is E1×2E2, reduced modulo one more than the maximum value representable in the result type. If E1 has a signed type and non-negative value, and E1×2E2 is representable in the result type, then that is the resulting value; otherwise, the behaviour is undefined.

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

The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a non-negative value, the value of the result is the integral part of the quotient of E1/2E2. If E1 has a signed type and a negative value, the resulting value is implementation-defined.

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).

顾挽 2024-07-11 17:59:12

根据 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.

浅语花开 2024-07-11 17:59:12

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

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

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:

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);
仄言 2024-07-11 17:59:12

当你这样做时
- 左移 1 乘以 2
- 右移1除以2

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

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

When you do
- left shift by 1 you multiply by 2
- right shift by 1 you divide by 2

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

 x = 5
 x << 1
 x = 10 (x=5*2)
欢烬 2024-07-11 17:59:12

好吧,我在维基百科上查了一下,他们是这么说的:

然而,C 只有一次右移
运算符,>>。 许多C编译器选择
执行哪个右移取决于
关于什么类型的整数
转移; 通常有符号整数是
使用算术移位进行移位,
并且无符号整数被移位
使用逻辑移位。

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

Well, I looked it up on wikipedia, and they have this to say:

C, however, has only one right shift
operator, >>. Many C compilers choose
which right shift to perform depending
on what type of integer is being
shifted; often signed integers are
shifted using the arithmetic shift,
and unsigned integers are shifted
using the logical shift.

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.

凤舞天涯 2024-07-11 17:59:12

左移 <<

这在某种程度上很简单,每当您使用移位运算符时,它始终是按位运算,因此我们不能将其与双精度和浮点运算一起使用。 每当我们左移一个零时,它总是添加到最低有效位(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 the MSB 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.

月下凄凉 2024-07-11 17:59:12

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

将在适用时使用它,就像其他编译器一样很可能会这样做。

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.

will will use this when applicable, as other compilers are likely to do.

只是在用心讲痛 2024-07-11 17:59:12

GCC 执行

  1. for -ve - > 算术移位

  2. For +ve -> 逻辑移位

GCC does

  1. for -ve - > Arithmetic Shift

  2. For +ve -> Logical Shift

巷雨优美回忆 2024-07-11 17:59:12

根据许多 编译器

  1. : < 是算术左移或按位左移。
  2. >> 是算术右移或按位右移。

According to many compilers:

  1. << is an arithmetic left shift or bitwise left shift.
  2. >> is an arithmetic right shiftor bitwise right shift.
一场春暖 2024-07-11 17:59:12

就您获得的转变类型而言,重要的是您要转变的值的类型。 错误的一个典型来源是当您将文字转换为屏蔽位时。 例如,如果您想删除无符号整数的最左边的位,那么您可以尝试将其作为掩码:

~0 >> 1

不幸的是,这会给您带来麻烦,因为掩码将设置其所有位,因为值被移动(~0) 带符号,因此执行算术移位。 相反,您希望通过显式地将值声明为无符号来强制逻辑移位,即通过执行如下操作:

~0U >> 1;

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:

~0 >> 1

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:

~0U >> 1;
め七分饶幸 2024-07-11 17:59:12

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

Here are functions to guarantee logical right shift and arithmetic right shift of an int in 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;
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文