无符号整数,以及为什么要避免使用它们
本节阅读量:无符号整数
在上一课中,我们讨论了有符号整数,这是一组可以保存正整数和负整数(包括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位)的无符号整数中,会发生什么?答案是溢出。
如果无符号值超出范围,则会将其对(该类型最大值加1)取模,只保留余数。
数字280超出了1字节0到255的范围。该类型最大值加1等于256。因此,将280除以256,得到余数24。最终存储的就是24。
换一种方式理解:超过该类型最大值的数字会”从头开始计数”(有时称为”取模环绕”)。255在1字节整数的范围内,所以255没问题。但256超出了范围,因此它环绕回到0。257环绕到1。280环绕到24。
让我们看一个使用2字节无符号整数的例子:
|
|
你认为这个程序的结果会是什么?
(注意:如果尝试编译上述程序,编译器可能会发出关于溢出或截断的警告——你需要禁用”将警告视为错误”选项才能运行该程序)
|
|
环绕当然也可以向反方向发生。如果一个2字节无符号整数的值为0,那么-1会环绕到取值范围的最大值,即65535;-2环绕到65534,以此类推。
|
|
上面的代码可能会在某些编译器中触发警告,因为编译器检测到整数字面量超出了给定类型的范围。如果仍想编译代码,请暂时禁用”将警告视为错误”选项。
旁白
由于无符号整数的环绕行为,游戏开发历史上出现过许多著名的bug。在街机游戏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)或其他处理器/内存受限的环境开发程序,出于性能考虑,使用无符号数字更为常见和可接受(在某些情况下甚至不可避免)。