如果在一个表达式中同时使用左移和右移,为啥会有所不同?
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
我原以为y
和z
是一样的。但它们根据是否使用中间变量而有所不同。知道为什么会这样会很有趣。
【问题讨论】:
(x<<7)>>7
原则上也存储中间结果。但我不知道它在哪里说这个中间结果应该是什么类型。
@ThePhoton:它在 C 标准中表示,用于评估 (x << 7) >> 7
的中间类型是 int
或 unsigned int
,具体取决于 unsigned char
和 int
的大小。跨度>
【参考方案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
可能实际上传递了 int
或 unsigned int
参数。
unsigned char tmp = x << 7;
要计算表达式 x << 7
,x
是 unsigned 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位,则值为128
(0x80
),但如果unsigned char
类型有更多位,tmp
中的值可以是0x180
、0x380
、@987654359 @、0xf80
、0x1f80
、0x3f80
甚至是0x7f80
。
如果unsigned char
类型大于int
,这可能发生在sizeof(int) == 1
、x
提升为unsigned int
并且在此类型上执行左移的罕见系统上。该值是0x7f80U
,它保证适合unsigned int
类型并将其存储到tmp
实际上不会丢失任何信息,因为类型unsigned char
与unsigned int
具有相同的大小。因此,在这种情况下,tmp
的值将是 0x7f80
。
unsigned char y = tmp >> 7;
求值过程同上,tmp
提升为int
或unsigned int
取决于系统,保留其值,并将此值右移7个位置,这是完全定义的,因为7
小于类型的宽度(int
或unsigned int
)并且值为正。根据unsigned char
类型的位数,存储在y
中的值可以是1
、3
、7
、15
、31
、63
、127
或@ 987654392@,最常见的架构有y == 1
。
再次printf("%x\n", y);
,最好不要写printf("%hhx\n", y);
,输出可能是1
(最常见的情况)或3
,7
,f
,1f
, 3f
、7f
或 ff
取决于类型 unsigned char
中的值位数。
unsigned char z = (x << 7) >> 7;
整数提升在x
上执行,如上所述,值 (255
) 然后左移 7 位作为int
或unsigned 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 char
和 signed int
之间匹配(因此允许 unsigned char 提升为 int),我也觉得说它们“相等”很奇怪。它们不能是相同的类型(它们的符号必须不同),并且具有相同的值范围上限(正端)意味着int
具有多 1 个值位。
@PeterCordes:符号位不是 值位 的一部分,如 C17 6.2.6.2 中使用的那样:[.. .] 对于有符号整数类型,对象表示的位应分为三组:值位、填充位和符号位。[...]。所以从技术上讲,int
和 unsigned char
可以有相同数量的 值位,但是它必须有一个单独的符号位,因此在这种奇怪的架构上至少有 CHAR_BIT-1
填充位.
啊,我的错误,感谢您纠正我关于 C 如何使用术语“值位”的问题。给出 8 与 31 的示例非常有助于明确它不包括符号位,以防其他人忘记。很好的编辑。【参考方案2】:
最后一种情况下的“中间”值是(完整)整数,因此原始unsigned char
类型的“超出范围”移位的位被保留,因此在转换结果时它们仍然被设置回到单个字节。
来自C11 Draft Standard:
6.5.7 移位运算符 ... 3 对每个操作数执行整数提升。的类型 结果是提升的左操作数的结果...
但是,在您的第一种情况下,unsigned char tmp = x << 7;
,当结果“完整”整数转换(即 截断)回单个字节时,tmp
丢失了六个“高”位,给出0x80
的值;然后在unsigned char y = tmp >> 7;
中右移时,结果是(如预期的那样)0x01
。
【讨论】:
太棒了!现在,由于原始类型是unsigned char
,所以整数提升到unsigned int
?否则,我可能希望在右移时看到符号扩展。
@FredLarson 提升的类型是有符号还是无符号都没有关系!由于值255
可以由任何一个正确表示,因此不会发生符号扩展。也就是说,即使您将 255
的 unsigned 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
。
【讨论】:
以上是关于如果在一个表达式中同时使用左移和右移,为啥会有所不同?的主要内容,如果未能解决你的问题,请参考以下文章