Google C++ Style Guide中,关于"无符号整数"的主题,有人建议

由于历史事故,C++标准还使用无符号整数来表示容器的大小——标准体的许多成员认为这是一个错误,但实际上不可能在这一点上进行修正.无符号算术不模拟简单整数的行为,而是由标准定义为模运算建模(围绕溢出/下溢),这意味着编译器无法诊断一类重要的错误.

模算术有什么问题?这难道不是无符号整数的预期行为吗?

本指南指的是哪种错误(一个重要的类)?虫子泛滥?

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

我可以考虑使用带符号整型而不是无符号整型的一个原因是,如果它确实溢出(变为负数),则更容易检测到.

推荐答案

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

根据我的经验,除了混合比较和升级规则之外,以下是无符号值成为bug磁铁的两个主要原因.

无符号值在零处不连续,这是编程中最常见的值

无符号整数和有符号整数的最小值和最大值都有discontinuities,它们会环绕(无符号)或导致未定义的行为(有符号).对于unsigned,这些分数分别为zeroUINT_MAX.int分为INT_MIN分和INT_MAX分.在具有4字节int值的系统上,INT_MININT_MAX的典型值是-2^312^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 number1次,然后进行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_tuint64_t)同时使用,因为您经常想知道变量的确切大小.一个提示是,某个特定变量值得进行这种处理,即只使用bitwise个运算符(如~|&^>>等)对其进行操作,而不使用算术运算(如+-*/等).

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

  • 当你真正想要模运算的时候.有时你真的需要2^N模运算.在这些情况下,"溢出"是一种特性,而不是bug.无符号值在这里给出了所需的值,因为它们被定义为使用模运算.有符号的值根本不能(轻松、高效地)使用,因为它们具有未指定的表示形式,并且溢出未定义.


0.5在我写完这篇文章后,我意识到这几乎等同于Jarod's example,这是我没有见过的-有充分的理由,这是一个很好的例子!

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

2在C++中情况并非如此,因为无符号值在上端比相应的有符号类型包含更多的值,但存在的基本问题是,操作无符号值可能会导致(逻辑上)有符号值,但有符号值没有相应的问题(因为有符号值已经包括无符号值).

C++相关问答推荐

带双指针的2D数组

C中空终止符后面的数字?

C strlen on char array

Ebpf内核代码:permission denied:invalid access to map value

丑陋的三重间接:可扩展的缓冲区管理 struct

无法用C++编译我的单元测试

模拟shell并运行.sh文件

使用AVX2的英特尔2022编译器的NaN问题&;/fp:FAST

C中的FREE函数正在触发断点

S在本文中的价值观到底出了什么问题?

将变量或参数打包到 struct /联合中是否会带来意想不到的性能损失?

C将数组传递给函数以修改数组

为什么Linux无法映射这个PT_LOAD ELF段?

为什么我的旧式&q;函数在传递浮点数时会打印2?

共享目标代码似乎不能在Linux上的进程之间共享

C 错误:对 int 数组使用 typedef 时出现不兼容的指针类型问题

为什么INT_MIN是在c语言的头文件limits.h中定义的(-INT_MAX-1)而不是直接使用-2147483648

如何使用 raylib 显示数组中的图像

定义 int a = 0, b = a++, c = a++;在 C 中定义了行为吗?

假设函数调用返回的 string(char *) 上有 free() 是否安全?