学无先后,达者为师

网站首页 编程语言 正文

C语言-剖析数据是如何在内存中存储的(整型与浮点型)

作者:c铁柱同学 更新时间: 2022-02-12 编程语言

本篇主要是深度剖析整型与浮点型数据在内存中是如何储存与读取的。弄明白数据在内存中的存储方式有助于我们更好的理解代码。

1.整型在内存中的存储

1.1有符号整型

在研究整型变量的存储之前,我们要知道,整型有以下几种类型

char   signed char
       unsigned char
short  signed short [int]
       unsigned short [int]
int    signed int
       unsigned int
long   signed long [int]
       unsigned long [int]

short 与long的全称是short int 与long int,后面的int可以省略,但是为什么会有char这个类型呢?,我们知道char类型叫做字符型,它表示的是用来存储一个字符的类型,把它作为整型是因为字符在内存中是以ASCII值储存的,所以我们把char型也归为整型。每一个类型又细分出signed和unsigned两个类型,叫做有符号型和无符号型,在我们不写有没有符号时,short,int,long这三个类型默认是有符号类型,只有char类型在不写时是有符号类型还是无符号类型是不确定的,取决于编译器,我使用的是vs2022,这个编译器下char默认为signed char。

当我们在代码中创建一个变量时,我们知道要在内存上开辟一块空间来储存这个变量,那么整型变量在内存中到底是怎么存储的呢?我们用代码来看一下。

我们创建了a与b两个整型变量,然后把3赋给a,-1赋给b,然后我们再在内存中寻找a与b,如上图,靠左边的第一个行就是b的地址与b内存储的数值,靠右边的第一行就是a的地址与a中存储的元素,vs在展示内存时,为了方便展示,显示的是十六进制的数据,而数据在内存中是以二进制存储的,一个字节的大小是八个比特位,八个比特位有2^8种数=16^2,刚好可以用两位十六进制数表示,所以在上图中存放数据的位置每两位表示一个字节,因为我们存放的是整型占四个字节,所以我把内存显示的宽度调为四列,方便我们观察。

我们知道,计算机中的有符号数有三种表示方法:原码,反码,补码。

这三种方法都有符号位和数值位,他们二进制位的第一位就是符号位,0表示正数,1表示负数,其他位就是数值位。那么这三种表示方法都是怎么得到的呢?

我们直接给出结论:正数的原码,反码,补码相同,负数的原,反,补码由以下规则计算

原码:直接把数转换成二进制序列

反码:原码的二进制序列符号位不变,数值位按位取反

补码:反码的二进制序列加一

现在我们知道了这三种表示形式都是怎么得到的,那么计算机在存储时到底使用的是哪一种呢?我们来计算一下

首先,我们来算3

因为3是正数,那么他的原,反,补码都是相同的,为

(为了方便观察,我会在后面每次展示二进制序列时每隔八位打一个空格)

00000000 00000000 00000000 00000011--3(二进制)
0   0    0   0    0   0    0   3   --3(十六进制)

我们再来看-1

原码:10000000 00000000 00000000 00000001-- -1(二进制原码)
反码:11111111 11111111 11111111 11111110-- -1(二进制反码)
补码:11111111 11111111 11111111 11111111-- -1(二进制补码)
补码:f   f    f   f    f   f    f   f   -- -1(十六进制补码)

经过一个简单的计算我们可以发现,我们内存中存储的其实是补码,那么为什么内存存储的是补码呢?

因为CPU只有加法器,当我们想要计算比如1-1时,计算机会模拟成1-(-1)这时候我们如果用原码来计算,那么结果是这样的

00000000 00000000 00000000 00000001--(1)
10000000 00000000 00000000 00000001--(-1)
10000000 00000000 00000000 00000010--(1+(-1))==(-2)?

我们发现这个值等于-2,是不正确的,如果我们使用补码来计算

 10000000 00000000 00000000 00000001--(-1)二进制原码
 11111111 11111111 11111111 11111110--(-1)二进制反码
 11111111 11111111 11111111 11111111--(-1)二进制补码
 00000000 00000000 00000000 00000001--(1)二进制补码
100000000 00000000 00000000 00000000--(1+(-1))的二进制补码
 00000000 00000000 00000000 00000000--(1+(-1))的二进制原码=0

我们发现,使用补码计算的结果是正确的,使用补码还有一个好处

11111111 11111111 11111111 11111111--(-1)二进制补码
10000000 00000000 00000000 00000000--对(-1)的二进制补码按位取反
10000000 00000000 00000000 00000001--又得到了(-1)的原码

即我们可以使用相同的方法完成原码与补码之间的转换。

使用补码可以将符号位和数值位统一处理,同时,加法与减法也可以统一处理。

1.2无符号整型

以上我们就明白了整型数是怎么在内存中存储的,但是注意,我在定义a与b时使用的是int,我们说每个整形都有两个细分类型,分别是有符号整型与无符号整型,在我们前面没有写的时候默认为有符号整型,那么无符号整型是怎么存储的呢?

因为char类型比较小,所以我们以一个char类型为例

如果这是一个有符号的char,第一位是符号位
00000000-->0
00000001-->1
00000010-->2
00000011-->3
  …………
01111111-->2^7-1-->127
10000000--不进行运算,直接翻译为-128
10000001-->-(2^7-1)-->-127
10000010-->-126
  …………
11111110-->-2
11111111-->-1

signed char的范围为(-128~127)

如果这是一个无符号char,八个都是数值位
00000000-->0
00000001-->1
00000010-->2
00000011-->3
  …………
01111111-->2^7-1-->127
10000000-->128
10000001-->129
10000010-->130
  …………
11111110-->254
11111111-->2^8-1-->255

unsigned char的范围为(0~255)

同理,其他整型的有符号型和无符号型也是可以这样计算

1.3大小端字节序

我们知道了整型在内存中的存储方法后,现在在回到最开始的代码,我们发现在存储3时我们存储的顺序是03 00 00 00,这是为什么呢?难到是反着存的吗?

 这与大小端字节序有关,那么什么是大小端字节序呢,我们往下看

比如我现在要存放一个十六进制数11 22 33 44,在内存中现在有这么两种方法

(1)

     11 22 33 44
低地址   --->    高地址   

(2)

     44 33 22 11
低地址   --->    高地址   

我们把方法(1)叫做大端字节序存储,即:把一个数的低位字节序(低位)内容放在高地址处,高位字节序放在低地址处。

把方法(2)叫做小段字节序存储,即:把一个数的高位字节序(高位)内容放在高地址处,低位字节序放在低地址处。

我们现在看一下当前编译器是采用了什么存储模式

 我们可以看到,地址是由低到高的,而存储顺序也是由低位到高位的,所以是小段字节序存储。

为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元 都对应着一个字节,一个字节为8bit。但是在C语言中除了8 bit的char之外,还有16 bit的short型,32 bit的long型(要看具体的编 译器),另外,对于位数大于8位 的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就 导致了大端存储模式和小端存储模式。 例如:一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为 高字节, 0x22 为低字节。对于大端 模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。小端模式, 刚好相反。我们常用的 X86 结构是 小端模式,而 KEIL C51 则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以 由硬件来选择是大端模式还是小端 模式。

2. 浮点数在内存中的储存

我们先来看这样一段代码

 

 我们现在来分析一下,为什么是这样的情况呢?

要搞明白这个结果,我们就要先弄清楚浮点数在计算机内部的表示方法。

根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:

  (-1)^S * M * 2^E (-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。

  M表示有效数字,大于等于1,小于2。

  2^E表示指数位。

 这是什么意思呢?,我们打个比方,比如我们现在要表示浮点数6.5

6.5               --(十进制)
110.1             --(二进制)
1.101*2^2         --(科学计数法)
(-1)^0*1.101*2^2  --(IEEE754)

按照IEEE754的规定,S=0,M=1.101,E=2。

那么在内存中我们是怎么来存储这些数的呢,我们以单精度浮点型为例

float占4个字节,32个比特位,我们如何来分配这32个bit位呢?

我们以一个*代表一个bit位,把32位的内存这样划分:
 *  ********  ***********************
S(1)  E(8)             M(23)

IEEE754规定:第一个比特位用来存符号位,接着八位存指数E,剩下的23位存有效数M

64位的double类型
 *  ***********  ****************************************************
S(1)   E(11)                           M(52)

对与64位的double型,最高位是符号位S,接着十一位是指数E,剩下的52位存有效数M

在规定中M是大于1小于2的数,那么我们可以知道M总是1.xxxxxxxx。也就是说M的第一位永远是1,那么我们在存储是是不是可以不存这一位,只存小数点后面的位,然后在使用的时候再把这个1加上即可,这样的话我们的23位存储M的空间就可以多存储一位数了,以此可以提高我们浮点数的精度。

在存储指数E时,首先,我们规定E是一个无符号数,如果E为8位,它的取值就是0~255,若E为11位,取值就为0~2047,但是当我们实际用来表示一个数时,(比如小于1的浮点数),指数位E就会是负数,这时候就出现了问题,所以IEEE754规定,在存储E时,E的真实值必须加上一个中间数,8位加127,11位加1023,然后再存入E区

在读取指数E时,又分为3种情况

(1)E不全为0或不全为1(正常情况)

把计算值减去127(或1023)得到真实值,再在M前面加上省去的那个1即可以还原

(2)E全为0

这时我们直接规定E的真实值为1-127或(1-1023),M前面不再加1,还原为0.xxxxx的小数

我们可以计算一下,E=0-127,这个数就是(-1)^S*M*2^(-127),可以想象的到这是一个非常小的数,这是为了表示±0以及无限接近0的数。

(3)E全为1

E+127=255,E=128,我们知道2^32的值为42亿多,那么2^128就是4个42亿相乘,可以想象这也是一个非常大的值了,所以这时的M全为0,表示±无穷大

知道了以上这些内容,我们再回过头来看那个代码

 首先是第一个printf,以整型打印n,结果是9,没有问题

再来看第二个printf,我们用*pFloat以浮点数的视角来访问这四个字节,这时候电脑会认为这里存的是浮点数。

00000000 00000000 00000000 00001001--9
 0 00000000 00000000000000000001001--以浮点数的视角来看
 S    E                M

这时这个二进制序列就会以浮点数的规则被翻译,我们发现它的E全为0,符号位为0,所以表示0.000000。所以打印的第二个值也没有问题了

我们再来看第三个printf,我们先是以浮点数的形式在这个地址存放一个9.0,再以整型的方式打印出来。

9.0-->1001.0-->1.001*2^3
E=3+127=130-->10000010
 0 10000010 10010000000000000000000--以浮点数的方式存放9.0
 S    E              M

我们再把这个数以整型的读取方式读出来的值就是1091567616

再看第四个printf,以浮点数的方式读取刚刚存贮的9.0,结果自然也是9.0

以上就是本篇的全部内容,如果大家觉得我的文章对你有帮助,希望能够给我点赞支持一下,铁柱在这里谢谢大家了!!

原文链接:https://blog.csdn.net/qq_45967533/article/details/122860682

栏目分类
最近更新