深度剖析数据在内存中的存储

简介: 深度剖析数据在内存中的存储

1.数据类型详细介绍

1.1数据类型介绍

我们已经知道了基本的内置类型:


char //字符数据类型

short //短整型

int //整型

long //长整型

long long //更长的整型

float //单精度浮点数

double //双精度浮点数

//以上类型都是C语言本身具有的类型,叫做内置类型

//C语言类型分为两类,其一就是上面的内置类型,其二就是自定义类型(也称构造类型)


类型的意义:1.类型决定了开辟空间的大小(大小又决定了使用范围)。2.看待内存空间的视角不同。

对于意义2,我们用代码举一个例子:


意义2的意思就是站在整型的角度,内存中存放的当然是整型了;而站在浮点型的角度,内存中存放的当然是浮点型了。


1.2类型的基本归类

整型家族

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类型怎么能在整型家族中呢?是这样的:由于char类型不好归类,而且char类型在内存中存储的是其ASCII码值,既然存储的是ASCII码值的话,ASCII码值又是一个整数,相当于一个字符在表示的时候是用整数来表示和存储的。因此我们把char归类到整型家族中去。在char类型中又分为有符号(八个比特位中最高位为符号位,如:01 01 00 01)和无符号(八个比特位中最高位不是符号位,如:00 01 00 01),无符号为也就没有正负之分。

对于无符号数如果把最高位不当成符号位而当成有效位的话,它能表示的数的范围会更大一些。比如:11111111这八个比特位如果最高位这一位不是符号位而是有效位,则把二进制11111111翻译成十进制的话是255;把最高位当成有符号位的话用十进制数来表示是一个负数,即-127。

其实一个char类型的变量如果表示有符号数的话,它的范围是-128到127;而如果表示无符号是,它的范围是0到255。

浮点型家族

float//单精度浮点型
double//双精度浮点型

构造类型(即自己创造的一些类型):

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

数组类型

结构体类型  struct
枚举类型    enum
联合类型    enum

指针类型

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

空类型:

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

例如:

2.png


2.整型在内存中的存储

我们知道一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同类型数据决定的。


接下来我们讨论数据在所开辟的内存中到底是如何存储的?

3.png

4.png


在清楚上面图片是什么意思的话,我们需要清楚原码、反码、补码的概念。这里不进行展开说明。

#include<stdio.h>
int main()
{
  int a = 20;
  //00000000000000000000000000010100原码
  //00000000000000000000000000010100反码
  //00000000000000000000000000010100补码
  int b = -10;
  //10000000000000000000000000001010原码
  //11111111111111111111111111110101反码
  //11111111111111111111111111110110补码
  return 0;
}

这里的二进制在内存中展示的时候是以16进制进行展示的(4个二进制位相当于1个16进制位)。

#include<stdio.h>
int main()
{
  int a = 20;
  //00000000000000000000000000010100原码
  //00000000000000000000000000010100反码
  //00000000000000000000000000010100补码
  //0x00000014
  int b = -10;
  //10000000000000000000000000001010原码
  //11111111111111111111111111110101反码
  //11111111111111111111111111110110补码
  return 0;
}
#include<stdio.h>
int main()
{
  int a = 20;
  //00000000000000000000000000010100原码
  //00000000000000000000000000010100反码
  //00000000000000000000000000010100补码
  //00000014
  int b = -10;
  //10000000000000000000000000001010原码
  //11111111111111111111111111110101反码
  //11111111111111111111111111110110补码
  //0xFFFFFFF6
  return 0;
}

直到这里我们知道内存中存一个整数的时候存的是其二进制序列的补码,相信大家依然会有一些疑问:

5.png

为什么这里的14 00 00 00和f6 ff ff ff是倒着存放的,但是不管怎么讲,整数在内存中存放的是其二进制序列的补码。

总结:整数存放内存中其实存放的是补码。

那为什么呢?


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


刚刚提到CPU中只有加法器,但是当我们使用补码时,加法、减法等就可以进行统一处理了。这里它是怎么进行统一处理的呢?我们举一个例子:

6.png

对于乘法和除法的话不就是多加几次的事吗


3.大小端字节序介绍及判断

对于刚刚这段代码:

7.png

我们发现0x00 00 00 14在内存中是倒着放的(即14 00 00 00),如果我们想解决这个问题,我们就需要知道大小端的概念。


什么是大端小端:

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

小段(存储)模式是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中;

8.png


为什么会有大端和小端:

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8bit。 但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于 8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。


设计程序判断机器大小端

现在我们写一段代码来告诉我们当前机器的字节序是什么。

9.png

10.png

现在我们把这段程序分装成一个函数:

#include<stdio.h>
int check_sys()
{
  int a = 1;
  return *(char*)&a;
}
int main()
{
  int ret = check_sys();
  if (ret == 1)
  {
  printf("小端\n");
  }
  else
  {
  printf("大端\n");
  }
  return 0;
}

在这里有必要在回顾一下:指针类型决定了指针在进行解引用操作时能过访问几个字节。


我们先以int*类型的指针为例:

11.png

我们可以看到int*类型的指针在进行解引用操作时可以访问4个字节的大小。

下面我们在举一个char*类型的指针:

12.png

这里我们依然可以发现char*类型的指针在进行解引用是只能访问一个字节。

再次强调指针类型决定了指针在进行解引用时可以访问空间的大小。


练习1

//以下程序输出什么
#include<stdio.h>
int main()
{
  char a = -1;
  signed char b = -1;
  unsigned char c = -1;
  printf("a=%d,b=%d,c=%d\n", a, b, c);
  return 0;
}
#include<stdio.h>
int main()
{
  char a = -1;
  //补码:11111111111111111111111111111111
  //当我们要把-1的二进制序列存放到a中去,a只能存放8个比特位
  //故a中存放的是11111111
  //a整型提示后:11111111111111111111111111111111
  signed char b = -1;
  //补码:11111111111111111111111111111111
  //当我们要把-1的二进制序列存放到b中去,b也只能存放8个比特位
  //故b中存放的是11111111
  //b整型提示后:11111111111111111111111111111111
  unsigned char c = -1;
  //c中存放的是11111111
  //c整型提示后:11111111111111111111111111111111
  //虽然a、b、c中存放的内容都是11111111,但是当我们再把它们拿出来的时候就不一样了。
  //如果要把a、b、c的整型打印出来,那我们就需要计算出a、b、c的整型形式,
  //所以就需要对a、b、c进行整型提升(整型提升按照原符号进行提升,无符号数进行整型提升时补0)
  printf("a=%d,b=%d,c=%d\n", a, b, c);
  //-1  -1  255
  return 0;
}

练习2

//以下程序输出什么
#include<stdio.h>
int main()
{
  char a = -128;
  printf("%u\n", a);
  return 0;
}


//以下程序输出什么
#include<stdio.h>
int main()
{
  char a = -128;
  //10000000000000000000000010000000---原码
  //11111111111111111111111101111111---反码
  //10000000000000000000000010000000---补码
  //10000000
  //由于要打印无符号十进制数字,故需要对10000000进行整型提升
  //整型提升之后:11111111111111111111111110000000---这依然是补码
  //按照正常道理我们需要把补码转换为原码来获得最终的结果,但注意这里是%u,即按照无符号数来进行打印
  //所以说计算机认为内存中存放的是一个无符号数,既然是这样的或,原、反、补码是相同的
  //所以直接把11111111111111111111111110000000拿出来充当我们的原码
  //最终结果为:4294967168
  printf("%u\n", a);//%u是打印十进制的无符号数字
  return 0;
}

char类型的范围是如何定义的

13.png

14.png

练习3

//以下输出结果是什么
#include<stdio.h>
int main()
{
  char a = 128;
  printf("%u\n", a);
  return 0;
}

这里的变量a是char类型的,所以根本存不下128,这里的128就是127+1,即127+1其实就是练习2中的-128,所以说这里变量a存放的就是-128,因此输出结果和练习2一样。即:

15.png

注意:把练习2和3对比着进行加深理解。


练习4

//以下输出结果是什么
#include<stdio.h>
int main()
{
  int i = -20;
  unsigned int j = 10;
  printf("%d\n", i + j);
  //可以按照补码的形式进行运算,最后格式化成整数
  return 0;
}

16.png

未来再遇到这种题时不要害怕,大不了不就是拿起笔来算一算嘛😀


练习5

//以下输出结果是什么
#include<stdio.h>
int main()
{
  unsigned int i;
  for (i = 9; i >= 0; i--)
  {
  printf("%u\n", i);
  }
  return 0;
}

17.png

打印时我们打印的是i,i又通过%u来打印,所以打印出来的是无符号的整数,不管是一个什么样的数字都会当作无符号数(永远>=0)来进行打印,打印出来的都是>0的数字,即使是一个负数进去,站在i的角度它都会认为是一个正数。所以说结果是一个死循环。

当i减到-1、-2、-3、-4…时,-1、-2、-3…的补码被当作整数的补码进行输出。这一点尤其要注意


练习6

//以下输出结果是什么
#include<stdio.h>
#include<string.h>
int main()
{
  char a[1000];
  int i;
  for (i = 0; i < 1000; i++)
  {
  a[i] = -1 - i;
  }
  printf("%d\n", strlen(a));
  return 0;
}

-1、-2、-3、-4…-998、-999、-1000这1000个数字如果真的存放到数组a中,任何一个数字放到数组a中去都会转换为-128->127之间的一个数字。

18.png

还记的这个圆吗,当i从-128到-129时我们发现又循环回去了,-128-1得到的结果时正的127,不可能得到-129.

数组a中存放的数:-1、-2、-3、-4…-128、127、126、125…2、1、0、-1、-2、-3…。所以程序最终运行的结果为255。即:

19.png

还要注意一个点:数字0和字符’0’不是一回事。\0其实就是数字0其对应的ASICC码值是0;而字符’0’对应的ASICC码值是48。


练习7

//以下输出结果是什么

#include<stdio.h>
unsigned char i = 0;
int main()
{
  for (i = 0; i <= 255; i++)
  {
  printf("hello world\n");
  }
  return 0;
}

这里的char类型是unsigned char,unsigned char类型存放的数的范围是0->255,而判断中的i<=255这个条件就恒成立,所以程序结果依然是死循环。

此题可以与练习5进行比较学习。

所以我们今后再使用无符号数时,如果条件把握的不合适,判断其是不是大于小于等于某个数时,极有可能导致程序死循环,无符号数很容易导致程序陷入死循环。在今后写代码过程中一定要特别注意无符号数。

至此练习题到此结束,这7道练习题跟数据在内存中的存储有着非常大的关系。一定要把这7道题理解透。


4.浮点型在内存中的存储解析

常见的浮点数:


3.14159 1E10(1.0*10^10)

浮点数家族:float 、double、 long double类型。


浮点数存储的例子:

#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;
}

再继续往下看之前,请务必思考一下上面的举例,想一想程序运行的结果是什么?

下面请看程序运行结果:

20.png

程序结果是不是和读者想的是一样的呢?如果是,那恭喜读者对浮点数在内存中的存储有着不错的理解,倘若不是的话,别灰心,接下来我会逐步的对这道题进行分析:

我们首先要知道整型和浮点型在内存中存储的方式是截然不同的。对于整型,其在内存中是以二进制序列的补码进行存储的(涉及到大端小端);那浮点数在内存中是怎样存储的呢?

请看:

根据国际标准IEEE(电气和电子工程协会)规定,任何一个浮点数NUM的二进制数可以写为:

NUM = (-1) ^ S * M * 2 ^ E

(这里S表示符号,E表示阶乘,M表示有效数字)

①当S为0时,表示一个正数;当S为1时,表示负数

②M表示有效数字,1<= M <2

③2^E表示指数


(-1) ^ S意思就是你要表示的数是一个正数还是一个负数,它就相当于一个符号位。

那M为什么1<= M <2呢?是这样的,科学计数法表示的二进制里是不可能出现2这个数的,所以我们要把它化成有效数字时就是1点多少多少。

2^E表示的是指数位


我们就拿9.0进行举例,可以表示为:


(-1)^0 *1.001 * 2^3
这里S=0 M=1.001 E=3
(-1) ^ S * M * 2 ^ E


所以浮点数在内存中存储的话我们只需要把S M E这些值存起来,我们就可以还原回来真实的浮点数是几。真正的IEEE754规定的确实是我们只要把S M E相关的一个值存放到内存中去,回头我们在回复回来就可以。但举例到底是怎么存储的呢?假设我们把一个浮点数首先由十进制写成二进制,之后再把这个二进制写成科学计数法即(-1) ^ S * M * 2 ^ E的表示形式,求出S M E的时候,这个时候我们再把S M E相关的值存起来,怎么存的呢?请看:

IEEE 754规定:对于32位(32个比特位)的浮点数,最高的一位是符号位S,接着的8位是指数E,剩下的23位是有效数字M

21.png

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

22.png

23.png

既然把S M E划分的三块空间之后,是不是把这些值直接扔到里面去就可以呢?当然不是这样的。请接着往下看:

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。

举个例子:

0.5
(-1)^0 * 1.0 *2^(-1)
S=0
M=1.0
E=-1
E+127=126(意思是虽然我们计算出来的结果为-1,但我们真正存放到内存中的那个值是126,真实值要减去127,所以倒着推过去可以得到真实值为-1。)

现在,我们重现整理一下思路:我们把一个十进制的浮点数首先写成一个二进制的浮点数,然后把它转换成科学计数法的表现形式,此时就有的M S E。S直接存进去就好了;M要去点小数点前面的1然后存进去;至于E要给它加一个中间值之后给它存进去。

我们用代码来简单验证一下:

24.png

.

.

.

以上我们是讨论是如何放进去,而指数E从内存中往外取的时候又分为三种情况:

E不为全0或不为全1

这是,浮点数就采用下面的规则表示,即指数Education计算值减去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位0000000000000000000000000000,则其二进制表示形式为:

0 01111110 0000000000000000000000000000

E为全0


这时,浮点数的指数E等于1-127或(1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。


E为全1


这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位)。


好了,关于浮点数的表示规则就说到这里。


现在我们回到之前未讲解的题目😜

#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;
}
#include<stdio.h>
int main()
{
  int n = 9;
  //0 00000000 00000000000000000001001-补码
  float* pFloat = (float*)&n;
  printf("n的值为:%d\n", n);//9
  printf("*pFloat的值为:%f\n", *pFloat);//0.000000
  //(-1)^0 * 0.00000000000000000001001 * 2^(-126)
  *pFloat = 9.0;
  //1001.0
  //1.001*2^3
  //(-1)^0 * 1.001 * 2^3   这里E为3,存的时候要加上127变为130,即10000010
  //0  10000010  00100000000000000000000这就是9.0存的内存的形式
  printf("num的值为:%d\n", n);//这个时候要打印n的话,
  //既然要打印n(n是一个整数)就要站在n的角度,认为内存中存的是一个整数的补码,
  //即01000001000100000000000000000000,这也是这个整数的原码
  //01000001000100000000000000000000转换为十进制为1091567616
  printf("*pFloat的值为%f\n", *pFloat);//9.0
  return 0;
}

25.png

到这里相信大家对浮点数在内存中的存储有了一定的了解。原来是浮点数的形式放进去和以整型的形式放进去的方式是不一样的,取出来的方式也不一样。所以,整型的形式放进去再以浮点型的形式取出来得到的结果肯定是不正确的,同理以浮点型的形式放进去再以整型的形式取出来得到的结果肯定也是不正确的。所以,浮点数就应该按照浮点数的形式存,按照浮点数的形式取;整型就应该按照整型的方式存,按照整型的方式取。

之所以我们要了解数据在内存中的存储是因为,在未来我们遇到某些特殊的值比较怪异时,我们有能力去分析它到底结果是怎样的。


到这里,整个数据在内存中的存储的讲解就结束了。

本文至此结束,感谢各位🙇‍。

目录
相关文章
|
20天前
|
消息中间件 存储 缓存
kafka 的数据是放在磁盘上还是内存上,为什么速度会快?
Kafka的数据存储机制通过将数据同时写入磁盘和内存,确保高吞吐量与持久性。其日志文件按主题和分区组织,使用预写日志(WAL)保证数据持久性,并借助操作系统的页缓存加速读取。Kafka采用顺序I/O、零拷贝技术和批量处理优化性能,支持分区分段以实现并行处理。示例代码展示了如何使用KafkaProducer发送消息。
|
3月前
|
存储 编译器 数据处理
C 语言结构体与位域:高效数据组织与内存优化
C语言中的结构体与位域是实现高效数据组织和内存优化的重要工具。结构体允许将不同类型的数据组合成一个整体,而位域则进一步允许对结构体成员的位进行精细控制,以节省内存空间。两者结合使用,可在嵌入式系统等资源受限环境中发挥巨大作用。
111 11
|
4月前
|
监控 算法 应用服务中间件
“四两拨千斤” —— 1.2MB 数据如何吃掉 10GB 内存
一个特殊请求引发服务器内存用量暴涨进而导致进程 OOM 的惨案。
118 14
|
4月前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
233 1
|
4月前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
3月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
635 1
|
2月前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
3月前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
3月前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
38 3
|
3月前
|
存储 缓存 监控
Elasticsearch集群JVM调优堆外内存
Elasticsearch集群JVM调优堆外内存
73 1