【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搞得一头雾水,而是反而能很亲切的想到这个“错误”的数值是怎样得来的了。


结语

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

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



相关文章
|
1月前
|
存储 编译器 C语言
C语言存储类详解
在 C 语言中,存储类定义了变量的生命周期、作用域和可见性。主要包括:`auto`(默认存储类,块级作用域),`register`(建议存储在寄存器中,作用域同 `auto`,不可取地址),`static`(生命周期贯穿整个程序,局部静态变量在函数间保持值,全局静态变量限于本文件),`extern`(声明变量在其他文件中定义,允许跨文件访问)。此外,`typedef` 用于定义新数据类型名称,提升代码可读性。 示例代码展示了不同存储类变量的使用方式,通过两次调用 `function()` 函数,观察静态变量 `b` 的变化。合理选择存储类可以优化程序性能和内存使用。
142 82
|
4天前
|
存储 C语言
C语言中的浮点数存储:深入探讨
C语言中的浮点数存储:深入探讨
|
1月前
|
存储 人工智能 C语言
数据结构基础详解(C语言): 栈的括号匹配(实战)与栈的表达式求值&&特殊矩阵的压缩存储
本文首先介绍了栈的应用之一——括号匹配,利用栈的特性实现左右括号的匹配检测。接着详细描述了南京理工大学的一道编程题,要求判断输入字符串中的括号是否正确匹配,并给出了完整的代码示例。此外,还探讨了栈在表达式求值中的应用,包括中缀、后缀和前缀表达式的转换与计算方法。最后,文章介绍了矩阵的压缩存储技术,涵盖对称矩阵、三角矩阵及稀疏矩阵的不同压缩存储策略,提高存储效率。
103 8
|
1月前
|
存储 算法 C语言
C语言手撕数据结构代码_顺序表_静态存储_动态存储
本文介绍了基于静态和动态存储的顺序表操作实现,涵盖创建、删除、插入、合并、求交集与差集、逆置及循环移动等常见操作。通过详细的C语言代码示例,展示了如何高效地处理顺序表数据结构的各种问题。
|
5月前
|
存储 C语言
C语言中的数据输入输出
C语言中的数据输入输出
48 0
|
12月前
|
缓存 C语言
C语言——数据的输入输出
C语言——数据的输入输出
|
C语言
C语言 字符数据输入输出
C语言 字符数据输入输出
101 0
C语言 字符数据输入输出
|
存储 C语言
初识C语言之数据输入输出篇——带你领略编程世界的文字艺术!
初识C语言之数据输入输出篇——带你领略编程世界的文字艺术!
167 0
初识C语言之数据输入输出篇——带你领略编程世界的文字艺术!
|
C语言 Web App开发
|
2天前
|
存储 程序员 编译器
C语言——动态内存管理与内存操作函数
C语言——动态内存管理与内存操作函数