3.2 算术类型
算术类型是一类能对数据进行算术运算的类型。C++ 中的算术类型包括整数类型和浮点类型。
整数类型
在前面的章节中,为了介绍表达式,介绍了三种类型 int
、bool
和 char
,这些类型都是整数类型。这里我们详细介绍整数类型。
C++ 中默认提供的整数类型包括:
类型 | 含义 | 字面量形式 | 类型大小 |
---|---|---|---|
short int | 短整数类型 | 没有字面量 | 至少 2 |
unsigned short int | 无符号短整数类型 | 没有字面量 | 至少 2 |
int | 整数类型 | 123 ,没有后缀 | 至少 2,且不小于 short 。 |
unsigned int | 无符号整数类型 | 123u ,后缀u ,不区分大小写 | 至少 2,且不小于 short 。 |
long int | 长整数类型 | 123l ,后缀l ,不区分大小写 | 至少 4,且不小于 int 。 |
unsigned long int | 无符号长整数类型 | 123ul ,后缀ul ,不区分大小写 | 至少 4,且不小于 int 。 |
long long int | 长长整数类型 | 123ll ,后缀ll ,不区分大小写 | 至少 8,且不小于 long 。 |
unsigned long long int | 无符号长长整数类型 | 123ull ,后缀ull ,不区分大小写 | 至少 8,且不小于 long 。 |
上面提到的这些类型,字面量的后缀 u
、l
、ll
不区分大小写,并且 u
和 l
/ ll
的顺序任意,例如:
123U; // 值为123,类型为 unsigned int
123ul; // 值为123,类型为 unsigned long int
123lu; // 值为123,类型为 unsigned long int
123LL; // 值为123,类型为 long long int
123LLU; // 值为123,类型为 unsigned long long int
注意,不可以写作 123lul
上面描述的这些类型的名字由多个关键字组成,其中的一部分是可以省略、或者无影响地添加的:
类型 | 等同的类型 | 最短形式 |
---|---|---|
short int | short 、signed short 、signed short int | short |
unsigned short int | unsigned short | unsigned short |
int | signed 、signed int | int |
unsigned int | unsigned | unsigned |
long int | long 、signed long 、signed long int | long |
unsigned long int | unsigned long | unsigned long |
组成这些类型关键词顺序是任意的,例如 signed int
和 int signed
是等价的、unsigned long long int
和 int long unsigned long
也是等价的。
语言习惯
上面提到的 int long unsigned long
虽然是符合语法的,但是人类在描述一件事物的时候,使用限定语通常会有一定的顺序。例如“一个大圆红苹果”很少有人会说成“红大一个圆苹果”。在使用多关键词类型名的时候,往往习惯于按照符号性、大小、中心词int
的顺序来描述类型。
前面提到过了,类型别名不能将这样多个关键字组成的类型拆开,例如:
using my_int = int; // 将 my_int 定义为 int 的别名
long my_int x = 42;// 错误:long my_int 不能组成 long int
除了上述用于表示基本整数的类型,C++ 还提供了一些用于表示字符的整数类型:
类型 | 含义 | 字面量形式 | 类型大小 |
---|---|---|---|
signed char | 有符号字符类型 | 没有字面量 | 1 |
unsigned char | 无符号字符类型 | 没有字面量 | 1 |
char | 字符类型 | 'A' | 1 |
char8_t | UTF-8 字符类型 | u8'A' ,u8前缀 | 1,与 unsigned char 相同 |
char16_t | UTF-16 字符类型 | u'A' ,u前缀 | 与 std::uint_least16_t 相同 |
char32_t | UTF-32 字符类型 | U'A' ,大写U前缀 | 与 std::uint_least32_t 相同 |
wchar_t | 宽字符类型 | L'A' ,大写L前缀 | 由平台决定 |
char
是字符类型,属于整数类型,它与 signed char
或者 unsigned char
之一的值表示相同,但是从语言上 char
类型是一个独立的类型,与 signed char
和 unsigned char
均不同。
char8_t
是 UTF-8 字符类型,属于整数类型。它和 unsigned char
有相同的大小和符号性、大小和对齐,但它是和 unsigned char
不同的类型。
char16_t
是 UTF-16 字符类型,属于整数类型。它的大小足够表示一个 UTF-16 编码单元。它和 std::uint_least16_t
有相同的大小和符号性、大小和对齐,但它是和 std::uint_least16_t
不同的类型(见后文)。
char32_t
是 UTF-32 字符类型,属于整数类型。它的大小足够表示一个 UTF-32 编码单元。它和 std::uint_least32_t
有相同的大小和符号性、大小和对齐,但它是和 std::uint_least32_t
不同的类型(见后文)。
wchar_t
是宽字符类型,属于整数类型,它的大小由平台决定,通常是2字节或者4字节。
在上面列出的类型中,signed char
、short
、int
、long int
、long long int
称为基础有符号整数类型;unsigned char
、unsigned short
、unsigned int
、unsigned long int
、unsigned long long int
称为基础无符号整数类型;char
、char8_t
、char16_t
、char32_t
、wchar_t
称为字符类型。基础有符号整数类型和基础无符号整数类型合起来称为基础整数类型。
除了上述的整数类型之外,C++ 还提供了一些特殊的整数类型:
类型 | 含义 | 字面量形式 | 类型大小 |
---|---|---|---|
bool | 布尔类型 | true 、false | 实现定义 |
std::size_t | 表示对象的大小的无符号整数类型 | 123uz ,后缀uz ,不区分大小写 | 实现定义 |
有符号形式的 std::size_t | 123z ,后缀z ,不区分大小写 | 实现定义 | |
std::ptrdiff_t | 有符号整数类型,用于指针差值 | 实现定义 |
bool
是布尔类型,属于整数类型,它只有两个值 true
和 false
。bool
类型的大小 C++ 没有规定,但是通常为1字节。
std::size_t
是用于表示对象的大小的无符号整数类型。是 sizeof
和 alignof
运算符的结果类型。std::size_t
的大小由平台决定,且 std::size_t
实现通常是前述整数类型之一的类型别名。
std::ptrdiff_t
是用于表示指针差值的整数类型。它足够容纳两个指针之间的差值。我们会在后面的章节中介绍指针类型。
一些实现的类型大小
下表展示了不同实现的整数类型的位宽。
数据模型 | short int | int | long int | long long int | std::size_t | 典型平台 |
---|---|---|---|---|---|---|
16 | 16 | 32 | 64 | C++ 标准规定的最小位宽 | ||
ILP32 | 16 | 32 | 32 | 64 | 32 | x86-32 |
LLP64 | 16 | 32 | 32 | 64 | 64 | Windows(包括 x86-64,IA-64,ARM64) |
LP64 | 16 | 32 | 64 | 64 | 64 | Unix和多数类Unix系统,如Linux,macOS,Solaris |
ILP64 | 16 | 64 | 64 | 64 | 64 | 一些科学计算领域的平台,如Cray |
SILP64 | 64 | 64 | 64 | 64 | 64 | Classic UNICOS |
16 | 16 | 32 | 64 | 16 (是 unsigned int 的别名) | 嵌入式系统,如AVR | |
8 | 8 | 16 | 32 | 16 (是 long unsigned int 的别名) | 嵌入式系统,如AVR(int8模式) |
扩展整数类型
除了上述的整数类型之外,根据实现,C++ 也会提供另外的扩展整数类型,其中典型的就是定宽整数类型。这些类型有:
类型 | 含义 | 备注 |
---|---|---|
std::int8_t | 有符号8位整数 | 可选提供 |
std::int16_t | 有符号16位整数 | 可选提供 |
std::int32_t | 有符号32位整数 | 可选提供 |
std::int64_t | 有符号64位整数 | 可选提供 |
std::uint8_t | 无符号8位整数 | 可选提供 |
std::uint16_t | 无符号16位整数 | 可选提供 |
std::uint32_t | 无符号32位整数 | 可选提供 |
std::uint64_t | 无符号64位整数 | 可选提供 |
std::int_fast8_t | 至少8位的最快整数类型 | |
std::int_fast16_t | 至少16位的最快整数类型 | |
std::int_fast32_t | 至少32位的最快整数类型 | |
std::int_fast64_t | 至少64位的最快整数类型 | |
std::uint_fast8_t | 至少8位的最快无符号整数类型 | |
std::uint_fast16_t | 至少16位的最快无符号整数类型 | |
std::uint_fast32_t | 至少32位的最快无符号整数类型 | |
std::uint_fast64_t | 至少64位的最快无符号整数类型 | |
std::int_least8_t | 至少8位的最小整数类型 | |
std::int_least16_t | 至少16位的最小整数类型 | |
std::int_least32_t | 至少32位的最小整数类型 | |
std::int_least64_t | 至少64位的最小整数类型 | |
std::uint_least8_t | 至少8位的最小无符号整数类型 | |
std::uint_least16_t | 至少16位的最小无符号整数类型 | |
std::uint_least32_t | 至少32位的最小无符号整数类型 | |
std::uint_least64_t | 至少64位的最小无符号整数类型 | |
std::intmax_t | 最大位宽有符号整数类型 | |
std::uintmax_t | 最大位宽无符号整数类型 | |
std::intptr_t | 整数类型,用于指针 | |
std::uintptr_t | 无符号整数类型,用于指针 |
这些类型的名字不是关键字,因此以 std::
作为前缀。
std::int8_t
、std::int16_t
等定宽整数类型的位宽是确定的,它仅当实现直接提供这样尺寸的整数类型时才会存在。如果前面提到的如 int
long int
这样的类型本身就满足定宽的要求,那么这些定宽类型可以是对应的类型的别名。例如,如果某个实现中 int
恰好是32位的,那么允许 using int32_t = int;
来声明 std::int32_t
类型。
std::int_fast8_t
、std::int_fast16_t
等最快整数类型是指在当前平台上最快的整数类型,它们的位宽是不确定的,但是至少是指定的位宽。由于平台上最快的整数类型是 int
, 因此尺寸小于 int
的最快整数类型往往是 int
。
实现可以提供 N
不在上述之列的 std::intN_t
,std::int_fastN_t
和 std::int_leastN_t
等类型,但也必须满足 std::intN_t
的位宽是 N
,std::int_fastN_t
和 std::int_leastN_t
的位宽至少是 N
。
std::intptr_t
和 std::uintptr_t
是用于指针的整数类型。std::intptr_t
是有符号整数类型,std::uintptr_t
是无符号整数类型。它们足够容纳对象指针类型的位宽。
整数类型的运算
整数类型的运算基本与章节中介绍的一致,除了由于位宽和 int
不同,结果的范围有所不同。
在前面的章节中提到过,如果 int
类型表达式的计算结果超出了 int
类型的范围,那么结果是未定义的。这种性质对其他有符号整数类型也是适用的。
但是,对于无符号整数类型,例如 unsigned int
,标准保证运算是模运算。即,如果结果超出了类型的范围,那么结果会被取模到类型的范围内。例如:
// 假定 unsigned int 是 32 位,因此 4294967295 是此类型的最大值
unsigned int a = 4294967295;
unsigned int b = 1;
// 保证 a + b 的结果,以及 c 的值是 0
unsigned int c = a + b;
整数提升
之前的章节中介绍过,在计算 时,如果操作数是 bool
或者 char
时,会转换为 int
类型。这种转换称为整数提升。
这个规则具体而言是:
- 如果操作数是除了
bool
、char8_t
、char16_t
、char32_t
、wchar_t
之外的、比int
更小的整数类型,如果int
能容纳操作数的值,那么将操作数转换为int
类型。否则,将操作数转换为unsigned int
类型。(例如,char
和short
类型会被提升为int
类型) - 如果操作数是
char8_t
、char16_t
、char32_t
、wchar_t
类型之一,按照下面的顺序:int
、unsigned int
、long int
、unsigned long int
,long long int
、unsigned long long int
。选择第一个值范围能够容纳的。将操作数转换为这个类型。 - 如果操作数是
bool
类型,那么将操作数转换到int
类型,false
转换为0
,true
转换为1
。
整数转换
在整数提升之外,整数还有一些转换情况。
- 如果目标类型是
bool
类型,那么如果操作数是0
,那么结果是false
,否则结果是true
。 - 如果源类型是
bool
类型,那么如果操作数是false
,那么结果是0
,如果操作数是true
结果是1
。
例如:
unsigned char a = true; // a 的值是 1
long long int b = false; // b 的值是 0
bool c = 42ull; // c 的值是 true
unsigned char f(bool b) {
return b;
}
unsigned char a = f(42); // a 的值是 1,42 转换为 true,然后 true 转换为 1
在上述情况之外,转换结果的值是,在目标类型范围内,源值对2^N取模的唯一值,其中 N 是目标类型的位宽。读者可以将这个规则理解为,在数学意义上的如下计算:
Result = Mod(Source - TargetMin, 2^N) + TargetMin
其中,Mod
是取余(模运算),Source
是源值,TargetMin
是目标类型的最小值,N
是目标类型的位宽。
例如:
// 从 int 类型的 256 转换到 unsigned char
// 假设 unsigned char 位宽为8
// 256 对 256 取模是 0
// a 的值是 0
unsigned char a = 256;
// 从 int 类型的 -1 转换到 unsigned long long
// 假设 unsigned long long 位宽为64
// -1 对 2^64 取模是 18446744073709551615
// b 的值是 18446744073709551615
unsigned long long b = -1;
// 从 int 类型的 144 转换到 signed char
// 假设 signed char 位宽为8
// 144 对 256 取模,signed char 的范围是 -128 到 127,结果是 -112
// c 的值是 -112
signed char c = 144;
浮点类型
在一节中,已经简要概述了浮点类型的字面量,这里我们详细介绍浮点类型。
标准浮点类型
C++ 中默认提供的浮点类型包括:
类型 | 含义 | 字面量形式 | 类型位宽 |
---|---|---|---|
float | 单精度浮点数,IEEE-754 binary32 浮点数 | 1.0f ,后缀f | 32 |
double | 双精度浮点数,IEEE-754 binary64 浮点数 | 1.0 ,没有后缀 | 64 |
long double | 扩展精度浮点数 | 1.0l ,后缀l | 实现定义 |
这些浮点数的行为基本上由 IEEE-754 标准所规定。其中,float
有 1 个符号位、8 个指数位和 23 个尾数位,double
有 1 个符号位、11 个指数位和 52 个尾数位,long double
的位数是实现定义的,如果实现为 x86 上实现的 IEEE-754 binary64 扩展模式,那么它的位数可能是 1 个符号位、15 个指数位和 64 个尾数位。
浮点数的表示
浮点数根据 IEEE-754 有确定的表示形式。例如,3.14f
的值表示是 0 10000000 1001000 11110101 11000011
,其中第一位是符号位,接下来的8位是指数,最后的23位是尾数。
浮点数的表示可以简单地理解成科学计数法的形式,其中指数部分是以 2 为底的指数,尾数部分是二进制小数(二进制 0.1 表示十进制 0.5,二进制 0.11 表示十进制 0.75,二进制 10.101 表示十进制 2.625)。科学记数法中,1e2 和 10e1 是等价的,因此,IEEE-754 规定,尾数部分的二进制小数总会标准化到 1.xxx
的形式;并且由于尾数第一位总是 1
,所以省略不存储。指数部分中,IEEE-754 规定,01111111
表示指数为 0,10000000
表示指数为 1,10000001
表示指数为 2,可以理解成一个 0 值与常规不同的 8 位无符号整数。
因此,我们反推上述的 3.14f
的值表示,它表示的是 + 1.10010001111010111000011 * 2,即 3.1400001049041748046875。读者会发现这是个近似数,毕竟有效位数有限,在进制转换时,经常会有精度损失。
不过,如果我们考虑 + 1.10010001111010111000010 * 2,也即把最后一位从 1 改成 0,即 3.139999866485595703125,可以发现它和十进制 3.14 的差距比上面的结果更大,也即是说,上面的表示是最接近十进制 3.14 的浮点数。这不是巧合,实现按照规定会将十进制浮点数字面量转换到最接近的 IEEE-754 浮点数。
有心的读者可能会意识到,假设某个十进制 A 转换到浮点数后,得到的是 IEEE-754 规定的浮点数 B,如果我们再利用输出函数(例如前面提到的std::println
)输出 B 会得到什么呢?在现实中,一个浮点数的默认输出常常是其唯一最短形式的十进制小数表示,也即上面的 0 10000000 1001000 11110101 11000011
一定输出为 3.14,而 0 10000000 1001000 11110101 11000010
一定输出为 3.1399999,并且这一转换是可逆的,也即 3.1399999f
一定转换到 0 10000000 1001000 11110101 11000010
。浮点数输入输出会计算出到无歧义的最短十进制小数表示,这是 IEEE-754 标准的要求。简单的来说,字面上有多个十进制小数都可以转换到同一个浮点数,但是浮点数转换到十进制小数时,只有一种最短表示,且这一最短表示一定转换成对应的浮点数。
扩展浮点类型
实现可能支持 ISO 60559 规定的扩展浮点类型,这些类型包括:
类型 | ISO 60559 名称 | 类型位宽 | 指数位宽 | 尾数位宽 |
---|---|---|---|---|
std::float16_t | binary16 | 16 | 5 | 10 |
std::float32_t | binary32 | 32 | 8 | 23 |
std::float64_t | binary64 | 64 | 11 | 52 |
std::float128_t | binary128 | 128 | 15 | 112 |
std::bfloat16_t | 16 | 8 | 7 |
浮点计算
浮点数类型不能进行全部的前面所介绍的运算。浮点数的计算是按照 IEEE-754 标准进行的,这主要包括:
- 和:将操作数浮点数的值增加1。
- 和:将操作数浮点数的值减少1。
- :表达式的值与操作数相同。
- :将操作数的符号取反。
- :如同数学计算,表达式的值是将操作数的值进行计算。由于浮点数存在精度,因此在计算时可能会有舍入。浮点数不能进行模运算(
%
)。 - :如同数学比较。不过浮点数有一些特殊值,这在后面进行介绍。三路比较时,浮点数的比较结果是
std::partial_ordering
类型,会出现一个特殊的“无顺序”结果。 - :如同整数的赋值。
简单的来看,浮点数和整数具有的运算基本相同,但是和位运算相关的部分则不适用于浮点数。
特殊浮点值
浮点数有一些特殊的值,它们是:正零(+0)、负零(-0)、正无穷(+∞)、负无穷(-∞)、qNaN,sNaN。
正零的值表示为 0 00000000 00000000000000000000000
,负零的值表示为 1 00000000 00000000000000000000000
。
正无穷的值表示为 0 11111111 00000000000000000000000
,负无穷的值表示为 1 11111111 00000000000000000000000
。
NaN是Not a Number,意思是不是一个数,当然这也是一种浮点数,而非“不是数”。qNaN是quiet NaN,sNaN是signaling NaN。qNaN 是静默 NaN,而 sNaN 是信号 NaN。NaN 的值表示为 0 11111111 1xxxxxx xxxxxxxx xxxxxxxx
,其中指数部分全为1,尾数部分不全为0(注意全为0时这就是正无穷了)。qNaN 的尾数部分(除了那个必须为1的位)最高位为1,而 sNaN 的对应位为0。sNaN 具有一些实现上的作用,例如:
- 把内存填满 sNaN,可以检测内存的初始化情况。
- 标记溢出的情况
- 标记计算结果可能具有更高精度
- 标记复数
这些特殊值在计算和比较的时候具有特殊规则(下面使用 ±0 表示 +0 或 -0,±∞ 表示 +∞ 或 -∞)。
对于加性表达式和乘性表达式,有以下规则:
- 任意浮点数(包括NaN)和 NaN 进行计算,结果是 NaN
- 非特殊浮点数除以 ±0 结果是 ±∞(符号与正常除法相同)
- 非特殊浮点数计算的结果如果溢出了浮点数范围,则上溢出的结果是 +∞,下溢出的结果是 -∞
- ±0 / ±0 结果是 NaN
- ±∞ / ±∞ 结果是 NaN
- ±0 * ±∞ 结果是 NaN
- +∞ + -∞ 和 +∞ + -∞ 结果是 NaN
- +∞ + +∞ 结果是 +∞,-∞ + -∞ 结果是 -∞
- +∞ - +∞ 结果是 NaN,-∞ - -∞ 结果是 NaN
对于关系表达式,有以下规则:
- +0 和 -0 相等
- +∞ 和 +∞ 相等, -∞ 和 -∞ 相等
- +∞ 大于任意非特殊浮点数,-∞ 小于任意非特殊浮点数
- 任意浮点数(包括NaN)和 NaN 进行任意比较,结果是
false
。- 对于三路比较(
<=>
),得到的结果是std::partial_ordering::unordered
。
- 对于三路比较(
浮点舍入
浮点数的计算可能会有舍入,这是因为浮点数的表示是有限的,而实数是无限的。IEEE-754 标准规定了浮点数的计算规则,每一次表达式求值都可能会按照这一规则发生舍入。例如:
double a = (1.01 * 3) - 3.03;
这里,1.01 * 3
得到的结果并不是准确的 3.03
,而是一个接近于 3.0300000000000002
的值。因此,a
的值可能并不是 0
,而是一个很小的值(大约 4.440892098500626e-16
)。
精度保证
C++ 本身并没有对浮点精度做任何保证。在上面的例子中,a
的值也可能被初始化为 0
。C++ 允许编译器对浮点数计算做出非常激进的优化,将字面量加减乘除的计算过程认为是无限精度,并只在最终结果上做舍入。此外,现实的处理器存在融合乘法和加法的指令,这种指令可以在计算先乘后加的表达式时保证结果,不会由于多余的一次舍入产生精度问题。因此,上面的例子中,a
的值可能是 0
,也可能是一个很小的值,这取决于编译器的优化策略和目标平台的浮点数计算能力。
在处理浮点数时,凡是进行了会损失精度的计算(包括加减乘除等),都可能会有类似的情况,因此,在进行浮点数比较时,应该使用一个误差范围,而不是直接比较两个浮点数的值,例如:
import std;
bool double_equal(double a, double b) {
// 误差范围为 1e-9
return (a + 1e-9) > b && (a - 1e-9) < b;
}
int main() {
double a = (1.01 * 3) - 3.03;
std::println("a = 0 results {}", double_equal(a, 0));
}
上面程序的输出应当是 a = 0 results true
。
浮点转换
在前面提到的,浮点数的计算中,如果操作数的类型不同,那么会按照以下规则进行转换:
- 如果另一个操作数不是浮点数,那么将那个非浮点数的操作数转换为相同的浮点数类型。
- 对于符合 IEEE-754 标准的浮点数,这一转换会转换到最近的浮点数。
- 如果另一个操作数是浮点数,但是类型不同,那么将较小的浮点数转换为较大的浮点数。
- 例如,
float
和double
参与计算时,float
会转换为double
。
- 例如,
此外,使用浮点类型可以初始化整数类型,这时候浮点数会被截断为整数,即舍弃小数部分,而不是四舍五入。例如:
int a = 3.14; // a 的值是 3
int b = 3.99; // b 的值是 3
int conversion(int x) {
return x;
}
int c = conversion(3.14); // c 的值是 3
如果截断之后,浮点数的值超出了整数类型的范围,这时行为是。
使用整数类型也可以初始化浮点数类型,这时候整数会被转换为浮点数,例如:
double d = 42; // d 的值是 42.0
此外,如果转换不能保证精度,那么实现可以选择向上或者向下舍入;对于 IEEE-754 标准的浮点数,这种转换会转换到最近的浮点数。
如果转换之后,整数的值超出了浮点数类型的范围,这时行为是。
一般算术转换
在目前介绍到的内容中,出现了多种形式的转换,包括整数提升、整数转换、浮点转换。
现在,我们来综合地介绍一下。在需要操作数是算术类型的表达式中,会进行一些形式相似的转换,这种转换称为一般算术转换。
对于算术类型,下列的表达式会先对操作数进行一般算术转换:
转换等级
在一般算术转换中,整数转换遵循如下的整数转换等级:
- 有符号整数类型的等级高于任意位宽更小的有符号整数类型(例如,
std::int32_t
高于std::int16_t
) - 下列类型的等级依次递减
long long int
long int
int
short int
signed char
- 任一无符号整数类型的等级,等于对应的有符号整数类型的等级(例如,
std::uint32_t
的等级等于std::int32_t
的等级) bool
类型的等级低于任何标准整数类型char8_t
、char16_t
、char32_t
、wchar_t
的等级等于其基础类型的等级(例如,char8_t
的等级等于unsigned char
的等级,char16_t
的等级等于std::uint_least16_t
的等级)- 扩展整数类型的等级由实现定义,但仍遵循上述规则(例如,如果
std::int_fast16_t
的位宽高于short int
,其等级也一定高于short int
)
在一般算术转换中,浮点数转换遵循如下的浮点转换等级:
- 如果某个浮点数类型的所有值是是另一个类型的所有值的子集,那么这个浮点数类型的等级低于另一个浮点数类型的等级
long double
的等级高于double
的等级,double
的等级高于float
的等级
- 具有相同值集合的扩展浮点数类型的等级相等
- 与基础整数类型值集合相同的扩展浮点数类型的等级相等
一般算术转换
一般算术转换的规则如下:
- 如果任一操作数是浮点数类型:
- 如果两个操作数的类型相同,不需要转换
- 否则,如果其中一个操作数的类型是非浮点数类型,将这个操作数转换到与另一个操作数相同的浮点数类型
- 否则,如果两个操作数的类型不同,将较小的浮点数转换为两个操作数中,转换等级较高的浮点数类型
- 否则,每个操作数会转换到一个公共类型。此时,每个操作数都会发生整数提升。
- 如果提升后两个操作数的类型相同,那么不需要转换
- 否则,如果提升后两个操作数都是有符号整数类型,或者都是无符号整数类型,那么将较小的操作数转换为较大的操作数
- 否则,假设提升后操作数类型中,无符号的类型为U,有符号的类型为S
- 如果U的转换等级高于S,那么将操作数都转换为U
- 否则,如果S的转换等级高于U,那么将操作数都转换为S
- 否则,将操作数都转换为S的无符号版本
上面这样一条一条的规则显然只适合于考试和编译器开发者,如果需要日常开发中天天想着这个规则,显然是不现实的。为了方便理解,这里对这个规则做出一个简略的总结:
- 将整数变成浮点
- 将小于
int
的类型变成int
- 将扩展类型变成标准类型
- 将较小的类型变成较大的类型
- 将一样大的有符号类型变成无符号类型。
举例而言:
int a = 1;
unsigned int b = 2;
float c = 3.0f;
// 没有操作数是浮点数
// 整数提升后,类型分别是 `int` 和 `unsigned int`,两者转换等级仍然相同
// 转换为 `int` 的无符号版本,结果是 `unsigned int` 类型
auto x1 = a + b;
// 有一个操作数是浮点数
// 将 `int` 转换为 `float`,结果是 `float` 类型
auto x2 = a + c;
这里的转换规则仍然是一定程度的简化,涉及值类别转换和枚举类型的部分,会在后续章节中介绍。
为什么?
一般算术转换的规则是对现实的概括和妥协。
在常见的平台上,浮点和整数是通过两套不同的电路处理的,这一现实在 C++ 语言上体现为浮点和整数仿佛是两个冤家,在一个浮点数加入了聚会之后,整个聚会就被迫变成了浮点数的聚会,这时候那些位运算就被请出了门。如果非要让浮点数加入整数的聚会,我们就不得不费劲地把浮点数塞进整数的衣服里。
这里这种“处理整数的电路”,它提供的很多功能要求数据至少有 int
的尺寸,并且参与处理的两个数据也要求大小相同。正因如此,一般算术转换中会将小于 int
的整数类型转换为 int
,并且必须找到一个公共的类型来进行运算。
基础算术类型是对平台这种特性的一种抽象,它将硬件支持最佳的几种数据表示抽象为 int
、long int
这些基础类型。然后再提供扩展类型,以适应更多的需求。
事实上,这种先抽象成基础类型,再抽象成扩展类型的二次抽象是一种过于复杂的历史遗留。程序员仍然需要浪费心智查看硬件手册,以理解 int
和 char
究竟是什么东西。已经进行了尝试的读者也会发现,几乎所有情况下,std::int32_t
就是 int
,std::int64_t
就是 long long int
,这层抽象在实际中几乎没有产生任何价值。于是,更现代的设计直接将扩展类型作为基础类型,具体如何选择表示和指令让编译器来分析。笔者也推荐读者直接使用扩展类型,而不要费神纠结基础类型的性质。