【问题标题】:Are the shift operators (<<, >>) arithmetic or logical in C?C中的移位运算符(<<,>>)是算术还是逻辑?
【发布时间】:2010-09-05 16:14:40
【问题描述】:

在 C 中,移位运算符(&lt;&lt;、&gt;&gt;)是算术还是逻辑?

【问题讨论】:

标签: c binary bit-manipulation bit-shift


【解决方案1】:

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

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

【讨论】:

  • 如此接近,格雷格。您的解释几乎是完美的,但是转换有符号类型和负值的表达式是实现定义的。参见 ISO/IEC 9899:1999 第 6.5.7 节。
  • @Rob:实际上,对于左移和有符号负数,行为是不确定的。
  • 实际上,如果生成的数学值(不受位大小限制)不能表示为该有符号类型的正值。底线是右移有符号值时必须小心。
  • @supercat:我真的不知道。但是,我确实知道有一些记录在案的情况下,具有未定义行为的代码会导致编译器执行非常不直观的事情(通常是由于积极的优化 - 例如,请参阅旧的 Linux TUN/TAP 驱动程序空指针错误:lwn.net/Articles/342330 )。除非我需要在右移时进行符号填充(我意识到这是实现定义的行为),否则我通常会尝试使用无符号值执行我的位移,即使这意味着使用强制转换来到达那里。
  • @MichaelBurr:我知道超现代编译器使用 C 标准未定义的行为这一事实(即使它已在 99% 的实现中定义)作为将其行为在所有可以预期运行的平台上完全定义的程序变成毫无价值的机器指令束而没有有用行为的理由。我承认,虽然(讽刺)我对为什么编译器作者错过了最大规模的优化可能性感到困惑:省略程序的任何部分,如果达到,会导致函数被嵌套......
【解决方案2】:

根据K&R 2nd edition,结果对于有符号值的右移取决于实现。

Wikipedia 表示 C/C++“通常”对有符号值实现算术移位。

基本上你需要测试你的编译器或者不依赖它。我对当前 MS C++ 编译器的 VS2008 帮助说他们的编译器进行了算术移位。

【讨论】:

  • 关于this answer,行为所依赖的不仅仅是编译器,而是编译器和(处理器)架构的组合。
  • @stephan:编译器的选择在某些情况下可能受处理器体系结构的影响,但当今的大多数编译器会将带符号值的&gt;&gt; 处理为算术右移即使它是需要添加符号扩展代码.
【解决方案3】:

TL;DR

将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,从而保留操作数的符号(假设 two's complement 编码为负值)。

换句话说,逻辑移位将移位的操作数视为位流并移动它们,而不用担心结果值的符号。算术移位将其视为(有符号)数字,并在进行移位时保留符号。

一个数 X 乘以 n 的算术左移相当于 X 乘以 2n,因此相当于逻辑左移;逻辑移位也会产生相同的结果,因为 MSB 无论如何都会落到最后,没有什么可保留的。

一个数 X 乘以 n 的算术右移等效于 X 除以 2n 仅当 X 为非负数时!整数除法不过是数学除法,round 向 0 (trunc)。

对于由二进制补码编码表示的负数,右移 n 位具有数学上将其除以 2n 并向 -∞ (floor) 舍入的效果;因此,非负值和负值的右移是不同的。

对于 X ≥ 0,X >> n = X / 2n = trunc(X ÷ 2n)

对于 X > n = floor(X ÷ 2n)

其中÷ 是数学除法,/ 是整数除法。我们来看一个例子:

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

作为Guy Steele pointed out,这种差异导致bugs in more than one compiler。这里非负(数学)可以映射为无符号和有符号的非负值(C);两者的处理方式相同,并且通过整数除法将它们右移。

所以逻辑和算术在左移中是等价的,对于非负值在右移中是等价的;它们的不同之处在于负值的右移。

操作数和结果类型

标准 C99 §6.5.7:

每个操作数都应具有整数类型。

对每个操作数执行整数提升。结果的类型是提升的左操作数的类型。如果右操作数的值为负数或大于或等于提升的左操作数的宽度,则行为未定义。

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

在上面的sn-p中,两个操作数都变成int(由于整数提升);如果E2 为负数或E2 ≥ sizeof(int) * CHAR_BIT,则操作未定义。这是因为移位多于可用位肯定会溢出。如果R 被声明为short,则移位操作的int 结果将隐式转换为short;缩小转换,如果值在目标类型中不可表示,则可能导致实现定义的行为。

左移

E1 E2,比结果类型中可表示的最大值多模一减。如果 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 有一个特殊的运算符 &gt;&gt;&gt; 用于逻辑移位,不同于通常的 &gt;&gt; 和 &lt;&lt;,C 和 C++ 只有算术移位,一些区域未定义和实现定义。我认为它们是算术的原因是由于标准措辞数学上的操作,而不是将移位的操作数视为位流;这也许就是为什么它将这些区域保留为未定义/实现定义而不是仅将所有情况定义为逻辑转换的原因。

【讨论】:

  • 不错的答案。关于四舍五入(在标题为 Shifting 的部分中) - 对于负数和正数,右移向 -Inf 舍入。向正数舍入为 0 是向-Inf 舍入的一种私人情况。截断时,您总是会丢弃正加权值,因此您会从原本精确的结果中减去。
  • @ysap 是的,很好的观察。基本上,正数向 0 舍入是更一般的向 -∞ 舍入的特例;这可以在表格中看到,其中正数和负数我都记为向 -∞ 舍入。
【解决方案4】:

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

~0 >> 1

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

~0U >> 1;

【讨论】:

    【解决方案5】:

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

    【讨论】:

      【解决方案6】:

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

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

      【讨论】:

      • 在 x>>a 和 x0 那么答案分别是 x=x/2^a ,x=x*2^a 那么答案是什么如果一个
      • @sunny: a 不能小于 0。这是 C 中未定义的行为。
      【解决方案7】:

      嗯,我看了it up on wikipedia,他们有话要说:

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

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

      【讨论】:

      • 尽管大多数 C 编译器曾经对有符号值进行算术左移,但这种有用的行为似乎已被弃用。当前的编译器理念似乎是假设变量左移的性能使编译器有权假设该变量必须是非负的,因此如果变量为负,则省略其他任何正确行为所必需的代码.
      【解决方案8】:

      左移&lt;&lt;

      这很简单,无论何时使用移位运算符,它始终是按位运算,因此我们不能将它与双精度和浮点运算一起使用。每当我们左移一位零时,它总是被添加到最低有效位 (LSB)。

      但是在右移&gt;&gt; 中,我们必须遵循一条额外的规则,该规则称为“符号位复制”。 “符号位复制”的含义是,如果最高有效位 (MSB) 被设置,那么在再次右移之后,MSB 将被设置,如果它被重置,那么它会再次被重置,这意味着如果之前的值为零然后在再次移位后,如果前一位为 1,则该位为零,然后在移位后再次为 1。此规则不适用于左移。

      最重要的右移示例如果将任何负数右移,然后在一些移位之后值最终达到零,然后在此之后如果将这个 -1 移位任意次数,值将保持不变。请检查。

      【讨论】:

        【解决方案9】:

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

        将在适用时使用它,因为其他编译器可能会这样做。

        【讨论】:

          【解决方案10】:

          GCC 可以

          1. for -ve -> 算术移位

          2. 对于 +ve -> 逻辑移位

          【讨论】:

            【解决方案11】:

            根据许多 编译器:

            1. &lt;&lt; 是算术左移或按位左移。
            2. &gt;&gt; 是算术右移或按位右移。

            【讨论】:

            • “算术右移”和“按位右移”是不同的。这就是问题的重点。问的问题是:“&gt;&gt; 是算术还是按位(逻辑)?”您回答“&gt;&gt; 是算术或按位。”这并不能回答问题。
            • 不,&lt;&lt; 和 &gt;&gt; 运算符是逻辑运算符,而不是算术运算符
            猜你喜欢
            • 2012-07-20
            • 2011-04-11
            • 1970-01-01
            • 1970-01-01
            • 2021-07-08
            • 1970-01-01
            • 2017-06-30
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多