数据在内存中的存储
- 导读
- 一、计算机中的数据类型
- 二、整数在计算机中的存储
- 2.1 整数的存储形式——原码、反码与补码
- 2.2 三种形式之间的相互转换
- 2.3 采用补码存储整数的原因
- 三、大小端字节序与字节序判断
- 3.1 大端存储与小端存储
- 3.2 为什么会出现大小端存储?
- 3.3 大端存储与小端存储的判断
- 四、浮点数在计算机中的存储
- 4.1 浮点数的表示格式
- 4.2 浮点数的数据范围
- 4.3 浮点数的二进制形式
- 4.4 浮点数的存储
- 4.5 浮点数的获取
- 4.6 注意事项
- 五、浮点数与整数的相关问题探讨
- 5.1 整数与浮点数之间的隐式类型转换
- 5.2 强制类型转换
- 5.3 小结
- 结语
导读
大家好,很高兴又和大家见面啦!!!
我们知道,计算机刚开始问世时主要是用于计算导弹的弹道的,因此计算机就需要具备比较强大的计算功能。而在进行计算时肯定是无法摆脱一个问题——计算的数据如何存储。
在前面的学习中我们简单的了解了一下整型在计算机中的存储,可是导弹的弹道并不会正正好好都是使用的整型运算,其中肯定会夹杂着各种各样的浮点型的数据的运算,那么浮点型的数据在内存中又是应该如何进行存储的呢?在今天的内容中我们将会详细的探讨计算机中的数据的存储方式……
一、计算机中的数据类型
在C语言中我们学习的数据类型有7个基本类型:字符类型、短整型、整型、长整型、更长的整型、单精度浮点型、双精度浮点型……当然还有像布尔类型、指针类型等等的其它数据类型。但是计算机在对不同类型的数据进行存储时,并不会将其进行区分,都是统一的以二进制的形式来存储各式各样的数据。
在我看来像字符型、布尔类型、指针类型……这些我们都可以看做是整型的拓展类型。
比如在字符类型中,每一个字符有与其对应的整型值;在布尔类型中,无非就是两个值——true
和 false
,它们同样有对应的整型值——1
和 0
;在指针类型中,数据则是以十六进制的形式进行存储的,对于十六进制的数据而言,我们同样可以将其转换成十进制的整型数据。
因此,在计算机中真实进行存放的数据类型实际上就是两种类型——整型与浮点型。接下来我们将会分别探讨整型数据在计算机中的存储与浮点型的数据在计算机中的存储。
二、整数在计算机中的存储
2.1 整数的存储形式——原码、反码与补码
在计算机中,整数分为无符号整数和有符号整数。
在之前我们有学习过,对于有符号整数而言,数值的二进制表示有三种形式——原码、反码与补码。
这三种表示形式都是由符号位与数值位两部分组成。符号位指的是二进制序列中的最高位:
- 符号位为0表示正数
- 符号位为1表示负数
数值位指的数除最高位以外的剩余的二进制位,因此对于一个int类型的数据而言,其正数的范围是 0 ~ 2 31 − 1 0~2^{31}-1 0~231−1,其负数的范围是 − 2 31 ~ − 1 -2^{31}~-1 −231~−1。
计算机在存储有符号整数时,无论正负,存储在内存中的都是其对应的补码。
而无符号整数所对应的二进制形式只有一种——通过数值的进制运算获取的二进制序列。我们可以将其理解为在无符号整数中,其二进制位都是数值位,不存在符号位。因此,无符号整型在内存中进行存储时,存储的是其数值所对应的二进制序列。
2.2 三种形式之间的相互转换
在有符号正整数中其数值所对应的三种二进制形式是相等的,即: 原码 = 反码 = 补码 原码 = 反码 = 补码 原码=反码=补码,因此我们通过进制转换获取到数值所对应的原码时,同时也获得了其对应的反码与补码。
在有符号负整数中其数值所对应的三种二进制形式是不同的,我们需要通过相应的转换才能获取其对应的二进制形式:
- 原码——通过数值的进制转换直接获取
- 反码——通过原码的数值位按位取反进行获取
- 补码——通过反码+1进行获取
这里我们可以通过编写一个函数来进行验证,如下所示:
这里我们通过函数分别获取到了数值3与-3在内存中存储的二进制序列。
当我们对3进行进制转换时,我们可以很容易的得到其对应的二进制序列为:
0000 0000 0000 0000 0000 0000 0000 0011
那么-3所对应的二进制序列则为:
1000 0000 0000 0000 0000 0000 0000 0011
但是从程序的输出结果中来看,我们会发现正数3的结果与我们直接计算获取的结果是一致的,而负数-3的结果与我们直接计算获取的结果是有差距的,而它们之间的差距正号就是前面我们介绍的负数补码的获取方式——原码按位取反再加1。
有细心的朋友会发现,对于负数的补码而言,如果我们将其进行按位取反再加1同样也能够得到其原码,因此对于负整数而言,其原码与补码之间的转换都可以通过按位取反再加1的方式获取。
从这次的测试结果我们可以得到以下结论:
- 有符号整数在内存中是以补码的形式进行存储
- 正整数的原码、反码与补码相等
- 负整数的原码按位取反得反码,反码+1得补码
- 负整数的补码按位取反再+1得原码
2.3 采用补码存储整数的原因
计算机在存储整数时之所以统一采用补码的方式,是因为使用补码,可以将符号位和数值域统一处理;
同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
三、大小端字节序与字节序判断
现在我们已经知道了对于无符号整型的数据而言,它们在内存中是直接以二进制的形式进行存储,而对于有符号整型而言,它们在内存中则是以补码的形式进行存储。
计算机在存储数据时,不同的数据类型所占用的内存空间也不相同:
- 字符类型/布尔类型占用1个字节的空间
- 短整型占用2个字节的空间
- 整型/单精度浮点型/指针类型占用4个字节的内存空间
- 长整型/双精度浮点型占用8个字节的内存空间(在有些环境中长整型也是占用4个字节)
对于这些内存所需内存空间超过1个字节的数据而言这时我们很容易的想到,它们在内存中可能是从高位到低位依次进行存储,也可能是从低位到高位依次进行存储。那它们在内存中具体是如何进行存储的呢?下面我们就需要来了解一下数据存储的两种方式——大端存储与小端存储。
3.1 大端存储与小端存储
大端存储,我们可以理解为数据从高位到低位依次存储——高位的数据存放在低地址处,低位的数据存放在高地址处;
小端存储,我们可以理解为数据从低位到高位依次存储——低位的数据存放在低地址处,高位的数据存放在高地址处;
这里的低位与高位指的是数据所对应的二进制位,如下所示:
int a = 0x11223344;
对于这个整型变量而言,其低字节处的数值为十六进制的44,高字节处的数值为十六进制的11;而在函数栈帧中我们有介绍过,在内存中,从上往下,地址逐渐增加,因此高地址位于内存的下方,低地址位于内存的上方。
因此,当我们通过大端存储的方式来存储该变量时,该变量的数值在内存中应该是以低字节存储在高地址,高字节存储在低地址的方式顺序存储,即通过0x11223344
的方式进行进行存储,从上往下以单字节的形式表示则是:
11——不打印字符
22——"
33——3
44——D
左侧内容为存储的十六进制的数值,右侧内容为该数值所对应的ASCII码值中的字符。当我们通过小端存储的方式来存储该变量时,其数值在内存中应该是以低字节存储在低地址处,高字节存储在高地址处的方式逆序存储,即通过0x44332211
的方式进行存储,从上往下以单字节的形式表示则是:
44——D
33——3
22——"
11——不打印字符
接下来我们就来通过内存窗口来验证一下该变量在内存中是如何进行存储的:
从内存窗口中我们不难发现,该数值的存储方式符合小端存储的方式。那是不是说明数据在内存中就是以小端存储的方式进行存储的呢?
实际上不管是大端存储还是小端存储都是计算机存储数据的方式,只不过在不同的环境中,计算机所采用的存储方式不相同,比如在x86
的环境中,计算机采用的是小端存储的方式来存储数据,而KEIL C51
中则是通过大端存储的方式来存储数据。
3.2 为什么会出现大小端存储?
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8
bit 位,但是在C语言中除了8 bit 的char 之外,还有16 bit 的short 型,32 bit 的long 型(要看
具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
也就是说,不管是大端存储还是小端存储我们都需要根据具体的存储环境来进行判断,并且当一个数据存储在内存中时,我们可以根据数据的存储顺序来进行区分:
- 从高位到低位顺序存储的是大端存储
- 从高位到低位逆序存储的是小端存储
3.3 大端存储与小端存储的判断
在了解了什么大小端存储之间的区别后,接下来如果我们要判断所使用的环境为哪种存储方式时,我们只需要通过获取首字节的元素的值即可进行判断。
比如,当我们在存储值0x00000001
时,如果环境采用的是大端存储,那么其首字节的值应该是0,如果采用的是小端存储,其首字节的值应该是1,因此,我们就可以首字节的值来进行具体的判断,如下所示:
当然,如果我们在今后面试的过程中有遇到需要判断该环境为大端存储还是小端存储时,如果在已知了数据在内存中的存储形式,我们可以通过数值的存储顺序来进行判断;如果未知数值的存储形式,我们则可以通过该代码来进行判断。
四、浮点数在计算机中的存储
浮点数在内存中同样是以二进制的形式进行存储,相比于有符号整型的符号位与数值为的存储形式,浮点数在存储时,是采用的另一中存储形式来存储数据。
4.1 浮点数的表示格式
根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
V
=
(
−
1
)
S
∗
M
∗
2
E
V = (−1)^S ∗ M ∗ 2^E
V=(−1)S∗M∗2E
- ( − 1 ) S (−1)^S (−1)S表示符号位,当S = 0,V为正数;当S = 1,V为负数
- M 表示有效数字,M是大于等于1,小于2的
- 2 E 2^E 2E 表示指数位
4.2 浮点数的数据范围
在浮点数中有float类型、double类型以及long double类型,其数据类型的数据范围存在于头文件<float.h>
中。如下所示:
这里我们展示的是正浮点数的取值访问,对于浮点数而言,它在不同条件下的取值范围都被整合在了该头文件中,有兴趣的朋友可以对照头文件的内容来尝试打印不同条件下的浮点数的取值范围,头文件网址给大家奉上:<float.h>大家点击链接即可进入对应的网站,这里我就不再过多的赘述。
4.3 浮点数的二进制形式
浮点数在进行存储时,其对应的二进制形式与其表示格式一样,被分为了3个部分——S、E、M。
- 最高位表示符号位,存储S的值:
- S=0,表示正数
- S=1,表示负数
- S之后存储E的值,此时的E表示的是无符号整型:
- 32位中,S后的8个比特位存储的是E的值,在进行存储时为了避免负值的出现,需要加上中间值127,再进行存储
- 64位中,S后的11个比特位存储的是E的值,在进行存储时为了避免负值的出现,需要加上中间值1023,再进行存储
- E之后存储的是M的值,此时的M表示的是一个
1
<
=
M
<
=
2
1<=M<=2
1<=M<=2的值,在存储时只存储小数部分:
- 32位中,E后的23个比特位用于存储M的值,从左到右依次表示的是 2 − 1 ~ 2 − 23 2^{-1}~2^{-23} 2−1~2−23
- 64位中,E后的52个比特位用于存储M的值,从左到右依次表示的是 2 − 1 ~ 2 − 52 2^{-1}~2^{-52} 2−1~2−52
如下图所示:
4.4 浮点数的存储
当我们要存储一个浮点数时,我们需要通过以下几个步骤:
- 获取浮点数对应的二进制数值,如5.5对应的二进制数值为101.1;
- 将二进制数值改写成科学计数法的形式,如101.1改写成 1.011 × 2 2 1.011 × 2^2 1.011×22
- 将科学计数法的形式改写成浮点数的格式:如 1.011 × 2 2 1.011 × 2^2 1.011×22改写成 ( − 1 ) 0 × 1.011 × 2 2 (-1)^0 × 1.011 × 2^2 (−1)0×1.011×22
- 获取浮点数格式中的 S E M SEM SEM值,如 ( − 1 ) 0 × 1.011 × 2 2 (-1)^0 × 1.011 × 2^2 (−1)0×1.011×22中 S = 0 , E = 2 , M = 1.011 S = 0, E = 2, M = 1.011 S=0,E=2,M=1.011
- 根据
S
E
M
SEM
SEM值获取其对应的二进制形式,如
S
=
0
,
E
=
2
,
M
=
1.011
S = 0, E = 2, M = 1.011
S=0,E=2,M=1.011对应的二进制形式:
- S位:值为0,对应的二进制形式为
0
- E位:32位中
2
+
127
=
130
2+127=130
2+127=130,对应的二进制形式为
10000010
- M位:仅存储小数部分011,对应的二进制形式为
01100000000000000000000
- 完整的二进制形式为
0 10000010 01100000000000000000000
- S位:值为0,对应的二进制形式为
4.5 浮点数的获取
当我们要将存放在内存中的浮点数给取出来是,我们则需要根据E的取值来进行还原,此时E可能会出现3种情况:
- E为全0
- E为全1
- E既有0也有1
我们在进行存入时,由于E表示的是无符号整型,为了避免出现负值的存入,因此我们需要在存入之前为其值加上一个中间值:
- 32位的中间值为127
- 64位的中间值为1023
因此当要进行还原时,我们则需要给E此时存入的值减去一个中间值,这时就需要分情况进行讨论了:
- 当E为全0时,表示的是一个无限接近于0的无穷小数,因此,还原时使用1 - 中间数
- 当E为全1时,表示的是一个无限接近于无穷的无穷大数,因此,还原时使用E的最大值 - 中间数
- 当E既有0也有1时,表示的是一个正常的数,因此还原时直接用E的值 - 中间数
而对于有效数字M而言,正常情况下的存入都是省略整数部分的1,仅存入小数部分,因此我们在还原时需要加上整数部分的1;
但是在E为全0时,表示存入的数是一个无限接近于0的无穷小数,因此在还原时,是不需要加上整数部分的1;
当E为全1时,表示存入的数是一个无限接近于无穷大的数,因此在还原时,我们可以忽略小数部分。
4.6 注意事项
-
对于浮点数而言,它在进行存储时由于存储的是有效数字的小数部分,因此容易出现精度丢失的情况;
-
double类型与long double类型在进行存储时之所以精度要高于float类型,是因为float类型占用4个字节也就是32个比特位,而以double类型为例,double类型占用的是8个字节也就是64个比特位。在进行存储时double类型的有效数字部分能够使用的比特位要远多于float类型的有效数字部分能够使用的比特位,因此double类型与long double类型要比float类型更加的精确。
-
由于浮点型的数据在存储时会出现精度丢失的情况,因此我们在对两个浮点数进行比较时,不能直接通过
'=='
来进行比较,而是可以通过两个浮点数作差来判断差值的精度范围,当差值在指定的精度范围内时,我们则可以认为这两个浮点数的值相等
五、浮点数与整数的相关问题探讨
对于咱们现阶段而言,浮点数的存储我们只需要对上述的知识点有一个简单的了解即可,正常情况下我们不会遇到一些比较奇怪的问题,比如整数与浮点数进行混合运算时结果出现较大偏差的情况。
但是,不怕一万就怕万一,如果哪天我们真的遇到了此类问题,我们应该需要知道问题出在哪里,以及如何解决问题。
接下来我们要探讨的是整型与浮点型数据之间运算的问题。在学习操作符的时候,我们有学习过两种转换方式——隐式类型转换与强制类型转换。接下来我们就来依次探讨两种转换方式
5.1 整数与浮点数之间的隐式类型转换
在隐式类型转换中,不同类型的数值之间的运算遵循一个默认的转换方式:
- 对于不满4个字节的数据类型
char、short
而言,它们在与整型进行运算时会进行整型提升 - 对于字节数大于等于4个字节的数据类型
int、unsigned int、long、unsigned long、float、double、long double
而言,左侧的数据类型与右侧的数据类型进行混合运算时,左侧的数据类型会通过算术转换成右侧的数据类型。
因此我们在正常的进行整数与浮点数的混合运算时,其数值会通过隐式类型转换先转换成对应的浮点数类型,然后再进行计算,此时会遇到两种情况:
- 将计算后的值存入整型变量中
- 将计算后的值存入浮点型变量中
对于这两种不同的情况,会产生什么样的效果呢?接下来我们就来做个简单的测试:
从这次测试结果中我们可以获取到几个信息:
- 在进行整型与浮点型的混合运算时,运算的结果为float类型;
- 当直接将该值以整型的形式进行输出时,结果会出现错误
- 当直接以整型与浮点型的混合运算的形式输出结果时,会出现算术溢出的问题
这组测试很好的说明了一个问题——整型与浮点型进行运算时,整型的数值会隐式转换成浮点型的数值然后再进行计算。
从变量c与变量d的输出来看,直接对这两种类型进行运算时没有任何问题的;但是当我们直接对结果进行输出时,则会出现算术溢出的问题,溢出的原因是整型在运算时由4个字节转换到了8个字节,系统给出的解决方法是在调用+之前将值强制类型转换成8个字节即可,如下所示:
可以看到,当我们在以整型的形式输出时,将变量b强制类型转换成int
之后没有出现算术溢出的问题;当我们以浮点型的形式输出时,如果我们将int类型强制转换成float
类型之后,还是会存在算术溢出的问题,但是将其转换成double
类型之后,则不再存在算术溢出的问题。
这也就是说明在VS中,浮点型的数据进行计算时,会将float
类型的数据自动的提升到double
之后再进行计算,并且在运算的过程中对数据的强制类型转换不会影响运算结果。
那是不是所有的强制类型转换都不会影响运算结果呢?下面我们先来看一个例子:
从这个例子中可以看到,当我们正常的在运算过程中进行强制类型转换时,运算结果是不受影响的,但是当我们借助指针进行强制类型转换时,结果却出现了错误。这个是为什么呢?下面我们就来分析一下这个例子;
5.2 强制类型转换
对于一个整数而言,其二进制的形式仅由符号位与数值位组成,当我们通过指针将一个整型强制转换成浮点型时,由于存储形式发生了变化,其所对应的值也会发生变化。
如有符号整型5所对应的二进制形式为
0000 0000 0000 0000 0000 0000 0000 0101
该二进制形式既是整型5的补码,也是整型5的反码与原码,当我们将其强制转换成浮点型时,存储形式则会发生变化,转变成:
0 00000000 00000000000000000000101
此时对应的 S E M SEM SEM的值分别为:
S
=
0
S = 0
S=0
E
=
−
126
E = -126
E=−126
M
=
0.00000000000000000000101
M = 0.00000000000000000000101
M=0.00000000000000000000101
经过还原后我们得到的值为:
(
−
1
)
0
×
0.00000000000000000000101
×
2
−
126
(-1)^0 × 0.00000000000000000000101 × 2^{-126}
(−1)0×0.00000000000000000000101×2−126
不难发现,此时我们获得的值是一个无限接近于0的无穷小值;
对于一个浮点数而言,其二进制的形式是由 S E M SEM SEM组成,当我们将一个浮点数通过指针强制转换成整数时,与整型转换成浮点型一样,由于存储形式发生了变化,其值也会发生变化。
如浮点数5.0所对应的二进制数为101.0,将其转换成浮点数格式后得到的式子为:
(
−
1
)
0
×
1.010
×
2
2
(-1)^0 × 1.010 × 2^2
(−1)0×1.010×22
该式子所对应的
S
E
M
SEM
SEM的值分别为:
S
=
0
S = 0
S=0、
E
=
2
E = 2
E=2、
M
=
1.010
M = 1.010
M=1.010
在32位下,将其存入内存时所对应的二进制形式为:
0 10000001 01000000000000000000000
当我们将其强制转换成整型时,由于存储形式发生了改变,其对应的二进制形式则变为:
0100 0000 1010 0000 0000 0000 0000 0000
所对应的整型值则是 2 30 + 2 23 + 2 21 2^{30} + 2^{23} + 2^{21} 230+223+221,可以看到获得的整型值是一个远大于5的值,为了验证我们分析的正确性,接下来我们借助计算机来计算一下该值的大小:
可以看到正如我们分析的一样,当我们通过指针来进行浮点数与整数的强制类型转换时,此时会因为数值在内存中的存储形式发生了变化而导致问题的产生。
5.3 小结
下面我们就来对整数与浮点数之间的类型转换做个小结:
-
当我们在进行浮点数与整数之间的正常运算,不涉及指针时,那么在运算的过程中整型会隐式转换成浮点型然后进行运算,此时我们如果对其值进行强制类型转换是不会影响运算结果的;
-
当我们进行浮点数与整数之间的混合运算,涉及指针时,那么我们对指针进行强制类型转换,就会导致数据在内存中的存储形式发生变化,从而导致其值在完成转换后也会发生改变;
结语
在今天的内容中我们介绍了整型数据与浮点数据在内存中的存储形式与存储方式:
- 数据在内存中都是以二进制的形式进行存储
- 无符号整型数据在存储时,是以数值所对应的二进制形式存储在内存中
- 有符号整型数据在存储时,是以数值所对应的二进制补码的形式存储在内存中
- 浮点数在内存中是根据
S
E
M
SEM
SEM来进行存储的:
- 32位中,E在存储时值需要加上127
- 64位中,E在存储时只需要加上1023
- 数据在内存中的存储方式有两种:
- 大端存储:数据的低字节位的值存储在高地址处,高字节位的值存储在低地址处。大端存储的方式会将数据以顺序存储的形式存放在内存中;
- 小端存储:数据的低字节为的值存储在低地址处,高字节位的值存储在高地址处。小端存储的方式会将数据以逆序存储的形式存储在内存中;
今天的内容到这里就全部结束了,如果大家喜欢博主的内容,可以点赞、收藏加评论支持一下博主,当然也可以将博主的内容转发给你身边需要的朋友。最后感谢各位朋友的支持,咱们下一篇再见!!!