4-5 无符号整数及其应避免使用的原因
无符号整数
在上一课(4.4——有符号整数)中,我们介绍了有符号整数,这组类型可存储正负整数(包括0)。
C++同样支持无符号整数。无符号整数Unsigned integers只能存储非负整数。
定义无符号整数
要定义无符号整数,我们使用 unsigned 关键字。按惯例,该关键字置于类型之前:
unsigned short us;
unsigned int ui;
unsigned long ul;
unsigned long long ull;
无符号整数范围
1字节无符号整数的取值范围为0至255。相比之下,1字节有符号整数的取值范围为-128至127。两者均可存储256种不同值,但有符号整数将半数范围用于表示负数,而无符号整数能存储两倍大的正数。
下表展示了无符号整数的取值范围:
| Size/Type | Range |
|---|---|
| 8 bit unsigned | 0 to 255 |
| 16 bit unsigned | 0 to 65,535 |
| 32 bit unsigned | 0 to 4,294,967,295 |
| 64 bit unsigned | 0 to 18,446,744,073,709,551,615 |
n位无符号变量的取值范围为0到(2n)-1。
当不需要表示负数时,无符号整数特别适合用于网络通信和内存有限的系统,因为它们能存储更多正数而不占用额外内存。
区分有符号与无符号的术语
新手程序员常会混淆有符号与无符号的概念。以下是区分二者的简易方法:为区分正负数,我们使用负号。若未标注符号,则默认该数为正数。因此,带符号的整数(有符号整数)能区分正负值。无符号整数(unsigned integer)则默认所有数值均为正数。
无符号整数溢出
如果尝试将数字280(需要9位表示)存储到1字节(8位)的无符号整数中会发生什么?答案是溢出。
作者注
奇怪的是,C++标准明确指出“涉及无符号操作数的计算永远不会发生溢出”。这与编程界的普遍共识相悖——整数溢出应同时涵盖有符号和无符号的使用场景(引用)。鉴于大多数程序员会将此视为溢出,我们将坚持称其为溢出,尽管C++标准对此持相反立场。
若无符号值超出范围,则将其除以该类型最大数值加1,仅保留余数。
数字280超出了我们1字节类型0至255的范围。该类型最大数值加1即为256。因此,280除以256得到余数24。最终存储的数值即为余数24。
另一种理解方式是:任何超出类型表示范围的数值都会“折返”(有时称为“模数折返”)。255在1字节整数的范围内,因此没有问题。但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;
}
你认为这个程序的结果会是什么?
(注:若尝试编译上述程序,编译器会发出关于溢出或截断的警告——需禁用“将警告视为错误”选项才能运行程序)

向相反方向溢出也是可能的。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;
}
(其实去掉-Werror就行,下面是个偷懒的做法)


上述代码在某些编译器中会触发警告,因为编译器检测到该整数常量超出了给定类型的范围。若仍需编译此代码,请暂时禁用“将警告视为错误”选项。
顺带一提……
电子游戏史上许多著名的漏洞都源于无符号整数的循环溢出行为。在街机游戏《大金刚》中,由于溢出漏洞导致玩家无法获得足够的奖励时间完成关卡,因此无法突破第22关。
在PC游戏《文明》中,甘地常被认为是最早使用核武器的领袖,这与其预期的温和性格相悖。玩家曾推测甘地的攻击性初始值为1,但若选择民主政体将获得-2攻击性修正值(即当前攻击值减2)。这会导致其攻击性溢出至255,使其攻击性达到最大值!不过近期游戏作者席德·梅尔澄清,实际情况并非如此。
关于无符号数的争议
许多开发者(以及谷歌等大型开发机构)认为开发者通常应避免使用无符号整数。
这主要源于两种可能引发问题的特性:
首先,带符号数值因远离零点,要意外溢出上下限需要一定操作。而无符号数值更容易发生下限溢出,因为其下限值为零,而多数数值都接近零点。
以两个无符号数(如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无法用无符号整数表示,因此会发生溢出,最终得到如下结果:

另一种常见的意外溢出发生在无符号整数被反复减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;
}
这也产生了以下结果:


在此情况下,若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;
}
这将输出:


该程序结构良好,能够编译通过,且从逻辑上看来完全一致。但它输出的结果却是错误的。虽然编译器在此情况下会提示有符号/无符号类型不匹配的警告,但它也会对其他不存在此问题的场景(例如两个数均为正数时)生成相同的警告,这使得实际问题难以被察觉。
相关内容
我们在第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)或其他处理器/内存受限环境开发,出于性能考量,无符号数的使用更为普遍且被广泛接受(某些情况下甚至不可避免)。

浙公网安备 33010602011771号