Cpp 基本数据类型
C/C++很烦人的一点在于,它最基本的数据类型都是不确定的,为了兼容某些奇怪的设备,C++标准并没有强制规定基本数据类型的位数,这可能导致很多 bug。
我们只考虑 64 位系统,考虑 x86-64 的 Windows/Linux 平台(32 位系统可能字节数更少,但是已经很少用了,本文不考虑)。
非常不建议使用整数类型long
,以及浮点数类型long double
,因为它们在不同的平台很可能位数不同。
整数类型
基本的整数类型大概有如下几种:(有符号类型,还有对应的无符号类型)
- char
- short (int)
- int
- long (int)
- long long (int)
注意:标准并没有严格规定它们的字节数大小,但是规定了字节数的大小关系(即表示范围的大小关系),以及它们至少需要的字节数。
1 | short <= int <= long <= long long |
主要参考 cpp reference 和 wiki,下图取自cpp reference
上图中的 64 位数据模型包括 LP64(主要对应 Unix-like 平台)和
LLP64(主要对应 Windows 平台),
详见下表,含义是long = pointer = 64
和long 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,参考文章。
- 在 Windows
平台下,
long double=double
,微软没有在这里实现四精度浮点数; - 对于 GCC,通常实现为扩展双精度(80 位,仍然占用 16 字节来对齐),而非 IEEE 标准下的四精度浮点数;
- 在 arm64 架构下,可能实现为 IEEE 标准下的四精度浮点数;
- 对于 Intel 的编译器,也可能具有不同的实现。
其它类型
布尔类型 bool
在 C89 中没有定义,只能通过宏定义使用;在 C99
中引入,包括true(1)
和false(0)
,和char
一样只占用一个字节。
在通常情况下,布尔变量和整数的对应关系都是:
true->1
,false->0
;- 非零值代表true,零值代表false。
然而对于程序返回值,情况并不相同:0返回值代表程序正常结束,非0返回值代表程序异常退出。
指针类型
这个完全和系统对应:64 位系统大小为 8 字节,32 位系统大小为 4 字节。
含字长整数类型
在 C99 中引入。
固定字长
固定字节数的整数类型,可以避免上面那些麻烦。
- 有符号整数:
int8_t
,int16_t
,int32_t
,int64_t
; - 无符号整数:
uint8_t
,uint16_t
,uint32_t
,uint64_t
。
至少字长
保证至少满足相应的字长,但是可能字节数更高的整数类型。
- 有符号整数:
int_least8_t
,int_least16_t
,int_least32_t
,int_least64_t
; - 无符号整数:
uint_least8_t
,uint_least16_t
,uint_least32_t
,uint_least64_t
。
至少字长且最快
在保证字长范围的基础上,确保最高效的整数类型。
- 有符号整数:
int_fast8_t
,int_fast16_t
,int_fast32_t
,int_fast64_t
; - 无符号整数:
uint_fast8_t
,uint_fast16_t
,uint_fast32_t
,uint_fast64_t
。
字面值
在 C 语言中,字面值是直接在代码中指定的固定值。字面值的类型由其表示形式以及带有的后缀共同决定。 字面值通常包括整数、浮点数、字符、字符串等,针对它们的类型判定有不同的规则。
整数字面值
整数字面量的值根据表示形式(进制)确定:
- 没有任何前缀(如
0x
或0b
)时,默认是十进制整数常量。 - 如果字面值以
0x
或0X
开头,它是十六进制整数常量。 - 如果字面值以
0
开头,它是八进制整数常量。(对于0
,无所谓它被当作八进制还是十进制,结果都是0) - 如果字面值以
0b
或0B
开头,它是二进制整数常量。
整数字面值的类型根据值的大小确定,标准规定了一个范围依次增大的整数类型序列, 首个可以吻合的类型即为字面量的类型,即值落在了该类型的表示范围中。
对于十进制的字面量,候选序列为 1
2
3int
long int
long long int
对于非十进制的字面量,候选序列为 1
2
3
4
5
6int
unsigned int
long int
unsigned long int
long long int
unsigned long long int
这里的明显区别是:对于十进制字面量的候选序列通常是不包括无符号类型的,对于非十进制的字面量则包含无符号类型。
这里只能大致提供一个候选序列,实际上具体结果是和平台相关的(Linux和Windows对
long
和long 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
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 long
和long
在不同平台的区别,
次要原因是p4
对应的字面量过大:g++会将其处理为内置类型__int128
,clang++则将其处理为unsigned long long
。
除此之外,还可以通过指定后缀改变类型,采用与之对应的类型候选列表
U
或u
指定为无符号类型L
或l
指定为长整数类型LL
或ll
指定为长整数类型
注意:
- 整数字面量均不包括前面可能存在的正负号,编译器将正负号视作额外的一元运算。
- 在特定进制下,字面量必须使用合法范围内的字符,例如八进制只能使用
0-7
。 - 无符号后缀可以和其它后缀组合使用,组合的顺序任意。
- 在数位之间可以插入
'
作为分隔符,用于提供可读性,编译器会直接忽略。
浮点数字面值
浮点数字面值是包含小数点或科学计数法的数字,它们的类型可能是
float
、double
或
long double
。
通常的浮点数字面量都是十进制下的,指数记号为e
或E
,除此之外,C语言还支持十六进制的浮点数,指数记号为p
或P
(因为十六进制已经把e
占用了),要求字面量以0x
或0X
开头。
默认情况下浮点数字面值均为double
,也可以使用后缀指定为float
(f
或F
)或long double
(l
或L
)。
例如1e6
这类数字虽然在数学上就是整数,但是出现科学计数法的字面量在C++中就是浮点数,无论它有没有小数部分。
下面的两个定义是不等价的,第一个语句存在double
到int
的隐式类型转换
1
2int n1 = 1e6;
int n2 = 1000000;
在通常情况下两者不会产生差异,但是在极端情况下仍然存在着两类隐患:浮点数误差和整数范围溢出。
其中浮点数误差的问题可能是比较隐蔽的,例如 1
2
3
4
5
6
7
8
9
10
11
12
13
14
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字符等等,这里略过不做讨论。