无符号整数,以及为什么要避免使用它们
本节阅读量:无符号整数
在上一课中,我们讨论了有符号整数,这是一组可以保存正整数和负整数(包括0)的类型。
C++还支持无符号整数。无符号整数是只能保存非负整数。
定义无符号整数
要定义无符号整数,我们使用unsigned关键字。按照惯例,它放在类型之前:
|
|
无符号整数范围
1字节无符号整数的范围为0到255。与1字节有符号整数的范围对比,两者都可以存储256个不同的值,但有符号整数将其范围的一半用于负数,所以无符号整数可以存储两倍大的正数。
下面的表格显示了无符号整数的范围:
类型大小 | 取值范围 |
---|---|
8位 | 0 to 255 |
16位 | 0 to 65,535 |
32位 | 0 to 4,294,967,295 |
64位 | 0 to 18,446,744,073,709,551,615 |
n位无符号变量的范围为0到(2^n)- 1。
当不需要负数时,无符号整数非常适合于网络传输或内存较少的系统,因为无符号整数可以存储更多的正数,而不会占用额外的内存。
无符号整数溢出
如果我们试图将数字280(需要9位来表示)存储在1字节(8位)无符号整数中,会发生什么情况?答案是溢出。
如果无符号值超出范围,则将其除以大于类型最大数的一,并仅保留余数。
数字280太大,无法容纳0到255的1字节范围。大于1的最大类型数为256。因此,我们将280除以256,得到1的余数24。24的其余部分是存储的内容。
这里有另一种思考同一件事的方法。大于类型所表示的最大数的任何数字都会“重新开始计数”(有时称为“模环绕”)。255在1字节整数的范围内,因此255可以。然而,256超出了范围,因此它回绕到值0。257绕到值1。280绕到值24。
让我们看一下使用2字节无符号整数的情况:
|
|
你认为这个程序的结果会是什么?
(注意:如果试图编译上述程序,编译器可能发出关于溢出或截断的警告——您需要禁用“将警告视为错误”才能运行该程序)
|
|
当然重新计数也可以换另一个方向。如果有一个2字节无符号整数0,那么-1将环绕到取值范围的最大值,产生值65535,-2折回至65534。以此类推。
|
|
上面的代码在某些编译器中触发警告,因为编译器检测到整数文本超出给定类型的范围。如果仍要编译代码,请暂时禁用“将警告视为错误”。
旁白
由于使用无符号整数的环绕行为,游戏开发历史中发生了许多显著的错误。在街机游戏Donkey Kong中,由于溢出漏洞,用户没有足够的奖励时间来完成关卡,因此不可能超过22级。
在PC游戏《文明》中,甘地经常是第一个使用核武器的人,这似乎与他预期的被动性格背道而驰。玩家们有一种理论,甘地的侵略特性最初设置为1,但如果他选择民主政府,他当前的侵略值会降低2。这将导致他的侵略性溢出到255,使他侵略达到最大!然而,最近Sid Meier(游戏的作者)澄清说,事实并非如此。
关于无符号数的争论
许多开发人员(以及一些大型开发公司,如Google)认为,开发人员通常应该避免使用无符号整数。
这在很大程度上是由于两种可能导致问题的行为。
首先,对于有符号的值,意外地溢出概率较低,因为最大值和最小值离0很远。对于无符号数字,从小于0溢出要容易得多,因为范围的底部是0,这接近于我们的大多数值所在的位置。
考虑两个无符号数字的减法,例如2和3:
|
|
我们都知道2-3是-1,但-1不能表示为无符号整数,因此我们得到溢出和以下结果:
|
|
当无符号整数重复递减1(使用–运算符)并小于0时,会发生另一种常见的不需要的重新计数。当学习循环时,您将看到这样的例子。
第二,更隐秘的是,当混合有符号整数和无符号整数时,可能会导致意外的行为。在C++中,如果数学运算(例如算术或比较)有一个有符号整数和一个无符号整数,则有符号整数通常会转换为无符号整数。因此,结果将是无符号的。例如:
|
|
在这种情况下,如果对u是有符号整数,将产生正确的结果。但由于u是无符号整数,s被转换为无符号的,并且结果(-1)是没有符号的,因此发生了溢出,得到了意外的答案。
下面是另一个示例:
|
|
该程序格式良好,可编译,并且逻辑上与肉眼看到第一印象一致。但它打印了错误的答案。在这种情况下,编译器应该警告您有符号/无符号不匹配,但编译器也会为没有出现此问题的其他情况生成相同的警告(例如,当两个数字都为正数时),这使得在出现实际问题时很难检测到。
此外,还有其他有问题的情况,难以检测。考虑以下内容:
|
|
doSomething() 的作者希望有人只使用正数调用此函数。但调用者传入了-1——这显然是一个错误。在这种情况下会发生什么?
-1的有符号参数隐式转换为无符号参数,-1不在无符号数字的范围内,因此它回绕到4294967295。然后你的程序就会失控。
更令人担忧的是,很难防止这种情况发生。除非您已经将编译器配置为积极主动地生成有符号/无符号转换警告(您应该这样做),否则编译器可能甚至不会有任何一点告警。
所有这些问题都是经常遇到的,产生意外的行为,并且很难找到,即使使用旨在检测对应问题的自动化工具也是如此。
鉴于上述情况,我们将提倡的有点争议的最佳实践是避免使用无符号类型,除非在特定情况下。
相关内容
我们将在后续章节介绍if语句。
最佳实践
对于保存物品的数量(即使应为非负的数字),使用有符号数字而不是无符号数字。避免混合有符号和无符号数字。
那么什么时候应该使用无符号数字呢?
在C++中,仍然有一些情况下可以/需要使用无符号数字。
首先,在处理位操作时,无符号数字是首选的。当需要处理良好的值环绕行为时,它们也很有用(在加密和随机数生成等某些算法中很有用)。
其次,在某些情况下,使用无符号数字仍然是不可避免的,主要是与数组索引有关的情况。我们将在关于数组和数组索引的课程中详细讨论这一点。
还要注意,如果您正在为嵌入式系统(例如Arduino)或其他处理器/内存有限的上下文开发,出于性能原因,使用无符号数字更为常见和接受(在某些情况下,不可避免)。
