4 基本数据类型
4.1 位、字节和内存寻址
在之前我们了解到,变量是用于存储信息的一块内存的名称。计算机拥有可供程序使用的随机存取存储器 (RAM)。定义变量时,会从内存中预留一块空间用于该变量。
内存的最小单位是位(bit),可以保存 0 或 1 的值。内存被组织成连续的单元,称为内存地址。在现代计算机架构中,每个位并没有自己独有的内存地址。这是因为内存地址的数量有限,逐位访问数据的需求很少。相反,每个内存地址保存 1 个字节的数据。字节(byte)是一组以一个单位进行操作的位。现代标准是,一个字节由 8 个连续的位组成。在 C++ 中,我们通常处理“字节大小”的数据块。
4.2 数据类型
因为计算机上的所有数据都只是一串比特序列,所以我们使用数据类型(通常简称为类型)来告诉编译器如何以某种有意义的方式解释内存内容。例如当我们将变量声明为整数时,我们实际上是在告诉编译器“该变量使用的内存块将被解释为一个整数值”。
当你赋予一个对象一个值时,编译器和 CPU 会负责将值编码为该数据类型对应的位序列,然后将其存储在内存中(记住:内存只能存储位)。例如,如果你将值赋给一个整数对象65
,该值会被转换为位序列0100 0001
,并存储在分配给该对象的内存中。
4.2.1 基本数据类型
C++ 语言自带了许多预定义的数据类型供您使用。其中最基本的类型称为基础数据类型。
以下是基本数据类型的列表:
类型 | 类别 | 意义 | 示例 |
---|---|---|---|
float double long double | Floating Point | a number with a fractional part | 3.14159 |
bool | Integral (Boolean) | true or false | true |
char wchar_t char8_t (C++20) char16_t (C++11) char32_t (C++11) | Integral (Character) | a single character of text | ‘c’ |
short int int long int long long int (C++11) | Integral (Integer) | positive and negative whole numbers, including 0 | 64 |
std::nullptr_t (C++11) | Null Pointer | a null pointer | nullptr |
void | Void | no type | n/a |
整数与整数类型
在数学中,“整数”是指没有小数或小数部分的数字,包括负数、正数和零。“整数”一词有多种不同含义,但在 C++ 中,其含义为“像整数一样”。
C++标准定义整数为:
- 标准整数类型(standard integer type)为
short
、int
、long
和longlong
。(包括它们的有/无符号变体)。 - 整数类型(integeral type)包括
bool
、各种字符类型和标准整数类型。
所有整数类型在内存中都以整数值的形式存储,但只有标准整数类型在输出时才会显示为整数值。然而,通常情况下,“整数类型(integer type)”更常被用作“标准整数类型(standard integer type)”的简写。
其他类型集
C++ 包含三组类型。前两个是语言本身内置的(并且不需要包含标题即可使用):
- “基本数据类型”提供了最基本和最必要的数据类型。
- “复合数据类型”提供更复杂的数据类型,并允许创建自定义(用户定义)类型。
通常可以将它们视为一组类型。
第三组(也是最大的一组)类型由 C++ 标准库提供。由于标准库包含在所有 C++ 发行版中,因此这些类型广泛可用,并且已标准化以实现兼容性。使用标准库中的类型需要包含相应的头文件并在标准库中进行链接。
上面的基本类型表中明显遗漏了一种用于处理字符串(通常用于表示文本的字符序列)的数据类型。这是因为在现代 C++ 中,字符串是标准库的一部分。
_t后缀
在较新版本的 C++ 中,许多定义的类型(例如std::nullptr_t
)都使用 _t 后缀。这个后缀表示“类型”,它是现代类型的常用命名法。
4.2.2 void
Void 是所有数据类型中最容易解释的。简单来说,void 的意思是“无类型”。
Void 是我们第一个不完整类型的示例。不完整类型是指已声明但尚未定义的类型。编译器知道此类类型的存在,但没有足够的信息来确定为该类型的对象分配多少内存。Void是故意设计的不完整类型,因为它表示缺少类型,因此无法定义。
不完整类型无法被实例化:
void value; // won't work, variables can't be defined with incomplete type void
Void 通常用于几种不同的语境中:
- 不返回值的函数
- 不接受参数的函数(这可以在 C++ 中编译,但关键字void的这种用法在 C++ 中被认为是弃用的)
- void指针
4.3 对象大小和 sizeof 运算符
4.3.1 对象大小
大多数对象实际上占用的内存不止 1 个字节。一个对象可能使用 1、2、4、8 个甚至更多的连续内存地址。对象使用的内存量取决于其数据类型。
由于我们通常通过变量名(而不是直接通过内存地址)访问内存,因此编译器能够向我们隐藏给定对象占用多少字节的细节。当我们在源代码中访问某个变量时,编译器知道需要检索多少字节的数据(基于变量的类型),并输出相应的机器语言代码来为我们处理这些细节。
即便如此,了解对象使用了多少内存还是很有用的。
首先,对象使用的内存越多,它可以容纳的信息就越多。
单个位可以保存 2 个可能的值,即 0 或 1;2 位可以保存 4 种可能的值;3 位可以保存 8 个可能的值;
概括地说,一个具有nnn位(其中nnn为整数)的对象可以存储 2n2^n2n个不同的值。因此,一个字节大小的对象可以存储 282^828(256)个不同的值。
因此,对象的大小限制了它能存储的唯一值的数量——占用更多字节的对象可以存储更多的唯一值。
其次,计算机的可用内存是有限的。每次我们定义一个对象,只要该对象存在,就会占用其中一小部分可用内存。由于现代计算机拥有大量内存,这种影响通常可以忽略不计。然而,对于需要大量对象或数据的程序(例如,渲染数百万个多边形的游戏),使用 1 字节和 8 字节对象之间的差异可能非常显著。
新程序员常常过于注重优化代码,以尽可能少地使用内存。大多数情况下,这几乎不会带来什么改进。你应该专注于编写可维护的代码,并且只在能够带来实质性好处的地方进行优化。
4.3.2 基本数据类型的大小
C++ 标准并没有定义任何基本类型的确切大小(以位为单位)。
C++标准规定如下:
- 一个对象必须占用至少 1 个字节(以便每个对象都有一个不同的内存地址)。
- 一个字节至少必须有 8 位。
- 整数类型
char
、short
、int
、long
和long long
的最小大小分别为 8、16、16、32 和 64 位。 char
和char8_t
正好是 1 个字节(至少 8 位)。
因此当我们谈论类型的大小时,我们实际上指的是该类型的实例化对象的大小。
基于以上规定,我们默认:
类别 | 类型 | 最小大小 | 典型大小 |
---|---|---|---|
Boolean | bool | 1 byte | 1 byte |
Character | char wchar_t char8_t char8_t char32_t | 1 byte (exactly) 1 byte 1 byte 2 bytes 4 bytes | 1 byte (exactly) 2 or 4 bytes 1 byte 2 bytes 4 bytes |
Integral | short int long long long | 2 bytes 2 bytes 4 bytes 8 bytes | 2 bytes 4 bytes 4 or 8 bytes 8 bytes |
Floating point | float double long double | 4 bytes 8 bytes 8 bytes | 4 bytes 8 bytes 8, 12, or 16 bytes |
Pointer | std::nullptr_t | 4 bytes | 4 or 8 bytes |
为了实现最大的可移植性,通常不应该假设对象大于指定的最小尺寸。
4.3.3 sizeof
为了确定特定机器上数据类型的大小,C++ 提供了一个名为 的运算符sizeof
。sizeof运算符是一个一元运算符,它接受一个类型或一个变量作为参数,并返回该类型对象的大小(以字节为单位)。
注意当尝试在不完整的类型(例如void
)上使用sizeof
将导致编译错误。
sizeof
不包括对象使用的动态分配内存。
在现代机器上,基本数据类型的对象速度很快,因此使用或复制这些类型时的性能通常不是问题。您可能会认为占用内存较少的类型会比占用内存较多的类型更快。但这并非总是如此。CPU 通常针对处理特定大小(例如 32 位)的数据进行优化,因此与该大小匹配的类型可能会处理得更快。在这样的机器上,32 位int
可能比 16 位 short
或 8 位 char
更快。
4.4 有符号整数
integer是一种整数类型(integral type),可以表示正整数和负整数,包括0(例如 -2、-1、0、1、2)。C++ 有4 种主要的基本整数类型可供使用:
Type | Minimum Size | Note |
---|---|---|
short int | 16 bits | |
int | 16 bits | 现代C++架构上通常为 32 bits |
long int | 32 bits | |
long long int | 64 bits |
各种整数类型之间的主要区别在于它们的大小不同——较大的整数可以容纳更大的数字。
C++ 仅保证整数具有一定的最小大小,而不是一定具有特定的大小。
从技术上讲,bool
和char
类型被视为整型(因为这些类型将其值存储为整数值),但是为了避免混淆,下面我们将排除它们。
4.4.1 有符号整数
在日常生活中书写负数时,我们会使用负号。这种正数、负数或零的属性称为数字的符号(sign)。
默认情况下,C++ 中的整数是有符号的,这意味着数字的符号会作为值的一部分存储。因此,有符号整数可以存储正数、负数(以及 0)。
4.4.2 定义有符号整数
以下是定义四种有符号整数的首选方法:
short s; // prefer "short" instead of "short int"
int i;
long l; // prefer "long" instead of "long int"
long long ll; // prefer "long long" instead of "long long int"
虽然short int、long int或long long int都可以使用,但我们更倾向于使用这些类型的短名称(不使用int后缀)。添加int后缀不仅会增加输入量,还会使该类型更难与int类型的变量区分开来。如果不小心漏掉了 short 或long修饰符,可能会导致错误。
整数类型还可以采用可选的signed关键字,按照惯例,该关键字通常放在类型名称之前:
signed short ss;
signed int si;
signed long sl;
signed long long sll;
但是一般不使用此关键字,因为整数默认是有符号的。一般我们优先使用没有int
后缀或signed
前缀的简写类型。
4.4.3 有符号整数范围
我们将数据类型能够保存的具体值的集合称为范围。整数变量的范围由两个因素决定:其内存大小(以位为单位)以及是否有符号。
例如,8 位有符号整数的范围是 -128 到 127。这意味着 8 位有符号整数可以安全地存储 -128 到 127(含)之间的任何整数值。n 位有符号变量的范围是 −2(n−1)-2^{(n-1)}−2(n−1) 到 2(n−1)−12^{(n-1)}-12(n−1)−1 。
下面是一个包含不同大小的有符号整数范围的表:
尺寸/类型 | 范围 |
---|---|
8位有符号 | -128 至 127 |
16位有符号 | -32,768 到 32,767 |
32位有符号 | -2,147,483,648 到 2,147,483,647 |
64 位有符号 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
以上范围假设采用“二进制补码”表示法。这种表示法是现代体系结构的事实标准(因为它更容易在硬件中实现),并且现在已成为 C++20 标准的要求。
4.4.4 溢出
如果我们尝试将值140赋给一个 8 位有符号整数,会发生什么?这个数字超出了 8 位有符号整数所能容纳的范围。数字 140 需要 9 位来表示(8 个数值位和 1 个符号位),但 8 位有符号整数只有 8 位可用(7 个数值位和 1 个符号位)。
C++20 标准做出了这样的概括性声明:“如果在表达式求值过程中,结果在数学上没有定义,或者不在其类型可表示的值范围内,则该行为未定义”。通俗地说,这叫做溢出。因此,将值 140 分配给 8 位有符号整数将导致未定义的行为。
如果算术运算(例如加法或乘法)试图创建超出其可表示范围的值,则称为整数溢出(或算术溢出)。对于有符号整数,整数溢出将导致未定义的行为。
4.4.5 整数除法
当一个除数和被除数均为整数的除法表达式的商为整数时,结果正常:
#include <iostream>
int main()
{
std::cout << 20 / 4 << '\n';
return 0;
}
结果:
5
但是当整数除法的商为小数时,结果会直接舍去小数部分,且并不是四舍五入:
#include <iostream>
int main()
{
std::cout << 8 / 5 << '\n';
return 0;
}
结果:
1
使用整数除法时要小心,因为商的小数部分会被丢失。但是,如果您需要整数除法,那么使用整数除法是安全的,因为结果是可预测的。
4.5 无符号整数
C++ 也支持无符号整数。无符号整数是指只能存储非负整数。
4.5.1 定义无符号整数
要定义无符号整数,我们要在类型之前使用unsigned关键字:
unsigned short us;
unsigned int ui;
unsigned long ul;
unsigned long long ull;
4.5.2 无符号整数范围
1 字节无符号整数的范围是 0 到 255。与 1 字节有符号整数的范围 -128 到 127 相比,两者都可以存储 256 个不同的值,但是有符号整数使用其范围的一半来表示负数,而无符号整数可以存储两倍的正数。
n 位无符号变量的范围是 000 到 2n−12^n-12n−1。
4.5.3 无符号整数溢出
如果我们尝试将这个数字280
(需要 9 位来表示)存储在一个 1 字节(8 位)无符号整数中,会发生什么?答案是溢出。奇怪的是,C++ 标准明确规定“涉及无符号操作数的计算永远不会溢出”。这与普遍的编程共识相悖,即整数溢出既包含有符号用例,也包含无符号用例。考虑到大多数程序员都会考虑这种溢出,尽管 C++ 标准声明并非如此,我们还是将其称为溢出。
如果无符号值超出范围,则将其除以比该类型最大数字大一的数字,并且只保留余数。
这个数字280
太大,无法放入 0 到 255 的 1 字节范围中。比该类型的最大数字大 1 是 256。因此,我们将 280 除以 256,得到 1 余数 24。余数24就是存储的数字。
也可以从另一个方向绕行。0 可以用 2 字节无符号整数表示,所以没问题。-1 无法表示,因此它会绕回到范围的顶部,产生值 255。电子游戏史上许多著名的 bug 都是由于无符号整数的回绕行为造成的。
4.5.4 无符号数的争议
许多开发人员认为应该避免使用无符号整数。这主要是因为两种行为可能会导致问题。
首先,对于有符号值,需要做一些工作才能意外溢出范围的顶部或底部,因为这些值远离 0。对于无符号数,溢出范围的底部要容易得多,因为范围的底部是 0,这接近我们的大多数值所在的位置。
其次,更隐蔽的是,混合使用有符号整数和无符号整数时,可能会导致意外行为。在 C++ 中,如果一个数学运算(例如算术或比较)涉及一个有符号整数和一个无符号整数,则有符号整数通常会转换为无符号整数。因此,结果将是无符号的。例如:
#include <iostream>
// assume int is 4 bytes
int main()
{
unsigned int u{ 2 };
signed int s{ 3 };
std::cout << u - s << '\n'; // 2 - 3 = 4294967295
return 0;
}
这也会产生以下结果:
4294967295
所有这些问题都是常见的,会产生意外行为,并且很难发现,即使使用旨在检测问题案例的自动化工具也很难发现。
鉴于上述情况,我们提倡的有争议的最佳实践是,除非在特定情况下,否则避免使用无符号类型。
4.5.5 什么时候使用无符号数
在 C++ 中仍有少数情况需要使用无符号数字。
首先,在进行位操作时,无符号数是首选。当需要明确定义的回绕行为时,它们也很有用。其次,在某些情况下,使用无符号数仍然是不可避免的,尤其是在数组索引方面。
还要注意,如果您正在为嵌入式系统(例如 Arduino)或其他处理器/内存有限的环境进行开发,出于性能原因,使用无符号数字会更常见和被接受。
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
中。然而,在int
是16位的机器上,该值不在 16 位整数的范围内(范围为 -32,768 到 32,767)。在这样的机器上,将导致溢出,变量x
将被赋值-32768
。
相反,如果你为了确保程序在所有架构上都能正常运行而假设int
仅为 16 位,那么可以安全存储在int
中的值的范围就会受到很大限制。而且,在int
实际上是 32 位的系统上,你只能利用每个int
分配的内存的一半。
大多数情况下,我们一次只实例化少量int
变量,这些变量通常在创建它们的函数结束时被销毁。在这种情况下,每个变量浪费 2 个字节的内存并不是什么问题(有限的变量范围才是更大的问题)。然而,如果我们的程序分配了数百万个int
变量,那么每个变量浪费 2 个字节的内存可能会对程序的整体内存使用情况产生重大影响。
4.6.1 为什么不固定整数类型的大小
这可以追溯到 C 语言的早期,当时计算机运行缓慢,性能是人们最关心的问题。C 语言故意保留整数的大小,以便编译器实现者能够选择在目标计算机架构上性能最佳的int
大小。按照现代标准,各种整数类型缺乏一致的范围,这很糟糕。
4.6.2 固定宽度整数
为了解决上述问题,C++11 提供了一组替代的整数类型,这些整数类型保证在任何体系结构上具有相同的大小。由于这些整数的大小是固定的,因此它们被称为定宽整数。
固定宽度整数定义(在“cstdint”头文件中)如下:
Name | Fixed Size | Fixed Range | Notes |
---|---|---|---|
std::int8_t | 1 byte signed | -128 to 127 | 在许多系统上,它被视为有符号。 |
std::uint8_t | 1 byte unsigned | 0 to 255 | 在许多系统上被视为无符号。 |
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 |
当您需要具有保证范围的整数类型时,请使用定宽。
std::int8_t
与std::uint8_t
通常表现出和字符一样的特征
由于 C++ 规范中的一个疏忽,现代编译器通常将std::int8_t
和std::uint8_t
(以及相应的快速和最小定宽类型)分别视为signed char
和unsigned 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
,但它很可能不会如此。
固定宽度整数实际上并没有定义新的类型——它们只是具有所需大小的现有整数类型的别名。对于每种固定宽度类型,实现(编译器和标准库)会确定哪个现有类型是别名。例如,在int
为32 位的平台上,std::int32_t
将是int
的别名。而在int
为16位的系统中(long
是32位),std::int32_t
是long
的别名。
由此可见,std::int8_t
表现地和singed char
相似的原因就是在大多数系统中,signed char
是唯一可用的8位有符号整数类型,在这种情况下std::int8_t
将是singed char
的别名。
4.6.3 其他固定宽度的缺点
定宽整数有一些潜在的缺点:
首先,固定宽度整数并不保证在所有架构上都定义。它们仅存在于存在与其宽度匹配且遵循特定二进制表示的基本整数类型的系统上。您的程序将在任何不支持您程序所使用的固定宽度整数的架构上编译失败。但是,鉴于现代架构已经标准化了 8/16/32/64 位变量,除非您的程序需要移植到某些特殊的大型机或嵌入式架构上,否则这不太可能成为问题。
其次,如果您使用定宽整数,在某些架构上,它可能比更宽的类型更慢。例如,如果您需要一个保证 32 位的整数,您可能会选择使用std::int32_t
,但您的 CPU 在处理 64 位整数时实际上可能更快。然而,仅仅因为您的 CPU 可以更快地处理给定类型并不意味着您的程序整体上会更快——现代程序通常受内存使用量而非 CPU 的限制。如果不对内存和CPU性能进行实际衡量,很难知道具体情况。
4.6.4 快速和最小整数类型
为了帮助解决上述缺点,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 位整数处理速度更快。
然而,这些快速且最小的整数也有其自身的缺点。首先,实际使用它们的程序员并不多,缺乏熟悉可能会导致错误。其次,快速类型还可能导致内存浪费,因为它们的实际大小可能比其名称指示的要大得多。
更严重的是,由于快速/最小整数的大小是由实现定义的,你的程序在不同架构上解析不同大小的整数时可能会表现出不同的行为。例如:
#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 位产生不同的结果!这正是我们最初使用固定宽度整数时试图避免的!
4.6.5 整数类型的最佳实践
鉴于基本整数类型、固定宽度整数类型、快速/最小整数类型以及有符号/无符号挑战的各种优缺点,对于整数类型的最佳实践几乎没有达成共识。
我们的原则是,正确比快速更重要,编译时失败比运行时失败更好。
因此,如需要具有保证范围的整数类型,建议避免使用快速/最小类型,而选择固定宽度类型。如果之后发现需要支持某个特定固定宽度整数类型无法编译的复杂平台,那么可以在那时决定如何迁移程序(并彻底重新测试)。
- 当整数的大小无关紧要时使用
int
- 存储需要保证范围的数量时优先使用
std::int#_t
- 当进行位操作或需要明确定义的环绕行为时使用
std::uint#_t
尽可能避免:
short
和long
整数(替换为对应的定宽整数类型)- 快速和最少整数类型
- 用于保存数量的无符号类型
- 8 位固定宽度整数类型(替换为16位固宽整数类型)
- 任何特定于编译器的固定宽度整数
4.6.6 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
是实现定义的无符号整型的别名,实际上是一个 typedef。它在标准库中用于表示对象的字节大小或长度。
std::size_t
在多个不同的头文件中定义。如果需要使用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 位无符号整数。
4.7 科学计数法
我们使用字母“e”(有时也用“E”)来表示等式中“乘以 10 的次方”部分。例如,1.2 x 10⁴
可以写成1.2e4
。对于小于1的数,指数可以为负数。该数5e-2
等于5 × 10⁻²
。
4.7.1 有效数字
用科学计数法表示,我们写3.14
为3.14e0
。由于有效数字有 3 个,因此该数有 3 个有效数字。
4.7.2 如何将十进制数转换为科学计数法
将小数点向左或向右滑动,使得小数点左边只有一个非零数字。
- 小数点每向左移动一位,指数就会增加 1。
- 小数点每向右滑动一位,指数就会减少 1。
去掉所有前导零(在有效数字的左端)。
仅当原始数字没有小数点时,才需要去掉尾随零(位于有效数字的右端)。
以 0.0078900 开头
将小数点向右滑动 3 位:0007.8900e-3
修剪前导零:7.8900e-3
不修剪尾随零:7.8900e-3(5 位有效数字)
以 42030 开头(没有信息表明尾随零是重要的)
将小数点向左滑动 4 位:4.2030e4
没有可修剪的前导零:4.2030e4
修剪尾随零:4.203e4(4 位有效数字)
以 42030 开头(假设尾随零是有效数字)
将小数点向左滑动 4 位:4.2030e4
没有可修剪的前导零:4.2030e4
保留尾随零:4.2030e4(5 位有效数字)
4.7.3 处理尾随零
对于没有小数点的数字,尾随零默认被视为无意义的。
然而,如果我们碰巧知道这个数字是经过精确测量的(或者实际数字介于 2099.5 和 2100.5 之间),那么我们应该将这些零视为有效数字。
4.8 浮点数
C++ 有三种基本浮点数据类型:单精度float
、双精度double
和扩展精度long double
。与整数一样,C++ 没有定义这些类型的实际大小。
Category | C++ Type | Typical Size |
---|---|---|
floating point | float | 4 bytes |
double | 8 bytes | |
long double | 8, 12, or 16 bytes |
4.8.1 浮点字面量
使用浮点字面量时,务必保留至少一个小数位(即使小数点后的数字为 0)。这有助于编译器理解该数字是浮点数,而不是整数。
int a { 5 }; // 5 means integer
double b { 5.0 }; // 5.0 is a floating point literal (no suffix means double type by default)
float c { 5.0f }; // 5.0 is a floating point literal, f suffix means float type
int d { 0 }; // 0 is an integer
double e { 0.0 }; // 0.0 is a double
请注意,浮点字面量默认为 double 类型。f
后缀用于表示 float 类型的字面量。
务必确保字面量的类型与要赋值或初始化的变量的类型匹配。否则,将导致不必要的转换,甚至可能导致精度损失。
4.8.2 打印浮点数
现在考虑这个简单的程序:
#include <iostream>
int main()
{
std::cout << 5.0 << '\n';
std::cout << 6.7f << '\n';
std::cout << 9876543.21 << '\n';
return 0;
}
这个看似简单的程序的结果可能会让你感到惊讶:
5
6.7
9.87654e+06
在第一种情况下,即使我们输入了5.0
,std::cout
也会打印5
。默认情况下,如果小数部分为 0,则std::cout
不会打印数字的小数部分。
在第二种情况下,数字打印符合我们的预期。
在第三种情况下,它以科学计数法打印数字。
输出浮点数时,std::cout
默认精度为 6。也就是说,它假定所有浮点变量仅对 6 位数字有效(浮点数的最小精度),因此它将截断此后的任何内容。另请注意,std::cout 在某些情况下会切换为以科学计数法输出数字。根据编译器的不同,指数通常会填充至最低位数。
我们可以使用名为std::setprecision()
的输出操作函数覆盖 std::cout 显示的默认精度。output manipulator
,并在iomanip标头中定义。
#include <iomanip> // for output manipulator std::setprecision()
#include <iostream>
int main()
{
std::cout << std::setprecision(17); // show 17 digits of precision
std::cout << 3.33333333333333333333333333333333333333f <<'\n'; // f suffix means float
std::cout << 3.33333333333333333333333333333333333333 << '\n'; // no suffix means double
return 0;
}
输出:
3.3333332538604736
3.3333333333333335
因为我们使用std::setprecision()
将精度设置为 17 位,所以上面的每个数字都打印了 17 位。但是,正如你所见,这些数字肯定没有精确到 17 位!而且由于浮点数的精度低于双精度数,浮点数的误差更大。
输出操纵器(和输入操纵器)是粘性的,这意味着如果您设置它们,它们将保持设置状态。
精度问题不仅会影响小数,还会影响任何有效数字过多的数字。除非空间非常宝贵,否则最好使用双精度而不是浮点,因为浮点的精度不足通常会导致不准确。
4.8.3 浮点数范围
Format | Range | Precision |
---|---|---|
IEEE 754 single-precision (4 bytes) | ±1.18 x 10-38 to ±3.4 x 1038 and 0.0 | 6-9 significant digits, typically 7 |
IEEE 754 double-precision (8 bytes) | ±2.23 x 10-308 to ±1.80 x 10308 and 0.0 | 15-18 significant digits, typically 16 |
x87 extended-precision (80 bits) | ±3.36 x 10-4932 to ±1.18 x 104932 and 0.0 | 18-21 significant digits |
IEEE 754 quadruple-precision (16 bytes) | ±3.36 x 10-4932 to ±1.18 x 104932 and 0.0 | 33-36 significant digits |
80 位 x87 扩展精度浮点类型在某种程度上是一个历史遗留问题。在现代处理器上,这种类型的对象通常会被填充到 12 或 16 字节。(这对于处理器来说是一个更自然的处理大小)。这意味着这些对象包含 80 位浮点数据,剩余的内存空间则用于填充。
80 位浮点类型与 16 字节浮点类型具有相同的范围,这看起来可能有点奇怪。这是因为它们具有相同数量的专用于指数的位——然而,16 字节数可以存储更多有效数字。
浮点类型的精度定义了在不丢失信息的情况下它可以表示多少个有效数字**。**
浮点类型的精度位数取决于大小(浮点数的精度低于双精度数)和所存储的特定值(某些值可以比其他值更精确地表示)。例如,浮点数的精度为 6 到 9 位。这意味着浮点数可以精确表示最多 6 位有效数字的任何数字。对于精度为 7 到 9 位的数字,其表示可能准确,也可能不准确,具体取决于具体值。而精度超过 9 位的数字则肯定无法精确表示。
4.8.4 舍入误差使浮点比较变得棘手
由于二进制(数据的存储方式)和十进制(我们的思维方式)之间的区别不明显,浮点数的处理比较棘手。考虑分数 1/10。在十进制中,它很容易表示为 0.1,我们习惯于将 0.1 视为一个易于表示且有效数字为 1 的数字。然而,在二进制中,十进制值 0.1 可以用无限序列表示:0.00011001100110011……
因此,当我们将 0.1 赋值给浮点数时,会遇到精度问题。
#include <iomanip> // for std::setprecision()
#include <iostream>
int main()
{
double d{0.1};
std::cout << d << '\n'; // use default cout precision of 6
std::cout << std::setprecision(17);
std::cout << d << '\n';
return 0;
}
输出:
0.1
0.10000000000000001
正如我们所期望的,在第一行std::cout
打印 0.1。
最终结果显示,虽然我们std::cout
展示了 17 位精度,但d
实际上它并不完全是0.1!这是因为 double 类型由于内存有限,不得不截断近似值。结果虽然精确到 16 位有效数字(double 类型保证如此),但并非恰好是0.1。舍入误差可能会使数字略小或略大,具体取决于截断发生的位置。
舍入误差可能会产生意想不到的后果:
#include <iomanip> // for std::setprecision()
#include <iostream>
int main()
{
std::cout << std::setprecision(17);
double d1{ 1.0 };
std::cout << d1 << '\n';
double d2{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 }; // should equal 1.0
std::cout << d2 << '\n';
return 0;
}
复制
1
0.99999999999999989
虽然我们可能期望d1
和d2
应该相等,但我们发现它们并不相等。如果我们在程序中比较d1
和,程序的性能可能不会达到预期。由于浮点数通常不精确,因此比较浮点数通常会存在问题。
关于舍入误差的最后一点说明:数学运算(例如加法和乘法)往往会使舍入误差增大。因此,即使 0.1 在第 17 位有效数字上存在舍入误差,当我们将 0.1 加 10 次时,舍入误差就已经蔓延到第 16 位有效数字。继续进行运算会导致该误差变得越来越大。
当数字无法精确存储时,就会发生舍入误差。即使是像 0.1 这样简单的数字也可能发生这种情况。因此,舍入误差随时都可能发生。舍入误差并非例外,而是常态。切勿假设浮点数是精确的。
4.8.5 NaN 和 Inf
IEEE 754 兼容格式还支持一些特殊值:
- Inf,表示无穷大。Inf 有符号,可以是正数 (+Inf) 或负数 (-Inf)。
- NaN,代表“非数字”。NaN 有几种不同的类型(这里就不讨论了)。
- 有符号零,意味着“正零”(+0.0)和“负零”(-0.0)有不同的表示。
4.9 布尔值
4.9.1 布尔变量
布尔变量是只能具有两个可能值的变量:true
、 和false
。
要声明布尔变量,我们使用关键字bool
。
bool b1 { true };
bool b2 { false };
b1 = false;
bool b3 {}; // default initialize to false