无符号整数和有符号整数的比较运算

61

看这段代码片段:

int main()
{ 
 unsigned int a = 1000;
 int b = -1;
 if (a>b) printf("A is BIG! %d\n", a-b);
 else printf("a is SMALL! %d\n", a-b); 
 return 0;
}   

这将输出: a is SMALL: 1001

我不理解这里发生了什么。大于号运算符是如何工作的?为什么"a"小于"b"?如果确实较小,为什么我得到一个正数(1001)作为差异?


2
如果您使用了-Wsign-compare编译器标志,那么在比较时会收到警告。 您应该始终使用-Wall(其中包含-Wsign-compare)。 请参阅此处以了解避免此问题的其他方法。 - Alejandro
请查看此帖子获取更多信息:https://dev59.com/u2kv5IYBdhLWcg3wnCDH - Adrian Monk
1
@Aleph7 - 从技术上讲,那并不完全正确,-Wsign-compare仅在编译C++时与-Wall一起使用。它不包括C(请参见此处https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html)。我已经测试过了,并可以确认上面的代码在-Wall下没有警告,但在-Wsign-compare下有警告(我正在使用gcc(Ubuntu 5.2.1-22ubuntu2)5.2.1 20151010)。 - bph
7个回答

65

不同整数类型之间的二进制操作是在所谓的“常规算术转换”(参见语言规范6.3.1.8)定义的“常规”类型中执行的。在您的情况下,“常规”类型是unsigned int。这意味着int操作数(您的b)将在比较之前转换为unsigned int,并且为执行减法而进行转换。

-1转换为unsigned int时,结果是最大可能的unsigned int值(与UINT_MAX相同)。不用说,它将大于您的无符号1000值,这意味着a>b确实是假的,并且a确实与(unsigned) b相比较。您代码中的if应该解析为else分支,这就是您在实验中观察到的。

减法使用相同的转换规则。您的a-b实际上被解释为a - (unsigned) b,其结果类型为unsigned int。这样的值不能用%d格式说明符打印,因为%d仅适用于有符号的值。您尝试使用%d将其打印出来会导致未定义的行为,因此从C语言的角度来看,您看到的打印值(即使在实践中它有一个逻辑上确定的解释)是完全无意义的。

编辑:实际上,我可能对未定义的行为部分错误。根据C语言规范,相应有符号和无符号整数类型的公共部分应具有相同的表示形式(暗示着“作为函数参数的互换性”)。因此,a-b表达式的结果是无符号的1001,如上所述,除非我遗漏了什么,否则使用%d说明符打印此特定的无符号值是合法的,因为它落在正int的范围内。使用%d打印(unsigned) INT_MAX + 1将是未定义的,但1001u没问题。


虽然我们对他的实现的调用约定了解得足够多,甚至可以猜测出发生的事情是无符号结果 a-b 的值为 1001,这个值已经通过可变参数传递并重新解释为带符号数,而没有改变其值。 - Steve Jessop
1
是的,传递 unsigned int 并仅执行 va_arg(ap, int) 是没有 UB 的。但是,如果违反 printf 对预期 int 的要求,则确实会产生 UB。尽管对我来说这听起来很愚蠢。为什么他们没有为 printf 指定:“下一个参数的类型应为有符号或无符号整数,并应在 int 范围内”。 - Johannes Schaub - litb
1
在这种情况下,fprintf的描述如下:(对于%d):“将int参数转换为...”,以及“如果任何参数与相应的转换规范不匹配,行为是未定义的。” 所以我不认为它是明确定义的。也许usenet 上的某个人知道? - Johannes Schaub - litb
我从6.2.6.2中无法理解,但我认为标准只比较相应的有符号和无符号整数类型的值位,而不是任何填充位。实现是否可以忽略无符号int中的填充位,但对于int中设置的任何填充位都是陷阱表示,这是否合法?我不知道,但如果是这样,那么生成的1001可能恰好具有填充位集,因此将其重新解释为int将是UB。并不是说一定有任何gcc目标在int中具有填充位,更不用说具有这种奇怪的属性了... - Steve Jessop
在以下情况下的规则是什么:x = 10 +- 10u + 10u +- 10;?无论x是有符号还是无符号,结果都相同! - mike
显示剩余2条评论

16
在一个典型的实现中,当 int 为32位时,将-1转换为 unsigned int 得到的结果是4,294,967,295,确实≥1000。
即使在无符号环境下进行减法运算,1000 - (4,294,967,295) = -4,294,966,295 = 1,001 这就是你得到的结果。
这就是为什么当你比较无符号类型和带符号类型时,gcc 会发出警告。(如果没有看到警告,请传递 -Wsign-compare 标志。)

2
我点踩是因为"4,294,967,295 (2's complement)"。这与二进制补码无关。在1的补码机器上,它将产生相同的值。而在不同位宽的整数上,它将产生不同的值。 - Johannes Schaub - litb
@Schaub:也许我表达不够清晰,但我的意思是4,294,967,295(即1的2补数)确实≥1。此外,在1的补码机器中,-1的表示形式为4,294,967,294。 - kennytm
5
正如litb所说,这与表示无关。在一个1s' complement的计算机上,将-1转换为unsigned会得到UINT_MAX,而不是重新解释1s' complement位模式。2's complement便利之一是C(un)signed转换不改变位模式。这特别适用于2's complement:C对无符号类型的转换是根据模算术定义的,而不是按位模式。在1s' complement上,实现必须做一些实际工作才能获得UINT_MAX。 - Steve Jessop
编辑得更好了,但仍然不能保证 UINT_MAX 是 4,294,967,295。另请参见 https://dev59.com/d3I-5IYBdhLWcg3wbHq-。 - Alok Singhal

1
 #include<stdio.h>
 int main()
 {
   int a = 1000;
   signed int b = -1, c = -2;
   printf("%d",(unsigned int)b);
   printf("%d\n",(unsigned int)c);
   printf("%d\n",(unsigned int)a);

   if(1000>-1){
      printf("\ntrue");
   }
   else 
     printf("\nfalse");
     return 0;
 }

为此,您需要了解运算符的优先级

  1. 关系运算符从左到右工作... 所以当它遇到

    if(1000>-1)

首先将-1更改为无符号整数,因为默认情况下int被视为无符号数字,并且其范围大于有符号数字

-1将更改为无符号数字,它会变成一个非常大的数字


1

你正在进行无符号比较,即将1000与2^32 - 1进行比较。

由于printf中的%d,输出是有符号的。

注意:当混合使用有符号和无符号操作数时,行为有时取决于编译器。我认为最好避免它们,并在怀疑时进行强制转换。


1
减法不会使任何东西带有符号。在有符号和无符号值中,减法是相同的操作。 - wj32
1
错误,intunsigned int 操作数之间的减法被计算为无符号减法,结果当然是无符号的。 - AnT stands with Russia

0
硬件设计用于比较有符号的和无符号的。如果您想要算术结果,请先将无符号值转换为更大的有符号类型。否则,编译器会认为比较实际上是在无符号值之间进行的。
而-1表示为1111..1111,因此它是一个非常大的数量...最大的...当被解释为无符号时。

硬件可能根本没有算术比较功能(如专用的比较或减法指令),但这样的硬件仍然可以用于运行C程序。硬件很重要,但超出了语言标准所定义的编程语言范畴。 - Alexey Frunze

0

找到一种简单的比较方法,当你无法摆脱无符号声明时可能会有用(例如,[NSArray count]),只需将“unsigned int”强制转换为“int”。

如果我错了,请纠正我。

if (((int)a)>b) {
    ....
}

-1
在比较一个无符号整型变量a和一个有符号整型变量b时,b会被强制转换为无符号整型,因此,有符号整型值-1会被转换成无符号整型的最大值**(范围:0到(2^32)-1)**。因此,a>b即(1000>4294967296)变为false。因此,else循环printf("a is SMALL! %d\n", a-b);被执行。

1
这是一个低努力的重复回答的顶级投票。 - emsimpson92

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