4.6 - 固定宽度整型和 size_t
Key Takeaway
- 从C语言继承而来的整型大小是不固定的,最小是2字节,但是更多时候是4字节。
- C99和C++11提供了固定宽度整型,通过头文件
<cstdint>
可以使用。缺点是并非所有机器上都有定义且可能会影响性能 - C++提供了速度优先和大小优先的两类固定宽度整型
- 避免使用8位固定宽度整型。如果一定要使用,请注意它们的行为更类似于字符。
- 整型使用最佳实践
- 如果整型的大小不重要(例如存放的数总是在2字节有符号整型范围内),推荐使用
int
。例如,如果你要求用户输入年龄,或者进行10以内的计数(此时2字节范围总是够用的)。那么使用int
就足够应对绝大多数场景了; - 如果需要存储数量值,且范围必须明确时,推荐使用
std::int#_t
; - 如果需要进行位运算或想要利用无符号数的翻转特性时,使用
std::uint#_t
。
- 如果整型的大小不重要(例如存放的数总是在2字节有符号整型范围内),推荐使用
- 避免:
- 使用无符号类型保存数量值;
- 使用 8 为固定宽度整型;
- 使用速度或大小优先的固定宽度类型;
- 任何与特定编译器相关的固定整型类型——例如 Visual Studio 定义的
__int8
,__int16
等。
std::size_t
是无符号整型,其宽度通常等于应用程序的位数,同时其能够表示的最大值也是系统能够创建对象大小的理论上限
上面的课程中我们介绍了整型,并且提到 C++ 只保证整型值占用空间的最小值——但是它占用的空间可以更大,这取决于具体的系统。
为什么整型的大小不是固定的?
简而言之,这还是从 C 语言继承而来的。在计算机非常慢的年代,性能是最需要被考虑的问题。C 语言选择故意不去固定整型的大小,以便编译器的实现者可以为目标计算机选择最合适的整型大小以提高性能。
很讨厌是吧?
从现代标准的角度来讲,是的。作为程序员还要处理类型范围不确定的情况确实有点荒唐。
考虑 int
类型,其占用内存最小为 2 字节,但是在现代计算机上却经常为 4 字节。如果你假设 int 是 4 个字节(确实 4 字节情况最有可能),那么你的程序在 int
实际只占两个字节的计算机上就会产生异常的行为(因为你很可能把一个需要 4 字节才能存放的值存放到了实际只有 2 个字节的变量中,这会导致溢出或未定义行为)。而如果你假设 int
只有两个字节以便获得最好的兼容性,那么在 4 字节系统上无疑会浪费两个字节,这会使得你的内存使用成倍增加!
固定宽度整型
为了解决这个问题,C99 定义了一组固定宽度整形(位于 stdint.h
)来确保整型在不同的计算机体系结构下都具有相同的大小。
名称 | 类型 | 范围 | 备注 |
---|---|---|---|
std::int8_t | 1 byte signed | -128 到 127 | 在大多数系统上被看做有符号字符处理,见下面备注。 |
std::uint8_t | 1 byte unsigned | 0 到 255 | 在大多数系统上被看做无符号字符处理,见下面备注 |
std::int16_t | 2 byte signed | -32,768 到 32,767 | |
std::uint16_t | 2 byte unsigned | 0 到 65,535 | |
std::int32_t | 4 byte signed | -2,147,483,648 到 2,147,483,647 | |
std::uint32_t | 4 byte unsigned | 0 到 4,294,967,295 | |
std::int64_t | 8 byte signed | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 | |
std::uint64_t | 8 byte unsigned | 0 到 18,446,744,073,709,551,615 |
C++ 在 C++11 中官方吸纳了这些固定宽度整型。通过包含 <cstdint>
头文件就可以使用它们,它们被定义在 std
命名空间中。
请看下面这个例子:
1 2 3 4 5 6 7 8 9 |
|
固定宽度整型通常来讲有两个缺陷。
首先,固定宽度整型不能保证在所有的体系结构中都被定义了。它只存在于, They only exist on systems where there are fundamental types matching their widths and following a certain binary representation.如果体系结构不支持固定宽度整型,则你的程序是无法编译的。不过,大多数的现代体系结构都以 8/16/32/64-位的变量为标准,因此除非你需要将程序移植到某个极其特殊的大型机或嵌入式系统上,否则一般没有问题。
其次,如果你使用了固定宽度整型,它的性能相较于使用更宽的类型可能或稍差(在同样的体系结构下)。例如,如果你需要一个大小确定为 32 位的整型,你可能会使用 std:: int32_t
,但是你的 CPU 可能在处理 64 位整形时更快。不过,即便 CPU 能够更快的处理某种给定的类型,也不一定意味着程序的运行速度更快——现代程序更多受限于内存使用而不是 CPU 处理速度。使用更大的内存足迹(memory footprint)带来的性能损失可能会超过 CPU 对其进行的加速。不经实际测试是很难进行对比的。
速度优先整型和尺寸优先整型
为了解决上述问题,C++还定义了另外两组整型,并确保它们总是具有定义。
速度优先的类类型(std::int_fast#_t
和 std::uint_fast#_t
) 提供了最快的有符号和无符号整型,同时其宽度最小为 #
位 ( #
= 8、16、32 或 64)。例如,std::int_fast32_t
可以定义宽度最小为 32 位且速度最快的有符号整型。
尺寸优先的类型(std::int_least#_t
和 std::uint_least#_t
) 提供了最小的有符号和无符号整型,其宽度为 #
位 (#
= 8、16、32 或 64)。例如,std::uint_least32_t
可以定义宽度至少为 32 位的最小的无符号整型。
以作者使用的 Visual Studio (32-bit 控制台程序)为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
输出结果如下:
1 2 3 4 5 6 7 |
|
可以看出 std::int_least16_t
是 16 位的,但是 std::int_fast16_t
实际上却是 32 位。这是因为在作者的机器上,32 位整型的处理速度要比 16 位块。
然而,这两种类型也有其缺点:首先,使用它们的程序员实际上并不多,不熟悉的情况使用它们会造成错误。另外,速度优先的类型同样也会带来和 4 比特宽整型一样的内存浪费问题。更严重的是,因为这两种整型类型的大小是变化的,那么可能会造成你的程序在不同体系结构的电脑上行为有所不同。例如:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
上述代码的输出结果取决于 std::uint_fast16_t
是 16 位、32 位还是 64 位。
除非能在对应的平台上进行测试,否则我们很难预知程序在什么平台上运行时会出问题,而对于开发者来说,我们能够使用的平台种类并不会很多。
std:: int8_t 和 std:: uint8_t 的行为可能更像字符而非整型
由于对C++标准研究不够仔细,很多编译器定义和对待 std::int8_t
和 std::uint8_t
类型(以及其对应的速度或大小优先的固定宽度类型) 的方式和 signed char
与unsigned char
是完全一样的。这也就意味着8位的类型可能(也可能不)与其他固定宽度类型具有不同的行为,这有可能造成错误。具体的行为和系统相关,所以在某个平台上正常工作的程序在其他平台上可能无法工作或编译。
我们会在4.12 - 类型转换和 static_cast中举例说明。
为了保持一致性,最好能够避免使用 _std::int8_t
和 std::uint8_t
(以及其相关的速度或大小优先类型) 。可以使用 std::int16_t
或 std::uint16_t
代替。
注意
避免使用8位固定宽度整型。如果一定要使用,请注意它们的行为更类似于字符。
整型类型选择的最佳实践
由于各种基础整型类型都存在各自的优缺点,如固定宽度整型类型,速度/大小优先的固定宽度整型类型以及有符号无符号整型。我们需要在使用时参考一些最佳实践。
我们的宗旨是,正确性优于速度,编译时发现错误优于运行时发现错误——基于这个指导原则,我们建议避免使用速度/大小优先的固定宽度整型类型,使用固定宽度整型类型。如果将来发现某个平台上固定宽度整型不能编译,那时你可以再决定是否要修改你的程序。
最佳实践
- 如果整型的大小不重要(例如存放的数总是在2字节有符号整型范围内),推荐使用
int
。例如,如果你要求用户输入年龄,或者进行10以内的计数(此时2字节范围总是够用的)。那么使用int
就足够应对绝大多数场景了; - 如果需要存储数量值,且范围必须明确时,推荐使用
std::int#_t
; - 如果需要进行位运算或想要利用无符号数的翻转特性时,使用
std::uint#_t
。
尽量避免:
- 使用无符号类型保存数量值;
- 使用 8 为固定宽度整型;
- 使用速度或大小优先的固定宽度类型;
- 任何与特定编译器相关的固定整型类型——例如 Visual Studio 定义的
__int8
,__int16
等。
std:: size_t 是什么?
考虑如下代码:
1 2 3 4 5 6 7 8 |
|
在作者的机器上打印结果如下:
1 |
|
很简单没错吧?我们知道sizeof
运算符肯定会返回一个整型数,但是这个整型数究竟是什么类型呢?是int
?还是short
?。 实际上sizeof
(以及其他返回长度或大小的函数)返回的返回值的类型为 std::size_t
。std::size_t
被定义为一个无符号整型,它通常被用来表示对象的大小或长度。
有趣的是,我们可以使用sizeof
运算符(返回类型为std:: size_t
)返回std:: size_t
本身的长度:
1 2 3 4 5 6 7 8 9 |
|
在32位(4字节)的控制台程序中,打印结果为:
1 |
|
和整型的大小可以改变一样, std::size_t
同样可能会改变。std::size_t
只保证它为无符号,并且最小为16位,但是在大多数系统中,它的实际大小通常等于应用程序的地址宽度。即对于32位应用程序,std::size_t
通常为 32 位无符号整型,而对于 64 位程序,size_t
通常为64位无符号整型。size_t
被定义为要足够大能以便能表示对应系统能够创建的最大的对象(以字节计)的尺寸。例如,如果 std::size_t
是4个字节宽,则该系统能够创建的最大的对象不可能超过 4,294,967,295 字节,因为这是4字节无符号整型能够存放的最大值。不过,这只是对象尺寸的理论上限,实际上限取决于你使用的编译器。
从定义上看,任何尺寸大于size_t
能够表示的值的对象,都被看做是错误的(会导致编译错),因为此时size_t
已经不能够正确表示该对象的大小(会发生无符号数翻转)。
题外话
有些编译器将最大可创建对象的大小限制为std::size_t
能够表示的最大值(有兴趣可以阅读这篇文章 ).
实际上,最大可创建对象的大小可能会小于(远小于)这个值,它取决于你的计算机有多少可用的连续内存。