在C语言中,移位运算符如何计算?

24

最近我注意到了一个(奇怪的)行为,当我使用位移操作符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字节。

7
阅读关于数字提升的内容。 - Some programmer dude
那么 a = ((a<<31)>>31); 就可以得到我想要的结果,对吗?(int 类型大小为 4 字节) - chouaib
@JoachimPileborg:为什么在a的情况下编译器不会优化操作,使a保持不变? - David C. Rankin
2
a & 1将产生与您的第二个操作相同的结果(并且更有意义)。 - Sean Latham
@chouaib a = ((a<<31)>>31); 的结果是未定义的行为,因为整数提升将 unsigned char 转换为 int,左移时的有符号溢出是未定义的行为。 - Virgile
显示剩余2条评论
3个回答

31
在第一个示例中:
  • a 被转换成一个 int,左移后再右移,最后转换回 unsigned char

这会导致 a=5 显然成立。

在第二个示例中:
  • b 被转换成一个 int,左移后再转换回 unsigned char
  • b 被转换成一个 int,右移后再转换回 unsigned char

不同之处在于,在第二个示例中,在转换为 unsigned char 时会丢失信息。


5
a = ((char)a<<7)>>7; 这行代码会进行移位、截断再移位(注意括号)。虽然更简单的做法是 a = a & 0x01; - Baldrickk
@chouaib 抱歉,我误读了你的问题。如果你想要丢失信息,那么可以将其转换为 char。然后你应该会得到与第二个示例相同的结果。同时要注意 Baldrickk 提到的括号。 - thumbmunkeys
2
@Baldrickk @thumbmunkeys:将其转换为char会破坏它,导致所有的0xFF,所以我想将其转换为unsigned char更合适。 - chouaib
2
@chouaib,最好使用a = a & 0x01;来掩盖它。当可用时,执行您想要执行的操作,不要利用系统的“怪癖”,除非您有意混淆代码。 - Baldrickk
是的@Baldrickk,我曾经在尝试使用一些位时使用了&,今天特别地我没有逻辑运算符的选择,所以我遇到了这种情况,并觉得很值得在SO上询问。 - chouaib
显示剩余3条评论

17

详细解释每行代码的操作:

情况 a:

  • 在表达式 a = ((a<<7)>>7); 中,先计算 a<<7
  • C 标准规定移位运算符的每个操作数都会隐式地进行整型提升。这意味着如果它们是 bool、char、short 等类型(统称为“小整数类型”),它们将被提升为 int 类型。
  • 这对于 C 中的几乎所有运算符来说都是标准做法。移位运算符与其他运算符不同之处在于它们不使用另一种常见的隐式提升方式:“平衡”。相反,移位的结果总是具有已提升左操作数的类型。在这种情况下是 int 类型。
  • 因此,a 被提升为 int 类型,仍然包含值 0x05。字面量 7 已经是 int 类型,所以它不会被提升。
  • 当您将此 int 值左移 7 位时,您将得到 0x0280。操作的结果是 int 类型。
  • 请注意,int 是带符号类型,所以如果你继续移位数据,那么符号位也会被移入,这将导致未定义行为。同样,如果左操作数或右操作数是负值,那么也会导致未定义行为。
  • 现在你有了表达式 a = 0x280 >> 7;。下一次移位操作不会进行提升,因为两个操作数已经是 int 类型。
  • 结果是 5,类型为 int。然后你将此 int 转换为无符号 char,这是可以的,因为结果足够小。

情况 b:

  • b <<= 7; 等价于 b = b << 7;
  • 与前面一样,b 被提升为 int 类型。结果仍然是 0x0280。
  • 您尝试将此结果存储在无符号 char 中,但它不适合,所以它将被截断为只包含最低字节的值 0x80
  • 在下一行中,b 再次被提升为 int,包含值 0x80。
  • 然后你将 0x80 右移 7 位,得到结果 1。这是 int 类型,但可以适合无符号 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;
} 

@chouaib 把所有代码都写在一行里没有任何意义。我将其分成几行只是为了可读性 - 生成的机器代码仍然是相同的。你也可以写成 a = (uint8_t)(((uint32_t)a<<7)>>7); 但那是一个难以阅读的混乱。 - Lundin
选择 uint32_t 看起来是任意的。32位宽度没有什么特殊之处,或者这只是一个例子,其中 uint162_tuint64_t 也可以使用?尽管进行到 unsigneduintmax_t 的转换可能具有一定的相关性。 - chux - Reinstate Monica
@chux,uint32_t有一些特殊之处,即它不是世界上任何已知系统中的小整数类型之一。我选择它是为了确保此示例代码在所有系统上都能正常工作。如果我选择了例如uint16_t,它将在8位和16位系统上正常工作,但在32位系统上仍会得到整数提升。但是,您可以使用任何其他“足够大”的无符号整数类型,例如uint_least32_tuint64_t,而不是uint32_t。 - Lundin
@chux 将类型转换为 unsigned 不是一个好主意,因为它的大小未知,代码会变得不可移植。将类型转换为 uintmax_t 也没有任何意义,因为它可能会产生一个不必要大的类型。 - Lundin
这篇答案还提到了32位的特殊性。您的前提和它依赖于int/unsigned永远不会超过32位以实现可移植性。这至少是一个合理的假设,但并非C规范所保证的。如何最好地实现可移植取决于所需的移位范围:像0到15或0到31这样的固定范围,0到unsigned位宽-1等 - 这是OP没有说明的事情。 - chux - Reinstate Monica
显示剩余2条评论

14

移位操作会对操作数进行整型提升,在您的代码中,结果得到的 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中也有类似的声明。


3
Char 一个整数类型。 - Oliver Charlesworth
@OliverCharlesworth 非常感谢您,我的问题是表达不够清楚。我已经编辑了答案。 - starrify
很难接受只有一个答案是“可接受的”,因为它们都是“可接受的”,我点赞了其中的3个,但由于starrify添加了引用,我决定接受他的答案,但说实话,我希望我可以接受不止一个答案 ;) - chouaib

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接