无符号和较大签名类型之间隐式转换的不一致行为

请考虑以下示例:

#include  int main(void) { unsigned char a = 15; /* one byte */ unsigned short b = 15; /* two bytes */ unsigned int c = 15; /* four bytes */ long x = -a; /* eight bytes */ printf("%ld\n", x); x = -b; printf("%ld\n", x); x = -c; printf("%ld\n", x); return 0; } 

编译我正在使用GCC 4.4.7(它没有给我任何警告):

 gcc -g -std=c99 -pedantic-errors -Wall -W check.c 

我的结果是:

 -15 -15 4294967281 

问题是为什么unsigned charunsigned short值都正确地“传播”到(signed) long ,而unsigned int不是? 对此有任何参考或规则吗?

以下是gdb (单词以小端顺序排列)的结果:

 (gdb) x/2w &x 0x7fffffffe168: 11111111111111111111111111110001 11111111111111111111111111111111 (gdb) x/2w &x 0x7fffffffe168: 11111111111111111111111111110001 00000000000000000000000000000000 

这是由于整数提升如何应用于操作数以及一元减号的结果具有相同类型的要求。 这将在6.5.3.3 一元算术运算符一节中说明( 强调我的未来 ):

一元运算符的结果是其(提升的)操作数的否定。 整数提升在操作数上执行,结果具有提升类型

和整数推广,包括在草案c99标准部分6.3 转换和说:

如果int可以表示原始类型的所有值,则该值将转换为int; 否则,它将转换为unsigned int。 这些被称为整数促销。 48)所有其他类型由整数促销不变。

在前两种情况下,促销将是int ,结果将是int 。 在unsigned int的情况下,不需要升级,但结果将需要转换回unsigned int

使用6.3.1.3 符号和无符号整数中规定的规则将-15转换为unsigned int ,其中:

否则,如果新类型是无符号的,则通过重复地添加或减去一个可以在新类型中表示的最大值来转换该值,直到该值在新类型的范围内。 49)

因此,我们最终得到-15 + (UMAX + 1) ,这导致UMAX - 14 ,这导致大的无符号值。 这有时为什么你会看到代码使用-1转换为无符号值来获得一个类型的最大无符号值,因为它总是最终为-1 + UMAX + 1 ,即UMAX

int很特别。 在int算术运算中,小于int所有东西都被提升为int

因此-a-b是一元减去到15的int值的应用,它只能工作并产生-15。 然后将该值转换为long

-c是不同的。 c不会提升为int因为它不小于int 。 应用于kunsigned int值的一元减号的结果再次是unsigned int ,计算为2 N -k(N是位数)。

现在这个unsigned int值正常转换为long

这种行为是正确的。 报价来自C 9899:TC2。

6.5.3.3/3:

一元运算符的结果是其(提升的)操作数的否定。 整数提升在操作数上执行,结果具有提升类型。

6.2.5 / 9:

涉及无符号操作数的计算永远不会溢出,因为无法通过生成的无符号整数类型表示的结果将以比结果类型可以表示的最大值大1的数量为模。

6.3.1.1/2:

如果可以使用intunsigned int则可以在表达式中使用以下内容:

  • 具有整数类型的对象或表达式,其整数转换等级小于或等于intunsigned int的等级。

  • _Boolintsigned intunsigned int类型的位字段。

如果int可以表示原始类型的所有值,则该值将转换为int ; 否则,它将转换为unsigned int 。 这些被称为整数促销 。 整数促销不会更改所有其他类型。

所以对于long x = -a; ,因为操作数aunsigned char ,转换级别小于intunsigned int的级别,并且所有unsigned char值都可以表示为int (在您的平台上),我们首先提升为int类型。 否定的很简单: int值为-15

对于unsigned short (在您的平台上)的相同逻辑。

促销不会更改unsigned int c 。 因此,使用模运算计算-c的值,得到结果UINT_MAX-14

C的整数提升规则就是它们的原因,因为标准编写者希望允许各种各样的现有实现做出不同的事情,在某些情况下是因为它们是在有“标准”之前创建的,以便继续做他们正在做的事情,同时为新实现定义规则比“做任何你想做的事情”更具体。 不幸的是,编写的规则使得编写不依赖于编译器整数大小的代码变得极其困难。 即使未来的处理器能够以比32位更快的速度执行64位操作,标准规定的规则也会导致大量代码在int超过32位时中断。

通过明确地认识到C的多种方言的存在,并且建议编译器实现一种以一致的方式处理各种事物的方言,但提供它们也可以实现方言,可能回想起来处理“奇怪的”编译器会更好。做不同的事。 这样的方法最终可能最终成为int超过32位的唯一方式,但我甚至没有听说有人考虑过这样的事情。

我认为无符号整数类型的问题的根源在于它们有时用于表示数值的事实,并且有时用于表示包装抽象代数环的成员。 在不涉及类型提升的情况下,无符号类型的行为与抽象代数环一致。 将一元减号应用于环的一个成员应该(并且确实)产生同一环的成员,当添加到原始环时,将产生零[即加法逆]。 只有一种方法可以将整数量映射到环形元素, 但存在多种方法可将环形元素映射回整数量 。 因此, 无论整数的大小如何 ,将环元素添加到整数量都应该产生相同环的元素,并且从环到整数的转换应该要求代码指定应该如何执行转换。 不幸的是,在环的大小小于默认整数类型的情况下,或者当操作使用具有较大类型的整数的环成员时,C隐式地将环转换为整数。

解决这个问题的正确解决方案是允许代码指定某些变量,返回值等应该被视为环类型而不是数字; 类似于-(ring16_t)2的表达式应该产生65534,无论int的大小如何,而不是在int为16位的系统上产生65534,而在-(ring16_t)2较大的系统上产生-2。 同样, (ring32)0xC0000001 * (ring32)0xC0000001应该产生(ring32)0x80000001即使int恰好是64位[注意,如果int是64位,如果代码试图将两个无符号32相乘,编译器可以合法地做任何它喜欢的事情。 -bit值等于0xC0000001,因为结果太大而无法表示为64位有符号整数。

否定是棘手的。 特别是在无符号值时。 如果你看一下c文档,你会注意到(与你期望的相反)unsigned chars和short被提升为用于计算的有符号整数,而unsigned int将被计算为unsigned int。

计算-c时,c被视为int,它变为-15,然后存储在x中,(仍然认为它是UNSIGNED int)并存储为。

澄清 – 当“否定”未签名时,不进行实际推广。 当您为任何类型的int指定负数(或取负数)时,将使用2的数字补码。 由于无符号值和有符号值之间唯一的实际区别在于MSB充当符号标志,因此将其视为非常大的正数而不是负数。