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


结语

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

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



相关文章
|
2月前
|
存储 编译器 C语言
C语言存储类详解
在 C 语言中,存储类定义了变量的生命周期、作用域和可见性。主要包括:`auto`(默认存储类,块级作用域),`register`(建议存储在寄存器中,作用域同 `auto`,不可取地址),`static`(生命周期贯穿整个程序,局部静态变量在函数间保持值,全局静态变量限于本文件),`extern`(声明变量在其他文件中定义,允许跨文件访问)。此外,`typedef` 用于定义新数据类型名称,提升代码可读性。 示例代码展示了不同存储类变量的使用方式,通过两次调用 `function()` 函数,观察静态变量 `b` 的变化。合理选择存储类可以优化程序性能和内存使用。
156 82
|
1月前
|
存储 C语言 C++
深入C语言,发现多样的数据之枚举和联合体
深入C语言,发现多样的数据之枚举和联合体
深入C语言,发现多样的数据之枚举和联合体
|
1月前
|
存储 C语言
深入C语言内存:数据在内存中的存储
深入C语言内存:数据在内存中的存储
|
1月前
|
C语言
回溯入门题,数据所有排列方式(c语言)
回溯入门题,数据所有排列方式(c语言)
|
1月前
|
存储 C语言
C语言中的浮点数存储:深入探讨
C语言中的浮点数存储:深入探讨
|
存储 C语言
【C语言】四行代码说明浮点型在内存中的储存
【C语言】四行代码说明浮点型在内存中的储存
【C语言】四行代码说明浮点型在内存中的储存
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
33 3
|
6天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
21 6
|
26天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
34 10
|
19天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。