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;
}

image

最佳实践
当需要具有保证范围的整数类型时,请使用固定宽度的整数类型。


警告: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;
}

image

虽然你可能期望上述程序输出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位控制台应用程序)
image

可以看出,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;
}

image

这段代码会根据 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;
}

在作者的机器上,这段代码输出:

image

很简单,对吧?我们可以推断出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;
}

image

最佳实践
若在代码中显式使用 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字节)是:
image


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位时代,这已鲜少成为问题,因此通常无需为此担忧。

其他因素也可能产生影响,例如计算机可用于分配的连续内存空间大小。

posted @ 2026-02-14 06:43  游翔  阅读(1)  评论(0)    收藏  举报