无符号类型和更大的有符号类型之间的隐式转换行为不一致

14

考虑以下例子:

#include <stdio.h>

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

问题是为什么无符号字符和无符号短整型的值都可以正确地“传递”给(有符号的)长整型,而无符号整型却不行?是否有任何参考或规则?

以下是来自 gdb 的结果(字节顺序为小端):

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

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

你确定 long 是 8 个字节吗?难道不应该是 long long 吗? - Quentin
不,这完全取决于平台。 - Happington
2
这就是为什么我在问的原因。int和long通常都是32位的。 - Quentin
1
@Quentin:是的,在该配置中,longlong long类型都是8字节。 - Grzegorz Szpetkowski
1
几乎是一个重复的问题:将无符号类型转换为有符号int / char。 - Lundin
5个回答

12
这是由于操作数的整数提升和一元减运算结果必须具有相同类型的要求所导致的。这在第6.5.3.3节“一元算术运算符”中有所涵盖,并且说道(强调从此开始):
一元减运算符的结果是其(提升后的)操作数的负数。操作数上进行整数提升,结果具有提升类型。
整数提升在草案c99标准第6.3节“转换”中有所涵盖,并且说:
如果int可以表示原始类型的所有值,则将该值转换为int;否则,将其转换为unsigned int。这些被称为整数提升。其他所有类型都不受整数提升的影响。
在前两种情况下,提升将是int类型,结果将是int类型。在unsigned int的情况下,不需要提升,但结果需要转换回unsigned int。
-15使用第6.3.1.3节“有符号和无符号整数”中规定的规则转换为unsigned int,该节说:
否则,如果新类型是unsigned,则通过反复添加或减去可以在新类型中表示的最大值加1,直到该值在新类型的范围内为止,对该值进行转换。因此,我们得到了-15 + (UMAX + 1),这导致UMAX - 14,从而得到一个很大的无符号值。这就是为什么有时会看到代码使用将-1转换为无符号值以获取类型的最大无符号值,因为它总是会变成-1 + UMAX + 1,这是UMAX

3

int是特殊的。在算术运算中,小于int的一切都被提升为int

因此,对15int值应用一元减得到-a-b,这个过程很简单,会产生-15。然后将该值转换为long

-c就不同了。c没有被提升为int,因为它不比int小。对kunsigned int值应用一元减的结果再次是一个unsigned int,计算方式为2N-k(其中N是位数)。

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


1
或者说,小于int的所有类型都是特殊的。它们被称为小整数类型。 - Lundin

3
这种行为是正确的。引用来自C 9899:TC2。
6.5.3.3/3:
一元运算符“-”的结果是其(提升后的)操作数的负数。操作数执行整型提升,结果具有提升类型。
6.2.5/9:
涉及无符号操作数的计算永远不会溢出,因为不能由结果无符号整数类型表示的结果将对比结果类型所能表示的最大值多1进行取模运算。
6.3.1.1/2:
以下内容可在表达式中使用,该表达式可以使用int或unsigned int: - 整数类型的对象或表达式,其整数转换等级小于或等于int和unsigned int的等级。 - 类型为_Bool、int、signed int或unsigned int的位域。
如果int可以表示原始类型的所有值,则将该值转换为int;否则,将其转换为unsigned int。这些称为整数提升。所有其他类型都不受整数提升的影响。
因此,对于long x = -a;,由于操作数a是unsigned char,其转换等级小于int和unsigned int的等级,并且在您的平台上,所有unsigned char值都可以表示为int,因此我们首先提升为int类型。它的负数很简单:值为-15的int。
对于unsigned short(在您的平台上),逻辑相同。 unsigned int c不受提升的影响。因此,使用模运算计算-c的值,得到结果UINT_MAX-14。

1
如果我理解正确的话,一元算术运算符(减号)会触发我们称之为“初始整数提升”的过程(因为它不是二元运算符,所以没有第二个操作数进行进一步比较),而赋值与此无关(它更像是在计算表达式(右值)后的下一步操作)。 - Grzegorz Szpetkowski

2
C语言的整数提升规则是由标准制定者所设定,因为他们希望允许各种现有实现继续执行它们正在做的事情。在某些情况下,这些实现是在“标准”出现之前创建的。同时,还要为新的实现定义比“随心所欲”的规则更加具体的规则。不幸的是,按照规则编写不依赖于编译器整数大小的代码非常困难。即使未来的处理器能够比32位操作更快地执行64位操作,标准所规定的规则也会导致很多代码崩溃,如果int超过32位。
回顾过去,通过明确承认C的多种方言,并建议编译器实现一种处理各种事物的方言,这可能会更好地处理“奇怪”的编译器。但是,同时也提供了可以使用不同方式执行它们的方言。这种方法最终可能成为int超过32位的唯一方法,但我从未听说过有人考虑过这样的事情。
我认为无符号整数类型的问题根源在于它们有时用于表示数值量,有时用于表示包含折叠抽象代数环的成员。在不涉及类型提升的情况下,无符号类型的行为与抽象代数环一致。将一元减号应用于环的成员应该(并且确实)产生同一环的成员,当添加到原始成员时,将产生零[即加法逆元]。将整数量映射到环元素仅有一种方法,但是存在多种将环元素映射回整数量的方法。因此,将环元素添加到整数量应该产生相同环的元素,而从环转换为整数量应该要求代码指定如何执行转换。不幸的是,在环的大小小于默认整数类型或操作使用具有较大类型的整数的环成员的情况下,C会隐式地将环转换为整数。
解决这个问题的正确方法是允许代码指定某些变量、返回值等应被视为环形类型而不是数字;表达式-(ring16_t)2应该产生65534,而不管int的大小,而在int为16位的系统上产生65534,在int更大的系统上产生-2。同样,(ring32)0xC0000001 * (ring32)0xC0000001应该产生(ring32)0x80000001,即使int恰好为64位[请注意,如果int为64位,则编译器在代码尝试将两个等于0xC0000001的无符号32位值相乘时可以合法地做任何它喜欢的事情,因为结果太大而无法表示为64位有符号整数]。

1
@GrzegorzSzpetkowski:stdint.h 的目的是提高可移植性,但默认的类型提升规则意味着如果使用像 uint16_t 这样的类型,但不知道 int 的大小,则代码很可能会以微妙的方式失败,如果 int 的大小发生变化。编写可移植代码的唯一方法是确保任何运算符的两个操作数都被强制转换为相同的类型,并且任何时候将无符号值的运算符的结果用作除法、右移或关系运算符的操作数时,都要显式地将其转换为相同的无符号类型。换句话说... - supercat
可移植的代码必须始终确保不管编译器使用什么类型提升规则都没有关系。就可移植性而言,C语言的类型提升规则并不比要求所有二元操作符接收匹配类型的操作数更有用,也不比对小于 "int" 的类型使用的所有二元运算符将其结果转换为与操作数类型匹配的规则更有用[这正是真正可移植的代码最终被迫做的事情]。 - supercat
@MattMcNabb:不幸的是,由于C语言规则的定义方式,各种“固定大小”的整数之间没有固定的提升顺序。例如,整数字面值和uint32_t的总和在某些编译器中必须是有符号值,在其他编译器中必须是无符号值。 - supercat
@supercat 是的 - 你需要了解这一点,并以这样的方式编写代码,使得结果无论是哪一个都不会影响。 - M.M
如果int比64位更大,那么这个表达式仍然会给出期望的结果。这个想法是在这里xuint64_t - M.M
显示剩余5条评论

0
负数很棘手,特别是无符号值。如果您查看C文档,您会注意到(与您所期望的相反)无符号char和short在计算时被提升为有符号int,而无符号int将作为无符号int进行计算。
当您计算-c时,c被视为int,它变成了-15,然后存储在x中(x仍然认为它是无符号int),并以此方式存储。
为了澄清-对无符号数进行"取负"时实际上没有进行任何推广。当您将负数分配给任何类型的int(或取负数)时,使用数字的2的补码。由于无符号值和有符号值之间唯一的实际区别是MSB充当符号标志,因此它被视为非常大的正数而不是负数。

第一段没问题,第二段不太行:在 -c 中,c 不会被视为 int - 它始终保持为 unsigned int。对于无符号整型的 - 操作行为是不涉及任何转换成有符号数或其他变换的。 - M.M
也许我没有表达清楚,我会更新我的答案来反映这一点。我的意思是“该操作影响二进制数好像它是有符号值”,即进行2的补码运算。它是被定义好了的,因为“它并不真的关心数据是什么,它会对其进行2的补码操作。” - Happington

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