为什么 ((unsigned char)0x80) << 24 将符号扩展为 0xFFFFFFFF80000000(64 位)?

Why does ((unsigned char)0x80) << 24 get sign extended to 0xFFFFFFFF80000000 (64-bit)?

下面的程序

#include <inttypes.h> /*  printf(" %" PRIu32 "\n"), my_uint32_t) */
#include <stdio.h> /* printf(), perror() */

int main(int argc, char *argv[])
{
  uint64_t u64 = ((unsigned char)0x80) << 24;
  printf("%"  PRIX64 "\n", u64);

  /* uint64_t */ u64 = ((unsigned int)0x80)  << 24;
  printf("%016"  PRIX64 "\n", u64);
}

生产

FFFFFFFF80000000
0000000080000000

在这种情况下,((unsigned char)0x80)((unsigned int)0x80) 有什么区别?

我猜 (unsigned char)0x80 被提升为 (unsigned char)0xFFFFFFFFFFFFFF80 然后被移位,但是为什么这个转换认为 unsigned char 是有符号的?

值得注意的是,0x80 << 16 产生了预期的结果,0x0000000000800000

C 编译器在执行移位之前执行 整数提升

标准的规则 6.3.1.1 说:

If an int can represent all values of the original type, the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions.

由于 unsigned char 的所有值都可以用 int 表示,因此 0x80 被转换为带符号的 intunsigned int 的情况并非如此:它的一些值不能表示为 int,因此在应用整数提升后它仍然是 unsigned int

<< 运算符的左操作数进行整数提升。

(C99, 6.5.7p3) "The integer promotions are performed on each of the operands."

意思是这个表达式:

 ((unsigned char)0x80) << 24

相当于:

 ((int) (unsigned char)0x80) << 24

相当于:

  0x80 << 24

在 32 位 int 系统中设置 int 的符号位。然后,当 0x80 << 24u64 声明中转换为 uint64_t 时,发生符号扩展以产生值 0xFFFFFFFF80000000.

编辑:

请注意,作为 Matt McNabb correctly added in the comments, technically 0x80 << 24 invokes undefined behavior in C as the result is not representable in the type of the << left operand. If you are using gcc, the current compiler version guarantees,它当前不会使此操作未定义。

转换的奇怪部分发生在将 << 的结果从 int32 转换为 uint64 时。您正在使用 32 位系统,因此整数类型的大小为 32 位。以下代码:

 u64 = ((int) 0x80) << 24;
 printf("%llx\n", u64);

打印:

 FFFFFFFF80000000

因为 (0x80 << 24) 给出 0x8000000 这是 -2147483648 的 32 位表示。该数字通过乘以符号位转换为 64 位,并给出 0xFFFFFFFF80000000.

C 标准演变的一个主要困难是,当人们努力对语言进行标准化时,不仅有在某些事情上彼此不同的实现,而且还有大量的代码为那些依赖于这些行为差异 的实现 编写。因为 C 标准的创建者想要避免禁止实现以这些实现的用户可能依赖的方式行事,所以 C 标准的某些部分真是一团糟。一些最糟糕的方面涉及整数提升的方面,例如您观察到的方面。

从概念上讲,将 unsigned char 提升为 unsigned int 似乎比提升为 signed int 更有意义,至少在用作除权利以外的任何东西时 - - 运算符的手操作数。其他运算符的组合可能会产生较大的结果,但 - 以外的任何运算符都不可能产生负结果。要了解为什么选择 signed int 尽管结果不能为负,请考虑以下内容:

int i1; unsigned char b1,b2; unsigned int u1; long l1,l2,l3;

l1 = i1+u1;
l2 = i1+b1;
l3 = i1+(b1+b2);

C 中没有一种机制可以让两种不同类型之间的运算产生一个不是原始类型的类型,因此第一个语句必须执行有符号或无符号的加法; unsigned 通常会产生稍微不那么令人惊讶的结果,特别是考虑到整数文字默认情况下是有符号的(如果将 1 而不是 1u 添加到无符号值可能会使其为负数,那将是非常奇怪的)。然而,令人惊讶的是,第三条语句可以将 i1 的负值变成一个大的无符号数。上面的第一个语句产生一个未签名的结果,但第三个语句产生一个带符号的结果意味着 (b1+b2) 必须被签名。

恕我直言,"right" 解决符号相关问题的方法是定义单独的数字类型,这些类型记录了 "wrapping" 行为(就像现在的无符号类型一样),与那些应该表现作为整数,并且这两种类型表现出不同的提升规则。实现必须继续支持使用现有类型的代码的现有行为,但新类型可以实施旨在促进可用性而非兼容性的规则。

您看到的是未定义的行为。 C99 §6.5.7/4 这样描述左移:

The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is E1 × 2E2, reduced modulo one more than the maximum value representable in the result type. If E1 has a signed type and nonnegative value, and E1 × 2E2 is representable in the result type, then that is the resulting value; otherwise, the behavior is undefined.

在您的例子中,E1 的值为 128,其类型是 int,而不是 unsigned char。正如其他答案所提到的,该值在评估之前得到 提升 int。涉及的操作数有符号int,128左移24位的值为2147483648,比你系统上int所能表示的最大值多1。因此,您的程序的行为是未定义的。

为避免这种情况,您可以确保 E1 的类型是 unsigned int,方法是将其类型转换为 unsigned char