C语言中,整数和实数在内存中的存放形式是怎么样的?它们相同吗?
【定义】 正数 的 补码 就是它的 原码;负数 的 补码 为 它的反码加一;
真值:+ 00000000 00000000 00000000 00100101
补码: 00000000 00000000 00000000 00100101
真值:- 00000000 00000000 00000000 00100101
补码: 11111111 11111111 11111111 11011011
$$ [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$ 的情况进行阐述;
对于三种编码方式,总结如下:
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 的原码: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
,计算结果明显是错的,所以为了解决减法问题,引入了反码;1 - 2
表示成1 + (-2)
,情况如下:+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
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
$value$:代表要表示的浮点数;
$sign$:代表 $value$ 的正负号,它的取值只能是 0 或 1:取值为 0 是正数,取值为 1 是负数;
$base$:代表基数,或者说进制,它的取值大于等于 2;
$fraction$:代表尾数,或者说精度,是 $base$ 进制的小数,并且 $1 \le fraction \lt base$,这意味着,小数点前面只能有一位数字;
$exponent$:代表指数,是一个整数,可正可负,并且为了直观一般采用 十进制 表示。
110011
放入内存。我们将内存中存储的尾数命名为 $f$,真正的尾数命名为 $fraction$,则么它们之间的关系为: $$fraction = 1.f$$float
和double
分配给指数位的比特位不同,所以需要分情况讨论;float
和double
的实际位数来举例说明实际内存中的存储方式。float
还是double
。float
类型,内存分布如下:double
类型,内存分布如下:浮点数类型 | 指数位数 | 指数范围 | 尾数位数 | 尾数范围 |
---|---|---|---|---|
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]$ |
#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
数;
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
数;
14.375000
```
整数用补码
实数用
一位符号位+省略个位1的若干位尾数+若干位移码表示的指数
类似十进制科学计数法的二进制科学计数法
表示