资讯专栏INFORMATION COLUMN

C语言进阶第一问:数据在内存中是如何存储的?(手把手带你深度剖析数据在内卒中的存储,超全解析,码住不

ghnor / 2152人阅读

摘要:在符号位中,表示正,表示负。我们知道对于整型来说,内存中存放的是该数的补码。在计算机系统中,数值一律用补码来表示和存储。表示有效数字,。规定对于位的浮点数,最高的位是

最近博主开学啦!更新节奏有点跟不上,这里做个检讨~

UU们开学感觉怎么样呢?见到同学或者舍友有没有很开心呢?又可以一起愉快地玩耍咯!

但是玩归玩,学习还是正事,话不多说,开始今天的内容。

在之前的内容中,我们已经把C语言的入门知识进行了一个全面的讲解,并介绍了一些实用的调试技巧,以及函数栈帧的创建和销毁,可以说对于C语言已经算是敲过开门砖了。

那么今天,我们就要开启深入学习C语言的旅程啦!首先解决C语言进阶第一问:数据在内存中是如何存储的?

当然,我们主要探讨的是整型和浮点型这两种类型。

数据类型

C语言中具以下几种基本内置类型:

这里说明一下:

  • C语言的基本内置类型只的是C语言本身具有的类型,而库函数本身是不属于C语言的,是独立于C语言之外的。
  • C语言是用来决定语言的语法形式的,而库函数是编译器的产商提供,当然库函数的使用是受C语言标准约束的(比如C语言标准规定了一些库函数的函数名、参数类型、返回值类型和函数功能)。
  • 这样做的好处是虽然不同编译器实现函数的方式不一样,但是对于我们来说,在不同的编译器下使用函数的方式是一样的。
  • 当然在有一些编译器中对一些库函数的支持提供得不是很好,比如在VS编译器下使用scanf函数就可能会报错,需要使用scanf_s。

由于字符类型在存储的时候是按照ASCII码值存储的,所以字符类型也属于整型家族中的一员。

前面我们已经学习了以上这么多的数据类型,并且了解了它们所占内存空间的大小。

我们还要明白,我们为什么要给数据分那么多的类型:

  1. 我们生活中出了的数除了整数就是小数,所以为了更好地存储这两种类型,数据分为整型和浮点型两大类
  2. 使用不同的类型时内存开辟的空间大小是不一样的(使用范围也不一样)。我们应该根据数据的大小选择合适的类型,如果选大了,则浪费空间;选小了,则数据无法完整存储。
  3. 不同类型决定了我们看待空间时应该采用哪种视角。即对于内存中不同空间的类型存和取的方式是不一样的。

类型的基本归类

整型家族

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

注意:



如何理解有符号和无符号:

以char类型举例:

这里也说明了不同类型决定了我们看待内存中的值时视角也不同。

同时,我们也可以由此推算出一个类型中最大能放一个多大的数字。

还是以char类型举例:

由此可以得出,有符号的char中可以存放的数的范围是 -128 ~ 127。

相同道理:

无符号的char中可以存放的数的范围是 0 ~ 255。

同理,我们可以得出short、int 、long等等类型的范围。

浮点型

float
double

这两种类型通常根据数据要求的精度来选择:精度高选择double类型(8个字节),精度低选择float类型(4个字节)。

一般我们更常选择float类型,但是记住噢~3.14默认是double类型哦!

构造类型

除了内置类型之外,还有构造类型,即可以自己创造的类型。

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

数组类型为什么也属于构造类型呢?

我们来看看数组的类型:


当我们定义的数组元素的类型和个数不同时,数组的类型也不同,可以通过sizeof反映出来,所以我们也说数组属于构造类型。

指针类型

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

空类型

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

这里说void作为函数参数是什么情况呢?

正常我们在调用函数的时候,如果这个函数不需要传参,则不写参数。

但是如果是一个不需要传参的函数,而我们又给它传参了,函数会怎样呢?在有的编译器中,会报错,但是有的编译器则会接受这种情况。

但是如果我们给参数加上一个void。编译器就会报错或者警告,告诉你不能传参啦!

整型在内存中的存储

我们知道,一个变量创建之后是要在内存中开辟空间的,那么这些变量在内存中到底是怎么存储的呢?

接下来我们就来看看~

首先看看整型在内存中是如何存储的。

我们知道,二进制中有符号的整数有原码、反码、补码三种表示方式。

这三种表示方式都有符号位和数值位两部分。

  • 在符号位中,0表示“正”,1表示“负”。
  • 在数值位中,正数的原、反、补码相同;负数的三种表示方法各不相同:
    原码:直接将二进制按照正负数的形式翻译成二进制。
    反码:将原码的符号位不变,其他位依次按位取反。
    补码:反码+1得到补码。

我们知道对于整型来说,内存中存放的是该数的补码。

在计算机系统中,数值一律用补码来表示和存储。

但是存放的是补码,而不是原码或者反码呢?

因为,使用补码可以将符号位和数值位作统一处理。

同时,加法和减法也可以统一处理(CPU只有加法器)。

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

比如,我们想计算1-1。但是因为CUP只有加法运算,所以我们可以把表达式写成1+(-1)。


我们可以看到,用原码计算,得不出正确的结果。

但是如果用补码计算,情况就不一样了。


从上图的计算中,我们可以看出,使用补码可以直接对符号位和数值位同一进行处理。

这里有一个有意思的事情:

我们知道原码、反码和补码都是怎么得到的,那么计算一下我们会发现用同样的方法,我们也可以通过补码得到原码。


(不信你试着算一下!)

所以,由此我们可以看到用补码来存储的好处。

大小端字节序

定义


我们看到,a是一个十六进制的数,当我们在内存中查看它的存储情况时,可以发现,它的存放是从低地址开始放44 33 22 11。

对于它们在内存中的存放顺序,其实可以有很多种存放的方式。

但是如果大家都按照自己的想法来写,则整个存储就会乱套,而且如果我们不按照正常的顺序存储时,取出来要获得原来的数就比较麻烦,所以我们最后决定只以下两种存储顺序。


注意:这里的存储顺序是以字节为单位来讨论的。因此,也称字节序。

其中,上面的存储顺序称为小端字节序;下面的存储顺序称为大端字节序。

  • 小端字节序存储
    把一个数的低位字节的内容,存储在内存的低地址出,把这个数的高位字节的内容,存储在内存的高地址处。
  • 大端字节序存储
    把一个数的低位字节的内容,存储在内存的高地址出,把这个数的高位字节的内容,存储在内存的低地址处。

意义

那么,为什么在存储时还要有大小端之分呢?

这是因为在计算机系统中,内存空间的最小单元是一个字节。所以,如果我们存放的变量大小大于一个字节时,我们就不得考虑每个字节应该以何种顺序存放在内存中了。

那么我们当前的编译器是采取大端存储还是小端存储呢?

别着急,回头看看内存中的值的顺序,就能知道啦!

所以,我们当前编译器采用的是小端字节序。

百度曾经出过一道笔试题,让被试者设计一个程序来判断当前机器的字节序。

那么我们应该如何做呢?

我们用1来进行观察。


所以,我们只需要拿出第一个字节,看看里面存的是0还是1就知道了。

当然,我们要实现的是查看一个机器字节序的功能,所以这里我们最好把程序封装成一个函数。

浮点型在内存中的存储

看完了整数在内存中是怎么存储的,那么浮点型在内存中又是怎么存储的呢?

我们接下来就来看看。

首先我们常见的浮点数有:

小数:3.1425926
科学计数法:1E10(1.0*1010

在浮点型中,我们有float和double两种类型。

而这两种类型的范围,我们用float.h来定义。

如果我们想看整数的最大值(最小值),我们就用INT_MAX(INT_MIN);并在前面包含头文件


然后我们就能看到int类型所能存放的最大值啦!


那么,浮点数所能存放的最大最小值是多少呢?

同理,我们可以用FLT_MAX(或者FLT_MIN、DBL_MAX等),引用头文件,然后转到定义。

就能得到他们的精度啦~

那么在内存中浮点数的存储和整数的存储是一样的吗?

我们可以先通过一段代码来进行验证。


那么如果浮点型的存储和整型不一样,它又是怎么存储的呢?

根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:

  • (-1)S * M * 2E
    (-1)s表示符号位,当S=0,V为正数;当S=1,V为负数。
    M表示有效数字,1≤M<2。
    2E表示指数位。

我们以小数5.5来举例。


同理,我们可以写出9.0的表示形式:

我们会发现,其实任何一个浮点数都可以写成这种形式。

那么,只要我们把一个浮点数写成(-1)S * M * 2E这种形式,那么只要我们存储了S、M、E的信息,就能还原出浮点数的值了。

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,因此这个1和它后面的小数点可以被舍去,只保存后面的XXXXXX部分。

比如:当我们要保存1.01的时候,只需要保存01,等到读取的时候,再把前面的1.加上去。

这样,我们就节省了1位有效数字。

以32位浮点数(float)为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字,这样精度就得到了提高。

  • 那么对于指数E来说,它又是怎么存进去的呢?

我们知道,对于指数E来说,它是可以去负数的,那么对于负数的E在存储的时候,我们又应该如何处理呢?

于是人们又想出了一个办法,不如给这个E加上一个中间数,让E即使是最小的负数,加上这个数之后也不会为负(等于0),那么就不会出现负数的情况了,只要我们取出来的时候再把这个中间数加上就行了。

这样,我们就可以把E作为一个无符号整数了。

如果E为8位,那么它的取值范围就是:0~255,那么它的中间数就是127。

对于11位的E,它的取值范围是0~2047,它的中间数就是1023。

比如:210的E是10,所以保存成32位浮点数时,就保存成10+127=137,即10001001。

以5.5为例,我们可以在内存中看到它的存储。

并且我们可以发现,对于浮点数,它在内存中的存储也是有大小端的。

以上,我们就知道了浮点数在内存中是怎么存的了。
那么它又是怎么取出来的呢?

按道理,我们是怎么放进去的,就应该是怎么取出来的。但是由于指数E比较特别,所以再取出来时,又有一些特别的规定。

在内存中取出E时,我们有一下三种情况:

E不全为0或不全为1

这时,E是怎么存进来的,我们就按照原路取出来。

即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。

比如: 0.5的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为1.0*2^(-1)。

同理,反过来,我们可以通过这个二进制序列得到0.5。

E全为0

当E全为0的时候,说明浮点数的指数E等于-127,而2-127是一个非常非常小的数,已经十分接近于0了。

所以这时候,我们就不再按照原来的计算方式,而是把浮点数的指数E直接记为1-127(或者1-1023), 并且有效数字M不再加上第一位的1,而是还原为0.XXXXXX的小数。这样做是为了表示±0,以及无穷接近于0的很小的数。

E全为1

当E全为1时,则得到的是255,减去127得到的是128,而2128是一个非常大的数。所以这时,如果有效数字M全为0,则该数就表示±无穷大(正负取决于符号位S)

当我们了解完以上浮点型在内存中的存储规则之后,我们就可以理解之前哪个代码打印出来的值了。

今天的文章就到这里啦!~

你学废了吗?记得点赞收藏加关注,在评论区留下你的脚印~

关注我!一起精进C语言!

今天的代码都在这里咯!
https://gitee.com/fang-qiuhui/my-code/blob/4ab3bddffe0dc8e520fabbee219aa421f67d6c1b/blog_2021_8_30_data/blog_2021_8_30_data.cpp

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/119293.html

相关文章

  • C语言中还有这些类型,别再说你不知道了!把手带你解锁C语言自定义类型,让你写你所想。

    摘要:结构体类型的特殊声明在初阶结构体中,我们已经将了结构体类型是如何进行声明的,那么在这里,我们将讲一些特殊的结构体声明不完全的声明。所以我们应该这样写通过指针来找到下一个同类型结构体的写法,我们就称之为结构体的自引用。 ...

    hizengzeng 评论0 收藏0
  • JavaScript - 收藏集 - 掘金

    摘要:插件开发前端掘金作者原文地址译者插件是为应用添加全局功能的一种强大而且简单的方式。提供了与使用掌控异步前端掘金教你使用在行代码内优雅的实现文件分片断点续传。 Vue.js 插件开发 - 前端 - 掘金作者:Joshua Bemenderfer原文地址: creating-custom-plugins译者:jeneser Vue.js插件是为应用添加全局功能的一种强大而且简单的方式。插....

    izhuhaodev 评论0 收藏0
  • C语言进阶学习】一、数据存储 (深度剖析数据内存存储)

    摘要:的理解和区别代表有符号,整数在内存中存储的二进制位的最高位为符号位,表示负数,表示正数。那接下来我们来学习数据在所开辟的内存空间时如何存储的。请看下面例子为什么内存中存储的是补码对于整数来说数据存放内存中其实存放的是补码。 ...

    AprilJ 评论0 收藏0
  • iOS进阶

    摘要:然而,副作用对于系统的可测试性来说就是一剂毒药,并且可能会因应用程序和请求的不同而出现差异性。这些事件并不具备特定时序性,甚至它们可能同时发生。粘性动画中,粘性小球会根据移动距离的大小拥有不同的弹性程度。 PPAsyncDrawingKit - 实现了一系列基础 UI 控件的轻量级 ASDK 一款轻量级的 ASDK,实现了一系列基础 UI 控件。 iOS 开发之 Runtime 常用示...

    Cheng_Gang 评论0 收藏0
  • Java 总结

    摘要:中的详解必修个多线程问题总结个多线程问题总结有哪些源代码看了后让你收获很多,代码思维和能力有较大的提升有哪些源代码看了后让你收获很多,代码思维和能力有较大的提升开源的运行原理从虚拟机工作流程看运行原理。 自己实现集合框架 (三): 单链表的实现 自己实现集合框架 (三): 单链表的实现 基于 POI 封装 ExcelUtil 精简的 Excel 导入导出 由于 poi 本身只是针对于 ...

    caspar 评论0 收藏0

发表评论

0条评论

阅读需要支付1元查看
<