Skip to content

3.2 算术类型

This content is not available in your language yet.

算术类型是一类能对数据进行算术运算的类型。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 intshort、signed short、signed short intshort
unsigned short intunsigned shortunsigned short
intsigned、signed intint
unsigned intunsignedunsigned
long intlong、signed long、signed long intlong
unsigned long intunsigned longunsigned long

组成这些类型关键词顺序是任意的,例如 signed int 和 int signed 是等价的、unsigned long long int 和 int long unsigned long 也是等价的。

前面提到过了类型别名,类型别名不能将这样多个关键字组成的类型拆开,例如:

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_tUTF-8 字符类型u8'A',u8前缀1,与 unsigned char 相同
char16_tUTF-16 字符类型u'A',u前缀与 std::uint_least16_t 相同
char32_tUTF-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_t123z,后缀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 intintlong intlong long intstd::size_t典型平台
16163264C++ 标准规定的最小位宽
ILP321632326432x86-32
LLP641632326464Windows(包括 x86-64,IA-64,ARM64)
LP641632646464Unix和多数类Unix系统,如Linux,macOS,Solaris
ILP641664646464一些科学计算领域的平台,如Cray
SILP646464646464Classic UNICOS
1616326416 (是 unsigned int 的别名)嵌入式系统,如AVR
88163216 (是 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,后缀f32
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 个尾数位。

实现可能支持 ISO 60559 规定的扩展浮点类型,这些类型包括:

类型ISO 60559 名称类型位宽指数位宽尾数位宽
std::float16_tbinary1616510
std::float32_tbinary3232823
std::float64_tbinary64641152
std::float128_tbinary12812815112
std::bfloat16_t1687

浮点数类型不能进行全部的前面表达式所介绍的运算。浮点数的计算是按照 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)。

在处理浮点数时,凡是进行了会损失精度的计算(包括加减乘除等),都可能会有类似的情况,因此,在进行浮点数比较时,应该使用一个误差范围,而不是直接比较两个浮点数的值,例如:

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的无符号版本

上面这样一条一条的规则显然只适合于考试和编译器开发者,如果需要日常开发中天天想着这个规则,显然是不现实的。为了方便理解,这里对这个规则做出一个简略的总结:

  1. 将整数变成浮点
  2. 将小于int的类型变成int
  3. 将扩展类型变成标准类型
  4. 将较小的类型变成较大的类型
  5. 将一样大的有符号类型变成无符号类型。

举例而言:

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;

这里的转换规则仍然是一定程度的简化,涉及值类别转换和枚举类型的部分,会在后续章节中介绍。