🎇作者:小树苗渴望变成参天大树
🎉 作者宣言:认真写好每一篇博客
🎊作者gitee:link
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
数据的存储
- 🧨 前言
- ✨一、数据类型的详细介绍
- 💖1.1类型的基本分类
- 💨二、整形在内存中的存储(limits.h)
- 💢2.1原码、反码、补码
- 💥2.2大小端介绍
- 💤2.3练习
- 三、浮点型在内存中的存储(float.h)
- 💦3.1一个例子
- 🎞️3.2浮点数存储规则
- 💞四、总结
🧨 前言
Hello,各位小伙伴们,好久不见。之前由于学校的考试,耽误了更新好的文章了,最近也是才回家没多久,想着尽快给友友们传递更多有用的知识,让大家久等了。
今天我们来讲点不一样的东西,我们的电脑也叫计算机,是帮助我们计算数据的,那计算机时怎么获取我们所想要计算的数据呢?这就是涉及到数据怎么在计算机里面存储的,只有把数据存在计算机里,它才能拿数据去计算,来达到我们想要的结果,所以我们这篇博客就会详细的介绍数据怎么在计算机里面存储的,让我们一起进入正文!!
本篇重点
- 数据类型详细介绍
- 整形在内存中的存储:原码、反码、补码
- 大小端字节序介绍及判断
- 浮点型在内存中的存储解析
✨一、数据类型的详细介绍
我们之前学习了许多类型,并且知道了他们占用了多大的空间,例如:
char //字符数据类型–1个字节
short //短整型–2个字节
int //整形–4个字节
long //长整型–4/8个字节
long long //更长的整形–4/8个字节
float //单精度浮点数–4/8个字节
double //双精度浮点数–4/8个字节
我们之所以要介绍数据类型是因为,我们把数据放到内存中,要开启自己的空间来存放数据,而类型的意义就在于开辟了多大的内存空间让我们来存放数据,并且决定了使用的范围和如何看待内存空间的视角。
💖1.1类型的基本分类
通过以上的数据类型,我们可以把数据类型分成两类
一.整型家族
char
unsigned char
signed char
那为什么字符型char有放在整型家族里面呢?
因为字符在存储的时候存储的是ASCII码,ASCII码是整数,所以当成整型家族的类型了。
short
unsigned short [int] = unsigned short
signed short [int]=signed short=short
int
unsigned int
signed int
long
unsigned long [int]=unsigned long
signed long [int]=signed long= long
long long
unsigned long long [int]=unsigned long long
signed long long [int]=signed long long=long long
unsinged是表示无符号的类型(都是正数),signed是表示有符号的类型(有正有负),=意思为等价的,但是 char不一定等价于signed char,因为char不知道是unsigned char,还是signed char,这个取决于编译器,大部分的时候还是等价的,但是避免出现错误,我们还是养成习惯加上signed或者unsigned;
二.浮点数家族
float
double
浮点数就是带有小数点的数,不是整数,这两个等会在具体介绍。
三.构造类型
数组类型
int a[10]; --他的数据类型是int [10].随着元素个数或者类型的改变,数组的类型也会改变,所以说这是我们构造出来的类型
结构体类型 struct
枚举类型 enum
联合类型 union
这三个我会在后期的博客详细的介绍他们是什么,并且为什么是构造类型和怎么去使用,相信那个时候你就会理解这些类型了
四.指针类型
int pi;
char pc;
float pf;
void pv;
我们在指针那篇博客中也详细介绍了什么是指针类型,指针类型表示指针的步子有多长,一步能访问几个字节,这个在一会要讲的大小端会用到,不会的可以先看一下我那篇讲解指针的博客
五.空类型
void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型
void f(voif)表面函数的返回类型是void,强调了不需要参数用void
说了这么多类型的分类,主要就是让大家更好的理解数据类型,但数据在内存中的存储就分为两种,一个是整型的存储方式,一个是浮点数的存储方式,那我们接下来就来重点介绍着两个存储方式。
💨二、整形在内存中的存储(limits.h)
我们刚才讲过一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
例:
int a = 20;
int b = -10;
我们是把数据放到int类型的变量里面,所以为我们开辟了四个字节的空间内,一个字节是八个比特位,所以我们将用三十二个比特位来存储数据。
我们既然知道为 a 分配四个字节的空间。
那如何存储?
那接下来就要理解三个概念。
💢2.1原码、反码、补码
计算机中的整数有三种表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位转换成对应的二进制数。
因为我在详解操作符那篇博客也详细介绍过这几个概念,所以这里我就直接写结果了。
一.正数
正数的原、反、补码都相同。
二.负数
原码:
直接将二进制按照正负数的形式翻译成二进制就可以。
反码:
将原码的符号位不变,其他位依次按位取反就可以得到了。
补码:
反码+1就得到补码。
int a = 20;
//00000000 00000000 00000000 00010100 原码
//00000000 00000000 00000000 00010100 反码
//0000 0000 0000 0000 0000 0000 0001 0100 补码
0 0 0 0 0 0 1 4 补码的十六进制表示
00 00 00 14
int b = -10;
//10000000 00000000 00000000 00001010 原码
//11111111 11111111 11111111 11110101 反码
//1111 1111 1111 1111 1111 1111 1111 0110 补码
f f f f f f f 6 补码的十六进制表示
ff ff ff f6
//并且两个十六进制表示一个字节
那让我们看看内存里到底是怎么存放的??
我们看到是存放的十六进制的数,原因是他是以十六进制的形式展示在内存窗口中,并且以字节为一个单位,如果以二进制的形式展示,那么就没有这么大的窗口给你看。
但结果和我们计算出来的又有点差别,我们发现他是倒着存放的,着就要涉及倒我接下来要讲的大小端存储模式了,但我们至少知道整型在内存中是以补码的形式存放的,至于按照什么顺序存放我们先不要管。
思考
为什么在内存中我们要使用补码来存储数据呢??,用原码或者反码不行吗??
我们来举个例子看看:
int a=-20;
int b=10;
int c=a+b;求c
a
//10000000 00000000 00000000 00010100--原码
b
//00000000 00000000 00000000 00001010--原码
c
//10000000 00000000 00000000 00011110--原码
结果为-30;答案是错误的
a
//11111111 11111111 11111111 11101011--反码
//11111111 11111111 11111111 11101100--补码
b
//00000000 00000000 00000000 00001010--反码
//00000000 00000000 00000000 00001010--补码
c
//11111111 11111111 11111111 11110110--补码
补码取反加1得原码
//10000000 00000000 00000000 00001010--原码
答案是-10;是正确的
读者可以自己找一些数字来验证一下。
那为什么要使用补码和为什么补码有这样作用?
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;
同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
我们从计组的角度来跟大家解释一下为什么要使用补码,并且为什么补码是那样计算出来的?
一.我们刚才说了在计算机中我们只有加法器,我们在使用原码的时候,我们想要实现一个减法是不行的,我们要转换成加法运算,如果想计算a-a只能计算a+(-a),我们就要找到一个数和(-a)是等效的
接下来我们先介绍一个概念,
模运算
相信大家可以清楚的看到我们计算机时如何把减法变成加法了吧
我相信细心的小伙伴已经发现了其中的巧合了。我们来看看我们取的补数和补码之间的差别
-14的补数为11110010
-14的补码为11110010
-66的补数为10111110
-66的补码为10111110
这就是补码的由来,所以计算机使用的都是补码之间的运算(补码让减法操作变成加法操作,节省硬件成本,可见那个时候人们是多么聪明)
相信读者看到这里应该完完全全知道了为什么整型数据存储在内存中要使用补码了吧,希望让你有一个更深的理解那我们还有一个问题没有解决啊,我们刚才算出来的补码却没有按照我们所想的方式去存放在内存中,居然是倒着存放的,那我们呢带着个这个疑问来了解大小端知识点吧
💥2.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处理器还可以由硬件来选择是大端模式还是小端模式
三.我们来看一道大厂的面试题
百度2015年系统工程师笔试题:
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)
简述刚才已经说过了,让我们用程序来帮我们去判断我们的机器是大端还是小端,
解题思路:我们要想知道是怎么模式存储的话,那我们就要深入内存,因为大小端是建立在数据在内存中存储的基础上,想到内存,我们就很容易的想到指针,通过指针来访问内存,我们在之前就讲过数据是需要在内存开辟一块内存空间,把数据放进去,所以我们用指针的时候,需要把先得到数据的地址,然后通过解引用来获取里面的数据,通过大小端的存储方式来对比取出来的数字,来看实例
int a=1;
小端 01 00 00 00
大端 00 00 00 01
我们可以发现通过第一个字节的内容可以区分大小端
哪我们怎么只得到第一个字节的内容呢??
这时候就要充分熟练的理解指针类型了,如果直接解引用就会一次访问·四个字节,所以我们通过强制类型转换为char*类型,这样解引用一次就访问一个字节了,话不多说我们来看代码
#include<stdio.h>
int main()
{
int a=1;
char*p=(char*)&a;
if(*p== 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
优化代码
#include<stdio.h>
int main()
{
int a=1;
if(*(char*)&a== 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
大家可以自己来测试一下
我们还可以通过联合体的方式来实现这个代码,具体为什么可以实现,我在讲到联合体知识的时候在细讲,大家可以先看看代码
int check_sys()
{
union
{
int i;
char c;
}un;
un.i = 1;
return un.c;
}
💤2.3练习
接下来我带大家练习几个题目,充分的在感受一下。
这几个题目要涉及倒截断和整型提升相关的知识,不了解的可以看我的详解操作符这篇博客。
一.
//输出什么?
#include <stdio.h>
int main()
{
char a= -1;
-1
//10000000 00000000 00000000 00000001原码
//11111111 11111111 11111111 11111110反码
//11111111 11111111 11111111 11111111补码
因为a是char类型,只能存放一个字节的数据发生截断所以
a 11111111
因为是以%d的形式打印所以要整型提升,补符号位是1,负数
得 11111111 11111111 11111111 11111111
在求原码得结果为1
signed char b=-1;
-1
//10000000 00000000 00000000 00000001原码
//11111111 11111111 11111111 11111110反码
//11111111 11111111 11111111 11111111补码
因为a是char类型,只能存放一个字节的数据发生截断所以
a 11111111
因为是以%d的形式打印所以要整型提升,是signed有符号类型 补符号位是1,负数
得 11111111 11111111 11111111 11111111
在求原码得结果为-1
unsigned char c=-1;
//10000000 00000000 00000000 00000001原码
//11111111 11111111 11111111 11111110反码
//11111111 11111111 11111111 11111111补码
因为a是char类型,只能存放一个字节的数据发生截断所以
a 11111111
因为c是unsigned类型是无符号类型,最高位1变成有效位,所以以%d的的形式打印时需要整型提升补0,正数
得00000000 00000000 00000000 11111111
在求原码得结果为255
printf("a=%d,b=%d,c=%d",a,b,c);-1 -1 255
return 0;
}
读者可以自己下来测试一下哦,看看结果为什么是这样的
通过这个题目,我相信大家对于整型数据在内存是怎么存储的并且知道了如何取使用它,那我们接下来讲解浮点数在内存是怎么存储的吧
三、浮点型在内存中的存储(float.h)
浮点数的存储和整型的存储时截然不同的,也不存在什么原反补码的形式,我们需要换一种思考方式了,不能在停留在刚才的思维模式了。
常见的浮点数:
3.14159
1E10 表示1*10^10
浮点数家族包括: float、double、long double 类型。
💦3.1一个例子
#include<stdio.h>
int main()
{
int n = 9;
float *pFloat = (float *)&n;
printf("n的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
return 0;
}
我们按照正常思维来想,
第一个打印的是9
第二个打印的是9.0
第三个打印的是9
第四个打印的是9.0
这可以说是按照整型的存储方式来思考问题的,但答案截然不同,这是为什么呢?让我们一起去探讨吧
🎞️3.2浮点数存储规则
num 和 *pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?
要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。
根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
(-1)^S * M * 2^E
(-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。
M表示有效数字,大于等于1,小于2。
2^E表示指数位。
举例来说:
十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2 。
那么,按照上面V的格式,可以得出s=0,M=1.01,E=2。
十进制的-5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。那么,s=1,M=1.01,E=2。
十进制的5.5,写成二进制是101.1(千万不要写成101.101,位权是一次降低的)相当于1.011×2^2。那么,s=0,M=1.011,E=2.
所以我们只需要知道s M E这三个值,把他们存放在内存中,在需要使用他们的时候,还原这三个数,进行相同的计算不久可以得出我们放进去的数字了吗?
那这三个值具体是以什么方式放在内存当中呢??
IEEE 754规定:
对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
这是规定,不需要问为什么s为什么是一位,E为什么是2位这样的问题。
所以刚才那三个数在内存应该是这样的
5.0 1.01×2^2 s=0,M=1.01,E=2
0 0000 0010 101 0000 0000 0000 0000 0000
0 1 5 0 0 0 0 0
01 50 00 00
-5.0 -1.01×2^2 s=1,M=1.01,E=2
1 0000 0010 101 0000 0000 0000 0000 0000
8 1 5 0 0 0 0 0
81 50 00 00
5.5,1.011×2^2 s=0,M=1.011,E=2
0 00000010 101 1000 0000 0000 0000 0000
0 1 5 8 0 0 0 0
01 58 00 00
我们看到结果和我们算的不一样说明,这种存储还有漏洞,所以我们还要改进存储方式
IEEE 754对有效数字M和指数E,还有一些特别规定。
前面说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。
IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时
候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字
至于放指数E,情况就比较复杂。
首先,E为一个无符号整数(unsigned int)
这意味着,如果E为8位,它的取值范围为0-255;如果E为11位,它的取值范围为0-2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,
对于8位的E,这个中间数是127;
对于11位的E,这个中间数是1023。
比如,2^10的E是10,所以保存成32位浮点数时,必须保10+127=137,即10001001。
指数E从内存中取出还可以再分成三种情况
一.E不全为0或不全为1
这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。
比如:
0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为1.0*2^(-1),其阶码E为-1+127=126,表示为
01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,
则其二进制表示形式为:
0 01111110 00000000000000000000000
二.E全为0
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,
有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。因为2^-126是一个无限接近0的数,所以就直接当成0来看
E全为1
这时,浮点数的指数E等于0-127=128,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);
我们按照这个规则再来计算一下我们刚才那三个数字的存储形式。
5.0 1.01×2^2 s=0,M=1.01,E=2
0 10000001 010 0000 0000 0000 0000 0000
4 0 a 0 0 0 0 0
40 a0 00 00
-5.0 -1.01×2^2 s=1,M=1.01,E=2
1 10000001 010 0000 0000 0000 0000 0000
c 0 a 0 0 0 0 0
c0 a0 00 00
5.5,1.011×2^2 s=0,M=1.011,E=2
0 10000001 011 0000 0000 0000 0000 0000
4 0 b 0 0 0 0 0
40 b0 00 00
我们可以看到数据跟我们计算的是一样的,不过也是倒着存放的,所以浮点数在内存中的存储也是属于小端存储。关于浮点数的规则我们就先讲到这里了。
我们带着这个规则来一起回头看看一开始那个例子为什么会出现这样的问题??
#include<stdio.h>
int main()
{
int n = 9;
float *pFloat = (float *)&n;
printf("n的值为:%d\n",n);//9
printf("*pFloat的值为:%f\n",*pFloat);//0.000000
*pFloat = 9.0;
printf("num的值为:%d\n",n);//1091567616
printf("*pFloat的值为:%f\n",*pFloat);//9.000000
return 0;
}
相信已经帮助了大家解决了刚才的疑惑。
特别提醒
我刚才i取的例子都是5.0,5.0这样的数,原因是这样的数字容易精准的得到他的二进制位,如果你举了一个3.14就很难在有限个位中精准的表示他,不信的小伙伴可以试试,如果一个数字,M是32个01组成,那我们在用单精度float类型存储时就会存在精度丢失的问题,这种情况时不可以避免的,那我们就可以采用双精度double类型存储,我们没法保证我们所求的数的M位在我们想要的问题,所以会存在精度丢失的问题,大家练习的时候不要给自己找麻烦
💞四、总结
各位友友们,本章的内容就先到这里了,我们使用计算机操作的就是数据之间的运算,如果不懂数据是怎么在计算机中存储的,那我们怎么能得心应手的学号计算机呢?本片知识量比较大,也是博主花了好长时间整理出来,希望大家可以慢慢消化,更上一层楼,我们下篇再见。