4.3 — 对象大小和 sizeof 运算符

对象大小

正如你在 4.1 — 基本数据类型介绍 课程中学到的,现代机器上的内存通常以字节为单位组织,每个内存字节都有一个唯一的地址。到目前为止,将内存想象成一堆小隔间或邮箱,我们可以在其中存取信息,并将变量视为访问这些小隔间或邮箱的名称,这是很有用的。

然而,这种类比在某一点上并不完全正确——大多数对象实际上占用不止 1 字节的内存。单个对象可能使用 1、2、4、8 甚至更多的连续内存地址。对象使用的内存量取决于其数据类型。

因为我们通常通过变量名(而不是直接通过内存地址)访问内存,所以编译器能够向我们隐藏给定对象使用了多少字节的细节。当我们在源代码中访问某个变量 x 时,编译器知道需要检索多少字节的数据(基于变量 x 的类型),并将输出适当的机器语言代码来为我们处理这个细节。

即便如此,了解一个对象使用多少内存仍有几个有用的原因。

首先,对象使用的内存越多,它能存储的信息就越多。

一个位可以保存 2 个可能的值,0 或 1

位 0
0
1

2 位可以保存 4 个可能的值

位 0位 1
00
01
10
11

3 位可以保存 8 个可能的值

位 0位 1位 2
000
001
010
011
100
101
110
111

概括地说,一个具有 n 位(其中 n 是一个整数)的对象可以保存 2n(2 的 n 次方,通常也写作 2^n)个唯一值。因此,对于一个 8 位字节,一个字节大小的对象可以保存 28 (256) 个不同的值。一个使用 2 字节的对象可以保存 2^16 (65536) 个不同的值!

因此,对象的大小限制了它可以存储的唯一值的数量——使用更多字节的对象可以存储更多的唯一值。我们将在讨论更多关于整数时进一步探讨这一点。

其次,计算机的可用内存是有限的。每次我们定义一个对象,一小部分可用内存就会被占用,只要该对象存在。由于现代计算机内存充足,这种影响通常可以忽略不计。然而,对于需要大量对象或数据的程序(例如,渲染数百万个多边形的游戏),使用 1 字节对象和 8 字节对象之间的差异可能很大。

关键见解

新程序员常常过于关注优化代码以使用尽可能少的内存。在大多数情况下,这带来的差异微乎其微。应专注于编写可维护的代码,并且仅在收益显著时才进行优化。

基本数据类型大小

下一个显而易见的问题是“给定数据类型的对象占用多少内存?”。也许令人惊讶的是,C++ 标准并未定义任何基本类型的确切大小(以位为单位)。

相反,标准规定如下:

  • 一个对象必须至少占用 1 字节(以便每个对象都有一个不同的内存地址)。
  • 一个字节必须至少为 8 位。
  • 整数类型 charshortintlonglong long 的最小大小分别为 8、16、16、32 和 64 位。
  • charchar8_t 恰好是 1 字节(至少 8 位)。

命名法

当我们谈论类型的大小时,我们真正指的是该类型实例化对象的大小。

在本系列教程中,我们将通过一些对现代架构普遍适用的合理假设来提供一个简化的视图

  • 一个字节是 8 位。
  • 内存是字节可寻址的(我们可以独立访问内存的每个字节)。
  • 浮点支持符合 IEEE-754 标准。
  • 我们是在 32 位或 64 位架构上。

基于上述假设,我们可以合理地做出以下陈述

类别类型最小尺寸典型尺寸
布尔布尔型1 字节1 字节
字符字符1 字节(精确)1 字节
宽字符1 字节2 或 4 字节
char8_t1 字节1 字节
char16_t2 字节2 字节
char32_t4 字节4 字节
整型短整型2 字节2 字节
整型2 字节4 字节
长整型4 字节4 或 8 字节
长长整型8 字节8 字节
浮点浮点数4 字节4 字节
双精度浮点数8 字节8 字节
长双精度浮点数8 字节8、12 或 16 字节
指针std::nullptr_t4 字节4 或 8 字节

提示

为了最大程度的兼容性,你不应该假设对象大于指定的最小大小。

或者,如果你想假设某种类型具有非最小大小(例如,int 至少是 4 字节),你可以使用 static_assert,这样如果该代码在不满足此假设的架构上编译,编译器将报错。我们将在课程 9.6 — Assert 和 static_assert 中介绍如何做到这一点。

相关内容

你可以在此处找到更多关于 C++ 标准对各种类型最小大小的规定。

sizeof 运算符

为了确定特定机器上数据类型的大小,C++ 提供了一个名为 sizeof 的运算符。sizeof 运算符是一个一元运算符,它接受一个类型或一个变量,并返回该类型对象的大小(以字节为单位)。你可以编译并运行以下程序以了解某些数据类型的大小

#include <iomanip> // for std::setw (which sets the width of the subsequent output)
#include <iostream>
#include <climits> // for CHAR_BIT

int main()
{
    std::cout << "A byte is " << CHAR_BIT << " bits\n\n";

    std::cout << std::left; // left justify output

    std::cout << std::setw(16) << "bool:" << sizeof(bool) << " bytes\n";
    std::cout << std::setw(16) << "char:" << sizeof(char) << " bytes\n";
    std::cout << std::setw(16) << "short:" << sizeof(short) << " bytes\n";
    std::cout << std::setw(16) << "int:" << sizeof(int) << " bytes\n";
    std::cout << std::setw(16) << "long:" << sizeof(long) << " bytes\n";
    std::cout << std::setw(16) << "long long:" << sizeof(long long) << " bytes\n";
    std::cout << std::setw(16) << "float:" << sizeof(float) << " bytes\n";
    std::cout << std::setw(16) << "double:" << sizeof(double) << " bytes\n";
    std::cout << std::setw(16) << "long double:" << sizeof(long double) << " bytes\n";

    return 0;
}

这是作者机器的输出

bool:           1 bytes
char:           1 bytes
short:          2 bytes
int:            4 bytes
long:           4 bytes
long long:      8 bytes
float:          4 bytes
double:         8 bytes
long double:    8 bytes

你的结果可能因编译器、计算机架构、操作系统、编译设置(32 位与 64 位)等而异……

尝试对不完整类型(例如 void)使用 sizeof 将导致编译错误。

对于 gcc 用户

如果你没有禁用编译器扩展,gcc 允许 sizeof(void) 返回 1 而不是产生诊断(指针算术)。我们将在课程 0.10 — 配置你的编译器:编译器扩展 中展示如何禁用编译器扩展。

你也可以对变量名使用 sizeof 运算符

#include <iostream>

int main()
{
    int x{};
    std::cout << "x is " << sizeof(x) << " bytes\n";

    return 0;
}
x is 4 bytes

致进阶读者

sizeof 不包括对象使用的动态分配内存。我们将在未来的课程中讨论动态内存分配。

基本数据类型性能

在现代机器上,基本数据类型的对象速度很快,因此在使用或复制这些类型时,性能通常不应成为问题。

题外话…

你可能会认为占用内存较小的类型会比占用内存较多的类型更快。这并非总是如此。CPU 通常会针对处理特定大小的数据(例如 32 位)进行优化,与该大小匹配的类型可能会处理得更快。在这样的机器上,一个 32 位的 int 可能比一个 16 位的 short 或一个 8 位的 char 更快。

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