数据的存储
创始人
2024-05-06 10:37:26

今天我们来看数据在内存中是如何存储的,让我们往下看吧

目录

1.数据类型的介绍

1.1类型的基本归类

2. 整形在内存中的存储

2.1原码、反码、补码

2.2大小端介绍

3. 浮点型在内存中的存储

3.1 浮点数存储规则


1.数据类型的介绍

char //字符数据类型
short //短整型
int //整形
long //长整型
long long //更长的整形
float //单精度浮点数
double //双精度浮点数
//c语言没有字符串类型

我们知道的数据类型,有这么多,数据类型的意义,在于这个类型开辟空间的大小,以及我们看待空间的视角。

1.1类型的基本归类

整形

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

我们知道整形也分为很多,长整形和多整形,有符号的无符号的,但是这里多了一个char,大家可能会有疑惑,为什么char在整形归类呢?这是因为字符在存储的时候,存储的是ASCII码,ASCII是整数,所以归类时就被归到了整形里面。

我们看到上面有些是带[ ]的,这是因为[ ]里的是可以省略的,比如我们创建short类型,可以用如下方式

short int num;
short num;

 另外,有符号的signed也是可以省略的

int num=0;
signed int num=0;

 这两个是完全等价的,但是对于char来说,我们只写一个char,到底是unsigned还是signed是无法确定的,这取决于编译器,对于不同的编译器,结果是不同的,我们常见的编译器下是signed char。

浮点型

float
double

相比整形,浮点型就很少了,我们下面会介绍他们和整形的区别,以及这两个类型的区别

构造类型

> 数组类型
> 结构体类型 struct
> 枚举类型 enum
> 联合类型 union

构造类型也叫自定义类型,对于结构体,枚举,联合都是我们自己创建的,数组类型其实也属于自定义类型,比如我们创建了int arr[10],这个数组的类型就是int [10],但是当我们写成int arr[11]时,此时数组的类型就变成了int [11],随着元素个数的变化,数组的类型也在变化,并且前面的int变化,类型也在变化。

指针类型

int *pi;
char *pc;
float* pf;
void* pv;

 指针类型随着前面数据类型的不同,也分为多种

其中void* 也是一种指针,是无具体类型的指针

空类型

void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型
比如

void fun(){}
void fun(void){}

两个fun函数是一样的,第二个只是更明确告诉我们这个函数不需要参数,不要传参,返回类型也都是void类型

2. 整形在内存中的存储

我们知道变量的创建需要在内存里开辟空间,空间大小是根据变量的类型来决定的,我们来看看整形是如何存储的

我们看到a是整形变量,是4个字节,我们看到a在内存的存储是14 00 00 00,这是为什么呢?

要想知道存储原理,我们首先要知道原码,反码和补码是什么

2.1原码、反码、补码

计算机中的整数有三种2进制表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位则是二进制的转换

正数的原、反、补码都相同。
负整数的三种表示方法各不相同。
原码
直接将数值按照正负数的形式翻译成二进制就可以得到原码。
反码
将原码的符号位不变,其他位依次按位取反就可以得到反码。

补码
反码+1就得到补码。
对于整形来说:数据存放内存中其实存放的是补码。

我曾在这篇文章里介绍过原码、反码和补码,大伙有兴趣可以去看看

(4条消息) 操作符详解---c语言_KLZUQ的博客-CSDN博客

在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统
一处理;
同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程
是相同的,不需要额外的硬件电路。
我们看个例子

int main() {int a = 1;int b = -1;int c = a + b;
}

 我们知道了cpu只有加法器,那么计算1-1就变成了1+(-1),我们知道1的原码是00000....0001,而-1的原码是1000.....0001,直接相加的话就变成了1000....00010,这就变成了-2,但是1+(-1)怎么就变成-2了呢?所以我们就需要补码,1的补码是0000....0001,而-1的补码是1111....1111,此时我们把他俩相加就变成了10000....0000(因为进一所以比之前多一位),而我们的存储是有限的,最上边那一位就舍去了,也就是说我们只存储了0000....0000,也就是0,这就是补码的作用,补码的作用就是可以把正数和负数联系起来

2.2大小端介绍

什么是大小端呢?我们先看看这个图片

这是我们刚才看到a在内存里的存储情况,知道了二进制之类的相关知识后,可能有人好奇为什么这个20在内存里倒着存储的?为什么不是00 00 00 14,这就是我们接下来要介绍的内容

 大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址(大端存储又叫大端字节序存储)
中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地
址中(小端存储又叫小端字节序存储)

我们来看这个例子,我们有一串16进制数:0x11223344,它在大小端的存储情况为

 当我们把内存栏调整成1列时可以看到

 可以看到我们的编译器是小端存储

那为什么会有大小端呢?

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8 bit。但是在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处理器还可以由硬件来选择是大端模式还是小端模式。

接着我们来看一道百度的面试题

请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。
我们已经知道了大小端的概念,我们直接来看代码

首先来说思路,我们可以设置一个整数,如何用char类型指针取出,接着进行判断,比如数字1,它在内存的存储是01 00 00 00,我们用字符型取出的话是01还是00就可以判断了,我们来看代码

int main() {int i = 1;char* p = (char*)&i;if (p) {printf("小端");}else {printf("大端");}return 0;
}

知道了思路,代码就非常简单了。

接下来我们来看几组练习

int main()
{
char a= -1;
signed char b=-1;
unsigned char c=-1;
printf("a=%d,b=%d,c=%d",a,b,c);
return 0;
}

这段代码会输出什么呢?

 我们来看结果

我们知道-1的补码全为1,即111111...111111,但是要把它存入到char里面,要发生截断,会从后边开始数8位存入a里,%d是打印有符号的整数,对于a来说,他是有符号char,要按整数打印,先要进行整形提升,会补符号位,补全后全为1,也就是-1的补码,对他进行还原为原码后为-1,所以a输出的是-1,对于b来说,也是一样的,对于c来说,c是无符号char,我们存入时的补码同样是8个1,但是按照整形输出时,c是无符号的,提升会补0,补全为0000...00011111111,对它进行还原就是255了,知道了这一段逻辑,我们接着往下看

int main()
{
char a = -128;
printf("%u\n",a);
return 0;
}

这段代码的输出为

 a是char类型,它的原码是10000...00010000000,转换为补码是11111....11110000000,要存入char里边,还是要发生截断,也就是存储的为10000000,接着我们要以%u进行输出,%u是无符号整形,要进行整形提升,a是有符号的char,所以我们提升补符号位,提升后为11111....11110000000,我们认为这段补码为无符号数,也就是直接是原码,把它转为10进制就是我们上边的数了,知道了这个,我们接着往下看

int main()
{
char a = 128;
printf("%u\n",a);
return 0;
}

 我们看结果

 这个结果的原因和上面是一样的,存储a的补码为1000000,而提升因为a是有符号char,第一位是符号位,所以补1,就变为了11111....11110000000,以无符号整形输出就是上边的结果啦,我们再往下看

int main() {int i = -20;unsigned int j = 10;printf("%d\n", i + j);
}

我们来看结果

 -20的补码为1111...11101100,10的补码为0000....00001010,对他俩相加,得到的结果为11111....11110110,然后我们把这个按着整形输出,要把它先变为原码,结果为1111...11110110,对它进行还原,结果就是-10,我们继续看

int main() {unsigned int i;for (i = 9; i >= 0; i--){printf("%u\n", i);}
}

结果为死循环

 为什么呢?因为i是无符号数,既然没有符号,说明i永远不可能小于0,当i从9减少到0后,会变成一个非常大的数字,所以就变成了死循环,我们再看一个例子

int main()
{
char a[1000];
int i;
for(i=0; i<1000; i++)
{
a[i] = -1-i;
}
printf("%d",strlen(a));
return 0;
}

结果为

 char的取值范围为-128~127,而我们strlen判断结尾为\0,要\0的ASCII码为0,-1-(-1)才为0,而要使i变为-1,要从0到127,再从127到-128,再从-128变为-1,所以输出了255,char类型的数据,可以围城一个圈(我之前有介绍,有兴趣的朋友可以看看,就在上边的操作符详解博客里),我们再看一个例子

unsigned char i = 0;
int main()
{
for(i = 0;i<=255;i++)
{
printf("hello world\n");
}
return 0;
}

结果同样是死循环

 因为无符号char类型的值为0~255,而限制条件刚好到了255,所以会死循环

相信看了这么多例子,大家就可以理解这些数据类型了

3. 浮点型在内存中的存储

常见的浮点数:
3.14159
1E10
浮点数家族包括: float、double、long double 类型。
浮点数表示的范围:float.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;
}

为什么会有如此奇怪的结果呢? 让我们来深入探究浮点数的秘密吧

3.1 浮点数存储规则

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。
IEEE 754规定:
对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。
 

IEEE 754规定:
对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。
 

 对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。

 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),其阶码为-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的很小的数字。
E全为1
这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);

 上面说了这么多,我们来看几个例子

v=5.5,对于这个数,我们把它写成二进制为:101.1,101代表了,.1代表了2的负一次方,即0.5,大家可千万不要写成101.101,这是二进制数,每一位都有自己的权重,然后我们要把它变为科学计数法的形式,即1.011*2^2(小数点向左移动两位乘以2的2次方,不懂可以对比十进制),然后我们把v写成(-1)^0*1.011*2^2,知道了这些,对于v来说,s=0,e=2,m=1.011,它的二进制序列为:

0100 0000 1011 0000 0000 0000 0000 0000,对于该二进制序列,我们可以把它转换为16进制看内存是如何存储,转换后为:40 b0 00 00,我们来看是不是这样

我们可以看到,和我们的推测是一样的(小端存储) 

有时候,因为是二进制,对于小数部分的某些情况,我们可能无论如何都凑不出来对应值

所以会出现这种情况 ,我们在使用浮点数时要注意,知道了这些问题,我们再回过头来看最初的问题

 对于n,9的补码为0000....00001001,而把它放入float后,看到的是e全为0,对于e全为0,浮点数的指数E等于1-127(或者1-1023),然后还要乘以2的-126次方,这是一个非常小的数,所以才会打印出0,对于第二个问题,9.0的二进制为:0 10000010 001 0000 0000 0000 0000 0000,将它还原为10进制后,就会变成一个非常大的数,就是1091567616

以上就是数据存储的全部内容,希望大家可以有所收获

如有错误,还请指正。

相关内容

热门资讯

阿西吧是什么意思 阿西吧相当于... 即使你没有受到过任何外语培训,你也懂四国语言。汉语:你好英语:Shit韩语:阿西吧(아,씨발! )日...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...