在C语言中,右移运算符有时候是算术右移,有时候是逻辑右移,这种行为很奇怪。

4

GCC版本5.4.0 Ubuntu 16.04

我注意到在C语言中,当我存储一个值时或者不存储时,右移操作的结果会出现一些奇怪的行为。

这段代码片段输出的是0xf0000000,符合预期行为。

int main() {
    int x = 0x80000000
    printf("%x", x >> 3);
}

以下两个代码片段将打印0x10000000,在我看来非常奇怪,它对负数执行逻辑移位。
1.
int main() {
    int x = 0x80000000 >> 3
    printf("%x", x);
}

2.

int main() {
    printf("%x", (0x80000000 >> 3));
}

非常感谢您的帮助。我不知道这是否是我个人电脑的特定问题,无法复制,还是C语言中的一种行为。


3
在使用32位整数的系统上,0x80000000是一个无符号整数,因为它不能表示为32位有符号整数而不变成负数。因此,移位是无符号的。在移位之后将结果赋值给有符号整数不会影响结果。 - Tom Karzes
@TomKarzes 在所有系统中,0x80000000 都是一个正数;因此,无论是有符号还是无符号的,0x8000000 >> 3 的值始终为 0x10000000 - M.M
1
printf("%x", x >> 3); causes undefined behaviour by using the wrong format specfier for int - M.M
@TomKarzes 这仅适用于 C99 之前带有 32 位整数的系统。现代编译器不会有这种行为。 - phuclv
[-2147483648> 0]在C++中返回true吗?(https://dev59.com/rGUq5IYBdhLWcg3wNtuB),为什么最小的int,-2147483648,有类型'long'? (https://dev59.com/ZVsW5IYBdhLWcg3wqYt7),为什么0 < -0x80000000?(https://dev59.com/PlsX5IYBdhLWcg3wJMrb),为什么MSVC选择long long作为-2147483648的类型?(https://dev59.com/TVsW5IYBdhLWcg3wqYt7) - phuclv
3个回答

4
https://en.cppreference.com/w/c/language/integer_constant引用,对于没有任何后缀的十六进制整数常量:
整数常量的类型是在以下类型列表中从可以容纳该值的第一个类型开始,这取决于使用了哪个数字基础和哪个整数后缀:
int
unsigned int
long int
unsigned long int
long long int(自C99起)
unsigned long long int(自C99起)
此外,稍后还有:
表达式中没有负整数常量。例如,表达式-1将逐位应用一元减运算符到所表示的值,这可能涉及隐式类型转换。
因此,如果您的计算机中int占32位,那么0x80000000的类型为unsigned int,因为它不能容纳int并且不能为负数。
语句:
int x = 0x80000000;

unsigned int 按照实现定义的方式转换为 int,但这个语句

int x = 0x80000000 >> 3;

将无符号整型 unsigned int 右移后再转换成 int,因此结果会有所不同。

编辑

另外,正如 M.M 指出的那样,格式说明符 %x 要求传入一个 无符号整型 参数,如果传入一个 int 参数会导致未定义的行为。


谢谢!我对C语言还很新,正在处理位级别的问题。我不知道0x80000000默认被解释为2^31和无符号数。你的解释帮了我很多! - Suhas
@Suhas C使用值语义;1230xabc等表示十进制或十六进制的数学数字,而不是作为该位模式存储在系统中的int值。 - M.M

0

负整数的右移具有实现定义的行为。因此,在向右移动负数时,您不能“期望”任何结果。

所以它就是在您的实现中的样子。这并不奇怪。

6.5.7/5 [...] 如果E1具有带符号类型和负值,则结果值是实现定义的。

它也可能引发UB(未定义行为)

6.5.7/4 [...] 如果E1具有带符号类型和非负值,并且E1×2E2可以在结果类型中表示,则该值就是结果值;否则,行为未定义。


1
使用32位整数时,0x80000000是无符号整数,因此移位是无符号的。您展示的引号不适用于此情况。 - Tom Karzes
@TomKarzes 但要应用于变量的类型。 - 0___________
@TomKarzes 当32位整数被分配给“int”变量时,它是有符号的。所以恐怕你是错的。 - 0___________
你说得没错,第一个情况在移位之前使用了有符号的32位变量,所以你的评论适用于该情况。我指的是另外两种情况,也就是 OP 问到的情况,在这两种情况下,直接对常数进行移位,从而产生无符号移位。 - Tom Karzes
@TomKarzes,所以原帖作者不能从他的第一个示例中期望任何东西。 - 0___________

-3

正如@P__J__所指出的那样,右移操作是依赖于实现的,因此您不应该依赖于它在不同平台上的一致性。

至于您特定的测试,它在单个平台上(可能是32位英特尔或另一个使用整数的二进制补码32位表示的平台)上显示出不同的行为:

GCC使用可用的最高精度(通常为64位,但可能更高)对文字常量执行操作。现在,语句x = 0x80000000 >> 3将不会被编译成在运行时进行右移的代码,而是编译器计算出两个操作数都是常量,并将它们折叠成x = 0x10000000。对于GCC,文字常量0x80000000不是负数。它是正整数2^31。

另一方面,x = 0x80000000会将2^31的值存储在x中,但32位存储无法将其表示为您提供的整数文字2^31的正整数 - 该值超出了32位二进制补码有符号整数可表示的范围。高位比特最终落在符号位上 - 因此这在技术上是一种溢出,尽管您不会收到警告或错误。然后,当您使用x >> 3时,该操作现在在运行时执行(而不是由编译器执行),采用32位算术 - 并将其视为负数。


1
使用32位整数,0x80000000是一个无符号整数,因此移位操作是无符号移位。 - Tom Karzes
@TomKarzes:实际上,0x80000000不是32位或64位(或任何其他大小)-它是一个字面常量,编译器可以随意处理。顺便说一句:按照标准,它是有符号整数-如果您想告诉编译器将其视为无符号整数,则必须执行0x80000000U。(它也被移位为有符号整数-简单地说,编译器在内部使用64位(或更高)的数学运算进行了移位,因此最终结果为0x10000000,如预期所示)。 - Leo K
@LeoK 证明它是无符号的(假设 sizeof(int) == 4 && CHAR_BIT == 8)。 - HolyBlackCat
@LeoK 我认为它的定义比那更严格,但规则很复杂。我手头没有标准副本,但根据此帖子,对于十六进制常量,类型是值适合的第一个类型,其中类型为intunsigned intlongunsigned longlong longunsigned long long。因此,对于32位整数大小,0x80000000的类型为unsigned int - Tom Karzes
“GCC在处理字面常量时使用可用的最高精度”这句话是不正确或不相关的;GCC遵循C标准。任意处理将是不符合规范的。 - M.M
0x80000000存储在int中也不是溢出,而是超出范围的赋值。 "溢出"意味着算术运算的结果将超出范围。(赋值不是算术运算) - M.M

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