前言:
这一篇我们来深度剖析数据在内存中的存储,让我们走进数据在内存中到底是任何进行存储的,不同的数据类型有何差异。
文章图片
文章图片
文章图片
进阶之数据的存储:
- 一、数据类型详细介绍及归类
-
- 1.数据类型介绍
- 2.类型的基本归类
-
- ① 整形家族
- ② 浮点数家族
- ③ 构造类型
- ④ 指针类型
- ⑤ 空类型
- 二、整形在内存中的存储
-
- 1.关于整形的大小范围
- 2.变量创建时的内存空间
- 3.原码、反码、补码
- 4.补码的表示与存储
- 三、大小端字节序介绍及判断
-
- 1.大小端介绍
- 四、浮点型在内存中的存储
-
- 1.常见的浮点数及范围
- 2.整形与浮点型存储方式
- 3.浮点型在内存中的存储
- 4.IEEE754的特别规定
- 5.重新观察代码
一、数据类型详细介绍及归类 1.数据类型介绍 基本内置类型(即为C语言本身具有的类型):
我们先前学到的这几类基本类型:
数据类型 | 说明 |
---|---|
char | 字符数据类型 |
short | 短整型 |
int | 整形 |
long | 长整型 |
long long | 更长的整形 |
float | 单精度浮点数 |
double | 双精度浮点数 |
类型的两个重要意义:
- 类型决定开辟内存空间的大小(大小决定了使用范围)。
- 类型决定了看待内存空间的视角。
1.这么多种类型又如何分类?我们可以归类一下,因为char字符在底层存储的时候是ASCII码值,ASCII码值为整数,所有也算属于整形。所以上面的char到long long 都属于整形家族,float和double为浮点数家族。
2.而为什么要有整形浮点型的划分呢?因为我们生活中到处都是整数和小数。
3.为什么要有如短整型长整形,单精度双精度之分呢?这是因为我们在创建类型的内存大小直接影响我们所能存储的范围大小,简单来说就是能使用适当的空间去存储适当的值,不至于太大的空间浪费或越界溢出。
而第二个意义中,就是类似于同一种事务以不同的看法去看待它和对待它。
比如下面代码:
int main()
{
int a = 10;
float b = 10.0f;
return 0;
}
在这里,a和b所创建的类型都是4个字节,而不同的是类型,当你用int类型创建的时候,a的创建,存取都是按照整形来操作;同理在创建b的时候,就是完全按照float类型来了。
具体有什么不同,我们接着往下看就知道了。
2.类型的基本归类
① 整形家族
ps:[ ]内可省略,如
signed short int
可写为signed short
,或者short int
也可以写为short
。类型 | 类别 |
---|---|
char | unsigned char signed char |
short | unsigned short [int] signed short [int] |
int | unsigned int signed int |
long | unsigned long [int] signed long [int] |
int a = 10;
定义的a就是有符号的,short和long同样适用。但char 在定义的时候,默认符号取决于编译器,大多数编译器中char是有符号的char,即为signed char。
即:
#include int main()
{
//有无符号? char a;
//取决于编译器
short b;
//等于 signed short
int c;
//等于 signed int
long d;
//等于 signed long return 0;
}
那么有符号和无符号有什么区别呢?我们知道,整数有原反补三种编码方式,原码为显示数值的形式,补码为在内存中存储的形式。
有符号的原码中,最高位为符号位,即0代表正号,1代表符号,其他位是有效(数值)位。而无符号的原码中,全部为有效(数值)位。
我们来看这一段代码:
int main()
{
unsigned char c1 = 255;
signed char c2 = 255;
char c3 = 255;
printf("%d\n", c1);
printf("%d\n", c2);
printf("%d\n", c3);
//三个输出分别是多少?
return 0;
}
答案是: 255 ,-1 , -1 。
文章图片
还不清楚原码反码补码的可以到 【C语言】从入门到入土(操作符篇)中的移位操作符处学习一下。也可以先看下面也有介绍。当我们将255储存的时候,其原码为11111111,因为正数原反补码相同,所有在内存中存储的时候也是11111111。不清楚我们可以打开内存中查看。在vs2019中 F10运行起来,然后点击调试——窗口——内存——内存1,然后输入地址就可以看见了。
文章图片
文章图片
这里是十六进制显示,一个f就是15,也就是二进制中1111,两个f所以就是11111111,证明我们的原码是正确的。那这里为什么有255和-1两个结果呢?答案就在符号位那里:
int main()
{
unsigned char c1 = 255;
signed char c2 = 255;
char c3 = 255;
//存进去时,都是 11111111
printf("%d\n", c1);
//取c1的时候,c1无符号,都是有效位,值为255
printf("%d\n", c2);
//取c2的时候,c2有符号,第一位符号位,是负数,需要算出原码再得值
//补码11111111反码11111110原码10000001所以c2= -1
printf("%d\n", c3);
//与c2同理得-1 return 0;
}
当有符号和无符号存进去的时候都是255,但取出来的时候就在判断有无符号的时候产生了差异,这里既然同一个数存储取出的时候有不同,那有无符号的范围也是有所不同的。
文章图片
有符号char因为有负数域,所以范围是 -128~127。
无符号char因为无符号,所以范围是0~255,没有负数。
而从char中,我们也可以延申计算出short,int,long等有无符号的范围,这里就不一一计算了,有兴趣的可以自己算一下。
② 浮点数家族
float浮点型家族中有单精度浮点型和双精度浮点型,什么时候用单精度什么时候用双精度取决于你需要精度多高,你需要更高精度用double,不需要就用float,在float足够用时用double的话会浪费内存空间。
double
③ 构造类型
构造类型就是自己能够创造的类型,自定义类型。
数组类型1.数组也是有类型的。
结构体类型 struct
枚举类型 enum
联合类型 union
int main()
{
int a = 0;
//整形类型
int arr[10] = { 0 };
//数组,arr为数组名
//去掉数组名就是数组类型,在这里数组类型是int return 0;
}
我们可以验证一下,当我们创建类型后需要计算类型大小的时候,我们可以用变量名来进行计算,也可以用类型来计算。
int main()
{
int a = 0;
int arr[10] = { 0 };
printf("%d\n", sizeof(a));
//4
printf("%d\n", sizeof(int));
//4 printf("%d\n", sizeof(arr));
//40
printf("%d\n", sizeof(int [10]));
//40
//答案是一样的,说明数组除去数组名就是数组类型
return 0;
}
2.关于结构体
关于结构体可以查看操作符篇中的详解。
【C语言】从入门到入土(操作符篇)3.枚举类型
枚举类型定义的一般形式为:
enum 枚举名{ 枚举值表 };
在枚举值表中应罗列出所有可用值。这些值也称为枚举元素。
4.联合类型
在后面的博客中再做详解。
④ 指针类型
int *pi;
char *pc;
float* pf;
void* pv;
指针也是有类型的。
当我们需要将一个变量的地址保存起来的时候,我们就需要用到指针,例如下面这段代码中,p就把
num
的地址储存了起来,p就是指针变量:int num = 10;
p = #
而指针的定义方式是: type + * ,如
char * p
;其实:而指针类型的意义就在于确定了该指针的类型,就决定了指针向前或者向后走一步有多大(距离)。char*
类型的指针是为了存放char
类型变量的地址。short*
类型的指针是为了存放short
类型变量的地址。int*
类型的指针是为了存放int
类型变量的地址。
⑤ 空类型
void 表示空类型(无类型)我们看下面两段代码:
通常应用于函数的返回类型、函数的参数、指针类型。
void fun()
{
printf("hello!");
}
int main()
{
fun();
fun(10086);
//正常运行,输出hello!hello! return 0;
}
void fun(void)
{
printf("hello!");
}
int main()
{
fun();
fun(10086);
//能运行,但错误列表中提示错误,输出hello!hello! return 0;
}
当我们调用自定义函数时,我们不设传参,但在主函数中有值传过去,结果也是不影响的,因为fun函数根本没有接收这个传参,我们也可以直接在函数后写上void拒绝接收。
二、整形在内存中的存储 1.关于整形的大小范围 我们可以直接在vs2019中去输入下列代码,然后点击
INT_MAX
,转到定义,就可以查看int的最大值和最小值了,同时还可以看到其他整形的最大最小值。#include
//整形限制大小头文件
INT_MAX;
文章图片
2.变量创建时的内存空间 当一个变量创建的时候是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
那数据在空间中是任何开辟的呢?
比如
int a = -1
,创建这个整形变量之后,内存中会如何变化,我们来观察观察。文章图片
这里我们创建了一个变量a,F10代码走起来之后,我们点击查看内存中的变化,地址写&a,我们会发现当执行完
int a = -1
后,&a处地址的值变成了ff ff ff ff,我们知道一个f就是4个1(十六进制转二进制),那-1为什么会储存中是8个f呢,这里就引进数据在储存是有原反补三种编码方式了。3.原码、反码、补码 计算机中的有符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位三种表示方法各不相同。正数的原、反、补码都相同,负数则按下面规则进行运算。
原码:
直接将二进制按照正负数的形式翻译成二进制就可以。
反码:
将原码的符号位不变,其他位依次按位取反就可以得到了。
补码:比如:
反码+1就得到补码。
int main()
{
int a = -1;
//原码:1 0000000 00000000 00000000 00000001
//反码:1 1111111 11111111 11111111 11111110
//补码:1 1111111 11111111 11111111 11111111 //原码:显示值
//补码:数据存放内存中其实存放的是补码
return 0;
}
4.补码的表示与存储 在计算机系统中,数值一律用补码来表示和存储。
【C语言进阶|【C语言进阶】从入门到入土(数据的存储)】原因在于,使用补码,可以将符号位和数值域统一处理; 同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。单纯理解可能有点困难,我们可以举例理解:
int main()
{ int a = 1;
int b = -1;
int c = a - b;
//c是怎么计算的呢 return 0;
}
由于CPU只有加法器,所以表示为1+(-1)。
假设是用原码计算(二进制运算):
a:00000000 00000000 00000000 00000001
b:10000000 00000000 00000000 00000001
—————————————————————
c:10000000 00000000 00000000 00000010
结果为-2,但是1-1应该等于0呀。
用补码计算:
a:00000000 00000000 00000000 00000001
b:11111111 11111111 11111111 11111111
—————————————————————
c:100000000 00000000 00000000 00000000
最前面的1会舍弃,保留后面的0得:
c:00000000 00000000 00000000 00000000
结果为0,这才是正确的答案。
所以说使用补码,可以将符号位和数值域统一处理。
而最后一句,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。就是说上面原码转反码,反码转补码的过程,还有倒回去也是成立的,不需要其他算法。
之前我们认识的原反补转换:
文章图片
而这句话中的原反补转换:
文章图片
也就是怎么从原码转换补码,也可以以同样的方式按顺序从补码转换为原码。
我们再看看,数据存储中有什么地方值得我们关注的,内存中有什么秘密,我们看一下这张图:
文章图片
图中我们创建了两个变量,一正一负,然后我们算出a和b在存储中的十六进制表现形式,发现和内存中倒了过来,我们明明得出a是00 00 00 ff,而在内存中显示为ff 00 00 00,这就涉及到大小段的概念了。
三、大小端字节序介绍及判断 1.大小端介绍
大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。为什么会有大小端:
因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为例如一个8bit
。但是在C语言中除了8bit
的char之外,还有16bit
的short型,32bit
的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
16bit
的 short 型 x ,在内存中的地址为 0x0010
, x 的值为 0x1122
,那么 0x11
为高字节, 0x22
为低字节。对于大端模式,就将 0x11
放在低地址中,即 0x0010
中, 0x22
放在高地址中,即 0x0011
中。小端模式,刚好相反。我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。我们来看一个更详细的例子:
文章图片
0x表示以十六进制表示值,这样子我们就很直观的看见。我们所存储的变量,在内存中恰恰反着存储了过来。
其实,数据在内存中存储时,我们可以选择千奇百怪的存储方式,我们可以存进去时为
11 33 22 44
,也可以是22 33 11 44
,但是为了规整统一,存取方便,我们就把其他的都除去了,留下了大小端这两种存储方式。大小端图示:
文章图片
其实大小端储存模式就是牵扯到数据存储到内存时字节顺序的一个问题,而我们当前的vs2019就是小段字节序。
四、浮点型在内存中的存储 1.常见的浮点数及范围 3.14159 ,1E10 (即为1.0乘以10的10次方),浮点数家族包括: float、double、long double 类型。
① 查看浮点数的大小范围与整形的查看类似,
float.h
头文件就是浮点数限制大小发文件。写出下列代码,然后点击FLT_MAX
,转到定义,就可以查看了。#include
FLT_MAX;
文章图片
②也可以在电脑中直接搜索float.h文件,然后把他拖到vs编译器里面,也可以查看,直接用此电脑查看估计比较缓慢,这里推荐一个查找器everything(基于名称快速定位文件和文件夹。)。
Everything 下载链接
快速查找定位文件和文件夹:
文章图片
2.整形与浮点型存储方式 整形与浮点型存储方式是不是一样的呢?
我们可以用一个代码来试验一下就知道了,我们存一个整形以浮点型的形式取出,存浮点型以整形的形式取出,看看数值是否正确便知。
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得到的是0.00000,而浮点型存进去整形拿出的9.0竟然成了1091567616。
文章图片
所以我们要重新理解浮点型是如何存储的。
3.浮点型在内存中的存储 根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
举例来说:
- (-1)^S * M * 2^E
- (-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。
- M表示有效数字,大于等于1,小于2。
- 2^E表示指数位。
文章图片
在这里,十进制小数点前面的,均按照二进制换算进行,而小数点后面的,则不是直接换算,而是小数点后面每一位,对应着2的负n次方的值,比如上面的5.5转换后是101.1,小数点后面第一位表示的就是2的负一次方,即为0.5,小数点后面第二位就是0.25。但不要纠结如3.3的数字,这类数是比较难以保存的。
同理,当浮点数为负数的时候,s就是1,其他的不变。比如十进制的-5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。那么,s=1,M=1.01,E=2。
所以根据上述IEEE754规定,浮点数存储的时候只需要存储S,M,E三个数就可以了。
而存储时空间的分配:
对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。
文章图片
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
文章图片
4.IEEE754的特别规定 IEEE 754对有效数字M和指数E,还有一些特别规定!
①有效数字M:
前面说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。
IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。
也就是说,所有的浮点数都可以化为
(-1)^S * M * 2^E
这样的形式,小数也不例外,比如0.5,化为0.1,也就是(-1)^0*1.0*2^(-1)
。②指数E:
1.首先,E为一个无符号整数(unsigned int)
这意味着,如果E为8位,它的取值范围为
0~255
;如果E为11位,它的取值范围为0~2047
。但是我们上面的0.5转换时就已经发现,E是有可能为负数的。所以IEEE754又规定了:存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如,
2^10
的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001;而2^(-1)
存进去时,保存的便是-1+127=126,011111110。2.那存进去的形式有了,取出的时候如何呢?
指数E从内存中取出还可以再分成三种情况:
- E不全为0或不全为1
这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1,即为怎么存的就怎么取。
- E全为0
当E全为0时,浮点数的指数E等于1-127(或者1-1023)即为真实值,是一个非常小的数!所以有效数字M不再加上第一位的1,而是还原为0.xxxxxx
的小数。这样做是为了表示±0,以及接近于0的很小的数字。
- E全为1
与E为全0一样,当E为全1的时候,是一个非常非常大的数字,再加上正负号,就类似接进于正负无穷大。所以这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)。5.重新观察代码 知道了浮点数的存取,我们重新来打量一下这段代码:
int main()
{
int n = 9;
//整形理解:00000000 00000000 00000000 00001001
//浮点型理解:0 00000000 0000000000000000001001 float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);
//9
printf("*pFloat的值为:%f\n", *pFloat);
//0
//所以按照上面的浮点型理解,E还要减去127,得出为
//(-1)^0 *1.001*2^(-126) 接近为 0,所以显示为0.000000
//默认打印小数点后六位
*pFloat = 9.0;
printf("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
而下面那一段也是同样的道理,按照浮点型存储,9.0应该存储为1001.0
所以为
(-1)^0 * 1.001*2^3
,而且E要加上127,然后转化为二进制数列就是浮点型理解:
0 10000010 00100000000000000000000
整形理解:
01000001000100000000000000000000
我们可以用计算机算一下:
文章图片
文章图片
的确和程序运行起来一样的数字,所以证明上面的说法都是正确的。
好啦,本篇的内容就到这里,小白制作不易,有错的地方还请xdm指正,互相关注,共同进步。
还有一件事:
文章图片
文章图片
文章图片
推荐阅读
- C|C进阶?- 01数据在内存中的存储形点
- C语言重难点进阶|数据的存储【C进阶】
- c语言|C语言进阶---数据存储——浮点型在内存中的存储
- C语言进阶|【C语言进阶2——数据的存储(2)- 整形提升】
- C语言进阶|【C语言进阶1——数据的存储(1)】
- C语言基础|【C语言基础9 ——操作符详解(2)】
- C语言基础|【C语言基础7——数组(3)扫雷】
- C知识点的总结|【读取字符串长度的函数my_strlen】
- c语言|C语言进阶(一)---数据的存储