已签名/未签名比较

105

我试图理解为什么下面的代码在标注的位置没有发出警告。

//from limits.h
#define UINT_MAX 0xffffffff /* maximum unsigned int value */
#define INT_MAX  2147483647 /* maximum (signed) int value */
            /* = 0x7fffffff */

int a = INT_MAX;
//_int64 a = INT_MAX; // makes all warnings go away
unsigned int b = UINT_MAX;
bool c = false;

if(a < b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a > b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a <= b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a >= b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a == b) // no warning <--- warning expected here
    c = true;
if(((unsigned int)a) == b) // no warning (as expected)
    c = true;
if(a == ((int)b)) // no warning (as expected)
    c = true;

我原以为这与背景提升有关,但最后两个似乎并非如此。

在我看来,第一个==比较和其他比较同样存在有符号/无符号不匹配的问题?


3
使用“-Wall”选项调用gcc 4.4.2时会打印警告信息。 - bobah
这只是猜测,但也许它在编译时就知道答案,因此优化掉了所有比较。 - Null Set
2
啊!re.bobah的评论:我打开了所有警告,现在缺失的警告出现了。我认为它应该与其他比较相同的警告级别设置同时出现。 - Peter
1
@bobah:我非常讨厌gcc 4.4.2打印出这个警告(没有方法可以指示它仅在不等式中才打印),因为所有消除该警告的方法都会使事情变得更糟。默认推广可靠地将-1或~0转换为任何无符号类型的最高可能值,但是如果您通过自己进行强制转换来消除警告,则必须知道确切的类型。因此,如果您更改类型(例如将其扩展为unsigned long long),则使用裸露的-1进行比较仍将起作用(但这些会产生警告),而使用-1u(unsigned) -1进行比较都将失败。 - Jan Hudec
我不知道为什么你需要一个警告,以及为什么编译器不能让它正常工作。-1是负数,因此小于任何无符号数。很简单。 - CashCow
显示剩余3条评论
6个回答

120

当有符号数和无符号数进行比较时,编译器会将有符号数转换为无符号数。在相等性比较中,这没有影响,-1 == (unsigned) -1。但在其他比较中就有影响了,例如下面的比较是成立的:-1 > 2U

编辑:参考文献:

5/9:(表达式)

许多预期算术类型或枚举类型的操作数的二元运算符会导致转换,并以类似的方式生成结果类型。目的是产生一个常见的类型,也是结果的类型。这种模式称为通常的算术转换,其定义如下:

  • 如果任一操作数的类型为 long double,则另一个操作数应转换为 long double。
  • 否则,如果任一操作数为 double,则另一个操作数应转换为 double。
  • 否则,如果任一操作数为 float,则另一个操作数应转换为 float。
  • 否则,应在两个操作数上执行整型提升(4.5)。54)
  • 然后,如果任一操作数为 unsigned long,则另一个操作数应转换为 unsigned long。
  • 否则,如果一个操作数为 long int,而另一个操作数为 unsigned int,则如果 long int 可以表示 unsigned int 的所有值,则 unsigned int 应转换为 long int;否则,两个操作数都应转换为 unsigned long int。
  • 否则,如果任一操作数为 long,则另一个操作数应转换为 long。
  • 否则,如果任一操作数为 unsigned,则另一个操作数应转换为 unsigned。

4.7/2:(整数转换)

如果目标类型是无符号的,则结果值是最小非负剩余类表达式的值。

无符号整数与源整数同余(模2的n次幂,其中n是用于表示无符号类型的位数)。[注意:在二进制补码表示中,如果没有截断,此转换是概念性的,并且位模式不会改变。]

编辑2: MSVC警告级别

MSVC不同警告级别所发出的警告内容,当然是开发者做出的选择。就我而言,在有符号/无符号相等性与大于/小于比较方面,他们的选择是有意义的,当然这完全是主观的:

-1 == -1 的意思与 -1 == (unsigned) -1 相同 - 我认为这是一种直观的结果。

-1 < 2 并不 意味着 -1 < (unsigned) 2 - 这一点乍一看不太直观,我认为应该提早发出警告。


4
如何将有符号数转换为无符号数?有符号值-1的无符号版本是什么?(有符号-1 = 1111,而无符号15 = 1111,按位它们可能相等,但在逻辑上不相等。)我知道如果你强制进行这种转换,它会起作用,但为什么编译器要这样做呢?这是不合逻辑的。此外,正如我在上面评论的那样,当我增加警告时,缺失的==警告出现了,这似乎支持我的说法? - Peter
1
正如4.7/2所述,从有符号到无符号意味着对于二进制补码没有任何改变。至于编译器为什么要这样做,这是C++标准所要求的。我认为VS在不同级别上发出警告的原因是表达式可能会出现意外情况 - 我同意他们的观点,即有符号/无符号的相等比较“不太可能”出现问题,而不等比较则可能会出现问题。当然,这是主观的 - 这些都是VC编译器开发人员所做的选择。 - Erik
好的,我差不多明白了。我的理解是编译器在(概念上)执行以下操作:'if(((unsigned _int64)0x7fffffff) == ((unsigned _int64)0xffffffff))',因为_int64是能够以无符号形式表示0x7fffffff和0xffffffff的最小类型? - Peter
假设你的整数宽度为32位,编译器对于第一次==比较会执行if ((unsigned) 0x7FFFFFFF == (unsigned) 0xFFFFFFFF),与你后面的代码行if(((unsigned int)a) == b)完全相同 - 因为转换是无符号而非有符号,所以表示你的值不需要超过32位。 - Erik
2
实际上,与(unsigned)-1-1u相比较通常比与-1相比较更糟糕。这是因为(unsigned __int64)-1 == -1,但(unsigned __int64)-1 != (unsigned)-1。因此,如果编译器发出警告,您尝试通过转换为无符号或使用-1u来消除它,如果该值实际上恰好是64位或者您稍后将其更改为64位,则会破坏您的代码!并且请记住,size_t是无符号的,在64位平台上是64位的,并且对于无效值使用-1非常常见。 - Jan Hudec
1
也许编译器不应该那样做。如果它比较有符号和无符号值,只需检查有符号值是否为负数。如果是,无论如何它都保证小于无符号值。 - CashCow

39

下面的例子展示了为什么有符号/无符号警告很重要,程序员必须注意它们。

猜测这段代码的输出是什么?

#include <iostream>

int main() {
        int i = -1;
        unsigned int j = 1;
        if ( i < j ) 
            std::cout << " i is less than j";
        else
            std::cout << " i is greater than j";

        return 0;
}

输出:

i is greater than j

惊讶吗?在线演示:http://www.ideone.com/5iCxY

Bottomline: 比较运算符中,如果其中一个操作数是unsigned类型,则另一个操作数在比较前会被隐式转换为unsigned类型只有当它的类型是有符号的!


3
他是正确的!这很愚蠢,但他是正确的。这是我以前从未遇到过的一个重要陷阱。为什么不将无符号值转换为(更大的)有符号值?如果您执行“if(i < ((int) j))”,它可以按预期工作。尽管“if(i < ((_int64) j))”可能更有意义(假设,你不能确定,_int64是int大小的两倍)。 - Peter
6
@Peter:“为什么不将无符号数转换为(更大的)有符号值?”答案很简单:可能没有更大的带符号值。在32位机器上,在long long出现之前,int和long都是32位,没有更大的类型。在比较有符号和无符号时,早期的C ++编译器将两者都转换为有符号数。由于我忘记了原因,C标准委员会对此进行了更改。您最好的解决方案是尽可能避免使用无符号数。 - James Kanze
5
我猜这也跟有符号整数溢出的结果是未定义行为有关,而无符号整数溢出的结果则不是,因此将负有符号值转换为无符号值是被定义的,但将大无符号值转换为负有符号值则不是 - Jan Hudec
2
@James 编译器总是可以生成汇编代码来实现更直观的比较语义,而不需要将其转换为某个更大的类型。在这个特定的例子中,首先检查 i<0 是否成立就足够了。然后可以确定 i 一定小于 j。如果 i 不小于零,则可以安全地将其转换为无符号数与 j 进行比较。当然,有符号数和无符号数之间的比较会更慢,但从某种意义上说,它们的结果会更正确。 - Sven
1
@Nawaz,很不幸,你的最终结论是错误的:如果有符号类型可以容纳无符号类型,则无符号类型将转换为有符号类型而不是相反。只需在你的例子中将“int i”替换为“long i”,它将打印出“i is less than j”。Erik的答案详尽地解释了这个棘手的规则。 - vvaltchev
显示剩余2条评论

6

5

等于运算符只进行位比较(通过简单的除法来判断是否为0)。

小于/大于比较更依赖于数字的符号。

4位示例:

1111 = 15?还是-1?

所以如果你有1111 < 0001 ...它是模棱两可的...

但是如果你有1111 == 1111 ...虽然你没有这个意思,但它是相同的东西。


我理解这一点,但它并没有回答我的问题。正如你所指出的,如果符号不匹配,1111 != 1111。编译器知道类型不匹配,那么为什么它不发出警告呢?(我的观点是,我的代码可能包含许多这样的不匹配,而我没有被警告。) - Peter
这是它的设计方式。相等性测试检查相似性。而且它是相似的。我同意你的观点,它不应该是这样的。你可以做一个宏或者其他东西来重载x==y,使其成为!((x<y)||(x>y))。 - Yochai Timmer

2
在使用2补码表示值的系统中(大多数现代处理器),它们在二进制形式中相等。这可能是为什么编译器不会抱怨a == b的原因。
对我来说,奇怪的是编译器没有警告你 a == ((int)b)。我认为它应该给你一个整数截断警告或者类似的提示。

1
C/C++的哲学是:编译器相信开发人员在显式转换类型时知道自己在做什么。因此,默认情况下不会发出警告(至少我相信有一些编译器如果将警告级别设置得比默认值高,就会生成警告)。 - Péter Török

1
问题中的代码行没有生成C4018警告,因为Microsoft使用了不同的警告号码(即C4389)来处理该情况,并且C4389默认未启用(即在3级别)。
Microsoft文档中了解C4389:
// C4389.cpp
// compile with: /W4
#pragma warning(default: 4389)

int main()
{
   int a = 9;
   unsigned int b = 10;
   if (a == b)   // C4389
      return 0;
   else
      return 0;
};

其他答案已经很好地解释了为什么微软可能决定将等号运算符作为一个特例,但如果没有提到C4389或如何在Visual Studio中启用它,这些答案可能并不是非常有帮助。
我还应该提到,如果您要启用C4389,您可能还应该考虑启用C4388。不幸的是,C4388没有官方文档,但它似乎会出现在以下表达式中:
int a = 9;
unsigned int b = 10;
bool equal = (a == b); // C4388

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