这里的一些答案提到了有符号和无符号值之间令人惊讶的提升规则,但这似乎更像是一个与混合有符号和无符号值有关的问题,并且不一定解释为什么在混合场景之外有符号变量比无符号变量更受欢迎。
根据我的经验,除了混合比较和提升规则之外,无符号值是错误磁铁的主要原因有两个,如下所示。
无符号值在零处不连续,这是编程中最常见的值
无符号和有符号整数在它们的最小值和最大值处都有不连续性,它们会环绕(无符号)或导致未定义的行为(有符号)。因为unsigned
这些点都在零和UINT_MAX
。因为int
它们在INT_MIN
和INT_MAX
。INT_MIN
和INT_MAX
在具有 4 字节值的系统上的典型int
值是-2^31
和2^31-1
,在这样的系统UINT_MAX
上通常是2^32-1
。
主要的导致错误的问题unsigned
不适用于int
它在零处的不连续性。当然,零是程序中非常常见的值,还有其他小的值,如 1、2、3。在各种构造中添加和减去小值是很常见的,尤其是 1,如果你从一个unsigned
值中减去任何东西并且它恰好为零,你就会得到一个巨大的正值和一个几乎肯定的错误。
考虑代码按索引遍历向量中的所有值,除了最后一个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)
永远不会因无符号值而终止。你的编译器可能会在右侧字面为零时警告你,但如果它是在运行时确定的值,肯定不会。
对位
有些人可能会争辩说有符号值也有两个不连续性,那么为什么要选择无符号值呢?不同之处在于两个不连续性都非常(最大)远离零。我真的认为这是一个单独的“溢出”问题,有符号和无符号值都可能在非常大的值处溢出。在许多情况下,由于值的可能范围受到限制,溢出是不可能的,并且许多 64 位值的溢出在物理上可能是不可能的)。即使可能,与“零”错误相比,与溢出相关的错误的可能性通常很小,并且无符号值也会发生溢出。所以 unsigned 结合了两全其美:可能会溢出非常大的幅度值,以及零处的不连续性。签名的只有前者。
许多人会与未签名争论“你失去了一点”。这通常是正确的 - 但并非总是如此(如果您需要表示无符号值之间的差异,无论如何您都会丢失该位:这么多 32 位的东西无论如何都限制为 2 GiB,或者您会有一个奇怪的灰色区域说一个文件可以是 4 GiB,但你不能在第二个 2 GiB 的一半上使用某些 API)。
即使在未签名的情况下对你有一点好处:它对你没有多大好处:如果你必须支持超过 20 亿个“东西”,你可能很快就会不得不支持超过 40 亿个。
从逻辑上讲,无符号值是有符号值的子集
在数学上,无符号值(非负整数)是有符号整数的子集(仅称为 _integers)。2 . 然而,有符号值自然会从仅针对无符号值的运算中弹出,例如减法。我们可能会说无符号值不会在减法下关闭。有符号值并非如此。
想在文件中找到两个无符号索引之间的“增量”吗?那么你最好按正确的顺序做减法,否则你会得到错误的答案。当然,您经常需要运行时检查来确定正确的顺序!在将无符号值作为数字处理时,您通常会发现(逻辑上)有符号值始终会出现,因此您不妨从有符号开始。
对位
如上面脚注 (2) 所述,C++ 中的有符号值实际上并不是相同大小的无符号值的子集,因此无符号值可以表示与有符号值相同数量的结果。
是的,但范围不太有用。考虑减法,范围为 0 到 2N 的无符号数,以及范围为 -N 到 N 的有符号数。在 _both 情况下,任意减法的结果都在 -2N 到 2N 范围内,并且任一类型的整数只能表示一半。事实证明,以 -N 到 N 的零为中心的区域通常比 0 到 2N 的范围更有用(在现实世界的代码中包含更多的实际结果)。考虑除均匀分布(log、zipfian、正态等)之外的任何典型分布,并考虑从该分布中减去随机选择的值:在 [-N, N] 中的值比在 [0, 2N] 中的值更多(实际上,结果分布总是以零为中心)。
64 位关闭了使用无符号值作为数字的许多原因
我认为上面的论点对于 32 位值已经很有说服力了,但是对于 32 位值,确实会发生在不同阈值下影响有符号和无符号的溢出情况,因为“20 亿”是一个可以被许多人超过的数字抽象和物理量(数十亿美元,数十亿纳秒,具有数十亿元素的阵列)。因此,如果有人对无符号值的正范围加倍足够确信,他们可以证明溢出确实很重要,并且它稍微有利于无符号。
在专用域之外,64 位值在很大程度上消除了这种担忧。带符号的 64 位值的上限为 9,223,372,036,854,775,807 - 超过 9 个quintillion。那是很多纳秒(大约值 292 年)和很多钱。它也是一个比任何计算机都更大的阵列,很可能在一个连贯的地址空间中长时间拥有 RAM。所以也许 9 quintillion 对每个人来说都足够了(现在)?
何时使用无符号值
请注意,样式指南并不禁止甚至不一定阻止使用无符号数字。它的结尾是:
不要仅仅使用无符号类型来断言变量是非负的。
事实上,无符号变量有很好的用途:
当您不想将 N 位数量视为整数,而只是将其视为“位袋”时。例如,作为位掩码或位图,或 N 个布尔值或其他。这种用法通常与固定宽度类型(例如uint32_t
,并且uint64_t
因为您经常想知道变量的确切大小)密切相关。一个特定变量值得这种处理的提示是,您只能使用按位运算符(如~
, |
, &
,^
等>>
)对其进行操作,而不使用算术运算(如+
, -
,*
等)对其进行操作/
。
无符号在这里是理想的,因为按位运算符的行为是明确定义和标准化的。有符号值有几个问题,例如移位时未定义和未指定的行为,以及未指定的表示。
当你真正想要模运算时。有时你实际上想要 2^N 模运算。在这些情况下,“溢出”是一个特性,而不是一个错误。无符号值在这里为您提供您想要的东西,因为它们被定义为使用模运算。有符号值根本不能(容易、有效地)使用,因为它们具有未指定的表示形式并且溢出是未定义的。
0.5写完这篇文章后,我意识到这与Jarod 的示例几乎相同,这是我从未见过的——而且有充分的理由,这是一个很好的示例!
1我们在这里谈论的size_t
通常是 32 位系统上的 2^32-1 或 64 位系统上的 2^64-1。
2 在 C++ 中,情况并非完全如此,因为无符号值在上端包含的值比相应的有符号类型多,但存在操作无符号值可能导致(逻辑上)有符号值的基本问题,但没有相应的问题有符号值(因为有符号值已经包含无符号值)。