海绵宝宝的菠萝屋* 2021-09-14 21:35 采纳率: 100%
浏览 143
已结题

C语言中,整数和实数在内存中的存放形式是怎么样的?它们相同吗?

C语言中,整数和实数在内存中的存放形式是怎么样的?它们相同吗?

  • 写回答

2条回答 默认 最新

  • 英雄哪里出来 2021年博客之星Top1 2021-09-15 06:07
    关注

    一、整数的存储 - 补码

    1、补码


    [^_^]:
    * 为了解决正负零的问题,引入了补码。

    1)定义

    【定义】 正数补码 就是它的 原码负数补码 为 它的反码加一

    2)举例

    • 1)对于十进制数 37,它的 真值 和 补码 关系如下:
      真值:+ 00000000 00000000 00000000 00100101
      补码:  00000000 00000000 00000000 00100101
      
    • 2)对于十进制数 -37,它的 真值 和 反码 的关系如下:
      真值:- 00000000 00000000 00000000 00100101
      补码:  11111111 11111111 11111111 11011011
      
    • 我们发现,对于负数的情况,反码 减去 真值(注意,这里真值为负数)后,负负得正,转换成二进制位相加正好等于 $1(\underbrace{0...0}_{32})_2$, 即 $2^{32}$,表示成公式如下:$$ [x]_补 - x = 2^{32}$$

      3)公式

    • 因此,通过移项,我们可以得出补码的十进制计算公式如下:

      $$ [x]_补 = \begin{cases} x & (0 \le x < 2^{n-1})\ 2^{n} + x & (-2^{n-1} \le x < 0) \end{cases} $$   这里 $x$ 代表真值,而 $n$ 的取值是 $8、16、32、64$,我们通常说的整型int都是 32位 的,本文就以 $n = 32$ 的情况进行阐述;

      4、编码总结

      对于三种编码方式,总结如下:
        1)这三种机器数的最高位均为符号位;
        2)当真值为正数时,原码、反码、补码的表示形式相同,符号位用 "0" 表示,数值部分真值相同;
        3)当真值为负数时,原码、反码、补码的表示形式不同,但是符号位都用 "1" 表示,数值部分:反码是原码的 "按位取反",补码是反码加一;

    正数

    真值:+ 00000000 00000000 00000000 00100101
    原码:  00000000 00000000 00000000 00100101
    反码:  00000000 00000000 00000000 00100101
    补码:  00000000 00000000 00000000 00100101
    

    负数

    真值:- 00000000 00000000 00000000 00100101
    原码:  10000000 00000000 00000000 00100101
    反码:  11111111 11111111 11111111 11011010
    补码:  11111111 11111111 11111111 11011011
    

    六、为什么要引入补码

    • 最后,我们来讲一下引入补码的真实意图是什么。

      1、主要目的

    • 计算机的四则运算希望设计的尽量简单。但是引入 符号位 的概念,对于计算机来说还要考虑正负数相加,等于引入了减法,所以希望是计算机底层 只设计一个加法器,就能把加法和减法都做了。

    2、原码运算

    • 对于原码的加法,两个正数相加的情况如下:
      ```c

    +1 的原码:00000000 00000000 00000000 00000001
    +1 的原码:00000000 00000000 00000000 00000001


    +2 的原码:00000000 00000000 00000000 00000010

    * 好像没有什么问题?于是人们开始探索减法,但是起初设计的人的初衷是希望不用减法,只用加法运算就能够将加法和减法都包含进来,于是,我们尝试用原码的负数表示来做运算;
    * 将 ```1 - 2```表示成```1 + (-2)```,然后用原码相加得到:
    ```c
    +1 的原码:00000000 00000000 00000000 00000001
    -2 的原码:10000000 00000000 00000000 00000010
    ----------------------------------------------
    -3 的原码:10000000 00000000 00000000 00000011
    
    • 我们发现1 + (-2) = -3,计算结果明显是错的,所以为了解决减法问题,引入了反码;

      3、反码运算

    • 对于正数的加法,两个正数反码相加的情况和原码相加一致,不会有问题。
    • 对于正数的减法,转换成一正一负两数相加。
    • 1 - 2表示成1 + (-2),情况如下:
      ```c

    +1 的反码:00000000 00000000 00000000 00000001
    -2 的反码:11111111 11111111 11111111 11111101


    -1 的反码:11111111 11111111 11111111 11111110

    * 没有什么问题?但是某种情况下,反码会有歧义,当两个相同的数相减时,即```1 - 1```表示成```1 + (-1)```,情况 如下:
    ```c
    +1 的反码:00000000 00000000 00000000 00000001
    -1 的反码:11111111 11111111 11111111 11111110
    ---------------------------------------------
    -0 的反码:11111111 11111111 11111111 11111111
    
    • 这里出现了一个奇怪的概念,就是 "负零",反码运算过程中会出现有两个编码表示零这个数值。
    • 为了解决正负零的问题引入了补码的概念。

      4、补码运算

      1)两个正数的补码相加。

    • 其和等于 它们的原码相加,已经验证过,不会有问题;

      2)一正一负两个数相加,且 答案非零
      ```c

    +1 的补码:00000000 00000000 00000000 00000001
    -2 的补码:11111111 11111111 11111111 11111110


    -1 的补码:11111111 11111111 11111111 11111111

    * 结果正确;
    > 3)一正一负两个数相加,且 **答案为零**。
    ```c
    +1 的补码   00000000 00000000 00000000 00000001
    -1 的补码: 11111111 11111111 11111111 11111111
    ---------------------------------------------- 
    0 的补码:1 00000000 00000000 00000000 00000000
    
    • 两个互为相反数的数相加后,得到的数的补码为 $2^n$(可以认为是是溢出了),所以那个 1 根本不会被存进计算机中,也就是表现出来的结果就是 零!
    • 而且,补码的这个运算,和我们之前提到的定义吻合。
    • 综上所述,补码解决了整数加法带来的所有问题。

    二、浮点数的存储

    1、科学计数法

    • C语言中,浮点数在内存中是以科学计数法进行存储的,科学计数法是一种指数表示,数学中常见的科学计数法是基于十进制的,例如 $5.2 × 10^{11}$;计算机中的科学计数法可以基于其它进制,例如 $1.11 × 2^7$ 就是基于二进制的,它等价于 $(11100000)_2$。
    • 科学计数法的一般形式如下:
    • $$value = (-1)^{sign} \times fraction \times base^{exponent}$$

        $value$:代表要表示的浮点数;
        $sign$:代表 $value$ 的正负号,它的取值只能是 0 或 1:取值为 0 是正数,取值为 1 是负数;
        $base$:代表基数,或者说进制,它的取值大于等于 2;
        $fraction$:代表尾数,或者说精度,是 $base$ 进制的小数,并且 $1 \le fraction \lt base$,这意味着,小数点前面只能有一位数字;
        $exponent$:代表指数,是一个整数,可正可负,并且为了直观一般采用 十进制 表示。

    1)十进制的科学计数法

    • 以 $14.375$ 这个小数为例,根据初中学过的知识,想要把它转换成科学计数法,只要移动小数点的位置。如果小数点左移一位,则指数 $exponent$ 加一;如果小数点右移一位,则指数 $exponent$ 减一;
    • 所以它在十进制下的科学计数法,根据上述公式,计算结果为:
    • $$(14.375)_{10} = 1.4375 \times 10^1$$
    • 其中 $value = 14.375$、$sign = 0$、$base = 10$、$fraction = 1.4375$、$exponent = 1$;
    • 这是我们数学中最常见的科学计数法。

      2)二进制的科学计数法

    • 同样以 $14.375$ 这个小数为例,我们将它转换成二进制,按照两部分进行转换:整数部分和小数部分。
    • 整数部分:整数部分等于 14,不断除 2 取余数,转换成 2 的幂的求和如下:
    • $$(14)_{10} = 1 \times 2^3 + 1 \times 2^2 + 1 \times 2^1 + 0 \times 2^0$$
    • 所以 14 的二进制表示为 $(1110)_2$。
    • 小数部分:小数部分等于 0.375,不断乘 2 取整数部分的值,转换成 2 的幂的求和如下:
    • $$(0.375)_{10} = 0 \times 2^{-1} + 1 \times 2^{-2} +1 \times 2^{-3}$$
    • 所以 0.375 的二进制表示为 $(0.011)_2$
    • 将 整数部分 和 小数部分 相加,得到的就是它的二进制表示:
    • $$(1110.011)_2$$
    • 同样,我们参考十进制科学计数法的表示方式,通过移动小数点的位置,将它表示成二进制的科学计数法,对于这个数,我们需要将它的小数点左移三位。得到:
    • $$(1110.011)_2 = (1.110011)_2 \times 2^3$$
    • 其中 $value = 14.375$、$sign = 0$、$base = 2$、$fraction = (1.110011)_2$、$exponent = 3$;
    • 我们发现,为了表示成科学计数法,小数点的位置发生了浮动,这就是浮点数的由来。

    2、浮点数存储概述

    • 计算机中的浮点数表示都是采用二进制的。上面的科学计数法公式中,除了 $base$ 确定是 2 以外,符号位 $sign$、尾数位 $fraction$、指数位 $exponent$ 都是未知数,都需要在内存中体现出来。还是以 $14.375$ 为例,我们来看下它的几个关键数值的存储。

      1)符号的存储

    • 符号位的存储类似存储整型一样,单独分配出一个比特位来,用 0 表示正数,1 表示负数。对于 $14.375$,符号位的值是 0。

      2)尾数的存储

    • 根据科学计数法的定义,尾数部分的取值范围为 $$1 \le fraction \lt 2$$
    • 这代表尾数的整数部分一定为 1,是一个恒定的值,这样就无需在内存中提现出来,可以将其直接截掉,只要把小数点后面的二进制数字放入内存中即可,这个设计可真是省(扣)啊。
    • 对于 $(1.110011)_2$,就是把110011放入内存。我们将内存中存储的尾数命名为 $f$,真正的尾数命名为 $fraction$,则么它们之间的关系为: $$fraction = 1.f$$
    • 这时候,我们就可以发现,如果 $base$ 采用其它进制,那么尾数的整数部分就不是固定的,它有多种取值的可能,以十进制为例,尾数的整数部分可能是 $1 \to 9$ 之间的任何一个值,如此一来,尾数的整数部分就无法省略,必须在内存中表示出来。但是将 $base$ 设置为 2,就可以节省掉一个比特位的内存,这也是采用二进制的优势。

      3)指数的存储

    • 指数是一个整数,并且有正负之分,不但需要存储它的值,还得能区分出正负号来。所以存储时需要考虑到这些。
    • 那么它是参照补码的形式来存储的吗?
    • 答案是否。
    • 指数的存储方式遵循如下步骤:
    • 1)由于floatdouble分配给指数位的比特位不同,所以需要分情况讨论;
    • 2)假设分配给指数的位数为 $n$ 个比特位,那么它能够表示的指数的个数就是 $2^n$;
    • 3)考虑到指数有正负之分,并且我们希望正负指数的个数尽量平均,所以取一半,$2^{n-1}$ 表示负数,$2^{n-1}$ 表示正数。
    • 4)但是,我们发现还有一个 0,需要表示,所以负数的表示范围将就一点,就少了一个数;
    • 5)于是,如果原本的指数位 $x$,实际存储到内存的值就是:$$x + 2^{n-1} - 1$$
    • 接下来,我们拿具体floatdouble的实际位数来举例说明实际内存中的存储方式。

    3、浮点数存储内存结构

    • 浮点数的内存分布主要分成了三部分:符号位、指数位、尾数位。浮点数的类型确定后,每一部分的位数就是固定的。浮点数的类型,是指它是float还是double
    • 对于float类型,内存分布如下:

    • 对于double类型,内存分布如下:


    • 1)符号位:只有两种取值:0 或 1,直接放入内存中;
    • 2)指数位:将指数本身的值加上 $2^{n-1}-1$ 转换成 二进制,放入内存中;
    • 3)尾数位:将小数部分放入内存中;
    浮点数类型指数位数指数范围尾数位数尾数范围
    float$8$$[-2^7+1,2^7]$$23$$[(0)2, (\underbrace{1...1}{23})_2]$
    double$11$$[-2^{10}+1,2^{10}]$$52$$[(0)2, (\underbrace{1...1}{52})_2]$

    4、内存结构验证举例

    • 以上文求得的 $14.375$ 为例,我们将它转换成二进制,表示成科学计数法,如下:
    • $$(1110.011)_2 = (1.110011)_2 \times 2^3$$
    • 其中 值 $value = 14.375$、符号位 $sign = 0$、基数 $base = 2$、尾数 $fraction = (1.110011)_2$、指数 $exponent = 3$;

      1)float 的内存验证

    • 为了方便阅读,我采用了颜色来表示数字,橙色代表符号位,蓝色代表指数位,红色代表尾数,绿色代表尾数补齐位;并且 八位一分隔,增强可视化。
    • 符号位的内存:0
    • 指数的内存(加上127后等于130,再转二进制):10000010
    • 尾数的内存(不足23位补零):1100110 00000000 00000000
    • 按顺序组织到一起后得到:01000001 01100110 00000000 00000000
    #include <stdio.h>
    int main() {
        int value = 0b01000001011001100000000000000000;  // (1)
        printf("%f\n",  *(float *)(&value) );            // (2)
        return 0;
    }
    

    运算结果如下:
      $(1)$ 第一步,就是把上面那串二进制的 01串 直接拷贝下来,然后在前面加上0b前缀,代表了 $value$ 这个四字节的内存结构就是这样的;
      $(2)$ 第二步,分三个小步骤:
       $(2.a)$ &value代表取value这个值的地址;
       $(2.b)$ (float *)&value代表将这个地址转换成float类型;
       $(2.c)$ *(float *)&value代表将这个地址里的值按照float类型解析得到一个float数;

    • 运行结果为:
      ```c

    14.375000

    * (有关取地址和指针相关的内容,由于前面章节还没有涉及,如果读者看不懂,也没有关系,后面在讲解指针时会详细讲解这块内容,敬请期待)。
    ### 2)double 的内存验证
    * 为了方便阅读,我采用了颜色来表示数字,<font color=orange>橙色</font>代表符号位,<font color=blue>蓝色</font>代表指数位,<font color=red>红色</font>代表尾数,<font color=green>绿色</font>代表尾数补齐位;并且 八位一分隔,增强可视化。
    * 符号位的内存:<font color=orange>0</font>
    * 指数的内存(加上1023后等于1026,再转二进制):<font color=blue>100 00000010</font>
    * 尾数的内存(不足52位补零):<font color=red>1100 11</font><font color=green>000000 00000000 00000000 00000000 00000000 00000000</font>
    * 按顺序组织到一起后得到:<font color=orange>0</font><font color=blue>1000000 0010</font><font color=red>1100 11</font><font color=green>000000 00000000 00000000 00000000 00000000 00000000</font>
    ```c
    #include <stdio.h>
    int main() {
        long long value = 0b0100000000101100110000000000000000000000000000000000000000000000;  // (1)
        printf("%lf\n",  *(double *)(&value) );                            // (2)
        return 0;
    }
    

    运算结果如下:
      $(1)$ 第一步,就是把上面那串二进制的 01串 直接拷贝下来,然后在前面加上0b前缀,代表了 $value$ 这个八字节的内存结构就是这样的;
      $(2)$ 第二步,分三个小步骤:
       $(2.a)$ &value代表取value这个值的地址;
       $(2.b)$ (double *)&value代表将这个地址转换成double类型;
       $(2.c)$ *(double *)&value代表将这个地址里的值按照double类型解析得到一个double数;

    • 没错,运行结果也是:
      ```c

    14.375000

    ```

    • 这块内容,如果你看的有点懵,没有关系,等我们学了指针的内容以后,再来回顾这块内容,你就会如茅塞一样顿开了!
    • 你学废了吗?🤣


    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论
查看更多回答(1条)

报告相同问题?

问题事件

  • 系统已结题 10月24日
  • 已采纳回答 10月16日
  • 创建了问题 9月14日

悬赏问题

  • ¥15 如何在scanpy上做差异基因和通路富集?
  • ¥20 关于#硬件工程#的问题,请各位专家解答!
  • ¥15 关于#matlab#的问题:期望的系统闭环传递函数为G(s)=wn^2/s^2+2¢wn+wn^2阻尼系数¢=0.707,使系统具有较小的超调量
  • ¥15 FLUENT如何实现在堆积颗粒的上表面加载高斯热源
  • ¥30 截图中的mathematics程序转换成matlab
  • ¥15 动力学代码报错,维度不匹配
  • ¥15 Power query添加列问题
  • ¥50 Kubernetes&Fission&Eleasticsearch
  • ¥15 報錯:Person is not mapped,如何解決?
  • ¥15 c++头文件不能识别CDialog