这里的一些答案提到了有符号和无符号值之间令人惊讶的提升规则,但这似乎更像是一个与mixing个有符号和无符号值有关的问题,并且不一定解释为什么在混合场景之外,signed个变量比unsigned个更受欢迎.
根据我的经验,除了混合比较和升级规则之外,以下是无符号值成为bug磁铁的两个主要原因.
无符号值在零处不连续,这是编程中最常见的值
无符号整数和有符号整数的最小值和最大值都有discontinuities,它们会环绕(无符号)或导致未定义的行为(有符号).对于unsigned
,这些分数分别为zero和UINT_MAX
.int
分为INT_MIN
分和INT_MAX
分.在具有4字节int
值的系统上,INT_MIN
和INT_MAX
的典型值是-2^31
和2^31-1
,在这样的系统上,UINT_MAX
的典型值是2^32-1
.
unsigned
不适用于int
的主要缺陷是它有discontinuity at zero.当然,在程序中,零是一个非常常见的值,还有其他小值,比如1,2,3.在各种构造中,加和减小值是很常见的,尤其是1,如果你从一个unsigned
的值中减go 任何值,而它恰好是零,你就得到了一个巨大的正值和一个几乎确定的错误.
考虑代码在一个向量中的所有值的迭代索引,除了最后的 0.5 :
for (size_t i = 0; i < v.size() - 1; i++) { // do something }
直到有一天你通过了一个空向量,这一切都很好.不是进行零次迭代,而是得到v.size() - 1 == a giant number
次1次,然后进行40亿次迭代,几乎会出现缓冲区溢出漏洞.
您需要这样写它:
for (size_t i = 0; i + 1 < v.size(); i++) { // do something }
因此,在这种情况下,它可以被"修复",但必须仔细考虑size_t
的无符号性质.有时,你不能应用上面的修正,因为你有一些变量偏移量,而不是一个常数,你想应用,这可能是正的,也可能是负的:所以你需要把它放在比较的哪一边取决于符号——现在代码变得很乱.
试图向下迭代到零并包含零的代码也存在类似的问题.类似于while (index-- > 0)
的值可以正常工作,但显然相当于while (--index >= 0)
的值永远不会因无符号值而终止.当右边为literal零时,编译器可能会发出警告,但如果它是在运行时确定的值,则肯定不会发出警告.
对位
有些人可能会争辩说,有符号的值也有两个不连续之处,那么为什么要挑无符号的呢?不同之处在于,这两个不连续性都非常(最大地)远离零.我真的认为这是一个单独的"溢出"问题,有符号和无符号的值都可能在非常大的值上溢出.在许多情况下,由于对值的可能范围的限制,溢出是不可能的,并且许多64位值的溢出在物理上可能是不可能的).即使可能,与"零"错误相比,与"零"错误相比,与溢出相关的错误的可能性通常微乎其微,而且是overflow occurs for unsigned values too.因此,UNSIGNED结合了这两种情况的最糟糕之处:具有非常大的幅值的潜在溢出,以及零值的不连续.签字人只有前者.
许多人会和未签名的人争论"你输了一点".这通常是正确的,但并不总是正确的(如果需要表示无符号值之间的差异,那么无论如何都会丢失该位:很多32位的东西都被限制为2 GiB,或者你会有一个奇怪的灰色区域,比如一个文件可以是4 GiB,但你不能在第二个2 GiB的部分使用某些API).
即使在unsigned买了你一点东西的情况下:它也买不了你多少东西:如果你必须支持20多亿个"东西",你可能很快就要支持40多亿个.
从逻辑上讲,无符号值是有符号值的子集
数学上,无符号值(非负整数)是有符号整数(简称_整数)的子集2.然而,仅仅对unsigned个值进行运算,比如减法运算,自然会弹出signed个值.我们可以说,在减法运算中,无符号值不是closed.有符号值的情况并非如此.
要在文件中查找两个无符号索引之间的"增量"吗?你最好按正确的顺序做减法,否则你会得到错误的答案.当然,您通常需要运行时判断来确定正确的顺序!当将无符号值作为数字处理时,您通常会发现(逻辑上)有符号值始终出现,因此您不妨从有符号开始.
对位
正如上面的脚注(2)中提到的,C++中的签名值实际上不是相同大小的未签名值的子集,因此未签名的值可以表示签名值可以相同的结果数.
没错,但是范围就没那么有用了.考虑减法和范围为0到2N的无符号数字,以及范围为-N到N的有符号数字.在这两种情况下,任意减法都会产生范围-2N到2N的结果,并且这两种类型的整数都只能表示其一半.事实证明,以-N到N的零为中心的区域通常比0到2N的范围更有用(在实际代码中包含更多的实际结果).考虑除均匀分布之外的任何典型分布(对数、zipfian、正态等),并考虑从该分布中减go 随机 Select 的值:以[-N,N]结尾的值远远多于以[0,2N]结尾的值(实际上,结果分布始终以零为中心).
64位关闭了使用无符号值作为数字的许多原因
我认为上面的论点对于32位值来说已经很有说服力了,但是溢出情况,在不同的阈值下影响有符号和无符号,对于32位值,do发生,因为"20亿"是一个可以被许多抽象和物理量(数十亿美元、数十亿纳秒、包含数十亿元素的数组)超越的数字.因此,如果有人对无符号值的正范围加倍感到足够信服,他们就可以证明溢出确实重要,而且它稍微有利于无符号值.
在专用域之外,64位值在很大程度上消除了这种担忧.有符号64位值的上限为9223372036854775807,大于9 quintillion.这需要很多纳秒(大约相当于292年)和很多钱.它也是一个比任何一台计算机都要大的数组,在一个连贯的地址空间中有很长时间的RAM.那么也许九千五百万对每个人来说都足够了(现在)?
何时使用无符号值
请注意,《样式指南》并不禁止甚至不鼓励使用无符号数字.它的结论是:
不要仅仅使用无符号类型来断言变量是非负的.
实际上,无符号变量有很好的用途:
当你不想把一个N位的量看作一个整数,而只是一个"比特包"的时候.例如,作为位掩码或位图,或N个布尔值或其他.这种用法通常与固定宽度类型(如uint32_t
和uint64_t
)同时使用,因为您经常想知道变量的确切大小.一个提示是,某个特定变量值得进行这种处理,即只使用bitwise个运算符(如~
、|
、&
、^
、>>
等)对其进行操作,而不使用算术运算(如+
、-
、*
、/
等).
Unsigned在这里是理想的,因为按位运算符的行为是定义良好且标准化的.有符号值有几个问题,例如移位时未定义和未指定的行为,以及未指定的表示.
当你真正想要模运算的时候.有时你真的需要2^N模运算.在这些情况下,"溢出"是一种特性,而不是bug.无符号值在这里给出了所需的值,因为它们被定义为使用模运算.有符号的值根本不能(轻松、高效地)使用,因为它们具有未指定的表示形式,并且溢出未定义.
0.5在我写完这篇文章后,我意识到这几乎等同于Jarod's example,这是我没有见过的-有充分的理由,这是一个很好的例子!
1我们在这里谈论的是size_t
,因此在32位系统上通常是2^32-1,在64位系统上通常是2^64-1.
2在C++中情况并非如此,因为无符号值在上端比相应的有符号类型包含更多的值,但存在的基本问题是,操作无符号值可能会导致(逻辑上)有符号值,但有符号值没有相应的问题(因为有符号值已经包括无符号值).