【C进阶】第十篇——数据在内存中的存储

简介: 【C进阶】第十篇——数据在内存中的存储

数据类型的介绍


char          //字符数据类型

short         //短整型

int             //整型

long          //长整型

long long  //更长的整型

float          //单精度浮点数

double      //双精度浮点数

以及他们所占存储空间的大小.类型的意义:

1.使用这个类型开辟内存空间的大小(大小决定了使用范围)

假设我们在内存中定义了两个变量,A变量是int类型,B变量是char类型,我们都知道int类型是4字节所以他在内存中占4个字节的空间,float类型是4字节,所以他在内存中占用4字节的空间.

2.如何看待内存空间的视角.

类型就决定了定义变量时他们的大小变量A是四字节,变量B也是四字节,站在int型的角度他的存取都是整数,所以他是根据整数的形式存取的,而站在float类型的角度,他存放的是实数,所以他的存取方式是按实数的方式存取的,不同的数据类型就决定了他们在内存中存取方式是如何的

类型的基本归类


整型类型

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码就是整数,所以char类型自然也就列为整型家族里面了。

在这里首先我们需要了解一下char类型.

int main()
{
  signed short int a = 0;//有符号短整型
  unsigned short b = 0;//无符号短整型
  char a;//有符号?还是无符号
  return 0;
}

char a是有符号的还是无符号的问题,取决的是编译器

char类型有符号和无符号类型的区别

int main()
{
  unsigned char c1 = 255;
  signed char c2 = 255;
  printf("%d\n",c1);//255
  printf("%d\n",c2); //-1
  return 0;
}

运行结果:

image.png

char类型是占一个字节的8比特位,如果它是无符号类型囊,那么它的符号也是有效位,而取值范围就是可以到达255,再对比有符号的char类型(signed char),首先255对应的二进制序列是11111111.

image.png

最终其实是得到得二进制序列是对应十进制得-1,而它的最高位代表的是符号位1即表示负数.


因为这里写的是signed char所以它的最高位为0的化表示的是一个正数,那么它的原码,反码,补码都是一个样的而第一个二进制序列表示的是0,再看他的最后一个二进制序列,因为signed chae的最高位是1表示的是负数,那么久需要对他的补码-1取反得到原码,那么就会是-1,而-127又是通过下面的计算过程得到的原码对应的二进制序列因为是有符号的所以他的最高位表示的是符号位,而剩下的7位才是他的有效位,而中间的10000000表示的是-128


那如果是无符号的char那么它的二进制序列对应的每一位就都是有效位,所以它的最大范围是0 ~ 255,既然我们知道了char类型的取值范围怎么运算的规则,是不是就可以知道其他数据类型的取值范围了?这里就不再继续了

总结:

1、signed char的取值范围是 0 ~ 127 | -128 ~ -1
2、unsigned char的取值范围是0 ~ 255

浮点类型

1. float
2. double

关于浮点型在后面的存储方式再详细介绍,这里简单了解一下:

1.float类型称为单精度,占用4个字节,数值范围位3.4E-38到3.4E+38,有效数字6-8位

2.double类型称为双精度,占用8个字节,数值范围位1.7E-308到1.7E+308,有效数字15-16位

构造类型

1. >数组类型
2. >结构体类型 struct
3. >枚举类型 enum
4. >联合类型 union

指针类型

1. int* pi;
2. char* pc;
3. float* pf;
4. void* pv;

空类型

void 表示空类型(无类型)

通常应用于函数的返回类型、函数的参数、指针类型

void参数列表声明

void func(void)//表示不需要传递参数
{
  printf("hehe\n");
}
int main()
{
  func(100);
  return 0;
}

虽然程序不报错,但是显示了一条警告,这也是不太好的,如果指定不需要参数,那么就不传参数,否则就不需要指定void

整型在内存中的存储


我们之前讲过一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的

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

image.png

我们为a分配四个字节的空间.那如何存储?

原码,反码,补码


计算机中的整数有三种表示方法,即原码,反码,补码.

三种表示方法均有符号位和数值位两个部分,符号位都是用0表示"正",用1表示"负",而数值1位负整数的三种表示方法各不相同.

原码

直接将二进制按照正负数的形式翻译成二进制就可以

反码

将原码的符号位不变,其他位依次按位取反即可

补码

反码+1就得到补码

注意:

(1)正数的原,反,补都是相同的

举个例子:

image.png

再看一个负数

image.png

(2)对于整型来说:数据存放内存中其实存放是补码,那么为什么?


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

总结而言,有两点原因:一是为了进行负数的有关运算;二是为了提高运算的效率。

下面来解释一下:

1.可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理

因为CPU只有加法器,所以对于1-1这样的表达式CPU要处理成1+(-1)来进行计算

而如果直接将两个操作数的原码进行相加,就可能会出错的.

举个例子:

image.png

1+(-1),我们用原码相加,得到错误的结果

用补码计算:

image.png

2.补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路

我们通过原码得到补码的方法:

原码的符号位不变,其他位按位取反得到反码,反码加1,得到补码;

然后我们创建两个变量,看一下,内存给我们战术出来的样子:

image.png

我们可以看到对于a和b分别存储的是补码。但是我们发现顺序有点不对劲,好像是相反的。

这是又为什么?

大小端介绍


什么是大小端


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

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

解释说明:

image.png

到这里就能明白了,编译器使用的的是小端存储模式.

为什么有大端和小端?


为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元

都对应着一个字节,一个字节为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处理器还可以由硬件来选择是大端模式还是小端模式。  

判断当前机器的字节序


概念我们上面已经说过了,那怎么设计程序呢?我们来思考一下:

我们可以用整数1来帮助判断,取出1的第一个字节的内容,1的补码是:00000000000000000000000000000001,16进制是:00 00 00 01;


如果第一个字节的值是0(高位在低地址),则为大端;

如果第一个字节的值是1(低位在低地址),则为小端。

(注:我们取出的第一个字节是处在低地址的那一个字节)

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

image.png

浮点型在内存中的存储


例题引入


思考下面的程序输出结果,并且想一下为什么会出现这种结果.

#include<iostream>
#define _CRT_SECURE_NO_WARNINGS
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;
}

运行结果如下:

image.png

根据国际标准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 。

那么,按照上面的格式,可以得出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。

image.png

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

image.png

M占用的比特位越多,数据精度越高。

E占用的比特位越多,数据范围越大。

所以在实际开发中使用double较多。

此外: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。

总结:
1、如果是float类型,这个中间数是127。比如,2^10的E 是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。
2、如果是double类型,这个中间数是1023。比如,2^10的E 是10,所以保存成32位浮点数时,
必须保存成10+1023=1033,即010000001001。

浮点数的存


下一个话题浮点数以二进制的形式在内存中存储

int main()
{
  float a = 5.5f;
  //(-1)^0 * 1.011 * 2^2
  //S = 0
  //M = 1.011
  //E = 2
  //因为是浮点型中间数是127,而E又是2
  //E = 2 + 127 = 129  
  //对应的二进制序列:01000000 10110000 00000000 00000000
  //对应的十六进制序列:40       B0   00      00
  return 0;
}

对应的二进制序列内存块

image.png

把这32位二进制序列转换成16进制就是40 B0 00 00

image.png

由于使用的是VS2017编译器采用的是小端存储,所以低地址处存放的是二进制序列低位的数据,而高地址处的是二进制序列高位的数据

浮点数的取


然后,指数E从内存中取出还可以再分成三种情况:

(1)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

(2)E全为0

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


注意:这个地方为什么不是0-127呢?这是规定!规定就是把E全为0的时候看作是1-127即-126。

(3)E全为1

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


到了这里,前面的题目就显而易见了,因为上面是以浮点数形式进行打印,就是将二进制的数字看成是浮点数,即按照浮点数在内存中存储的形式进行打印。


将9.0存入*pFloat中时,先将9.0按照浮点数的形式存储起来,就是按照上面的格式,按照整数的形式进行打印的时候自然就会出现一个比较大的数字。


回到之前的问题

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

当我们把以上的知识弄明白之后,对于这个问题只要把它的二进制序列求出来,再截取它的6有效位数(因为浮点型的有效位数是6位有效数字) 所以会截取0.000000

image.png

解析这两句代码的意义

*pFloat = 9.0;

image.png

printf(“num的值为:%d\n”,n);

所以它的二进制序列是== 01000001000100000000000000000000==当以%d的形式打印,而他的最高为0所以是正数,而正数的原、反、补相同所以打印的结果是

浮点数的比较


使用这种方式来存储的时候,会带来一个很大的问题,保存的小数往往不是一个精确值,而只是一个近似值。

示例:

#include <stdio.h>
int main()
{
  float a = 11.0;
  float b = a / 3.0;
  if (b * 3.0 == a) {
    printf("相等!\n");
  } else {
    printf("不相等\n");
  }
  system("pause");
  return 0;
}

实际上11.0/3.0*3.0肯定等于11.0。

但是我们看看运行结果:

image.png

所以,浮点数在内存中存储的时候,很多时候是有误差的

正确的比较方法:

使用做差的方法,然后判断差值是不是在允许误差范围内,如果在的话,就相等。

#include <stdio.h>
#define N 1e-4
int main()
{
  float a = 11.0;
  float b = a / 3.0;
  if (b * 3.0 - a < N && b * 3.0 - a > -N) {
    printf("相等, 此处不是严格相等, 而是允许误差\n");
  } else {
    printf("不相等\n");
  }
  system("pause");
  return 0;
}

运行结果:

image.png

相关文章
|
24天前
|
存储 编译器 数据处理
C 语言结构体与位域:高效数据组织与内存优化
C语言中的结构体与位域是实现高效数据组织和内存优化的重要工具。结构体允许将不同类型的数据组合成一个整体,而位域则进一步允许对结构体成员的位进行精细控制,以节省内存空间。两者结合使用,可在嵌入式系统等资源受限环境中发挥巨大作用。
51 11
|
2月前
|
监控 算法 应用服务中间件
“四两拨千斤” —— 1.2MB 数据如何吃掉 10GB 内存
一个特殊请求引发服务器内存用量暴涨进而导致进程 OOM 的惨案。
|
2月前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
91 1
|
2月前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
2月前
|
监控 Java easyexcel
面试官:POI大量数据读取内存溢出?如何解决?
【10月更文挑战第14天】 在处理大量数据时,使用Apache POI库读取Excel文件可能会导致内存溢出的问题。这是因为POI在读取Excel文件时,会将整个文档加载到内存中,如果文件过大,就会消耗大量内存。以下是一些解决这一问题的策略:
212 1
|
2月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
2月前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
51 2
|
21天前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
169 1
|
11天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
20天前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80