当前位置: 首页 > 知识库问答 >
问题:

使用无符号而不是有符号的int更有可能导致错误吗?为什么?

毋举
2023-03-14

在Google C风格指南中,关于“无符号整数”的主题,建议

由于历史上的意外,C标准也使用无符号整数来表示容器的大小-标准机构的许多成员认为这是一个错误,但实际上在这一点上是不可能解决的。无符号算术不模拟简单整数的行为,而是由标准定义为模拟模块算术(在溢出/下溢时环绕),这意味着编译器无法诊断一类重要的错误。

模运算有什么问题?这不是无符号int的预期行为吗?

指南中提到了什么类型的bug(重要类)?溢出的错误?

不要仅使用无符号类型来断言变量是非负的。

我可以想到使用签名 int 而不是无符号 int 的一个原因是,如果它确实溢出(为负数),则更容易检测。

共有3个答案

景建业
2023-03-14

最令人毛骨悚然的错误示例之一是混合有符号和无符号值:

#include <iostream>
int main()  {
    auto qualifier = -1 < 1u ? "makes" : "does not make";
    std::cout << "The world " << qualifier << " sense" << std::endl;
}

输出:

这个世界没有意义

除非你有一个微不足道的应用程序,否则不可避免地会出现有符号值和无符号值之间的危险混合(导致运行时错误),或者如果你增加警告并使其成为编译时错误,你的代码中最终会有很多static_casts。这就是为什么最好严格使用有符号整数作为数学或逻辑比较的类型。只对位掩码和表示位的类型使用无符号。

根据数字值的预期域将类型建模为无符号是一个坏主意。大多数数字比20亿更接近0,因此对于无符号类型,您的许多值更接近有效范围的边缘。更糟糕的是,最终值可能在已知的正范围内,但是在计算表达式时,中间值可能会下限溢位,如果它们以中间形式使用,可能是非常错误的值。最后,即使你的值被期望总是正的,这并不意味着它们不会与其他可能是负的变量交互,所以你最终会被迫混合有符号和无符号类型,这是最糟糕的地方。

甘明朗
2023-03-14

如上所述,混合使用<code>无符号</code>和<code>有符号</code>可能会导致意外行为(即使定义良好)。

假设您想迭代vector的所有元素,除了最后五个,您可能会写错:

for (int i = 0; i < v.size() - 5; ++i) { foo(v[i]); } // Incorrect
// for (int i = 0; i + 5 < v.size(); ++i) { foo(v[i]); } // Correct

假设< code>v.size()

如果 v.size() 将返回有符号值,则 s.size() - 5 将为负数,在上述情况下,条件将立即为假。

另一方面,索引应该在[0;v.size()[之间,所以unsigned是有意义的。Signed也有自己的问题,因为UB有溢出或实现定义的行为,用于负数的右移,但迭代的bug源较少。

章增
2023-03-14

这里的一些答案提到了有符号值和无符号值之间令人惊讶的提升规则,但这似乎更像是一个与混合有符号值与无符号值有关的问题,并不一定解释为什么在混合场景之外,有符号变量会比无符号变量更受青睐。

根据我的经验,除了混合比较和提升规则之外,无符号值是错误磁铁的主要原因有两个,如下所示。

无符号整数和有符号整数在其最小值和最大值处都有不连续性,它们环绕(无符号)或导致未定义的行为(有符号)。对于无符号,这些点位于零和UINT_MAX。对于int,它们位于INT_MININT_MAX。在具有4字节int值的系统上,INT_MININT_MAX的典型值是-2^312^31-1,在这样的系统上UINT_MAX通常是2^32-1

不适用于int的主要错误是它在零处有不连续性。当然,零在程序中是一个非常常见的值,还有其他小值,如1,2,3。在各种结构中,添加和减去小值是很常见的,尤其是1,如果你从无符号值中减去任何东西,而它恰好是零,你就得到了一个巨大的正值和一个几乎肯定的错误。

考虑代码按索引遍历向量中除最后<sup>0.5</sup>之外的所有值:

for (size_t i = 0; i < v.size() - 1; i++) { // do something }

这工作正常,直到有一天你通过一个空的向量。而不是执行零迭代,你得到 v.size() - 1 == 一个巨大的数字1,你将进行 40 亿次迭代,几乎有一个缓冲区溢出漏洞。

你需要这样写:

for (size_t i = 0; i + 1 < v.size(); i++) { // do something }

所以在这种情况下,它可以被“修复”,但只能通过仔细考虑size_t的无符号性质。有时你不能应用上面的修复,因为你想要应用一些可变偏移,而不是一个常量偏移,它可能是正的或负的:所以你需要把它放在比较的哪一边取决于签名——现在代码变得非常混乱。

尝试向下迭代到并包含零的代码也有类似的问题。类似于while(index--

有些人可能会认为有符号的值也有两个不连续性,那么为什么要选择无符号的呢?区别在于,两个不连续性都非常(最大)远离零。我真的认为这是一个单独的“溢出”问题,有符号和无符号值都可能在非常大的值时溢出。在许多情况下,由于可能的值范围的限制,溢出是不可能的,而许多64位值的溢出在物理上可能是不可能)。即使可能,与“零”错误相比,发生与溢出相关的错误的可能性通常也很小,无符号值也会发生溢出。因此,unsigned结合了两个世界中最糟糕的情况:可能会溢出非常大的震级值,以及在零处出现不连续性。签名只有前者。

许多人会认为“你失去了一点”与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的范围更有用(在现实世界代码中包含更多实际结果)。考虑除均匀分布(对数、zipfian、正态等)以外的任何典型分布,并考虑从该分布中减去随机选择的值:最终出现在 [-N, N] 中的值多于 [0, 2N](实际上,生成的分布始终以零为中心)。

我认为上面的论点对于32位值已经很有说服力,但溢出情况在不同的阈值下影响有符号和无符号的值,因为“20亿”是一个可以被许多抽象和物理量(数十亿美元、数十亿纳秒、包含数十亿元素的数组)超过的数字。因此,如果有人对无符号值的正值范围加倍感到足够确信,他们可以认为溢出确实很重要,它稍微有利于无符号值。

在专业领域之外,64位值在很大程度上消除了这种担忧。有符号的64位值的上限为9,223,372,036,854,775,807 -超过九万亿分之一。那是很多纳秒(大约相当于292年),也是很多钱。它也是一个比任何计算机都要大的阵列,很长一段时间内,它都不可能在一个连贯的地址空间中拥有RAM。所以也许9兆对每个人来说都足够了(目前来说)?

请注意,样式指南并不禁止甚至不鼓励使用无符号数字。它的结论是:

不要仅使用无符号类型来断言变量是非负的。

事实上,无符号变量有很好的用途:

>

  • 当你想把一个N位的量当作一个整数,而仅仅是一个“位包”时。例如,作为位掩码或位图,或N个布尔值或其他。这种用法通常与固定宽度类型(如uint32_tuint64_t)密切相关,因为您经常想知道变量的确切大小。一个特殊变量值得这样处理的提示是,您只能使用位运算符对其进行操作,例如~|

    无符号在这里是理想的,因为按位运算符的行为是明确定义和标准化的。有符号的值有几个问题,例如移位时的未定义和未指定行为,以及未指定的表示。

    当你真正需要模运算时。有时你实际上想要2^N模运算。在这些情况下,“溢出”是一个特性,而不是一个错误。无符号值给你你想要的,因为它们被定义为使用模运算。有符号值根本不能(容易、有效地)使用,因为它们有一个未指定的表示,并且溢出是未定义的。

    0.5写完这篇文章后,我意识到这与Jarod的例子几乎相同,我没见过——而且有充分的理由,这是一个很好的例子!

    1 我们在这里谈论的是size_t,因此通常在 32 位系统上为 2^32-1,在 64 位系统上通常为 2^64-1。

    2 在C中,情况并非如此,因为无符号值在上限处包含的值比相应的有符号类型多,但存在一个基本问题,即操作无符号值会导致(逻辑上)有符号值,但有符号值没有相应的问题(因为有符号值已经包含无符号值)。

  •  类似资料:
    • 为什么在C 11中无符号短*无符号短转换为int? int太小,无法处理这行代码显示的最大值。 MinGW 4.9.2溢流 因为(来源) USHRT_MAX=65535 (2^16-1)或更大* INT_MAX=32767 (2^15-1)或更大* 和(2^16-1)*(2^16-1)=~2^32。 这个解决方案会有什么问题吗? 此程序 给出输出 在…上 两者都有 这证明在这些编译器上,被转换为。

    • 问题内容: 我正在修改继承的代码,并不断收到奇怪的“找不到符号”错误,这使我不知所措。 同行车: 公司简介: 我已经三重检查,但到目前为止找不到任何错误的代码。我正在Netbeans 7.0.1中构建它。我应该提到在构建时遇到此错误,但是我可以运行Web应用程序而没有任何问题(尚未)。但是我担心这种情况可能会重新出现在后面。 我只是在文件树中注意到,在CompanyDAO类上方是类似命名的文件,其

    • 未定义行为的一个例子是在flow上的整数行为 有没有一个历史的或者(甚至更好!)造成这种差异的技术原因是什么?

    • 我认为2补码的全部意义在于可以以相同的方式实现有符号和无符号数字的操作。维基百科甚至特别将乘法列为受益的操作之一。那么为什么x86对每个都有单独的说明,和?x86-64仍然如此吗?

    • 请解释以下关于“找不到符号”、“无法解析符号”或“找不到符号”的错误(在Java中): 这是什么意思 这个问题旨在产生一个全面的问题

    • 我的目标是了解两者的互补性。 有符号整数中的等于。但是,如果您使用十六进制的或十进制的按位与它,它将等于。 在我看来,这不应该发生,因为顶部字节与结果字节相同 我的预期结果是该值不应该改变,它等于< code>-121。 我的实际结果是值发生变化。 我的猜测是,是一个。因此,顶部字节的有符号位和下部字节的无符号位将导致无符号位。但是......那不可能是对的。