固定宽度整数和size_t
本节阅读量:在前面关于整数的课程中,我们提到C++仅保证整数变量具有最小大小——但它们的实际取值范围可能更大,具体取决于目标系统。
为什么整数变量的大小不固定?
简短的回答是,这可以追溯到C语言时代,当时计算机速度很慢,性能至关重要。C选择故意将整数的大小保持为开放状态,以便编译器实现者可以为int选择在目标计算机体系结构上性能最优的大小。
这不好吗?
按照现代标准来看,确实如此。作为程序员,不得不使用范围不确定的类型,这多少有些荒谬。
考虑int类型。int的最小大小是2个字节,但在现代架构上通常是4个字节。如果您假设int是4个字节(因为这是最常见的情况),那么您的程序可能会在int实际为2个字节的架构上出现异常行为(因为您可能把需要4个字节的值存储到了2字节的变量中,从而导致溢出或未定义行为)。如果假设int只有2个字节以确保最大兼容性,那么在int为4个字节的系统上,每个整数都会浪费2个字节,内存使用量翻倍!
固定宽度整数
为了解决上述问题,C99定义了一组固定宽度的整数(在stdint.h头文件中),这些整数在任何体系结构上都保证具有相同的大小。
这些定义如下:
| 类型 | 类别 | 范围 |
|---|---|---|
| std::int8_t | 一字节有符号 | -128 to 127 |
| std::uint8_t | 一字节无符号 | 0 to 255 |
| std::int16_t | 两字节有符号 | -32,768 to 32,767 |
| std::uint16_t | 两字节无符号 | 0 to 65,535 |
| std::int32_t | 四字节有符号 | -2,147,483,648 to 2,147,483,647 |
| std::uint32_t | 四字节无符号 | 0 to 4,294,967,295 |
| std::int64_t | 八字节有符号 | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
| std::uint64_t | 八字节无符号 | 0 to 18,446,744,073,709,551,615 |
C++在C++11中正式采用了这些固定宽度的整数。可以通过包含头文件来访问它们,它们定义在std命名空间中。下面是一个示例:
|
|
固定宽度整数有两个缺点。
首先,固定宽度整数并不保证在所有架构上都可用。某些系统可能不支持特定长度的整数,这会导致您的程序无法在此类架构上编译。不过,考虑到大多数现代架构已经标准化了8/16/32/64位变量,除非您的程序需要移植到一些特殊的大型机或嵌入式平台,否则这通常不会成为问题。
其次,固定宽度整数在某些架构上可能比更宽的类型运行得更慢。例如,如果您需要一个保证为32位的整数并选择使用std::int32_t,但您的CPU实际上处理64位整数时更快。当然,CPU能更快地处理某种类型,并不意味着您的程序总体上就会更快——现代程序通常受到内存速度的限制而非CPU的限制,较大的内存占用反而可能导致程序运行得比较慢。不进行实际测量的话,很难判断具体情况。
快速整数和至小整数
为了帮助解决上述缺点,C++还定义了两个可选整数集。
快速类型(std::int_fast#_t和std::uint_fast#_t)提供宽度至少为#位的最快有符号/无符号整数类型(其中#=8、16、32或64)。例如,std::int_fast32_t将为您提供至少为32位的最快有符号整数类型。这里所说的"最快",是指CPU能够最快处理的整数类型。
至小类型(std::int_least#_t和std::uint_least#_t)提供宽度至少为#位的最小有符号/无符号整数类型(其中#=8、16、32或64)。例如,std::uint_least32_t将为您提供至少为32位的最小无符号整数类型。
下面是在作者的Visual Studio(32位控制台应用程序)中的运行示例:
|
|
这产生了以下结果:
|
|
您可以看到,std::int_least16_t是16位,而std::int_fast16_t实际上是32位。这是因为在作者的机器上,32位整数的处理速度比16位整数快。
然而,这些快速类型和至小类型有各自的缺点:首先,很少有程序员真正使用它们,不够熟悉容易导致错误。其次,快速类型可能会造成内存浪费,因为它们的实际大小可能大于其名称所示的大小。最严重的是,由于快速/至小整数的大小可能因平台而异,您的程序可能在不同体系结构上表现出不同的行为。例如:
|
|
根据std::uint_fast16_t是16、32还是64位,此代码将产生不同的结果。
在对应架构上进行充分测试之前,很难知道您的程序在哪些地方可能无法按预期工作。
std::int8_t和std::uint8_t的行为可能类似于字符,而不是整数
由于C++规范中的疏忽,大多数编译器将std::int8_t和std::uint8_t(以及相应的快速和至小固定宽度类型)分别定义和处理为与有符号char和无符号char相同的类型。这意味着这些8位类型的行为可能与其他固定宽度类型不同,从而可能导致错误。这种行为依赖于具体系统,因此在一个体系结构上正确运行的程序可能无法在另一个体系结构上正确编译或运行。
在类型转换章节中,我们会介绍相关的例子。
在存储整数值时,通常最好避免使用std::int8_t和std::uint8_t(以及相关的快速和至小类型),而改用std::int16_t或std::uint16_t。
警告
8位固定宽度整数类型通常被视为字符而不是整数值(这可能因系统而异)。在大多数情况下,首选16位固定宽度整数类型。
最佳实践
鉴于上述提到的各种利弊,目前并没有关于使用固定宽度整数的统一最佳实践。
我们的立场是,正确性比速度更重要,编译时报错比运行时报错更好。因此,如果需要固定大小的整数类型,我们建议避免使用快速/至小类型,而使用固定宽度类型。
最佳做法
- 当整数的大小无关紧要时,首选int(例如,int的范围始终能容纳2字节有符号整数的范围)。例如,如果您要求用户输入年龄,或者从1数到10,那么int是16位还是32位并不重要(两种情况都能满足需求)。这将涵盖您可能遇到的绝大多数场景。
- 存储需要保证范围的数值时,首选std::int#_t。
- 当执行位操作或需要定义良好的环绕行为时,首选std::uint#_t。
尽可能避免以下情况:
- 使用无符号类型来表示物品的数量
- 8位固定宽度整数类型
- fast和least固定宽度类型
- 任何编译器特定的固定宽度整数——例如,Visual Studio定义__int8、__int16等…
什么是std::size_t?
考虑以下代码:
|
|
在作者的机器上,此命令打印:
|
|
很简单,对吧?我们可以推断sizeof操作符返回一个整数值——但这个返回值是什么整数类型呢?是int?还是short?答案是sizeof(以及许多返回大小或长度值的函数)返回的值类型为std::size_t。size_t被定义为无符号整数类型,通常用于表示对象的大小或长度。
有趣的是,我们可以使用sizeof操作符(返回std::size_t类型的值)来查询std::size_t本身的大小:
|
|
在作者的系统上编译为32位(4字节)控制台应用程序时,它打印:
|
|
std::size_t在许多不同的头文件中都有定义,其中<cstddef>是最推荐的头文件,因为它包含的其他标识符最少。
就像整数的大小可以因系统而异一样,std::size_t的大小也会变化。std::size_t保证是无符号的且至少16位宽,但在大多数系统上,它的宽度与应用程序的地址宽度相同。也就是说,对于32位应用程序,std::size_t通常是32位无符号整数;对于64位应用程序,std::size_t通常是64位无符号整数。
系统上可创建的最大对象的大小(以字节为单位)等于std::size_t能容纳的最大值。如果要创建更大的对象,sizeof将无法返回其大小,因为它会超出std::size_t的表示范围。
因此,任何大小(以字节为单位)超过std::size_t最大值的对象都被视为格式错误(会导致编译错误)。
例如,4字节无符号整数类型的范围是0到4294967295。假设std::size_t的大小为4个字节,这意味着在这样的系统上可创建的最大对象为4294967295个字节。
旁白
std::size_t的大小为对象的大小施加了严格的数学上限。在实践中,最大可创建对象的大小可能小于该值(甚至可能明显小于该值)。
一些编译器将最大可创建对象限制为std::size_t最大值的一半。
此外,其他因素也会产生影响,例如计算机有多少可用的连续内存可供分配。