使用无符号整数溢出是否是良好的实践?

22

我前几天在阅读C标准时注意到,与有符号整数溢出(未定义)不同,无符号整数溢出是定义良好的。 我看到它在很多代码中用于计算最大值等,但考虑到整数溢出的神秘性质,这是否被认为是良好的编程实践?它是否存在安全方面的问题? 我知道许多现代语言(如Python)不支持此功能-相反,它们继续扩展大数字的大小。

我发现C标准规定了当无符号整数发生溢出时会回绕并从零开始计数。 因此,在使用无符号整数进行计算时,如果您能确定您的操作不会导致溢出,那么这是可以使用的良好实践。 但是,如果存在任何潜在的溢出风险,则应采取其他措施来确保程序的正确性和安全性。

12个回答

23

无符号整数溢出(以环绕的形式)经常被哈希函数利用自年代以来一直如此。


由于计算机起源于密码学,我怀疑是在20世纪40年代某个时间。 - anon
虽然我实际上不明白为什么要坚持使用有符号整数溢出。使用无符号整数同样好,并且可以通过解决方案检测溢出。 - Eduard - Gabriel Munteanu
1
@Martin:在UNIX上。但是自从1900年Windows上,以及1904年Mac上,微软就一直在Excel中使用这种方式了。 - Steve Jessop
1
@eduard 我们正在讨论UNSIGNED OVERFLOW。没有人坚持使用有符号溢出-恰恰相反! - anon
@Neil:抱歉,在阅读其他答案/评论后,“signed”这个词卡在我的脑海里了。 - Eduard - Gabriel Munteanu

5
简而言之:
只要您注意并遵守定义(无论是为了优化、超级聪明的算法等任何目的),使用未签名整数溢出是完全合法/安全的。

4
仅仅因为你了解标准的细节,并不意味着维护你的代码的人也知道。这个人可能会在以后调试时浪费时间担心这个问题,或者必须去查找标准以验证这种行为。
当然,我们期望工作程序员具有语言合理特性的熟练掌握能力,而不同的公司/组织对于合理的熟练度的期望也不同。但对于大多数团队来说,期望下一个人能够脱口而出并且不需要思考这些内容似乎是过分的。
如果这还不足以说明问题,那么当你在标准边缘工作时,你更有可能遇到编译器错误。或者更糟的是,将此代码移植到新平台的人可能会遇到这些错误。
总之,我建议不要这样做!

3

1
我在询问无符号溢出的问题。 - user122147
这是无符号的。使用无符号算术,从100中减去200。您将得到一个大值(大小取决于值的范围)。在C中,无符号算术被定义为模数,或者可以说是环绕。 - David Thornley
@DavidThornley:我认为问题是是否依赖于这种行为是一个好的实践方式。我的个人想法是,如果有人希望强制执行它,只有在每次使用严格类型转换时才可以依赖这种行为。例如,如果abUInt32,则表达式(UInt32)(a-b)将产生包装行为,并且清楚地表明了期望出现这种行为。但是,期望(a-b)产生包装行为远不如此明显,在int比32位更大的机器上,它实际上可能并没有产生这种行为。 - supercat

2

无符号溢出可以有用地用于从给定的无符号类型向后迭代的另一个场所:

void DownFrom( unsigned n )
{
    unsigned m;

    for( m = n; m != (unsigned)-1; --m )
    {
        DoSomething( m );
    }
}

其他的替代方案并不那么简洁。尝试做 m >= 0 并不奏效,除非你将m改为signed,但这样可能会截断n的值 - 或者更糟的是,在初始化时将其转换为负数。

否则,你必须做 !=0 或 >0,然后在循环后手动处理0的情况。


3
或者使用 "do {DoSomething(n);} while (n-- != 0);",在开头加上 "if (n != (unsigned)-1)",如果实际上希望 -1 成为一种特殊的输入值,表示“什么也不做”,就像上面的代码中那样。 - Steve Jessop
@Niki:不起作用,你的循环没有执行任何操作,因为在第一次询问时m < n就失败了。 - Steve Jessop
是的,通常情况下(unsigned)-1 - 即最大可能的无符号值是一个特殊值,但它不一定非得是这样。 - CB Bailey
我认为如果某个输入被特殊处理并且是有效的,最好在代码中明确表示出来,以确保阅读代码的人能够注意到。但是,注释与我的“if”语句一样有效,而且不需要额外的比较操作。 - Steve Jessop
1
为了提高代码的可读性,使用UINT_MAX比(unsigned)-1更好,不是吗?此外,我认为这可能是使用do...while循环的正确位置;m=n; do { DoSomething(m); } while(m-- >0);将执行0的情况。 - Benubird

2

我经常使用它来判断是否是做某事的时候。

UInt32 now = GetCurrentTime()

if( now - then > 100 )
{
   // do something
}

只要在“现在”超过“然后”之前检查值,您就可以适用于所有“现在”和“然后”的值。
编辑:我想这确实是一个下溢。

我相信上述代码在int为64位的系统上会失败,因为减法操作数将被扩展为有符号的64位整数,所以如果'now'为0且then为4294967295u(0xFFFFFFFF),则减法的结果不是1而是-4294967295。在比较之前将减法的结果转换为UInt32可以避免这个问题,因为(UInt32)(-4294967295)将是1。 - supercat

1

仅出于可读性的原因,我不会依赖它。在你找到将变量重置为0的位置之前,你将花费数小时来调试你的代码。


并不是这样,只要行为有文档记录和注释即可。此外,在固定宽度时间戳等情况下,存在合法的(无)符号整数溢出原因,其中精度更为重要,而溢出可以被合理地检测到。 - Eduard - Gabriel Munteanu

1

只要你知道什么时候会发生溢出,就可以依靠溢出。

例如,当我迁移到更近期的编译器时,使用C实现MD5时遇到了麻烦。 代码确实预期溢出,但它也预期32位整数。

使用64位时,结果是错误的!

幸运的是,这就是自动化测试的用途:我很早就发现了问题,但如果没有注意到这个问题,这可能成为一个真正的恐怖故事。

您可以争辩说,“但这种情况很少发生”:是的,但这使得它变得更加危险! 当存在错误时,每个人都会对最近几天编写的代码持怀疑态度。 没有人怀疑多年来“正常工作”的代码,通常也没有人知道它的工作原理...


1
这是一个很好的例子,可以使用编译时断言来检查 sizeof(int)==4 的假设是否有效。这样甚至在运行测试之前就会抛出错误。 - RBerteig

0
我建议每次依赖于无符号数包装时都要有显式转换。否则可能会有意外情况发生。例如,如果“int”是64位的,那么像下面这样的代码:
UInt32 foo,bar;
if ((foo-bar) < 100) // Report if foo is between bar and bar+99, inclusive)
  ... do something

可能会失败,因为“foo”和“bar”将被提升为64位有符号整数。在将结果与100进行比较之前,添加一个UInt32的类型转换将防止出现问题。

顺便说一下,我认为直接获取两个UInt32乘积的最低32位的唯一可移植方法是在执行乘法之前将其中一个int强制转换为UInt64。否则,UInt32可能会转换为有符号Int64,导致乘法溢出并产生未定义的结果。


0

由于CPU上的有符号数字可以用不同的方式表示,目前99.999%的所有CPU都使用二进制补码表示法。由于这是大多数机器的情况,尽管编译器可能会检查它(几乎没有可能),但很难找到不同的行为。然而,C规范必须考虑100%的编译器,因此没有定义其行为。

因此,这会使事情更加混乱,这是避免它的一个好理由。但是,如果您有一个非常好的理由(比如,在代码的关键部分提高3倍的性能),那么请详细记录并使用它。


2
你所提到的是有符号整数。C标准记录了无符号整数的行为,这也是问题所在。 - David Thornley

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