4-6 定宽整数与size_t
在之前的整数课程中,我们提到C++仅保证整型变量的最小尺寸——但实际大小可能更大,具体取决于目标系统。
例如,int的最小尺寸为16位,但在现代架构中通常为32位。
若因32位数最常见就假设int为32位,那么在实际为16位架构的系统上程序很可能出现异常(因为你可能将需要32位存储空间的值存入仅有16位存储空间的变量,导致溢出或未定义行为)。
例如:
#include <iostream>
int main()
{
int x { 32767 }; // x may be 16-bits or 32-bits
x = x + 1; // 32768 overflows if int is 16-bits, okay if int is 32-bits
std::cout << x << '\n'; // what will this print?
return 0;
}
在 int 为 32 位的机器上,数值 32768 完全在 int 的取值范围内,因此可无障碍存储于变量 x 中。在此类机器上,该程序将输出 32768。然而在 int 为 16 位机的系统上,32768 超出 16 位整型数值范围(-32768 至 32767)。此时执行 x = x + 1 将导致溢出,x 将存储为 -32768 并输出该值。
反之,若为确保程序在所有架构上运行而假定 int 仅为 16 位,则 int 安全存储的数值范围将受到显著限制。而在 int 实际为 32 位的系统上,你将浪费每个 int 分配内存的一半空间。
关键洞察
在大多数情况下,我们每次仅实例化少量整型变量,且这些变量通常会在创建它们的函数结束时被销毁。此时,每个变量浪费2字节内存并不构成问题(有限的整数范围才是更大挑战)。然而,当程序需要分配数百万个整型变量时,每个变量浪费的2字节内存将对程序整体内存消耗产生显著影响。
为什么整数类型的大小不是固定的?
简而言之,这要追溯到C语言的早期阶段,当时计算机运行缓慢,性能是首要考量。C语言刻意将整数大小设为开放式,以便编译器实现者能根据目标计算机架构选择最优的int大小。这样程序员只需使用int类型,无需担心是否存在更高效的替代方案。
以现代标准衡量,各种整数类型缺乏统一的大小范围实在令人失望(尤其对于设计为可移植的语言而言)。
固定宽度整数
为解决上述问题,C++11提供了一组替代的整数类型,这些类型在任何架构上都保证具有相同大小。由于这些整数的大小是固定的,因此被称为固定宽度整数fixed-width integers。
固定宽度整数在
| Name | Fixed Size | Fixed Range | Notes |
|---|---|---|---|
| std::int8_t | 1 byte signed | -128 to 127 | Treated like a signed char on many systems. See note below. |
| std::uint8_t | 1 byte unsigned | 0 to 255 | Treated like an unsigned char on many systems. See note below. |
| std::int16_t | 2 byte signed | -32,768 to 32,767 | |
| std::uint16_t | 2 byte unsigned | 0 to 65,535 | |
| std::int32_t | 4 byte signed | -2,147,483,648 to 2,147,483,647 | |
| std::uint32_t | 4 byte unsigned | 0 to 4,294,967,295 | |
| std::int64_t | 8 byte signed | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 | |
| std::uint64_t | 8 byte unsigned | 0 to 18,446,744,073,709,551,615 |
以下是一个示例:
#include <cstdint> // for fixed-width integers
#include <iostream>
int main()
{
std::int32_t x { 32767 }; // x is always a 32-bit integer
x = x + 1; // so 32768 will always fit
std::cout << x << '\n';
return 0;
}

最佳实践
当需要具有保证范围的整数类型时,请使用固定宽度的整数类型。
警告:std::int8_t 和 std::uint8_t 通常表现得像 char 类型
由于C++规范中的疏漏,现代编译器通常将std::int8_t和std::uint8_t(以及稍后将介绍的对应快速类型和最小固定宽度类型)分别视为有符号char和无符号char。因此在多数现代系统中,8位固定宽度整数类型将表现得如同char类型。
作为一个快速预告:
#include <cstdint> // for fixed-width integers
#include <iostream>
int main()
{
std::int8_t x { 65 }; // initialize 8-bit integral type with value 65
std::cout << x << '\n'; // You're probably expecting this to print 65
return 0;
}

虽然你可能期望上述程序输出65,但它很可能不会这样做。
我们将在第4.12节——类型转换与static_cast简介中讨论该示例的实际输出结果(以及如何确保其始终输出65),该节内容紧接在第4.11节——字符(及其输出方式)之后。
警告
8位固定宽度整数类型常被视为字符而非整数值(且不同系统可能存在差异)。16位及更宽的整数类型不存在此问题。
对于高级读者
固定宽度整数实际上并未定义新类型——它们只是现有整数的类型的别名,用于指定所需的大小。对于每种固定宽度类型,具体实现(编译器和标准库)将决定其对应的现有类型别名。例如,在 int 为 32 位的平台上,std::int32_t 将成为 int 的别名。而在 int 为 16 位(long 为 32 位)的系统中,std::int32_t 将成为 long 的别名。
那么8位固定宽度类型如何处理?
多数情况下,std::int8_t会成为有符号char的别名,因为这是唯一可用的8位有符号整数类型(bool和char不被视为有符号整数类型)。此时在该平台上,std::int8_t的行为将完全等同于char。
但极少数情况下,若平台存在实现特有的8位有符号整数类型,实现方可能将std::int8_t设为该类型的别名。此时std::int8_t的行为将遵循该类型特性,其表现可能更接近int而非char。
std::uint8_t的行为原理与此类似。
其他固定宽度的缺点
固定宽度整数存在一些潜在缺点:
首先,固定宽度整数无法保证在所有架构上都定义。它们仅存在于具备与宽度匹配的基础整数类型,且遵循特定二进制表示法的系统中。若程序使用的固定宽度整型在某架构上不被支持,编译将失败。不过鉴于现代架构已标准化8/16/32/64位变量,除非程序需移植至特殊大型机或嵌入式架构,否则此问题罕见。
其次,在某些架构上使用固定宽度整型可能比宽型更耗时。例如当需要保证32位整型时,若选择使用std::int32_t,而实际CPU处理64位整型更高效。然而,CPU对特定类型的加速处理并不必然提升程序整体运行速度——现代程序常受限于内存使用而非CPU性能,更大的内存占用可能导致程序变慢,其程度甚至超过CPU加速带来的提升。若不实际测量,很难判断具体情况。
不过这些都只是些微不足道的小问题。
快速且最小的整数类型(可选)
为解决上述缺陷,C++还定义了两组替代整数类型,其存在性得到保证。
快速类型fast types(std::int_fast#_t 和 std::uint_fast#_t)提供宽度至少为 # 位(其中 # = 8、16、32 或 64)的最快有符号/无符号整数类型。例如,std::int_fast32_t 将提供至少 32 位的最快有符号整数类型。所谓最快,指的是 CPU 能够最快速处理的整数类型。
最小类型least types(std::int_least#_t 和 std::uint_least#_t)提供最小有符号/无符号整数类型,其宽度至少为 # 位(其中 # = 8、16、32 或 64)。例如,std::uint_least32_t 将提供最小且至少为 32 位的无符号整数类型。
以下是作者在 Visual Studio(32 位控制台应用程序)中的示例:
#include <cstdint> // for fast and least types
#include <iostream>
int main()
{
std::cout << "least 8: " << sizeof(std::int_least8_t) * 8 << " bits\n";
std::cout << "least 16: " << sizeof(std::int_least16_t) * 8 << " bits\n";
std::cout << "least 32: " << sizeof(std::int_least32_t) * 8 << " bits\n";
std::cout << '\n';
std::cout << "fast 8: " << sizeof(std::int_fast8_t) * 8 << " bits\n";
std::cout << "fast 16: " << sizeof(std::int_fast16_t) * 8 << " bits\n";
std::cout << "fast 32: " << sizeof(std::int_fast32_t) * 8 << " bits\n";
return 0;
}
这产生了以下结果:
least 8: 8 bits
least 16: 16 bits
least 32: 32 bits
fast 8: 8 bits
fast 16: 32 bits
fast 32: 32 bits
我:clang(64位控制台应用程序)
可以看出,std::int_least16_t 是 16 位类型,而 std::int_fast16_t 实际上是 32 位类型。这是因为在作者的机器上,32 位整数的处理速度比 16 位整数更快。
再举一例:假设我们处于仅支持16位和64位整数类型的架构环境。此时std::int32_t将不存在,而std::least_int32_t(及std::fast_int32_t)则会采用64位实现。
然而这些快速整型和最小的整数的类型integral types也存在缺陷。首先,实际使用它们的程序员不多,不熟悉可能导致错误。其次快速类型可能造成内存浪费,因其实际大小可能远大于名称暗示的值。
最严重的是,由于快速/最小整型的大小由实现定义,当它们在不同架构上解析为不同大小时,程序可能表现出不同行为。例如:
#include <cstdint>
#include <iostream>
int main()
{
std::uint_fast16_t sometype { 0 };
sometype = sometype - 1; // intentionally overflow to invoke wraparound behavior
std::cout << sometype << '\n';
return 0;
}

这段代码会根据 std::uint_fast16_t 的位数(16、32 或 64 位)产生不同的结果!这正是我们最初采用固定宽度整数试图避免的情况!
最佳实践
避免使用快速且最小的整数的类型integral types,因为在不同架构上它们可能表现出不同的行为,因为它们在不同架构上可能解析为不同的大小。
整数的类型的最佳实践
鉴于基础整数的类型、固定宽度整数的类型、快速/最小整数的类型以及有符号/无符号挑战各自存在诸多优缺点,目前尚未就整数最佳实践达成共识。
我们的立场是:正确性优于速度,编译时失败优于运行时失败。因此,若需使用具有保证范围的整数类型,建议避免使用快速/最小类型,而选择固定宽度类型。若后续发现需支持特定固定宽度整数的类型无法编译的冷门平台,届时再决定如何迁移程序(并彻底重新测试)。
最佳实践
- 当整数大小无关紧要时(例如数值始终在2字节有符号整数的范围内),优先使用int。例如,若要求用户输入年龄或进行1到10的计数,int是16位还是32位都无关紧要(数值都能容纳)。这将覆盖绝大多数常见场景。
- 存储需保证范围的量值时,优先使用 std::int#_t。
- 进行位操作或需要明确的溢出行为时(如加密或随机数生成),优先使用 std::uint#_t。
尽可能避免使用:
- 短整型和长整型(建议改用固定宽度整型)。
- 快速整型和最小整型(建议改用固定宽度整型)。
- 用于存储数值的无符号类型(建议改用有符号整型)。
- 8位固定宽度整数类型(建议改用16位固定宽度整数类型)。
- 任何编译器专属的固定宽度整数类型(例如Visual Studio定义的__int8、__int16等)。
什么是 std::size_t?
请看以下代码:
#include <iostream>
int main()
{
std::cout << sizeof(int) << '\n';
return 0;
}
在作者的机器上,这段代码输出:

很简单,对吧?我们可以推断出sizeof运算符返回一个整数值——但这个返回值属于哪种整数的类型?是int?还是short?答案是sizeof返回的值属于std::size_t类型。std::size_t是实现定义的无符号整数的类型的别名。换言之,编译器决定了 std::size_t 是 unsigned int、unsigned long、unsigned long long 等类型中的哪一种。
关键洞察
std::size_t 是实现定义的无符号整数的类型的别名。它在标准库中用于表示对象的字节大小或长度。
对于进阶读者
std::size_t 实际上是一个 typedef。我们在第 10.7 课——Typedefs与type aliases中会讲解 typedef。
std::size_t 在多个不同的头文件中定义。若需使用 std::size_t,建议包含
例如:
#include <cstddef> // for std::size_t
#include <iostream>
int main()
{
int x { 5 };
std::size_t s { sizeof(x) }; // sizeof returns a value of type std::size_t, so that should be the type of s
std::cout << s << '\n';
return 0;
}

最佳实践
若在代码中显式使用 std::size_t,请包含定义该类型的头文件(推荐使用)。
使用 sizeof 语法无需包含头文件(尽管其返回值类型为 std::size_t)。
与整型大小因系统而异类似,std::size_t 的大小同样存在差异。std::size_t 保证为无符号类型且至少为 16 位,但在多数系统中将等同于应用程序的地址宽度。也就是说,对于32位应用程序,std::size_t通常是32位无符号整数;对于64位应用程序,std::size_t通常是64位无符号整数。
sizeof 运算符返回类型为 std::size_t 的值(可选)
作者注
以下内容为可选阅读。理解后续内容并非关键。
有趣的是,我们可以使用 sizeof 运算符(其返回类型为 std::size_t)来查询 std::size_t 本身的大小:
#include <cstddef> // for std::size_t
#include <iostream>
int main()
{
std::cout << sizeof(std::size_t) << '\n';
return 0;
}
在作者的系统上编译为32位(4字节)控制台应用程序后,该程序输出:
4
我的64-bit(8字节)是:
std::size_t 对对象大小施加上限(可选)
sizeof 运算符必须能够将对象的字节大小作为 std::size_t 类型的值返回。因此,对象的字节大小不能超过 std::size_t 能够容纳的最大值。
C++20标准([basic.compound] 1.8.2)规定:“构造出其对象表示的字节数超过std::size_t类型可表示最大值(17.2)的类型属于非法构造。”
若允许创建更大对象,sizeof 将无法返回其字节大小,因为该值超出 std::size_t 的存储范围。因此,创建字节大小超过 std::size_t 最大值的对象是非法的(并将导致编译错误)。
例如,假设系统中 std::size_t 的大小为 4 字节。无符号 4 字节整型数据的取值范围为 0 至 4,294,967,295。因此,4字节的std::size_t对象可存储0至4,294,967,295之间的任意值。任何字节大小在0至4,294,967,295之间的对象,其大小均可通过std::size_t类型返回,这完全合理。然而,若对象的字节大小超过4,294,967,295字节,则sizeof将无法准确返回该对象的大小,因为该值已超出std::size_t的取值范围。因此,在该系统上无法创建大于4,294,967,295字节的对象。
顺带一提……
std::size_t 的大小对对象尺寸设定了严格的数学上限。实际中,可创建的最大对象可能小于该数值(甚至可能小得多)。某些编译器将最大可创建对象限制为 std::size_t 最大值的一半(相关解释可参见此处)。
当8位和16位应用程序成为主流时,这一限制对对象的大小造成了显著制约。而在32位和64位时代,这已鲜少成为问题,因此通常无需为此担忧。
其他因素也可能产生影响,例如计算机可用于分配的连续内存空间大小。



浙公网安备 33010602011771号