无符号整数概述
在上一课(4.4 —— 有符号整数)中,我们介绍了有符号整数,这是一种可以存储正数、负数和零的整数类型。
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到(2^n - 1)。
当不需要负数时,无符号整数非常适合用于网络和内存较少的系统,因为无符号整数可以在不占用额外内存的情况下存储更多的正数。
记住有符号和无符号的术语
新程序员有时会混淆有符号和无符号。以下是一种简单的方法来记住它们的区别:为了区分负数和正数,我们使用负号。如果没有提供符号,我们假设数字是正数。因此,带有符号的整数(有符号整数)可以区分正数和负数。没有符号的整数(无符号整数)假设所有值都是正数。
无符号整数的问题
无符号整数溢出
如果我们试图将数字280(需要9位来表示)存储在一个1字节(8位)的无符号整数中,会发生什么?答案是溢出。
作者注
奇怪的是,C++标准明确指出"涉及无符号操作数的计算永远不会溢出"。这与整数溢出包括有符号和无符号两种情况的一般编程共识相悖(引用)。鉴于大多数程序员会认为这是溢出,尽管与C++标准的说法相反,我们仍将称之为溢出。
如果无符号值超出范围,它将被除以该类型的最大数字加1,并且只保留余数。
数字280太大,无法放入我们1字节的范围(0到255)中。该类型的最大数字加1是256。因此,我们将280除以256,得到1余24。余数24就是被存储的值。
思考这个问题的另一种方式是,任何大于该类型可表示的最大数字的数字都会简单地"环绕"(有时称为"模运算环绕")。255在1字节整数的范围内,因此255是可以的。然而,256超出了范围,因此它环绕到值0。257环绕到值1。280环绕到值24。
让我们用2字节的short类型来看看这个例子:
#include <iostream>
int main()
{
unsigned short x{ 65535 }; // 最大的16位无符号值
std::cout << "x was: " << x << '\n';
x = 65536; // 65536超出了我们的范围,因此我们得到模运算环绕
std::cout << "x is now: " << x << '\n';
x = 65537; // 65537超出了我们的范围,因此我们得到模运算环绕
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 }; // 最小的2字节无符号值
std::cout << "x was: " << x << '\n';
x = -1; // -1超出了我们的范围,因此我们得到模运算环绕
std::cout << "x is now: " << x << '\n';
x = -2; // -2超出了我们的范围,因此我们得到模运算环绕
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)澄清,实际上并非如此。
关于无符号数字的争议
许多开发人员(以及一些大型开发公司,例如谷歌)认为开发人员通常应该避免使用无符号整数。
这主要是因为两种可能导致问题的行为。
意外的溢出问题
首先,对于有符号值来说,要意外地超出范围的顶部或底部需要费点劲,因为这些值离0很远。而对于无符号数字来说,要超出范围的底部要容易得多,因为范围的底部是0,这与大多数值所在的位置很接近。
考虑两个无符号数字的减法,例如2和3:
#include <iostream>
// 假设int是4字节
int main()
{
unsigned int x{ 2 };
unsigned int y{ 3 };
std::cout << x - y << '\n'; // 打印4294967295(错误!)
return 0;
}
你和我都知道2 - 3等于-1,但-1无法表示为无符号整数,因此我们得到溢出和以下结果:
4294967295
另一种常见的不希望的环绕发生在无符号整数被反复减1,直到它试图减到一个负数。当你学习循环时,你会看到一个这样的例子。
类型转换陷阱
其次,更隐蔽的是,当混合使用有符号和无符号整数时,可能会产生意外的行为。在C++中,如果一个算术运算(例如算术或比较)有一个有符号整数和一个无符号整数,有符号整数通常会被转换为无符号整数。因此,结果也将是无符号的。例如:
#include <iostream>
// 假设int是4字节
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>
// 假设int是4字节
int main()
{
signed int s { -1 };
unsigned int u { 1 };
if (s < u) // -1被隐式转换为4294967295,而4294967295 < 1是false
std::cout << "-1 is less than 1\n";
else
std::cout << "1 is less than -1\n"; // 执行这条语句
return 0;
}
这将打印:
1 is less than -1
这个程序结构良好,可以编译,并且在视觉上逻辑一致。但它打印了错误的答案。尽管在这种情况下你的编译器应该会警告你有符号/无符号不匹配,但你的编译器也会为其他没有这个问题的情况(例如,当两个数字都是正数时)生成相同的警告,这使得很难检测到实际问题。
相关内容
我们在第10.5课 —— 算术转换中介绍了需要二元运算符的两个操作数为相同类型的转换规则。 我们在即将推出的第4.10课 —— if语句简介中介绍if语句。
函数参数的问题
此外,还有其他难以检测的问题情况。考虑以下代码:
#include <iostream>
// 假设int是4字节
void doSomething(unsigned int x)
{
// 运行一些代码x次
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)或其他受处理器/内存限制的上下文开发,由于性能原因,使用无符号数字更为常见和被接受(在某些情况下,不可避免)。