为什么这个if条件在比较正负整数时会失败

8
#include <stdio.h>

int arr[] = {1,2,3,4,5,6,7,8};
#define SIZE (sizeof(arr)/sizeof(int))

int main()
{
        printf("SIZE = %d\n", SIZE);
        if ((-1) < SIZE)
                printf("less");
        else
                printf("more");
}

使用gcc编译后的输出为"more"。为什么即使-1 < 8if条件也会失败?

13
编译时开启更多警告,你就会明白原因。 - Some programmer dude
1
做更多的事情:printf(" %lu %lu", sizeof(-1), sizeof(SIZE)); 试一下吧! - Grijesh Chauhan
2
@GrijeshChauhanԾƚłćŤ¶ĀŚĮĻsize_tÁĪĽŚěčŤŅõŤ°ĆŚĀáŤģĺ„Äā "%zu"śėĮś≠£Á°ģÁöĄś†ľŚľŹ„Äā - Jens Gustedt
2
@GrijeshChauhan,自1999年以来一直是C语言。 - Jens Gustedt
这不是问题,但是在-1周围的括号是不必要的。 - Pete Becker
显示剩余2条评论
6个回答

18

问题出在你的比较方式上:

    if ((-1) < SIZE)

sizeof 通常返回一个 unsigned long,因此 SIZE 将会是 unsigned long,而 -1 只是一个 int。在 C 和相关语言中的提升规则意味着在比较之前,-1 将被转换为 size_t,因此 -1 将变成一个非常大的正值(即 unsigned long 的最大值)。

修复这个问题的一种方法是将比较改为:

    if (-1 < (long long)SIZE)
虽然这是一个毫无意义的比较,因为按定义,无符号值总是 >= 0,而编译器可能会警告你这一点。正如@Nobilis后来指出的那样,您应该始终启用编译器警告并注意它们:如果您使用例如gcc -Wall ...进行编译,编译器将会警告您存在的错误。

1
@DieterLücking 这个答案是完全正确的,因为不仅是有符号/无符号问题,而且-1的数据类型和sizeof返回值也不同。在比较表达式中,-1的类型被提升为unsigned long int,这就是为什么在if条件语句中,SIZE与2,147,483,648UL(我考虑了4字节int和8字节long)进行比较,而不是-1 - Grijesh Chauhan
2
@DieterLücking,为什么你不喜欢使用一个更好的建议来替代 if ((-1) < (int)sizeof(x)) 呢? - Grijesh Chauhan
1
更好的解决方案是对于 size_types 使用无符号类型(就像标准库所做的那样)。因此,如果 (-1 < (signed)size) 总是为真,则可以忽略在转换中出现的巨大无符号数变为负数的情况。 - user2249683
2
@PaulR 实际上我应该取消我的反对票:“尽管这实际上是一个无意义的比较,因为按定义无符号值始终>= 0” - user2249683
1
@PaulR 在我的眼中,拥有 C + C++ 徽章的人就是专家了 :-) - TemplateRex
显示剩余14条评论

10

TL;DR

当使用混合的有符号/无符号操作时要小心(使用-Wall编译器警告)。标准对此有一个很长的章节。特别地,通常但不总是有符号值转换为无符号值(尽管在您的特定示例中确实如此)。请参见下面的解释(摘自此问答)。

C++标准相关引用:

5 表达式 [expr]

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

[省略了关于相等类型或相等符号类型的2个子句]

— 否则,如果具有无符号整数类型的操作数的级别大于或等于另一个操作数的类型的级别,则应将具有有符号整数类型的操作数转换为具有无符号整数类型的操作数的类型。

— 否则,如果具有有符号整数类型的操作数的类型可以表示无符号整数类型的所有值,则应将具有无符号整数类型的操作数转换为具有有符号整数类型的操作数的类型。

— 否则,两个操作数都应转换为与具有有符号整数类型的操作数相对应的无符号整数类型。

您的实际示例

要查看程序属于这3种情况中的哪一种,请稍微修改一下它:

#include <stdio.h>

int arr[] = {1,2,3,4,5,6,7,8};
#define SIZE (sizeof(arr)/sizeof(int))

int main()
{
        printf("SIZE = %zu, sizeof(-1) = %zu,  sizeof(SIZE) = %zu \n", SIZE, sizeof(-1), sizeof(SIZE));
        if ((-1) < SIZE)
                printf("less");
        else
                printf("more");
}
在Coliru在线编译器上,这将对-1SIZEsizeof()分别打印4和8,并选择“more”分支(实时示例)。
原因是无符号类型的等级比有符号类型高。因此,条款1适用,有符号类型被值转换为无符号类型(在大多数实现中,通常通过保留位表示来实现,因此会包装成非常大的无符号数字),然后比较继续选择“more”分支。

主题的变化

将条件重写为if ((long long)(-1) < (unsigned)SIZE)将采取“less”分支(实时示例)。
原因是有符号类型的等级高于无符号类型并且还可以容纳所有无符号值。因此,条款2适用,无符号类型转换为有符号类型,然后比较继续选择“less”分支。
当然,您永远不会编写这样一个牵强的if()语句,带有显式转换,但是如果您比较具有long longunsigned类型的变量,则可能发生相同的效果。因此,它阐明了混合有符号/无符号算术非常微妙,并取决于相对大小(标准的用语为“排名”)。特别地,没有固定的规则表明有符号将始终转换为无符号。

3
+1 针对全面的回答,毫无疑问将受到未来世代的赞赏。 - Paul R

7

这是C语言中的一个历史性设计缺陷,也在C++中重复出现。

它可以追溯到16位计算机时代,错误在于决定使用所有16位来表示大小,最高可达65536,放弃了表示负大小的可能性。

如果unsigned的含义是“非负整数”(大小在逻辑上不能为负),那么这本身不会是一个错误,但它与语言的转换规则有关,存在问题。

考虑到语言的转换规则,在C语言中,unsigned类型并不表示非负数,而更像是一个位掩码(数学术语实际上是“ℤ/n环的成员”)。要看到为什么,请考虑以下内容:

  • unsigned - unsigned得到一个unsigned结果
  • signed + unsigned得到一个unsigned结果

如果你把unsigned理解为“非负数”,那么它们都显然毫无意义。

当然,说一个对象的大小是ℤ/n环的成员根本就没有任何意义,这就是错误所在。

实际影响:

每当你处理对象的大小时,要小心,因为该值是unsigned类型,在C/C++中,这种类型具有许多对于数字来说是不合逻辑的属性。请永远记住,unsigned并不意味着“非负整数”,而是“ℤ/n代数环的成员”,最危险的是,在混合操作的情况下,一个int会被转换为unsigned int,而不是相反。

例如:

void drawPolyline(const std::vector<P2d>& pts) {
    for (int i=0; i<pts.size()-1; i++) {
        drawLine(pts[i], pts[i+1]);
    }
}

这段代码存在缺陷,因为如果传递一个空向量,它将执行非法(UB)操作。原因是pts.size()是一个unsigned

语言规则将把整数1转换为1{mod n},在ℤ/n中执行减法,结果为(size-1){mod n},还会将i转换为{mod n}表示,并在ℤ/n中进行比较。

C/C++实际上在ℤ/n中定义了<运算符(在数学中很少使用),即使输入向量为空,您最终仍然会访问pts[0]pts[1]等等直至极大的数字。

正确的循环应该是:

void drawPolyline(const std::vector<P2d>& pts) {
    for (int i=1; i<pts.size(); i++) {
        drawLine(pts[i-1], pts[i]);
    }
}

但我通常更喜欢。
void drawPolyline(const std::vector<P2d>& pts) {
    for (int i=0,n=pts.size(); i<n-1; i++) {
        drawLine(pts[i], pts[i+1]);
    }
}

换句话说,尽快摆脱“unsigned”,只使用常规整数进行操作。永远不要使用“unsigned”表示容器或计数器的大小,因为“unsigned”的意思是“ℤ/n”的成员,而容器的大小不属于这些内容。无符号类型非常有用,但不适合表示对象的大小。标准的C/C++库不幸地做出了错误的选择,现在已经太晚改正。然而,您不必犯同样的错误。Bjarne Stroustrup的话

使用无符号整数代替整数来获得一个额外的比特位表示正整数几乎从来不是一个好主意。通过声明变量为无符号来确保某些值为正数的尝试通常会被隐式转换规则所打败。


+1 很好的解释,我之前不知道 unsigned/signed 表示法背后的历史先例。 - Nobilis

7
当你在signedunsigned之间进行比较时,其中unsigned至少具有与signed类型相等的级别(请参见TemplateRex的答案以获取确切规则),signed将转换为unsigned类型。
关于您的情况,在32位机器上,-1的二进制表示形式作为unsigned是4294967295。因此实际上您正在比较4294967295是否小于8(它不是)。
如果您启用了警告,则编译器会发出警告,指出某些可疑的情况: warning: comparison between signed and unsigned integer expressions [-Wsign-compare] 由于讨论已经有些偏离,关于使用unsigned的适当性,让我引用James Gosling的一句话,他谈到了Java中缺乏unsigned类型(我还会不要脸地link到我的另一篇文章)。
Gosling: 对于我作为一名语言设计师来说,现在我并不认为自己是这样的角色,"简单" 的真正含义是我能否期望普通开发人员将规范牢记在心。这个定义表明,例如,Java并不是一个简单的语言 - 实际上,很多这些语言最终都会出现许多边角情况,没有人真正理解。询问任何C开发人员有关无符号的问题,很快你就会发现几乎没有C开发人员真正理解无符号的运算和无符号算术。这些东西使得C变得复杂。Java的语言部分我认为相当简单。需要查找的是库。

我没有对这里的任何答案进行负面评价,除非有明显错误,并且被多个帖子指出,回答问题的人也没有纠正它,否则我不会进行负面评价。如果你不相信我,那么你可以查看我的声望,如果你对一个答案进行负面评价,你会失去声望。 - Nobilis
2
在SO上,只是简单地点个踩却不给出解释评论的白痴数量相当高。 - 6502
@6502 我同意,如果你对一个答案进行了负评,那么你应该失去更多的声望,除非你在负评后添加了一条评论,并清楚地指出(通过前置“-1”或其他方式)你是那个进行负评的人,并提供了解释(很多人都这样做,这很好)。如果SO可以删除问题开头的问候语,我相信它也可以处理这种解析魔法 :) - Nobilis
1
被下投票并附带以下原因:signed 并不总是转换为 unsigned,可以看看我的回答,尽管在这种特定情况下是这样的(因为 size_t 至少与 int 的等级相同)。 - TemplateRex
是的,我应该澄清它们需要具有相同的等级,否则更高级别的变量将会覆盖另一个变量(例如,long long signed 的存在将会转换为带符号的 unsigned 变量)。 - Nobilis

2

嗯,我不会重复Paul R说的强烈话语,但当你比较无符号和整数时,你会遇到一些问题。

请使用if ((-1) < (int)SIZE)代替您的if条件。


0

将从sizeof运算符返回的无符号类型转换为有符号类型

当您比较两个无符号和有符号数字时,编译器会隐式地将有符号数字转换为无符号数字。
在4字节int中,-1的有符号表示为11111111 11111111 11111111 11111111,当转换为无符号时,该表示将指2^16-1。
因此,基本上您正在比较2^16-1>SIZE,这是正确的。
您必须通过将无符号值显式转换为有符号值来覆盖它。 由于sizeof运算符返回unsigned long long,因此应将其强制转换为signed long long。

if((-1)<(signed long long)SIZE)

在你的代码中使用这个条件语句


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