C/C++很烦人的一点在于,它最基本的数据类型都是不确定的,为了兼容某些奇怪的设备,C++标准并没有强制规定基本数据类型的位数,这可能导致很多 bug。

我们只考虑 64 位系统,考虑 x86-64 的 Windows/Linux 平台(32 位系统可能字节数更少,但是已经很少用了,本文不考虑)。

非常不建议使用整数类型long,以及浮点数类型long double,因为它们在不同的平台很可能位数不同。

整数类型

基本的整数类型大概有如下几种:(有符号类型,还有对应的无符号类型)

  1. char
  2. short (int)
  3. int
  4. long (int)
  5. long long (int)

注意:标准并没有严格规定它们的字节数大小,但是规定了字节数的大小关系(即表示范围的大小关系),以及它们至少需要的字节数。

1
short <= int <= long <= long long

主要参考 cpp reference 和 wiki,下图取自cpp reference

上图中的 64 位数据模型包括 LP64(主要对应 Unix-like 平台)和 LLP64(主要对应 Windows 平台), 详见下表,含义是long = pointer = 64long long = pointer = 64

type LP64 LLP64
char 8 bits 8 bits
short 16 16 bits
int 32 bits 32 bits
long 64 bits 32 bits
long long 64 bits 64 bits
pointer 64 bits 64 bits

char

单个字符的类型,当然同时也是最小的整数类型,因为字符可以按照 ASCII 码与整数隐式转换。

type size(bytes) range
char 1 \([-2^7,2^7-1]\) or \([0,2^8-1]\)
signed char 1 \([-2^7,2^7-1]\)
unsigned char 1 \([0,2^8-1]\)

注:

  • char通常是有符号的signed char,但是这并不是强制的,在某些特殊平台上可能出现char = unsigned char的情况,例如 arm-linux-gcc 把char定义为 unsigned char
  • char相比于其它的整数类型是非常不同的,它主要被用于表示字符类型,但是在算术运算中又作为整数出现,在涉及输入输出时尤其需要注意,因为输入输出对字符和整数会有不同的处理。

short

type size(bytes) range
short, short int, signed short, signed short int 2 \([-2^{15},2^{15}-1]\)
unsigned short, unsigned short int 2 \([0,2^{16}-1]\)

int

type size(bytes) range
int, signed, signed int 4 \([-2^{31},2^{31}-1]\)
unsigned, unsigned int 4 \([0,2^{32}-1]\)

long (Do not use it)

type size(bytes) range
long,long int,signed long,signed long int 4(Windows) \([-2^{31},2^{31}-1]\)
unsigned long, unsigned long int 4(Windows) \([0,2^{32}-1]\)
long,long int,signed long,signed long int 8(Unix-like) \([-2^{63},2^{63}-1]\)
unsigned long, unsigned long int 8(Unix-like) \([0,2^{64}-1]\)

注:尤其注意这里的long在常见的 Windows/unix-like 平台上不一致,因此对于 mingw 之类的交叉环境,尽可能地避免long的使用。

long long

type size(bytes) range
long,long int,signed long,signed long int 8 \([-2^{63},2^{63}-1]\)
unsigned long, unsigned long int 8 \([0,2^{64}-1]\)

浮点数类型

float

含义为 IEEE 标准的单精度浮点数,长度为 4 个字节,其中指数部分占8比特,可表示的最大值约为

\[ 2 \times 2^{127} = 2^{128} \approx 3.4 \times 10^{38} \]

double

含义为 IEEE 标准的双精度浮点数,长度为 8 个字节,其中指数部分占11比特,可表示的最大值约为

\[ 2 \times 2^{1023} = 2^{1024} \approx 1.8 \times 10^{308} \]

long double (Do not use it)

标准只是规定long double >= double,并没有说明它应当是 IEEE 标准下的四精度浮点数。 关于这个类型的实现,不同环境下的不同编译器作法非常混乱,极容易产生 bug,参考文章

  1. 在 Windows 平台下,long double=double,微软没有在这里实现四精度浮点数;
  2. 对于 GCC,通常实现为扩展双精度(80 位,仍然占用 16 字节来对齐),而非 IEEE 标准下的四精度浮点数;
  3. 在 arm64 架构下,可能实现为 IEEE 标准下的四精度浮点数;
  4. 对于 Intel 的编译器,也可能具有不同的实现。

其它类型

布尔类型 bool

在 C89 中没有定义,只能通过宏定义使用;在 C99 中引入,包括true(1)false(0),和char一样只占用一个字节。

在通常情况下,布尔变量和整数的对应关系都是:

  • true->1false->0
  • 非零值代表true,零值代表false。

然而对于程序返回值,情况并不相同:0返回值代表程序正常结束,非0返回值代表程序异常退出。

指针类型

这个完全和系统对应:64 位系统大小为 8 字节,32 位系统大小为 4 字节。

含字长整数类型

在 C99 中引入。

固定字长

固定字节数的整数类型,可以避免上面那些麻烦。

  1. 有符号整数:int8_t, int16_t, int32_t, int64_t
  2. 无符号整数:uint8_t, uint16_t, uint32_t, uint64_t

至少字长

保证至少满足相应的字长,但是可能字节数更高的整数类型。

  1. 有符号整数:int_least8_t, int_least16_t, int_least32_t, int_least64_t
  2. 无符号整数:uint_least8_t, uint_least16_t, uint_least32_t, uint_least64_t

至少字长且最快

在保证字长范围的基础上,确保最高效的整数类型。

  1. 有符号整数:int_fast8_t, int_fast16_t, int_fast32_t, int_fast64_t
  2. 无符号整数:uint_fast8_t, uint_fast16_t, uint_fast32_t, uint_fast64_t

字面值

在 C 语言中,字面值是直接在代码中指定的固定值。字面值的类型由其表示形式以及带有的后缀共同决定。 字面值通常包括整数、浮点数、字符、字符串等,针对它们的类型判定有不同的规则。

整数字面值

整数字面量的值根据表示形式(进制)确定:

  • 没有任何前缀(如 0x0b)时,默认是十进制整数常量。
  • 如果字面值以 0x0X 开头,它是十六进制整数常量。
  • 如果字面值以 0 开头,它是八进制整数常量。(对于0,无所谓它被当作八进制还是十进制,结果都是0)
  • 如果字面值以 0b0B 开头,它是二进制整数常量。

整数字面值的类型根据值的大小确定,标准规定了一个范围依次增大的整数类型序列, 首个可以吻合的类型即为字面量的类型,即值落在了该类型的表示范围中。

对于十进制的字面量,候选序列为

1
2
3
int
long int
long long int

对于非十进制的字面量,候选序列为

1
2
3
4
5
6
int
unsigned int
long int
unsigned long int
long long int
unsigned long long int

这里的明显区别是:对于十进制字面量的候选序列通常是不包括无符号类型的,对于非十进制的字面量则包含无符号类型。

这里只能大致提供一个候选序列,实际上具体结果是和平台相关的(Linux和Windows对longlong long的处理不同),还和编译器相关,甚至和标准的版本相关。除此之外,如果提供的字面量值过大,编译器具体的行为也是各不相同的。(又是一堆烂账)

例如在Windows上使用msvc或clang(msvc)可以顺利编译下面的代码,以验证字面量的具体类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <type_traits>

int main() {
// 4字节有符号和无符号(十六进制)
auto s1 = 0x7fffffff; // int
auto s2 = 0xffffffff; // unsigned int
static_assert(std::is_same<decltype(s1), int>::value);
static_assert(std::is_same<decltype(s2), unsigned int>::value);

// 8字节有符号和无符号(十六进制)
auto s3 = 0x7fffffffffffffff; // long long
auto s4 = 0xffffffffffffffff; // unsigned long long
static_assert(std::is_same<decltype(s3), long long>::value);
static_assert(std::is_same<decltype(s4), unsigned long long>::value);

// 4字节有符号和无符号(十进制)
auto p1 = 2147483647; // int
auto p2 = 4294967295; // long long
static_assert(std::is_same<decltype(p1), int>::value);
static_assert(std::is_same<decltype(p2), long long>::value);

// 8字节有符号和无符号(十进制)
auto p3 = 9223372036854775807; // long long
auto p4 = 18446744073709551615; // unsigned long long
static_assert(std::is_same<decltype(p3), long long>::value);
static_assert(std::is_same<decltype(p4), unsigned long long>::value);

return 0;
}

但是这段代码在Linux上会编译失败,主要原因是long longlong在不同平台的区别, 次要原因是p4对应的字面量过大:g++会将其处理为内置类型__int128,clang++则将其处理为unsigned long long

除此之外,还可以通过指定后缀改变类型,采用与之对应的类型候选列表

  • Uu指定为无符号类型
  • Ll指定为长整数类型
  • LLll指定为长整数类型

注意:

  • 整数字面量均不包括前面可能存在的正负号,编译器将正负号视作额外的一元运算。
  • 在特定进制下,字面量必须使用合法范围内的字符,例如八进制只能使用0-7
  • 无符号后缀可以和其它后缀组合使用,组合的顺序任意。
  • 在数位之间可以插入'作为分隔符,用于提供可读性,编译器会直接忽略。

浮点数字面值

浮点数字面值是包含小数点或科学计数法的数字,它们的类型可能是 floatdoublelong double

通常的浮点数字面量都是十进制下的,指数记号为eE,除此之外,C语言还支持十六进制的浮点数,指数记号为pP(因为十六进制已经把e占用了),要求字面量以0x0X开头。

默认情况下浮点数字面值均为double,也可以使用后缀指定为floatfF)或long doublelL)。

例如1e6这类数字虽然在数学上就是整数,但是出现科学计数法的字面量在C++中就是浮点数,无论它有没有小数部分。 下面的两个定义是不等价的,第一个语句存在doubleint的隐式类型转换

1
2
int n1 = 1e6;
int n2 = 1000000;

在通常情况下两者不会产生差异,但是在极端情况下仍然存在着两类隐患:浮点数误差和整数范围溢出。 其中浮点数误差的问题可能是比较隐蔽的,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <cstdint>
#include <iostream>

int main(int argc, char *argv[]) {
int64_t N1 = 9e18 + 1 - 9e18;
std::cout << N1 << std::endl; // 0

int64_t tmp = 9e18;
std::cout << tmp << std::endl; // 9000000000000000000
int64_t N2 = tmp + 1 - tmp;
std::cout << N2 << std::endl; // 1

return 0;
}

这里的大整数并不存在溢出问题,9e18仍然在int64_t的可表示范围内。 两者的区别在于:N1的定义右侧执行的是浮点数的加减法,N2的定义右侧执行的是整数的加减法。

字符和字符串字面值

字符字面值是用单引号 (') 括起来的单个字符,其类型是 char,包括普通字符(例如'a')和转义字符(例如'\n')。

字符串字面值是用双引号 (") 括起来的一系列字符,其类型通常是 const char[],即指向常量字符数组的数组类型, 有时也会自动退化为const char*指针类型(例如传参时或使用auto推导时)。

字符串字面值会自动在字符串末尾添加空字符 '\0' 作为字符串结束标志,例如字符串 "hello"const char[6] 类型。

实际上这部分语法也是非常复杂的,还需要考虑宽字符,甚至UTF-8字符,UTF-16字符等等,这里略过不做讨论。