数据在内存中的存储

简介: 数据在内存中的存储

一、整型数据在内存中存储

在学习计算机基础时,就接触过整型的二进制表示:原码,反码,补码

对于有符号的整数,这三种表示方式的有符号位数值位符号位用0表示,用1表示,用二进制最高位来表示符号位,其他都是数值位。

对于正整数来说:原码,反码和补码都相同

负整数三种表示方法各不相同

       原码:直接将整数按照正负转换为二进制得到的就是原码

       反码:将原码的符号位不变,其他位依次按位取反就得到反码

       补码:反码+1就是补码

反码与补码之间的转换就是,取反加一

对于整型数据来说:数据就是以二进制补码的形式存放在内存中

       在计算机系统中,数值一律用补码来存储和表示。原因就在于,使用补码可以将符号位与数值位统一处理。

       同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

二、大小端字节序

第一次看到这个大小端字节序,可能感到很疑惑,具体什么是大小端字节序呢?

这里,我们写代码调试来看一下:

int main(){
    int a = 0x11223344;
 
    return 0;
}

这里我们可以看到a当中0x11223344这个数字是以字节为单位,倒着存储的,这又是为什么呢?

       什么是大小端?

我们知道,在内存中存储数据一般是以字节为单位的,而当超过一个字节大小的数据,存储的过程中就要遇到顺序问题,所以,内存中存储数据是有一定顺序的,按照不同的存储顺序,就分为大端字节序存储小端字节序存储,具体概念如下:

大端字节序存储:

是指数据的 低位字节 内容保存在 内存的高地址 处,而数据的 位字节内容,保存在内存的 地址处。

小端字节序存储:

是指数据的 低位字节 内容保存在 内存的低地址 处,而数据的 位字节内容,保存在内存的 地址处。

为什么要有大小端字节序呢?

       这是因为在计算机系统中,是以字节为单位的,每个地址单元都对应一个字节,一个字节是8个bit位,但是在计算机中,有8bit的char型,有16bit的short类型,还有32个bit的long型(具体大小取决于编译器);此外,对于那些位数大于8位的处理器,如16或者32位处理器(还有64位等),它们的寄存器宽度都要大于一个字节,这样就必然存在着如何处理多个字节安排这一个问题。因此就有了大端存储模式和小端存储模式。

我们了解了大小端字节序存储这个东西,接下来就写代码来判断一下VS编译器是大端存储模式还是小端存储模式呢?

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

这里 *(char*)&i将整型指针强制类型转化成char*再解引用就只访问一个字节,(访问的就是地址低的那一个字节)这样如果访问到的是1,就说明将int将低位字节存储到低地址处,存储方式就是小段存储方式;如果访问到的不是1,就说明存储方式是大端存储方式。

当然,也有其他的方式来判断:

int check_sys()
{
     union
     {
         int i;
         char c;
     }un;
     un.i = 1;
     return un.c;
}
int main()
{
  int ret = check_sys();
  if (ret == 1)
  {
    printf("小端\n");
  }
  else
  {
    printf("大端\n");
  }
  return 0;
}

这里用到了自定义类型的联合体这里简单说一下联合体(后面会详细讲解):

       联合体也叫做共用体,联合体可以由多个成员不同类型构成,联合体特点就是所以的成员共用一块内存,(编译器只会给最大的成员分配足够的内存),给联合体的其中一个成员赋值,其他成员的值也会发生变化。

       这里也是,给int类型的i进行赋值,然后用char类型去访问,只访问一个字节。

三、试题练习

接下来,看一些例题,来加强一下理解

练习1、

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

看到这里,应该要知道一个问题,字符char类型在内存中占一个字节,在内存中也是以二进制的方式存储的,例如a,b等这些存储的其实是对应的ASSIC码值。

接下来,看一下代码,char a = -1;char一个字节,8个bit位,整型存储的是二进制,所以将-1转化位二进制就是 :          

10000001 ——原码

11111110 ——反码

11111111 ——补码

将补码存储到a当中

补充:char默认就是有符号型(signed char)

这里signed char与char存储方式一样

再看 unsigned char c = -1;这里还是将-1转化位二进制

11111111 ——补码

将补码存储到c当中去,而c是无符号类型,它就会把符号位当成数值位来看待;

接下来以%d的形式输出,由于char只占一个字节,这里就会涉及到整型提升将char类型提升至int型:

整型提升知识补充:

整型提升的规则:

       整型提升分为有符号和无符号两种,对于有符号的:整型提升时是按照变量的补码被截断时的最高位(符号位)是什么进行补位的,如果截断后最高位即最左面的一位数(符号位)为 1 则在最高位前补 1 ,如果最高位(符号位)是 0 则在前面补 0 ,补够32位即int类型即可。 无符号的: 直接在被截断的前面补 0 即可。

简单了解一下整型提升,再来接着看代码

以%d的形式输出,首先就要先整型提升

11111111 ——a

11111111 11111111 11111111 11111111 —— (补码)整型提升以后的a

10000000 00000000 00000000 00000000 —— 反码

10000000 00000000 00000000 00000001 —— 原码

然后进行输出,因为是%d是以有符号整型输出,所以这里输出的就是 -1;

b与a一样,都位有符号类型。

再看无符号型的c,

11111111 —— c

00000000 00000000 00000000 11111111 —— (补码)整型提升以后的c

00000000 00000000 00000000 11111111 —— 反码

00000000 00000000 00000000 11111111 ——原码

因为c是无符号类型(即是正数)原反补码都相同

然后以%d形式进行输出,输出结果就是255。

练习2、

#include <stdio.h>
int main()
{
     char a = -128;
     printf("%u\n",a);
     return 0;
}

char 有符号类型 -128转化位二进制

10000000 —— 原码

10000000 —— 反码

10000000 —— 补码

a里存储的就是-128的补码;

然后以%u(无符号整型)输出,首先还是要整型提升 -128是有符号类型

10000001 —— a

11111111 11111111 11111111 10000000 —— 整型提升后

因为是以有符号类型输出,所以编译器就会将整型提升后的符号位当作数值来看待,直接将其转化成十进制,然后输出,结果就是4294967168

练习3、

#include <stdio.h>
int main()
{
     char a = 128;
     printf("%u\n",a);
     return 0;
}

练习2中是将-128存储,现在来存储128然后以%u形式输出。

这里 char类型取值范围 -128 —127

128存储到char类型中,可能会出现数据丢失的现象

这里  10000000 —— a

然后整型提升,符号位是0

00000000 00000000 00000000 10000000 —— 整型提升后(补码)

11111111 11111111 11111111 01111111 —— 反码

11111111 11111111 11111111 10000000 —— 原码

以%u形式输出,将符号位当成数值位来看待,直接转化成十进制数然后输出,结果就是4294967168

练习4、

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

在第一眼看到这个代码时,第一感觉可能会跟我一样,一看循环进行赋值了1000,那用strlen来求字符串长度,那不就是1000吗?

在讲述这一个练习之前先补充一点知识

知识补充:

       我们知道,char类型只占用一个字节,也就是8个bit位,那再存储数据的时候,它所存储的数据就是有限制的

对于有符号的char类型

图中我们可以看到,127再加一后  0111 1111 就变成了 1000 0000;

而1111 1111加一就变成了 0000 0000

对于无符号类型的char就简单了

这里  1111 1111加一就变成了  0000 0000。

知道了char类型的取值范围再看这个练习题

a[i] = -1 - i ;

i从零开始,a[0]就等于-1,这样一直赋值下去,当 (-1 - i )等于-128 时,i++;这时表达式的值就变成了127,再接着进行赋值,这个表达式的值总会变成0,而我们又知道strlen遇到'\0'停止,'\0'的ASSIC码对应的值就时0,所以,strlen遇到a这个数组中的0时,就停止计数了,所以这里输出的结果是255。

练习5、

在有了以上对char类型数据的理解,看以下两个题就容易多了

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

这个代码会一直循环输出 hello world  , unsignned char取值范围是 0 -- 255, 255再加一就变成0,循环会一直运行,如果使用VS编译器,代码会有错误它提示

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

这个也是同理,unsigned char 类型  0再减去1 就变成255,循环一直进行下去

四、浮点型数据在内存中存储

了解了整型数据在内存中的存储,接下来,来了解浮点型数据在内存中的存储:

浮点型数据的存储,根据国际标准IEEE(电器和电子工程协会)754,任意一个浮点数V都可以表示成一下形式:

  • 这里-1的S次方表示符号位,S = 0,V就是正数;S=1,V就是负数
  • M表示有效数字,M是大于等于1,小于等于2的
  • 2的E次方表示指数位

举例:

十进制的5.0,写成二进制 101.0 ,相当于1.01*2^2

用以上格式表示就是 S=0,M=1.01,E=2。

浮点型数据的存储

对于32位的浮点型数据,最高位存符号位S,接着8位存指数E,剩下的23位存储有效数字M。

而对于64位的浮点型数据,最高位存符号位S,接着11位存指数E,剩下的的52位存有效数字M。

知道是怎样存进内存的,那也要知道在存储过程中,存储的规定

IEEE 754 对于有效数字M和指数E,还有一些规定

对于有效数字E:

有效数字   1<=M<2,也就是M可以写成 1.xxxxx的形式,其中xxxxx是小数部分。

IEEE 754 规定,在计算机内存保存M时,默认这个数的第一位总是1,所以可以将其舍去,只保存后面的小鼠部分;

       比如,在保存1.01时,只保存01,在读取的时候,再把第一位的1加上去。这样做,是省略1位有效数字。比如,32位浮点数,留给M只有23位 ,舍去第一位的1,就相当于可以保存24位有效数字

对于指数E,存储的情况就比较复杂

首先,E是一个无符号整数,这就说明,如果E是8位,它的取值就是 0-255;如果E是11位,取值就是0-2047.

但是,我们知道指数是存在负数的,所以IEEE 754 就规定了在存储时E的真实值必须加上一个中间数,(8位的E,中间数是127; 11位的E,这个中间数就是1023),比如 2^10的E是10 ,保存成32位浮点数,在存储时就必须保存成 10+127 = 137,就是 1000 1001。

浮点型数据的数取

大致可以分为三种情况:

1、E不全为0或不全为1

       这时,浮点数采用下面规则表示,(E的计算值减去127或1023),得到真实值,在将有效数字M前加上第一位的1.

       比如:0.5二进制是0.1,根据规定正数部分必须是1,所以就表示位 1.0^2*(-1),其阶码(E的存储值)是 -1+127 = 126,表示为  0111 1110,位数1.0 去掉整数部分为0,补齐23位 ,所以二进制表示形式:

0 01111110 00000000000000000000000

2、E全为0

这时,浮点型的指数E就是1-127(或者1-1023)即为真实值,有效数字M不在加上第一位的1,而是还原为0.xxxxxx的小数。(这是为了表示+/- 0,和接近于0的很小的数字)

3、E全为1

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

了解了这些,就看一段代码:

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

在之前看到这样的运行结果,可能会感到很懵,在我们知道了浮点型数据的存储以后,来看这样的代码:

       首先第一个 printf("n的值为:%d\n",n); 这个应该都可以理解,n是整型数据,以%d形式输出,就是9;

       然后来看 printf("*pFloat的值为:%f\n", *pFloat);

9 以整型的形式存储在内存中,二进制如下:

0000 0000 0000 0000 0000 0000 0000 1001

要以%f 形式输出,那就会把这个二进制序列按照浮点型数据二进制来处理,

符号位S = 0 ,后面8位即指数位 E = 0000 0000,

最后的23位就是有效数字 M= 000 0000 0000 0000 0000 1001。

这样指数E全为0,就按照指数E全为0 的方式,

浮点数 V= (-1)^0 *000 0000 0000 0000 0000 1001 *2^(-126);

这样,V显然就是应该很小的接近于0的正数,用十进制小数表示就是0.000000

       紧接着来看 *ploat = 9.0 以后,在以%d形式输出:

这里就要按照浮点型数据存储将9.0存储到内存中,

9 的二进制 1001.0 换成科学计数法就是 1.001 * 2^3

所以S = 0,E = 3 + 127, M等于001后面追加20个0  , 写成二进制就是

0 10000010 001 0000 0000 0000 0000

这个二进制数,被当作正数来解析时,就被当中内存中的补码,原码 转换为十进制就是 1091567616

       最后以%f的形式在输出以浮点型存储到内存中的9.0,输出结果就是 9.000000。

感谢观看,希望一下内容对你有所帮助,如果内容对你有作用,可以一键三连加关注,作者也正在学习中,有错误的地方还请指出,感谢!!!

相关文章
|
1月前
|
存储 编译器 数据处理
C 语言结构体与位域:高效数据组织与内存优化
C语言中的结构体与位域是实现高效数据组织和内存优化的重要工具。结构体允许将不同类型的数据组合成一个整体,而位域则进一步允许对结构体成员的位进行精细控制,以节省内存空间。两者结合使用,可在嵌入式系统等资源受限环境中发挥巨大作用。
59 11
|
2月前
|
监控 算法 应用服务中间件
“四两拨千斤” —— 1.2MB 数据如何吃掉 10GB 内存
一个特殊请求引发服务器内存用量暴涨进而导致进程 OOM 的惨案。
|
2月前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
102 1
|
2月前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
2月前
|
监控 Java easyexcel
面试官:POI大量数据读取内存溢出?如何解决?
【10月更文挑战第14天】 在处理大量数据时,使用Apache POI库读取Excel文件可能会导致内存溢出的问题。这是因为POI在读取Excel文件时,会将整个文档加载到内存中,如果文件过大,就会消耗大量内存。以下是一些解决这一问题的策略:
254 1
|
2月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
2月前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
58 2
|
2月前
|
存储 编译器
数据在内存中的存储
数据在内存中的存储
43 4
|
2月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
60 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
2月前
|
存储 C语言
深入C语言内存:数据在内存中的存储
深入C语言内存:数据在内存中的存储