【发布时间】:2009-06-12 19:29:23
【问题描述】:
前几天我在阅读 C 标准,并注意到与有符号整数溢出(未定义)不同,无符号整数溢出是明确定义的。我已经看到它在很多代码中用于最大值等,但是考虑到关于溢出的巫术,这被认为是好的编程实践吗?无论如何都不安全吗?我知道许多现代语言(如 Python)不支持它——相反,它们继续扩展大数的大小。
【问题讨论】:
前几天我在阅读 C 标准,并注意到与有符号整数溢出(未定义)不同,无符号整数溢出是明确定义的。我已经看到它在很多代码中用于最大值等,但是考虑到关于溢出的巫术,这被认为是好的编程实践吗?无论如何都不安全吗?我知道许多现代语言(如 Python)不支持它——相反,它们继续扩展大数的大小。
【问题讨论】:
无符号整数溢出(以环绕形式)通常在散列函数中被利用,并且从年点开始。
【讨论】:
简单地说:
只要您注意并遵守定义(无论出于何种目的 - 优化、超级聪明的算法等),使用您认为合适的无符号整数溢出是完全合法/OK/安全的
我>【讨论】:
仅仅因为您知道标准的细节并不意味着维护您的代码的人知道。该人可能不得不在稍后调试时浪费时间担心这一点,或者必须在以后查找标准以验证此行为。
当然,我们希望在职程序员能够熟练掌握一门语言的合理特性,而不同的公司/团体对合理熟练程度的期望也不同。但是对于大多数群体来说,期望下一个人知道他/她的头脑而不必考虑它似乎有点过分。
如果这还不够,当您在标准的边缘工作时,您更有可能遇到编译器错误。或者更糟的是,将此代码移植到新平台的人可能会遇到它们。
简而言之,我投票不这样做!
【讨论】:
我认为导致问题的无符号整数溢出的一种方法是从一个小的无符号值中减去,导致它包装成一个大的正值。
【讨论】:
a 和 b 是例如UInt32,表达式 (UInt32)(a-b) 将产生包装行为,很明显这种行为是预期的。然而,(ab) 应该产生包装行为的期望远不那么明显,并且在 int 大于 32 位的机器上,它实际上可能不会产生这种行为。
我一直用它来判断是不是该做某事了。
UInt32 now = GetCurrentTime()
if( now - then > 100 )
{
// do something
}
只要你检查'now'和'then'之前的值,你就可以得到'now'和'then'的所有值。
编辑:我想这确实是一个下溢。
【讨论】:
int 是 64 位的系统上会失败,因为减法的操作数将扩展到有符号的 64 位整数,所以如果 'now' 是 0 并且then 是 4294967295u (0xFFFFFFFF) 减法的结果不是 1,而是 -4294967295。在比较之前将减法结果转换为 UInt32 可以避免该问题,因为 (UInt32)(-4294967295) 将为 1。
另一个可以有用地使用无符号溢出的地方是当您必须从给定的无符号类型向后迭代时:
void DownFrom( unsigned n )
{
unsigned m;
for( m = n; m != (unsigned)-1; --m )
{
DoSomething( m );
}
}
其他替代方案并不那么整洁。除非您将 m 更改为有符号,否则尝试执行 m >= 0 不起作用,但是您可能会截断 n 的值 - 或者更糟 - 在初始化时将其转换为负数。
否则你必须做 !=0 或 >0 然后在循环之后手动做 0 的情况。
【讨论】:
我不会仅仅出于可读性的原因而依赖它。在确定将变量重置为 0 的位置之前,您将调试代码数小时。
【讨论】:
只要你知道溢出会在什么时候发生就可以依赖溢出...
例如,我在迁移到更新的编译器时遇到了 MD5 的 C 实现问题... 该代码确实预期溢出,但它也预期 32 位整数。
64 位的结果是错误的!
幸运的是,这就是自动化测试的用途:我很早就发现了问题,但如果没有引起注意,这可能是一个真正的恐怖故事。
你可以争辩说“但这很少发生”:是的,但这就是让它变得更加危险的原因! 当出现错误时,每个人都会怀疑最近几天编写的代码。 没有人怀疑“工作多年”的 f 代码,通常没有人知道它是如何工作的......
【讨论】:
由于 CPU 上的有符号数可以用不同的方式表示,因此当前所有 CPU 中有 99.999% 使用二进制补码表示法。由于这是大多数机器,因此很难找到不同的行为,尽管编译器可能会检查它(机会很大)。然而,C 规范必须占 100% 的编译器,因此尚未定义其行为。
所以它会让事情变得更加混乱,这是避免它的一个很好的理由。但是,如果您有一个非常好的理由(例如,代码的关键部分的性能提升了 3 倍),那么请好好记录并使用它。
【讨论】:
如果您明智地使用它(注释和可读性都很好),您可以通过编写更小更快的代码从中受益。
【讨论】:
siukurnin 提出了一个很好的观点,即您需要知道何时会发生溢出。避免他描述的可移植性问题的最简单方法是使用 stdint.h 中的固定宽度整数类型。 uint32_t 在所有平台和操作系统上都是一个无符号的 32 位整数,在为不同的系统编译时不会有不同的行为。
【讨论】:
我建议在任何时候依赖无符号数字包装时始终进行显式转换。否则可能会有惊喜。例如,如果 "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 位的唯一可移植方法是在进行乘法之前将其中一个整数转换为 UInt64。否则 UInt32 可能会转换为有符号的 Int64,乘法会溢出并产生未定义的结果。
【讨论】: