Peter_Matthew的博客

C++内置数据类型与二进制存储

2021-08-01

本文共5.1k字,大约需要阅读22分钟。

平时,很多同学因为两个int类型的变量相乘变成了负数的问题而头疼,将变量换成了long long类型后便可以解决,但不知道为什么。本文笔者就将向大家介绍C++内置数据类型与二进制存储,以便大家理解。

C++内置数据类型

C++有非常多的数据类型,尤其是STL预写了十分多的数据类型,本文仅介绍原生内置的数据类型。

最基本的内置类型

C++最基本的内置类型有以下七种:

  1. 布尔型,关键字是bool
  2. 窄字符型,关键字是char
  3. 宽字符型,关键字是wchar_t
  4. 整型,关键字是int
  5. 单精度浮点型,关键字是float
  6. 双精度浮点型,关键字是double
  7. 无类型,关键字是void

基本的内置类型

同时,一个最基本的内置类型可以被以下的一个或者多个类型修饰符修饰:

  • signed
  • unsigned
  • short
  • long

因此,我们组合出了C++基本的内置类型。
下表列出了这些基本的内置类型在内存中占用的空间以及该类型的变量所能存储的最小值和最大值。
1字节为8位。

关键字([ ]表示可省略) 占用空间(字节) 变量范围 备注
bool 1 $0$~$1$
char 1 $-2^7$~$2^7-1$
signed char 1 $-2^7$~$2^7-1$
unsigned char 1 $0$~$2^8-1$
wchar_t 2 $0$~$2^{16}-1$
signed wchar_t 2 $0$~$2^{16}-1$
unsigned wchar_t 4 $0$~$2^{32}-1$
int 2或4 $-2^{15}$~$2^{15}-1$ 或 $-2^{31}$~$2^{31}-1$ 16位系统为2字节,32位和64位系统为4字节
signed [int] 2或4 $-2^{15}$~$2^{15}-1$ 或 $-2^{31}$~$2^{31}-1$ 16位系统为2字节,32位和64位系统为4字节
unsigned [int] 2或4 $0$~$2^{16}-1$ 或 $0$~$2^{32}-1$ 16位系统为2字节,32位和64位系统为4字节
short [int] 2 $-2^{15}$~$2^{15}-1$
signed short [int] 2 $-2^{15}$~$2^{15}-1$
unsigned short [int] 2 $0$~$2^{16}-1$
long [int] 4或8 $-2^{31}$~$2^{31}-1$ 或 $-2^{63}$~$2^{63}-1$ GCC、Clang 等实现中,64位代码的long类型为8字节,而MSVC中则维持4字节
signed long [int] 4或8 $-2^{31}$~$2^{31}-1$ 或 $-2^{63}$~$2^{63}-1$ GCC、Clang 等实现中,64位代码的long类型为8字节,而MSVC中则维持4字节
unsigned long [int] 4或8 $0$~$2^{32}-1$ 或 $0$~$2^{64}-1$ GCC、Clang 等实现中,64位代码的long类型为8字节,而MSVC中则维持4字节
long long [int] 8 $-2^{63}$~$2^{63}-1$
signed long long [int] 8 $-2^{63}$~$2^{63}-1$
unsigned long long [int] 8 $0$~$2^{64}-1$
float 4 $\pm 1.17549\times{10}^{-38}$~$\pm 3.40282\times{10}^{+38}$ ,有效数字7位
double 8 $\pm 2.22507\times{10}^{-308}$~$\pm 1.79769\times{10}^{+308}$ ,有效数字15位
long double 10或16 $\pm 3.3621\times{10}^{-4932}$~$\pm 1.18973\times{10}^{+4932}$ ,有效数字18位或33位 在大多数平台上的实现与double相同,实现由编译器定义。

如上表所示,加signed修饰符和不加修饰符的内存占用和数据范围一致,一般无需添加。

上表所有的数据会由于编译器与系统环境的不同而有所差异。
读者可以运行如下代码查看自己电脑对应的数值。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include<iostream>  
#include<limits>
using namespace std;
int main()
{
cout << "bool: \t\t\t" << "所占字节数:" << sizeof(bool);
cout << "\t最大值:" << (numeric_limits<bool>::max)();
cout << "\t\t\t最小值:" << (numeric_limits<bool>::min)() << endl;
cout << "char: \t\t\t" << "所占字节数:" << sizeof(char);
cout << "\t最大值:" << (int)(numeric_limits<char>::max)();
cout << "\t\t\t最小值:" << (int)(numeric_limits<char>::min)() << endl;
cout << "signed char: \t\t" << "所占字节数:" << sizeof(signed char);
cout << "\t最大值:" << (int)(numeric_limits<signed char>::max)();
cout << "\t\t\t最小值:" << (int)(numeric_limits<signed char>::min)() << endl;
cout << "unsigned char: \t\t" << "所占字节数:" << sizeof(unsigned char);
cout << "\t最大值:" << (int)(numeric_limits<unsigned char>::max)();
cout << "\t\t\t最小值:" << (int)(numeric_limits<unsigned char>::min)() << endl;
cout << "wchar_t: \t\t" << "所占字节数:" << sizeof(wchar_t);
cout << "\t最大值:" << (numeric_limits<wchar_t>::max)();
cout << "\t\t\t最小值:" << (numeric_limits<wchar_t>::min)() << endl;
cout << "signed wchar_t: \t" << "所占字节数:" << sizeof(signed wchar_t);
cout << "\t最大值:" << (numeric_limits<signed wchar_t>::max)();
cout << "\t\t\t最小值:" << (numeric_limits<signed wchar_t>::min)() << endl;
cout << "unsigned wchar_t: \t" << "所占字节数:" << sizeof(unsigned wchar_t);
cout << "\t最大值:" << (numeric_limits<unsigned wchar_t>::max)();
cout << "\t\t最小值:" << (numeric_limits<unsigned wchar_t>::min)() << endl;
cout << "int: \t\t\t" << "所占字节数:" << sizeof(int);
cout << "\t最大值:" << (numeric_limits<int>::max)();
cout << "\t\t最小值:" << (numeric_limits<int>::min)() << endl;
cout << "signed int: \t\t" << "所占字节数:" << sizeof(signed int);
cout << "\t最大值:" << (numeric_limits<signed int>::max)();
cout << "\t\t最小值:" << (numeric_limits<signed int>::min)() << endl;
cout << "unsigned int: \t\t" << "所占字节数:" << sizeof(unsigned int);
cout << "\t最大值:" << (numeric_limits<unsigned int>::max)();
cout << "\t\t最小值:" << (numeric_limits<unsigned int>::min)() << endl;
cout << "short int: \t\t" << "所占字节数:" << sizeof(short int);
cout << "\t最大值:" << (numeric_limits<short int>::max)();
cout << "\t\t\t最小值:" << (numeric_limits<short int>::min)() << endl;
cout << "signed short int: \t" << "所占字节数:" << sizeof(signed short int);
cout << "\t最大值:" << (numeric_limits<signed short int>::max)();
cout << "\t\t\t最小值:" << (numeric_limits<signed short int>::min)() << endl;
cout << "unsigned short int: \t" << "所占字节数:" << sizeof(unsigned short int);
cout << "\t最大值:" << (numeric_limits<unsigned short int>::max)();
cout << "\t\t\t最小值:" << (numeric_limits<unsigned short int>::min)() << endl;
cout << "long int: \t\t" << "所占字节数:" << sizeof(long int);
cout << "\t最大值:" << (numeric_limits<long int>::max)();
cout << "\t\t最小值:" << (numeric_limits<long int>::min)() << endl;
cout << "signed long int: \t" << "所占字节数:" << sizeof(signed long int);
cout << "\t最大值:" << (numeric_limits<signed long int>::max)();
cout << "\t\t最小值:" << (numeric_limits<signed long int>::min)() << endl;
cout << "unsigned long int: \t" << "所占字节数:" << sizeof(unsigned long int);
cout << "\t最大值:" << (numeric_limits<unsigned long int>::max)();
cout << "\t\t最小值:" << (numeric_limits<unsigned long int>::min)() << endl;
cout << "long long int: \t\t" << "所占字节数:" << sizeof(long long int);
cout << "\t最大值:" << (numeric_limits<long long int>::max)();
cout << "\t最小值:" << (numeric_limits<long long int>::min)() << endl;
cout << "signed long long int: \t" << "所占字节数:" << sizeof(signed long long int);
cout << "\t最大值:" << (numeric_limits<signed long long int>::max)();
cout << "\t最小值:" << (numeric_limits<signed long long int>::min)() << endl;
cout << "unsigned long long int: " << "所占字节数:" << sizeof(unsigned long long int);
cout << "\t最大值:" << (numeric_limits<unsigned long long int>::max)();
cout << "\t最小值:" << (numeric_limits<unsigned long long int>::min)() << endl;
cout << "float: \t\t\t" << "所占字节数:" << sizeof(float);
cout << "\t最大值:" << (numeric_limits<float>::max)();
cout << "\t\t最小值:" << (numeric_limits<float>::min)() << endl;
cout << "double: \t\t" << "所占字节数:" << sizeof(double);
cout << "\t最大值:" << (numeric_limits<double>::max)();
cout << "\t\t最小值:" << (numeric_limits<double>::min)() << endl;
cout << "long double: \t\t" << "所占字节数:" << sizeof(long double);
cout << "\t最大值:" << (numeric_limits<long double>::max)();
cout << "\t\t最小值:" << (numeric_limits<long double>::min)() << endl;
return 0;
}

二进制存储

不知道有没有同学好奇过为什么这些类型变量的数据范围是这样的,笔者在这里向大家讲述下这些范围是怎么得出来的。

二进制

相信很多同学们都看出来了,这些变量的范围大多都与2有关,这是因为计算机内部存储数据时使用二进制,在向用户显示时使用八进制、十进制、十六进制等进制。

二进制,就是逢二进一的一种进制。对于整数部分,比如 $13_{(10)}$ ,由于 $13_{(10)}=8+4+1=2^3+2^2+2^0$ ,转化为二进制就是 $1101_{(2)}$ ;对于小数部分,比如 $0.6875_{(10)}$ ,由于 $0.6875_{(10)}=0.5+0.125+0.0625=2^{-1}+2^{-3}+2^{-4}$ ,转化为二进制就是 $0.1011_{(2)}$ 。

在计算机中,这些类型的变量存储都是采用二进制的,一般1字节就表示有8位,2字节就表示有16位,以此类推。但是布尔型(关键字bool)和长双精度浮点型(关键字long double)是个例外,布尔型占用1字节但仅使用1位,长双精度浮点数在部分平台上内存占16个字节,但仅使用80位。

范围为整数的类型的计算方法

对于非负整数,直接使用二进制表示,所有关键字以unsigned开头的类型,保存的都是非负整数。例如,对unsigned char,是1字节8位无符号二进制数, $13_{(10)}$ 表示为 $00001101_{(2)}$ 。因此,对于这种类型,最小值就是全0, $00000000_{(2)}$ 即 $0_{(10)}$ ,最大值就是全1, $11111111_{(2)}$ 即 $255_{(10)}$ 也就是 $2^8-1_{(10)}$ 。需要注意的是,如果已经到了最大值 $11111111_{(2)}$ 再加1的话,就会变到最小值 $00000000_{(2)}$ ,如果再加1就是 $00000001_{(2)}$ 。
对于负整数,我们将其分为符号和数字两部分。对于符号,由于二进制无法直接保存符号,我们需要转化,由于一个数只有有符号和无符号两种情况,因此我们取变量中最前面的一位作为符号位,0表示无符号,1表示有符号;对于数字,有原码、反码、补码和移码四种编码方式,在下文介绍。

原码

原码是指负数与其相反数原来的编码相同,即一个数与其相反数只有符号位不同。例如,对char,是1字节8位二进制数, $-13_{(10)}$ 用原码表示为 $10001101_{(2)}$ 。
这种表示方法有两个问题:

  1. 正数运算与负数运算不同,混用很不方便。
    例如, $12_{(10)}$ 、 $13_{(10)}$ 、 $-12_{(10)}$ 和 $-13_{(10)}$ 分别表示为 $00001100_{(2)}$ 、 $00001101_{(2)}$ 、 $10001100_{(2)}$ 和 $10001101_{(2)}$ 。
    在十进制下从 $12_{(10)}$ 变到 $13_{(10)}$ 是加1,在二进制下 $00001100_{(2)}$ 变到 $00001101_{(2)}$ 是加1,即对于正数,十进制加1是二进制加1。
    但在十进制下 $-13_{(10)}$ 变到 $-12_{(10)}$ 是加1,在二进制下 $10001101_{(2)}$ 变到 $10001100_{(2)}$ 是减1,即对于负数,十进制加1是二进制减1。
    因此不统一。
  2. 存在两个0。
    即存在 $00000000_{(2)}$ 和 $10000000_{(2)}$ 两个 $0_{(10)}$ 。
    在十进制下从 $-1_{(10)}$ 加到 $1_{(10)}$ 需要加2,即从 $-1_{(10)}$ 加1变到 $0_{(10)}$ ,再加1变到 $1_{(10)}$ 。
    但在二进制下从 $10000001_{(2)}$ 加到 $00000001_{(2)}$ 需要加3,即从 $10000001_{(2)}$ 加1变到 $10000000_{(2)}$ ,再加1变到 $00000000_{(2)}$ ,然后再加1变到 $00000001_{(2)}$ 。

反码

反码是指负数与其相反数原来的编码相反,即一个数与其相反数的每一位都不一样。例如,对char,是1字节8位二进制数, $-13_{(10)}$ 用反码表示为 $11110010_{(2)}$ 。
这种表示方法解决了一个问题:

  1. 正数运算与负数运算不同的问题。
    例如, $12_{(10)}$ 、 $13_{(10)}$ 、 $-12_{(10)}$ 和 $-13_{(10)}$ 分别表示为 $00001100_{(2)}$ 、 $00001101_{(2)}$ 、 $11110011_{(2)}$ 和 $11110010_{(2)}$ 。
    在十进制下从 $12_{(10)}$ 变到 $13_{(10)}$ 是加1,在二进制下 $00001100_{(2)}$ 变到 $00001101_{(2)}$ 是加1,即对于正数,十进制加1是二进制加1。
    在十进制下 $-13_{(10)}$ 变到 $-12_{(10)}$ 是加1,在二进制下 $11110010_{(2)}$ 变到 $11110011_{(2)}$ 是加1,即对于负数,十进制加1是二进制加1。
    由此统一。

但这种表示方法还存在一个问题:

  1. 存在两个0。
    即存在 $00000000_{(2)}$ 和 $11111111_{(2)}$ 两个 $0_{(10)}$ 。
    在十进制下从 $-1_{(10)}$ 加到 $1_{(10)}$ 需要加2,即从 $-1_{(10)}$ 加1变到 $0_{(10)}$ ,再加1变到 $1_{(10)}$ 。
    但在二进制下从 $11111110_{(2)}$ 加到 $00000001_{(2)}$ 需要加3,即从 $11111110_{(2)}$ 加1变到 $11111111_{(2)}$ ,因为已经全是1的缘故所以再加1变到 $00000000_{(2)}$ ,然后再加1变到 $00000001_{(2)}$ 。

补码

补码是在反码的基础上通过将负数所有的数的编码加1得到的。例如,对char,是1字节8位二进制数, $-13_{(10)}$ 用反码表示为 $11110011_{(2)}$ 。
这种表示方法又解决了一个问题:

  1. 两个0的问题。
    即这种表示方法只存在 $00000000_{(2)}$ 一个 $0_{(10)}$ 。
    在十进制下从 $-1_{(10)}$ 加到 $1_{(10)}$ 需要加2,即从 $-1_{(10)}$ 加1变到 $0_{(10)}$ ,再加1变到 $1_{(10)}$ 。
    在二进制下从 $11111111_{(2)}$ 加到 $00000001_{(2)}$ 也只需要加2,即 $11111111_{(2)}$ 因为已经全是1的缘故所以加1变到 $00000000_{(2)}$ ,再加1变到 $00000001_{(2)}$ 。

计算机内部对于负数采用的是补码的表示方法。那么,对char,是1字节8位无符号二进制数,最小值就是符号位为1,其他位为0, $10000000_{(2)}$ 即 $-128_{(10)}$ 也就是 $-2^7_{(10)}$ ,最大值就是符号位为0,其他位为1, $01111111_{(2)}$ 即 $127_{(10)}$ 也就是 $2^7-1_{(10)}$ 。

移码

移码,是取消最高位作为符号位的设定,转而将所有数字向正数方向移动一定数值,使负数最小值为二进制全0,正数最大值为二进制全1。
移码是在补码的基础上提出的一种编码方式,思想与补码移动一位弥补反码的思路一致。一般,对于一个n位二进制数,其移码的偏移值为 $2^{n-1}-1$ ,例如,对于一个1字节8位二进制数,其移码的偏移值位 $2^{8-1}-1=127_{(10)}$ ,如果用移码表示 $13_{(10)}$ 和 $-13_{(10)}$ ,就需要先对其加上偏移值得到 $140_{(10)}$ 和 $114_{(10)}$ ,然后转换位二进制得到 $10001100_{(2)}$ 和 $01110010_{(2)}$ 。
当然,偏移值也可以为其他值。

一般整数不常用移码进行表示,除了浮点数的指数部分。

浮点数范围的计算方法

刚才介绍的是大部分类型变量的数据范围计算的方法,下面我们介绍一下比较特别的一类数据类型——浮点数。
浮点数,是属于有理数中某特定子集的数的数字表示,在计算机中用以近似表示任意某个实数。例如,0.00210245、5012.35和3140520000都是浮点数。
浮点数在计算机中表示时使用的是一种以2为底数科学计数法。我们首先了解下以10为底数的科学计数法是怎么表示一个数的,科学计数法分为三部分,符号、指数和因数三部分。符号是指正负号,指数是指十次方幂的指数,因数就是和幂相乘的因数,因数大于等于1小于10,因数又分为整数部分和小数部分。例如对于 $5012.35$ ,应当写成 $5.01235 \times 10^3$ ,其本质是 $( 5 \times 10^{0} + 1 \times 10^{-2} + 2 \times 10^{-3} + 3 \times 10^{-4} + 5 \times 10^{-5} ) \times 10^3$ 。
以2为底数科学计数法与以10为底的科学计数法类似,例如对于 $5012.35_{(10)}$ 即 $1001110010100.010110011001100110011001100110011001101_{(2)}$ ,应当写成 $1.001110010100010110011001100110011001100110011001101 \times 2^{12}$ ,其本质是 $( 1 \times 2^{0} + 1 \times 2^{-3} + 1 \times 2^{-4} + 1 \times 2^{-5} + \cdots + 1 \times 2^{-51} ) \times 2^{12}$ 。

结构

C/C++语言编辑器采用IEEE 754标准,对float和double都采用下图所示的符号、指数和小数三个域划分,整数位不写入内存。
IEEE 754浮点数的三个域
比较特别的一个是long double,采用符号、指数、整数、小数四个域划分。
三个关键字每个域对应的长度如下表所示:

关键字 符号 指数 整数 小数 总长度
float 1 8 0 23 32
double 1 11 0 52 64
long double 1 15 1 63 80

float的示意图如下:
float三个域的示意图
double的示意图如下:
double三个域的示意图
long double的示意图如下:
long double三个域的示意图
这里的指数使用的是偏移值为 $2^{n-1}-1$ 的移码(已在上文介绍)。
按照这个规则,大家就会得到5012.35转换得到的三个类型的数
float: $ \color{lightskyblue}{0}\color{lightseagreen}{10001011}\color{hotpink}{00111001010001011001101} $ (近似处理)
double: $ \color{lightskyblue}{0}\color{lightseagreen}{10000001011}\color{hotpink}{0011100101000101100110011001100110011001100110011010} $
long double: $ \color{lightskyblue}{0}\color{lightseagreen}{100000000001011}\color{pink}{1}\color{hotpink}{001110010100010110011001100110011001100110011001101000000000000} $

规约形式的浮点数

如果浮点数指数的移码值不为全0和全1(全0和全1有特殊用途),且在科学计数法的表示方式下,整数部分是1(小数部分值为全0到全1),那么这个浮点数被称为规约形式的浮点数。“规约”是指用唯一确定的浮点形式表示一个值。

非规约形式的浮点数

如果浮点数指数的移码值为全0 ,小数部分非全0,那么这个浮点数是非规约形式的浮点数。一般只有某个数相当接近0的时候才会使用非规约形式的浮点数表示。
实际上,非规约形式的浮点数是可以正常使用的,只是它的绝对值小于所有规约形式的浮点数,更接近0。
需要注意的是,对非规约形式的浮点数,虽然其指数的移码值为全0,但值并不是最小值,这是因为,对非规约形式的浮点数,其指数的移码偏移值为 $2^{n-1}-2$ ,例如float非规约形式的浮点数其指数为-126而不是-127。
同时,非规约形式的浮点数其因数的整数部分为0,整个因数大于0小于1。

特殊值

这里有三个特殊值需要指出:

  1. 如果指数为全0并且因数的小数部分是全0,这个数是±0(取决于符号位)。
  2. 如果指数为全1并且因数的小数部分是全0,这个数是±∞(取决于符号位)。
  3. 如果指数为全1并且因数的小数部分不是全0,这表示的不是一个数字(NaN,Not a Number)。

以上,对浮点数的形式总结如下

形式 指数 小数部分
0 全0 全0
非规约形式 全0 不为全0
规约形式 不为全0和全1 无要求
全1 全0
NaN 全1 不为全0

由此,我们计算出了三类浮点数规约形式的数据范围。

关于精度

float、double和long double的小数部分存储位数分别为23、52、63,所以一共记录的二进制数位数就为24、53、64。
$\lg 2^{24}\approx 7.22$
$\lg 2^{53}\approx 15.95$
$\lg 2^{64}\approx 19.27$
由上计算可得,float可保证7位十进制有效数字,double可保证15位十进制有效数字,long double可保证19位十进制有效数字。

参考

中文维基百科:
数据类型 (C语言)
有符号数处理
IEEE 754


知识共享许可协议

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏