如果在一个表达式中同时使用左移和右移,为啥会有所不同?

Posted

技术标签:

【中文标题】如果在一个表达式中同时使用左移和右移,为啥会有所不同?【英文标题】:Why does it make a difference if left and right shift are used together in one expression or not?如果在一个表达式中同时使用左移和右移,为什么会有所不同? 【发布时间】:2020-09-09 12:23:52 【问题描述】:

我有以下代码:

unsigned char x = 255;
printf("%x\n", x); // ff

unsigned char tmp = x << 7;
unsigned char y = tmp >> 7;
printf("%x\n", y); // 1

unsigned char z = (x << 7) >> 7;
printf("%x\n", z); // ff

我原以为yz 是一样的。但它们根据是否使用中间变量而有所不同。知道为什么会这样会很有趣。

【问题讨论】:

(x&lt;&lt;7)&gt;&gt;7 原则上也存储中间结果。但我不知道它在哪里说这个中间结果应该是什么类型。 @ThePhoton:它在 C 标准中表示,用于评估 (x &lt;&lt; 7) &gt;&gt; 7 的中间类型是 intunsigned int,具体取决于 unsigned charint 的大小。跨度> 【参考方案1】:

这个小测试实际上比看起来更微妙,因为行为是由实现定义的:

unsigned char x = 255; 这里没有歧义,x 是一个unsigned char,其值为255,类型unsigned char 保证有足够的范围来存储255

printf("%x\n", x); 这会在标准输出上产生ff,但将printf("%hhx\n", x); 写成printf 期望unsigned int 用于转换%x 会更简洁,而x 则不是。传递 x 可能实际上传递了 intunsigned int 参数。

unsigned char tmp = x &lt;&lt; 7; 要计算表达式 x &lt;&lt; 7xunsigned char 首先经历 C 标准 6.3.3.1 中定义的整数提升如果int 可以表示原始类型的所有值(受宽度限制,对于位域),则该值将转换为int;否则,它将转换为unsigned int。这些被称为整数提升。

因此,如果unsigned char 中的值位数小于或等于int 的位数(目前最常见的情况是8 vs 31),则x 首先被提升为int,具有相同的值,然后将其左移7 位置。结果0x7f80 保证适合int 类型,因此行为定义明确,将此值转换为unsigned char 类型将有效地截断该值的高位。如果unsigned char类型有8位,则值为1280x80),但如果unsigned char类型有更多位,tmp中的值可以是0x1800x380、@987654359 @、0xf800x1f800x3f80 甚至是0x7f80

如果unsigned char 类型大于int,这可能发生在sizeof(int) == 1x 提升为unsigned int 并且在此类型上执行左移的罕见系统上。该值是0x7f80U,它保证适合unsigned int 类型并将其存储到tmp 实际上不会丢失任何信息,因为类型unsigned charunsigned int 具有相同的大小。因此,在这种情况下,tmp 的值将是 0x7f80

unsigned char y = tmp &gt;&gt; 7; 求值过程同上,tmp 提升为intunsigned int 取决于系统,保留其值,并将此值右移7个位置,这是完全定义的,因为7 小于类型的宽度(intunsigned int)并且值为正。根据unsigned char类型的位数,存储在y中的值可以是137153163127或@ 987654392@,最常见的架构有y == 1

再次printf("%x\n", y);,最好不要写printf("%hhx\n", y);,输出可能是1(最常见的情况)或37f1f3f7fff 取决于类型 unsigned char 中的值位数。

unsigned char z = (x &lt;&lt; 7) &gt;&gt; 7; 整数提升在x 上执行,如上所述,值 (255) 然后左移 7 位作为intunsigned int,总是产生@987654410 @ 然后右移 7 个位置,最终值为 0xff。这种行为是完全定义的。

printf("%x\n", z); 再一次,格式字符串应为printf("%hhx\n", z);,输出始终为ff

如今,字节超过 8 位的系统变得越来越少,但一些嵌入式处理器(例如专用 DSP)仍然可以做到这一点。当通过unsigned char%x 转换说明符传递一个不正常的系统会失败,但使用%hhx 或更便携的写printf("%x\n", (unsigned)z); 会更干净

在这个例子中,用8 而不是7 移动会更加做作。它在具有 16 位 int 和 8 位 char 的系统上会有未定义的行为。

【讨论】:

我准备争辩说,将 unsigned char 传递给 printf 时失败是不合规范的。 您说unsigned char 在具有sizeof(int)==1 的系统上可以大于int。根据定义,在这种情况下它们将具有相同的sizeof(),因此说“更大”可能会产生误导。 unsigned char 有可能比 int 有更多的值位(int 可以有填充;unsigned char 不允许有)。但即使没有这些,unsigned char 的值范围的高端对于相同数量的值位也可能大于int,这仅仅是因为它是无符号的。 如果值范围的上限在 unsigned charsigned int 之间匹配(因此允许 unsigned char 提升为 int),我也觉得说它们“相等”很奇怪。它们不能是相同的类型(它们的符号必须不同),并且具有相同的值范围上限(正端)意味着int 具有多 1 个值位。 @PeterCordes:符号位不是 值位 的一部分,如 C17 6.2.6.2 中使用的那样:[.. .] 对于有符号整数类型,对象表示的位应分为三组:值位、填充位和符号位。[...]。所以从技术上讲,intunsigned char 可以有相同数量的 值位,但是它必须有一个单独的符号位,因此在这种奇怪的架构上至少有 CHAR_BIT-1 填充位. 啊,我的错误,感谢您纠正我关于 C 如何使用术语“值位”的问题。给出 8 与 31 的示例非常有助于明确它不包括符号位,以防其他人忘记。很好的编辑。【参考方案2】:

最后一种情况下的“中间”值是(完整)整数,因此原始unsigned char 类型的“超出范围”移位的位被保留,因此在转换结果时它们仍然被设置回到单个字节。

来自C11 Draft Standard:

6.5.7 移位运算符 ... 3 对每个操作数执行整数提升。的类型 结果是提升的左操作数的结果...

但是,在您的第一种情况下,unsigned char tmp = x &lt;&lt; 7;,当结果“完整”整数转换(即 截断)回单个字节时,tmp 丢失了六个“高”位,给出0x80的值;然后在unsigned char y = tmp &gt;&gt; 7; 中右移时,结果是(如预期的那样)0x01

【讨论】:

太棒了!现在,由于原始类型是unsigned char,所以整数提升到unsigned int?否则,我可能希望在右移时看到符号扩展。 @FredLarson 提升的类型是有符号还是无符号都没有关系!由于值255 可以由任何一个正确表示,因此不会发生符号扩展。也就是说,即使您将 255unsigned char 值显式转换为 signed 32 位 int,其值也将是 255(而不是 INT_MIN)。跨度> @FredLarson 你肯定不会看到无符号类型的符号扩展。至于它提升到什么,它提升到 int(假设 int 大于所述系统上的 char)根据 C11 草案标准第 6.3.1.1 节:“如果 int 可以表示原始类型的所有值(受宽度限制,对于一个位域),该值被转换为 int;否则,它被转换为 unsigned int.【参考方案3】:

没有为char 类型定义移位运算符。任何char 操作数的值都转换为int,表达式的结果转换为char 类型。 因此,当您将左右移位运算符放在同一个表达式中时,计算将作为int 类型执行(不丢失任何位),结果将转换为char

【讨论】:

以上是关于如果在一个表达式中同时使用左移和右移,为啥会有所不同?的主要内容,如果未能解决你的问题,请参考以下文章

[c语言]左移和右移

如何计算位运算中的左移和右移

测试左移和右移:不是左右逢源而是左右突击

测试左移和右移:不是左右逢源而是左右突击

测试左移和右移:不是左右逢源而是左右突击

转载:(jQuery实现左移和右移)