为什么这个if条件失败用于比较负整数和正整数
#include 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 < 8
为什么if
条件也会失败?
问题出在你的比较中:
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 ...
编译gcc -Wall ...
编译器会警告你你的错误。
TL; DR
注意混合签名/未签名操作(使用-Wall
编译器警告)。 标准有很长的篇幅。 特别是,通常但不总是将signed的值转换为unsigned(尽管在您的特定示例中也是如此)。 请参阅下面的解释(摘自此问答 )
C ++标准的相关引用:
5表达式[expr]
10许多期望算术或枚举类型操作数的二元运算符会以类似的方式引起转换并产生结果类型。 目的是产生一个通用类型,它也是结果的类型。 这种模式称为通常的算术转换,其定义如下:
[省略了相同类型或类型的等号的2条款]
– 否则,如果具有无符号整数类型的操作数的秩大于或等于另一个操作数的类型的秩,则带有符号整数类型的操作数应转换为具有无符号整数类型的操作数的类型。
– 否则,如果带有符号整数类型的操作数的类型可以表示具有无符号整数类型的操作数类型的所有值,则具有无符号整数类型的操作数应转换为带有符号整数类型的操作数的类型。
– 否则,两个操作数都应转换为与带符号整数类型的操作数类型相对应的无符号整数类型。
你的实际例子
要了解您的程序落入哪3个案例,请稍微修改一下
#include 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在线编译器上,它分别为-1
和SIZE
的sizeof()
打印4和8,并选择“更多”分支( 实例 )。
原因是无符号类型的等级大于有符号类型。 因此,第1节适用,并且签名类型被值转换为无符号类型(在大多数实现中,通常通过保留位表示,因此包裹到非常大的无符号数),然后比较然后继续选择“更多“分支。
主题的变化
将条件重写为if ((long long)(-1) < (unsigned)SIZE)
将采用“less”分支( 实例 )。
原因是签名类型比无符号类型具有更高的等级,并且还可以容纳所有无符号值。 因此,第2节适用,无符号类型转换为有符号类型,然后比较继续选择“less”分支。
当然,你永远不会用明确的强制转换来编写这样一个设计的if()
语句,但如果你将变量与long long
和unsigned
类型进行比较,就会发生同样的效果。 因此,它说明了混合有符号/无符号算术非常微妙并且取决于相对大小(标准单词中的“排名”)。 特别是, 没有固定的规则说签名将始终转换为无符号 。
当你在有signed
和unsigned
unsigned
之间进行比较时, 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
类型的引用(我将无耻地链接到我关于该主题的另一篇文章):
Gosling:对于我作为一名语言设计师而言,我现在并不像以前那样真实地认为自己是什么“简单”真正意义上的结果是我可以期待J. Random Developer能够掌握这个规范。 这个定义说,例如,Java不是 – 实际上很多这些语言都有很多极端情况,这些都是没有人真正理解的。 测试任何C开发人员关于无符号的,很快你就会发现几乎没有C开发人员真正理解无符号算法是什么,无符号算术是什么。 这样的事情让C变得复杂。 我认为Java的语言部分非常简单。 你必须查找的库。
这是C的历史设计错误,也在C ++中重复。
它可追溯到16位计算机,错误决定使用所有16位来表示高达65536的大小,从而放弃了表示负大小的可能性。
如果unsigned
含义是“非负整数”(大小在逻辑上不能为负),这就不会出错,但这是语言转换规则的问题。
给定语言的转换规则,C中的unsigned
类型不表示非负数,但它更像是一个位掩码(数学术语实际上是“ ℤ/n
环的成员 ”)。 要了解为什么要考虑C和C ++语言
-
unsigned - unsigned
给出unsigned
结果 -
signed + unsigned
给出和unsigned
结果
如果你将unsigned
读作“非负数”,它们都显然毫无意义。
当然,说对象的大小是ℤ/n
环的成员根本没有任何意义,这里它就是错误所在的位置。
实际影响:
每次处理对象的大小时都要小心,因为该值是unsigned
并且C / C ++中的类型具有许多对于数字而言不合逻辑的属性。 请永远记住, unsigned
并不意味着“非负整数”而是“ ℤ/n
代数环的成员”,并且最危险的是,在混合操作的情况下, int
被转换为unsigned int
而不是相反。
例如:
void drawPolyline(const std::vector& pts) { for (int i=0; i
有错误,因为如果传递一个空的点向量,它将进行非法(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& pts) { for (int i=1; i
但我通常更喜欢
void drawPolyline(const std::vector& pts) { for (int i=0,n=pts.size(); i
换句话说,尽快摆脱unsigned
,只需使用常规的整数。
永远不要使用unsigned
来表示容器或计数器的大小,因为unsigned
意味着“ ℤ/n
成员”和容器的大小不是其中之一。 无符号类型很有用,但不能表示对象的大小。
不幸的是,标准的C / C ++库做出了错误的选择,现在修复它已经太晚了。 但是,你并没有被迫犯同样的错误。
用Bjarne Stroustrup的话来说 :
使用无符号而不是int来再获得一位来表示正整数几乎不是一个好主意。 通过声明无符号变量来确保某些值为正的尝试通常会被隐式转换规则所取代
好吧,我不会重复保罗R所说的强烈的话,但是当你比较无符号和整数时,你将会遇到圆顶坏事。
做if ((-1) < (int)SIZE)
而不是你的if条件
将sizeof运算符返回的无符号类型转换为signed
比较两个无符号和有符号数的编译器时隐式地将signed转换为unsigned。
在4字节int中的-1签名表示是11111111 11111111 11111111 11111111当转换为无符号时此表示将引用2 ^ 16-1
所以基本上你比较那个2 ^ 16-1> SIZE,这是真的。
您必须通过将unsigned value显式转换为signed来覆盖它。 由于sizeof运算符返回unsigned long long,因此应将其转换为signed long long
if((-1)<(signed long long)SIZE)
在代码中使用this if条件