【C语言】整形数据和浮点型数据在内存中的存储

简介: 【C语言】整形数据和浮点型数据在内存中的存储

一.观察现象,提出问题

       为什么我们用%f打印整形数值时结果总为0.000000,而用%d打印浮点型数值时结果总很大的一个数字

       为了一次性搞清楚这个问题,我们先来看一个案例:

#include<stdio.h>
int main()
{
  int a = 8;
  //创建整形变量a并赋值一个整数8
 
  float* p = (float*) &a;
  //取出a的地址,并强制类型转换成(浮点型指针)的形式存储在浮点型指针变量p中
 
  printf("a的值为:%d\n", a);
  printf("*p的值为:%f\n", *p);
  //分别以整形和浮点型的方式打印a和*p的值
 
  *p = 8.0;
  //通过指针解引用的方式将a的值改为8.0
 
  printf("a的值为:%d\n", a);
  printf("*p的值为:%f\n", *p);
  //再分别以整形和浮点型的方式打印a和*p的值
 
  return 0;
}

       该程序放入vs编译器后的运行果如下

       可以发现一个有趣的现象,当我们使用%f来打印一个整形时,大概率编译器都会打印出一个0.000000出来,而使用%d来打印一个浮点型数据时编译器大概率会打印出一个(看似)非常大且没有规律的数字

       有许多同学会认为这是编译器报错的一种方式,即遇到用%f打印整形的“错误指令”时就固定打印出0.000000来提醒程序员代码写错了,而遇到用%d来打印浮点型的“错误指令”时就打印一个随机值来提醒程序员代码写错了。

       但接下来我们一起探究一下整形数据和浮点型数据在内存中的存储后,就能明白其实编译器给出的这些数字是经过非常严格的计算得来的,而不是我们想象的那样是个随机值。


二.了解整形在内存中的存储方式

      首先,计算机中的整数三种2进制表示方法,即原码反码补码。三种表示方法符号位数值位两部分,符号位都是用0表示“”,用1表示“”。

       整形在内存中的存储图示:

要注意的是:

正数原、反、补码相同

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

原码

直接将数值按照正负数的形式翻译成二进制就可以得到原码。

反码

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

补码

反码+1就得到补码。

       我们拿-8来举例:

int b=-8;

首先写出它的原码:1000 0000 0000 0000 0000 0000 0000 1000(原码)

符号位不变,取反:1111 1111 1111 1111 1111 1111 1111 0111(反码)

给反码加一得到:1111 1111 1111 1111 1111 1111 1111 1000(补码)

补码转换成16进制:F       F       F       F       F        F       F       8

       接着我们打开编译器查看内存中变量b的地址:(注:该编译器为小端存储模式,因此是倒着依次存入每个字节的数据的,注意,小端存储模式是将整形内部的四个字节顺序颠倒存储,而每个字节内部的信息不会颠倒的,因此不是8f ff ff ff,而是f8 ff ff ff)

        由此可见,对于整形来说:数据存放内存中其实存放的是补码。而以补码的形式存储数据的主要原因是因为计算机cpu只有加法器,使用补码,可以将符号位和数值域统一处理


三.了解浮点型数据在内存中的存储方式

       了解了整形数据在内存中的存储方式后,我们再来看浮点型数据是如何在内存中存储的,

首先我们来看看浮点数是什么:(来源:百度百科)

       看定义可能不太好理解,通俗的讲,浮点数之所以叫浮点数就是因为它的小数点可以左右任意浮动的,看个例子可能就比较好理解了:

......

......

       用这种科学计数法的方式表示小数时,小数点的位置就变得「漂浮不定」了,这就是浮点数名字的由来。使用同样的规则,对于二进制数,我们也可以用科学计数法表示,也就是说把基数 10 换成 2 即可

       接下来我们一起看看IEEE 754关于浮点数的定义:(来源:百度百科)

       首先要注意,浮点数在内存中的存储不论正数负数一律是原码!接着我们来看看官方是怎样定义浮点数的存储的:

       通俗来讲,一个浮点数V必定能够写成以下形式:

       其中各个变量的含义为:

  • S符号位,取值 为 0 或 1,决定一个浮点数的符号,0 表示正号,1 表示负号
  • F尾数,用小数表示,如前面所看到的 3.14* 10^0,其中3.14就是尾数
  • R基数,如果表示十进制数 R 就是 10,如果表示二进制数 R 就是 2
  • E指数,用整数表示,如前面看到的 10^-1,-1 即是指数

       单抛一个公式可能有点难理解,下面我们来举个例子吧:

float c=5.5;

我们定义一个单精度浮点型变量c并赋值为5.5

而5.5的二进制表示为:101.1

因为是二进制表示数字,所以R=2

因为5.5是正数,所以我们的S=0

而101.1又可以将小数点左移2位得到1.011*2^(2),

所以我们的E取2,即二进制的:10

而最后剩下的1.011就是我们的F.

既然现在我们的变量S,F,R,E都求出来了,现在就剩下怎么向浮点型变量开辟的32/64个比特位填充的问题了:

IEEE 754规定 提供了 2 种浮点格式:

  • 单精度浮点数 float:32 位,符号位 S 占 1 bit,指数 E 占 8 bit,尾数 M 占 23 bit
  • 双精度浮点数 float:64 位,符号位 S 占 1 bit,指数 E 占 11 bit,尾数 M 占 52 bit

但还要注意

  1. 因为F的第一位固定是1.xxx,因此IEEE规定在存储时直接就将这个1省略不存了。这样f有限的23位就可以多存一位有效数据了。
  2. 因为指数 E 是个无符号整数,表示 float 时,一共占 8 bit,所以它的取值范围为 0 ~ 255。但因为指数可以是负的,所以规定在存入 E 时在它原本的值加上一个中间数 127,这样 E 的取值范围为 -127 ~ 128。表示 double 时,一共占 11 bit,存入 E 时加上中间数 1023,这样取值范围为 -1023 ~ 1024。

除此之外,针对指数E,还有一些相关的规定:

  1. 指数 E 非全 0 且非全 1:规格化数字,按上面的规则正常计算
  2. 指数 E 全 0,尾数非 0:非规格化数,尾数隐藏位不再是 1,而是 0(M = 0.xxxxx),这样可以表示 0 和很小的数
  3. 指数 E 全 1,尾数全 0:正无穷大/负无穷大(正负取决于 S 符号位)
  4. 指数 E 全 1,尾数非 0:NaN(Not a Number)

编译器内给出的结果和我们的计算值是一致的,证明我们的计算是完全正确的。


四.探究问题成因

       掌握了以上知识,我们再回到最开始的那个程序上:

       现在我们知道变量a以整形的方式存入内存空间的,即内存中为a开辟的地址中存储的是数字8的补码,即:0000 0000 0000 0000 0000 0000 0000 1000

       当我们以浮点型的视角来读取这个数据时,就会得到:S=0,E=-126,

       F=000 0000 0000 1000

       根据公式v=(-1)^S*F*R^E=(-1)^0*000 0000 0000 1000*2^(-126)=1.000*2^(-146)

       借助计算器,我们可以得到:

       这已经是一个非常非常小的数了,甚至我们都可以认为它趋于无穷小了,而计算机的精度最多只能表示到0.000000,所以我们看到的结果就是0.000000。


      而*p以浮点型的方式存入内存空间的,即内存中为*p的地址中存储的是浮点数8.0的v,经过计算,我们可以得到:

8.0的二进制:1000.0000

左移3位,得:1.000*2^3

因此:S=0;F=1.000(但因为F不记录小数点前的1.的值,因此实际的F是0000)。

E=3+127=130,130换为二进制:10000010

即*p在内存中存储的是:0  10000010 00000000000000000000000

为了方便计算每四位分隔开:0100 0001 0000 0000 0000 0000 0000 0000

       用计算器进行一下进制转换:

        可以发现,该二进制序列转换为10进制后恰好就是我们之前程序输出的值

       综上所述,不论是用%f打印整形数值时结果为0.000000,还是用%d打印浮点型数值的结果是很大的一个数字,都绝不是随便得出的一个随机的结果,而是计算机遵循其数据存储逻辑,经过精密计算的结果。之所以我们之前会误以为它是一个随机的值,那是因为之前我们根本不了解计算机内部的存储数据的逻辑。当了解了这些后,今后我们再遇到这样的情况就不会只是被bug搞得一头雾水,而是反而能很亲切的想到这个“错误”的数值是怎样得来的了。


结语

最后,希望这些小小的知识碎片拼凑起来能让您感受到计算机世界的乐趣。

我真的会赞叹,计算机真是设计的精妙绝伦。



相关文章
|
4月前
|
消息中间件 存储 缓存
kafka 的数据是放在磁盘上还是内存上,为什么速度会快?
Kafka的数据存储机制通过将数据同时写入磁盘和内存,确保高吞吐量与持久性。其日志文件按主题和分区组织,使用预写日志(WAL)保证数据持久性,并借助操作系统的页缓存加速读取。Kafka采用顺序I/O、零拷贝技术和批量处理优化性能,支持分区分段以实现并行处理。示例代码展示了如何使用KafkaProducer发送消息。
|
5月前
|
存储 编译器 程序员
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
在C语言中,内存布局是程序运行时非常重要的概念。内存布局直接影响程序的性能、稳定性和安全性。理解C程序的内存布局,有助于编写更高效和可靠的代码。本文将详细介绍C程序的内存布局,包括代码段、数据段、堆、栈等部分,并提供相关的示例和应用。
137 5
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
|
6月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
556 13
|
6月前
|
存储 编译器 数据处理
C 语言结构体与位域:高效数据组织与内存优化
C语言中的结构体与位域是实现高效数据组织和内存优化的重要工具。结构体允许将不同类型的数据组合成一个整体,而位域则进一步允许对结构体成员的位进行精细控制,以节省内存空间。两者结合使用,可在嵌入式系统等资源受限环境中发挥巨大作用。
189 12
|
6月前
|
传感器 人工智能 物联网
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发,以及面临的挑战和未来趋势,旨在帮助读者深入了解并掌握这些关键技术。
154 6
|
6月前
|
存储 C语言 开发者
C 语言指针与内存管理
C语言中的指针与内存管理是编程的核心概念。指针用于存储变量的内存地址,实现数据的间接访问和操作;内存管理涉及动态分配(如malloc、free函数)和释放内存,确保程序高效运行并避免内存泄漏。掌握这两者对于编写高质量的C语言程序至关重要。
188 11
|
6月前
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
6月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
140 1
|
6月前
|
存储 C语言 计算机视觉
在C语言中指针数组和数组指针在动态内存分配中的应用
在C语言中,指针数组和数组指针均可用于动态内存分配。指针数组是数组的每个元素都是指针,可用于指向多个动态分配的内存块;数组指针则指向一个数组,可动态分配和管理大型数据结构。两者结合使用,灵活高效地管理内存。
|
7月前
|
监控 算法 应用服务中间件
“四两拨千斤” —— 1.2MB 数据如何吃掉 10GB 内存
一个特殊请求引发服务器内存用量暴涨进而导致进程 OOM 的惨案。
156 14

热门文章

最新文章