4.6 — 固定宽度整数和 size_t

在之前关于整数的课程中,我们提到 C++ 只保证整数变量具有最小大小——但它们可能会更大,具体取决于目标系统。

例如,一个 int 的最小大小为 16 位,但在现代架构上通常是 32 位。

如果你假设 int 是 32 位,因为这最可能,那么你的程序在 int 实际上是 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 位整数的范围(其范围为 -32,768 到 32,767)。在这种机器上,x = x + 1 将导致溢出,值 -32768 将存储在 x 中然后打印。

相反,如果你假设 int 只有 16 位以确保你的程序在所有架构上都能正常运行,那么你可以安全地存储在 int 中的值范围将受到显著限制。而在 int 实际上是 32 位的系统上,你没有利用为每个 int 分配的一半内存。

关键见解

在大多数情况下,我们一次只实例化少量 int 变量,并且这些变量通常在其创建的函数结束时被销毁。在这种情况下,每个变量浪费 2 字节内存并不是一个问题(有限的范围是更大的问题)。然而,在我们的程序分配数百万个 int 变量的情况下,每个变量浪费 2 字节内存可能会对程序的整体内存使用量产生显著影响。

为什么整数类型的大小不固定?

简短的回答是,这要追溯到 C 语言的早期,当时计算机速度很慢,性能是首要考虑的问题。C 语言选择有意地将整数的大小开放,以便编译器实现者可以选择一个在目标计算机架构上性能最佳的 int 大小。这样,程序员就可以直接使用 int,而不必担心他们是否可以使用更具性能的类型。

按照现代标准,各种整数类型缺乏一致的范围是很糟糕的(尤其是在一种旨在可移植的语言中)。

固定宽度整数

为了解决上述问题,C++11 提供了一组替代的整数类型,这些类型在任何架构上都保证具有相同的大小。由于这些整数的大小是固定的,因此它们被称为固定宽度整数

固定宽度整数在 <cstdint> 头文件中定义如下:

名称固定大小固定范围备注
std::int8_t1 字节有符号-128 到 127在许多系统上被视为有符号 char。参见下文注释。
std::uint8_t1 字节无符号0 到 255在许多系统上被视为无符号 char。参见下文注释。
std::int16_t2 字节有符号-32,768 到 32,767
std::uint16_t2 字节无符号0 到 65,535
std::int32_t4 字节有符号-2,147,483,648 到 2,147,483,647
std::uint32_t4 字节无符号0 到 4,294,967,295
std::int64_t8 字节有符号-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
std::uint64_t8 字节无符号0 到 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_tstd::uint8_t 通常表现得像字符

由于 C++ 规范中的一个疏忽,现代编译器通常将 std::int8_tstd::uint8_t(以及相应的快速和最小固定宽度类型,我们稍后将介绍)分别视为 signed charunsigned 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_tsigned char 的别名,因为它是唯一可用的 8 位有符号整数类型(boolchar 不被视为有符号整数类型)。在这种情况下,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++ 还定义了另外两组保证存在的整数。

快速类型(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 位控制台应用程序) 中的一个示例

#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

可以看到 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 位。

然而,这些快速和最小整数也有其自身的缺点。首先,没有多少程序员真正使用它们,缺乏熟悉可能会导致错误。其次,快速类型也可能导致内存浪费,因为它们的实际大小可能远大于其名称所示。

最严重的是,由于快速/最小整数的大小是实现定义的,因此你的程序在它们解析为不同大小的架构上可能会表现出不同的行为。例如

#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 位而产生不同的结果!这正是我们最初试图通过使用固定宽度整数来避免的问题!

最佳实践

避免使用快速和最小整数类型,因为它们在解析为不同大小的架构上可能会表现出不同的行为。

整数类型的最佳实践

鉴于基本整数类型、固定宽度整数类型、快速/最小整数类型以及有符号/无符号挑战的各种优缺点,关于整数最佳实践的共识很少。

我们的立场是,正确比快速更重要,编译时失败比运行时失败更好。因此,如果您需要一个具有保证范围的整数类型,我们建议避免使用快速/最小类型,而倾向于使用固定宽度类型。如果您后来发现需要支持某个特定的固定宽度整数类型无法编译的冷门平台,那么您可以在那时决定如何迁移您的程序(并彻底重新测试)。

最佳实践

  • 当整数大小无关紧要时(例如,数字将始终适合 2 字节有符号整数的范围),首选 int。例如,如果您要求用户输入他们的年龄,或者从 1 数到 10,那么 int 是 16 位还是 32 位都无关紧要(数字都能适合)。这涵盖了您可能遇到的大多数情况。
  • 在存储需要保证范围的数量时,首选 std::int#_t
  • 当需要进行位操作或定义明确的环绕行为(例如,用于密码学或随机数生成)时,首选 std::uint#_t

尽可能避免以下情况:

  • shortlong 整数(改用固定宽度整数类型)。
  • 快速和最小整数类型(改用固定宽度整数类型)。
  • 用于保存数量的无符号类型(改用有符号整数类型)。
  • 8 位固定宽度整数类型(改用 16 位固定宽度整数类型)。
  • 任何编译器特定的固定宽度整数(例如,Visual Studio 定义的 __int8, __int16 等…)

什么是 std::size_t?

考虑以下代码:

#include <iostream>

int main()
{
    std::cout << sizeof(int) << '\n';

    return 0;
}

在作者的机器上,这会打印出

4

很简单,对吧?我们可以推断 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 和类型别名中介绍 typedefs。

std::size_t 在许多不同的头文件中定义。如果您需要使用 std::size_t,<cstddef> 是最佳包含头文件,因为它包含的定义标识符最少。

例如

#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,请 #include 定义 std::size_t 的一个头文件(我们推荐 <cstddef>)。

使用 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

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 位时代,这很少是一个问题,因此通常不需要担心。

guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
549 评论
最新
最早 最多投票
内联反馈
查看所有评论