无符号整数
在上一课(4.4 -- 有符号整数)中,我们学习了有符号整数,它们是一组可以存储正负整数(包括 0)的类型。
C++ 也支持无符号整数。无符号整数是只能存储非负整数的整数。
定义无符号整数
要定义无符号整数,我们使用 unsigned 关键字。按照惯例,它放在类型之前
unsigned short us;
unsigned int ui;
unsigned long ul;
unsigned long long ull;
无符号整数范围
一个 1 字节无符号整数的范围是 0 到 255。与此相比,一个 1 字节有符号整数的范围是 -128 到 127。两者都可以存储 256 个不同的值,但有符号整数将其范围的一半用于负数,而无符号整数可以存储两倍大的正数。
下表显示了无符号整数的范围
大小/类型 | 范围 |
---|---|
8 位无符号 | 0 到 255 |
16 位无符号 | 0 到 65,535 |
32 位无符号 | 0 到 4,294,967,295 |
64 位无符号 | 0 到 18,446,744,073,709,551,615 |
一个 n 位无符号变量的范围是 0 到 (2n)-1。
当不需要负数时,无符号整数非常适合网络和内存较小的系统,因为无符号整数可以在不占用额外内存的情况下存储更多正数。
记住有符号和无符号的术语
新程序员有时会将有符号和无符号混淆。以下是记住区别的简单方法:为了区分负数和正数,我们使用负号。如果没有提供符号,我们假定数字是正数。因此,带有符号的整数(有符号整数)可以区分正数和负数。没有符号的整数(无符号整数)假定所有值都是正数。
无符号整数溢出
如果我们尝试将数字 280
(需要 9 位来表示)存储在一个 1 字节(8 位)无符号整数中会发生什么?答案是溢出。
作者注
奇怪的是,C++ 标准明确规定“涉及无符号操作数的计算永远不会溢出”。这与普遍的编程共识相反,即整数溢出涵盖有符号和无符号用例(引用)。鉴于大多数程序员会认为这是溢出,我们将它称为溢出,尽管 C++ 标准有相反的说法。
如果无符号值超出范围,它将被该类型最大值加一后除,只保留余数。
数字 280
太大,无法放入我们 0 到 255 的 1 字节范围内。该类型最大值加一为 256。因此,我们将 280 除以 256,得到 1 余 24。存储的是余数 24。
这是思考同一件事的另一种方式。任何大于该类型可表示的最大值的数字都会简单地“回绕”(有时称为“模数回绕”)。255
在 1 字节整数的范围内,所以 255
是可以的。然而,256
超出范围,所以它回绕到值 0
。257
回绕到值 1
。280
回绕到值 24
。
让我们用 2 字节短整数来看看这个
#include <iostream>
int main()
{
unsigned short x{ 65535 }; // largest 16-bit unsigned value possible
std::cout << "x was: " << x << '\n';
x = 65536; // 65536 is out of our range, so we get modulo wrap-around
std::cout << "x is now: " << x << '\n';
x = 65537; // 65537 is out of our range, so we get modulo wrap-around
std::cout << "x is now: " << x << '\n';
return 0;
}
你认为这个程序的结果会是什么?
(注意:如果你尝试编译上面的程序,你的编译器应该会发出溢出或截断的警告——你需要禁用“将警告视为错误”才能运行程序)
x was: 65535 x is now: 0 x is now: 1
也可以向另一个方向回绕。0 在 2 字节无符号整数中是可表示的,所以没问题。-1 不可表示,所以它回绕到范围的顶部,产生值 65535。-2 回绕到 65534。依此类推。
#include <iostream>
int main()
{
unsigned short x{ 0 }; // smallest 2-byte unsigned value possible
std::cout << "x was: " << x << '\n';
x = -1; // -1 is out of our range, so we get modulo wrap-around
std::cout << "x is now: " << x << '\n';
x = -2; // -2 is out of our range, so we get modulo wrap-around
std::cout << "x is now: " << x << '\n';
return 0;
}
x was: 0 x is now: 65535 x is now: 65534
上述代码在某些编译器中会触发警告,因为编译器检测到整数字面量超出了给定类型的范围。如果你仍然想编译代码,请暂时禁用“将警告视为错误”。
题外话…
许多著名的电子游戏漏洞都是由于无符号整数的回绕行为而发生的。在街机游戏《大金刚》中,由于溢出漏洞导致用户没有足够的奖励时间来完成关卡,因此无法通过第 22 关。
在 PC 游戏《文明》中,甘地以经常是第一个使用核武器而闻名,这似乎与他预期的被动本性相悖。玩家有一个理论,即甘地的攻击性设置最初设置为 1,但如果他选择民主政府,他会得到一个 -2 的攻击性修正(将其当前的攻击性值降低 2)。这将导致他的攻击性溢出到 255,使他具有最大攻击性!然而,最近 Sid Meier(游戏作者)澄清说事实并非如此。
关于无符号数字的争议
许多开发人员(以及一些大型开发公司,如 Google)认为开发人员通常应避免使用无符号整数。
这主要是因为两种可能导致问题的行为。
首先,对于有符号值,意外溢出范围的顶部或底部需要一些工作,因为这些值远离 0。对于无符号数,溢出范围底部要容易得多,因为范围底部是 0,这接近我们大多数值的位置。
考虑两个无符号数(例如 2 和 3)的减法
#include <iostream>
// assume int is 4 bytes
int main()
{
unsigned int x{ 2 };
unsigned int y{ 3 };
std::cout << x - y << '\n'; // prints 4294967295 (incorrect!)
return 0;
}
你和我知道 2 - 3
是 -1
,但 -1
不能表示为无符号整数,所以我们得到溢出和以下结果
4294967295
另一个常见的意外回绕发生在无符号整数重复递减 1,直到它尝试递减到一个负数。当引入循环时,你将看到一个示例。
其次,更阴险的是,当你混合使用有符号和无符号整数时,可能会导致意外行为。在 C++ 中,如果数学操作(例如算术或比较)有一个有符号整数和一个无符号整数,则有符号整数通常会转换为无符号整数。因此,结果将是无符号的。例如
#include <iostream>
// assume int is 4 bytes
int main()
{
unsigned int u{ 2 };
signed int s{ 3 };
std::cout << u - s << '\n'; // 2 - 3 = 4294967295
return 0;
}
这也产生结果
4294967295
在这种情况下,如果 u
是有符号的,则会产生正确的结果。但由于 u
是无符号的(这很容易被忽略),s
被转换为无符号,结果(-1
)被视为无符号值。由于 -1
不能存储在无符号值中,所以我们得到溢出和意外的答案。
这是另一个出现问题的例子
#include <iostream>
// assume int is 4 bytes
int main()
{
signed int s { -1 };
unsigned int u { 1 };
if (s < u) // -1 is implicitly converted to 4294967295, and 4294967295 < 1 is false
std::cout << "-1 is less than 1\n";
else
std::cout << "1 is less than -1\n"; // this statement executes
return 0;
}
这会打印
1 is less than -1
这个程序格式良好,可以编译,并且在逻辑上看起来一致。但是它打印了错误的答案。尽管你的编译器在这种情况下应该警告你符号/无符号不匹配,但你的编译器也会对没有此问题的其他情况(例如,当两个数字都是正数时)生成相同的警告,这使得很难检测何时存在实际问题。
相关内容
我们在课程 10.5 -- 算术转换 中介绍了要求某些二元操作的两个操作数具有相同类型的转换规则。
我们将在即将到来的课程 4.10 -- if 语句介绍 中介绍 if 语句。
此外,还有其他难以检测的问题情况。考虑以下内容
#include <iostream>
// assume int is 4 bytes
void doSomething(unsigned int x)
{
// Run some code x times
std::cout << "x is " << x << '\n';
}
int main()
{
doSomething(-1);
return 0;
}
doSomething() 的作者期望有人只用正数调用此函数。但调用者传入了 -1 ——显然是一个错误,但无论如何还是发生了。在这种情况下会发生什么?
有符号参数 -1
被隐式转换为无符号参数。-1
不在无符号数的范围内,所以它回绕到 4294967295。然后你的程序就会失控。
更成问题的是,这很难阻止。除非你已将编译器配置为积极生成有符号/无符号转换警告(你应该这样做),否则你的编译器可能甚至不会抱怨。
所有这些问题都是常见的问题,会产生意外行为,而且即使使用旨在检测问题情况的自动化工具也很难找到。
鉴于上述情况,我们将倡导的有些争议的最佳实践是,除了特定情况外,避免使用无符号类型。
最佳实践
对于存储数量(即使是非负数量)和数学运算,优先使用有符号数而不是无符号数。避免混合使用有符号和无符号数。
相关内容
支持上述建议的补充材料(也涵盖对一些常见反驳的驳斥)
- 交互式 C++ 面板(参见 9:48-13:08、41:06-45:26 和 1:02:50-1:03:15)
- 下标和大小应该是有符号的(来自 C++ 的创建者 Bjarne Stroustrup)
- 来自 libtorrent 博客的无符号整数
那么什么时候应该使用无符号数呢?
在 C++ 中,仍然有一些情况下可以使用/必须使用无符号数。
首先,在处理位操作时(在 O 章中介绍——这是一个大写字母“o”,不是“0”),首选无符号数。当需要明确定义的环绕行为时,它们也很有用(在某些算法中,如加密和随机数生成)。
其次,在某些情况下,无符号数的使用仍然是不可避免的,主要是与数组索引相关的情况。我们将在关于数组和数组索引的课程中详细讨论这一点。
另请注意,如果你正在为嵌入式系统(例如 Arduino)或其他处理器/内存受限环境开发,出于性能原因,使用无符号数更常见和被接受(在某些情况下,也是不可避免的)。