【问题标题】:How is shift operator evaluated in C?如何在 C 中评估移位运算符?
【发布时间】:2014-11-17 07:40:57
【问题描述】:

我最近在使用 shift >> << 进行操作时注意到一个(奇怪的)行为!

为了解释它,让我编写这个可执行的小代码,它执行两个应该是相同的操作(在我的理解中),但我对不同的结果感到惊讶!

#include <stdio.h>

int main(void) {
    unsigned char a=0x05, b=0x05;

    // first operation
    a = ((a<<7)>>7);

    // second operation
    b <<= 7;
    b >>= 7;

    printf("a=%X b=%X\n", a, b);
    return 0;
} 

运行时,a = 5b = 1。我希望它们都等于1!有人能解释一下为什么我会得到这样的结果吗?

P.S:在我的环境中,unsigned char 的大小是 1 个字节

【问题讨论】:

  • 所以a = ((a&lt;&lt;31)&gt;&gt;31); 会得到我想要的,对吧? (int 大小为 4 个字节)
  • @JoachimPileborg:为什么在a 的情况下编译器不只是优化操作,而让a 保持不变?
  • @DavidC.Rankin 当然,没有什么可以阻止编译器这样做,唯一确定的方法是查看生成的汇编代码。但在一般的情况下,结果就是数字提升的结果。
  • a &amp; 1 将产生与您的第二次操作相同的结果(并且更有意义)。

标签: c bit-shift integer-promotion


【解决方案1】:

在第一个例子中:

  • a 转换为 int,左移,然后右移,然后再转换回 usigned char

这显然会导致a=5

在第二个例子中:

  • b 转换为int,左移,然后转换回unsigned char
  • b 转换为int,右移,然后转换回unsigned char

不同之处在于第二个示例在转换为unsigned char的过程中丢失了信息

【讨论】:

  • 我明白了!编写长代码时有点令人困惑,现在假设我的目标是丢失该信息,我该如何在一行中做到这一点?可以转换为 char a = ((char)(a&lt;&lt;7)&gt;&gt;7) 吗?
  • 我想我明白了,一行代码将是a = ((a&lt;&lt;31)&gt;&gt;31); 对吧?好吧,假设 sizeof(int) = 4 bytes
  • a = ((char)a&lt;&lt;7)&gt;&gt;7; 会这样做(移位,截断然后向后移位)(注意你的括号)。虽然更容易做到a = a &amp; 0x01;
  • @Baldrickk @thumbmunkeys: 投射到char 会毁了它给所有0xFF 所以我想它相当适合投射到unsigned char
  • @chouaib 最好用a = a &amp; 0x01; 来掩盖它,在可用时点您想要执行的操作,而不是利用系统的“怪癖”,除非出于某种原因您想故意混淆您的代码。
【解决方案2】:

移位操作会对其操作数进行整数提升,并且在您的代码中生成的 int 被转换回 char,如下所示:

// first operation
a = ((a<<7)>>7); // a = (char)((a<<7)>>7);

// second operation
b <<= 7; // b = (char) (b << 7);
b >>= 7; // b = (char) (b >> 7);

引自 N1570 草案(后来成为 C11 的标准):

6.5.7 移位运算符:

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

而且假设在C99和C90中也有类似的说法。

【讨论】:

  • Char 整数类型。
  • @OliverCharlesworth 谢谢你,我的问题是没有正确表达这个想法。我已经编辑了答案。
  • 很难接受一个答案,因为它们都是“可接受的”,我投票赞成其中的 3 个,但由于添加了引号,我决定接受 starrify,但老实说,我希望我可以接受多个;)
【解决方案3】:

字里行间的事情的详细解释:

案例一:

  • 在表达式a = ((a&lt;&lt;7)&gt;&gt;7); 中,首先计算a&lt;&lt;7
  • C 标准规定,移位运算符的每个操作数都被隐式提升为整数,这意味着如果它们的类型为 bool、char、short 等(统称为“小整数类型”),它们将被提升为 int .
  • 这是 C 中几乎每个运算符的标准做法。移位运算符与其他运算符的不同之处在于它们不使用另一种常见的隐式提升,称为“平衡”。相反,移位的结果总是具有提升的左操作数的类型。在这种情况下int
  • 所以a 被提升为类型int,仍然包含值0x05。 7 文字已经属于 int 类型,因此不会被提升。
  • 当您将此int 左移 7 位时,您将得到 0x0280。运算结果的类型为int
  • 请注意,int 是有符号类型,因此如果您继续将数据进一步移动到符号位,您将调用未定义的行为。同样,如果左操作数或右操作数为负值,您也会调用未定义的行为。
  • 您现在有了表达式 a = 0x280 &gt;&gt; 7;。因为两个操作数都已经是 int,所以下一个班次操作不会发生任何提升。
  • 结果为 5,类型为 int。然后将此 int 转换为 unsigned char,这很好,因为结果足够小以适应。

案例b:

  • b &lt;&lt;= 7; 等价于 b = b &lt;&lt; 7;
  • 和以前一样,b 被提升为int。结果将再次为 0x0280。
  • 然后您尝试将此结果存储在无符号字符中。它不适合,因此将被截断为仅包含最低有效字节0x80
  • 在下一行,b 再次被提升为一个 int,包含 0x80。
  • 然后将 0x80 移动 7,得到结果 1。这是 int 类型,但可以放入 unsigned char,因此它适合 b。

好建议:

  • 永远不要对有符号整数类型使用按位运算符。这在 99% 的情况下没有任何意义,但可能会导致各种错误和定义不明确的行为。
  • 使用按位运算符时,请使用 stdint.h 中的类型,而不是 C 中的原始默认类型。
  • 使用按位运算符时,对预期类型使用显式强制转换,以防止出现错误和意外的类型更改,同时也让您清楚了解隐式类型提升的工作原理,并且您不只是得到代码意外运行。

编写程序的更好、更安全的方法是:

#include <stdio.h>
#include <stdint.h>    

int main(void) {
    uint8_t a=0x05;
    uint8_t b=0x05;
    uint32_t tmp;

    // first operation
    tmp = (uint32_t)a << 7;
    tmp = tmp >> 7;
    a = (uint8_t)tmp;

    // second operation
    tmp = (uint32_t)b << 7;
    tmp = tmp >> 7;
    b = (uint8_t)tmp;

    printf("a=%X b=%X\n", a, b);
    return 0;
} 

【讨论】:

  • 非常感谢您的详细解释和大量信息和提示,我对无法在一行代码中做到这一点感到有点失望(操作 1)跨度>
  • @chouaib 在一行代码中编写所有内容并不能满足其自身的目的。我只是为了可读性将它分成几行——生成的机器代码仍然是相同的。你也可以写a = (uint8_t)(((uint32_t)a&lt;&lt;7)&gt;&gt;7);,但那是一个难以理解的混乱。
  • 选择uint32_t 似乎是任意的。 32 位宽度没有什么特别之处,或者这只是uint162_tuint64_t 也可以工作的例子?不过,转换为 unsigneduintmax_t 会有一些相关性。
  • @chux uint32_t 有一些特别之处,即它不是世界上任何已知系统上的小整数类型之一。我选择它是为了确保这个示例代码在所有系统上都能正常工作。如果我选择了例如uint16_t,它会在 8 位和 16 位系统上运行良好,但在 32 位系统上我仍然会得到整数提升。但除了 uint32_t,您可以使用任何其他“足够大”的无符号整数类型,例如 uint_least32_tuint64_t
  • @chux 转换为unsigned 不是一个好主意,因为它的大小未知,因此代码变得不可移植。转换为 uintmax_t 也没有任何意义,因为它可能会产生不必要的大类型。
猜你喜欢
  • 1970-01-01
  • 2020-03-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-03
  • 1970-01-01
  • 2013-07-30
  • 1970-01-01
相关资源
最近更新 更多