情境引入:
先来看看下面的代码,你们觉得运行的结果会是怎么样呢?
#include<stdio.h>
int main()
{
int a = 5;
printf("a的值为:%d\n", a);
float b = (float)a;
printf("b的值为:%f\n", b);
float* c = (float*)&a;
printf("c的值为:%f \n", *c);
*c = 5.0;
printf("a的值为:%d \n", a);
printf("a的值为:%f \n", *c);
return 0;
}
我还没学数据存储的时候,我认为的答案是5 , 5.0, 5.0 ,5 , 5.0
但答案实际上是:
是不是很纳闷为什么是这个结果?接下来就给你解密原理。
整形在内存中的存储
先来了解一下最基本的三个概念吧:
原码:原码是指一个二进制数左边加上符号位后所得到的码,且当二进制数大于0时,符号位为0;二进制数小于0时,符号位为1;二进制数等于0时,符号位可以为0或1(+0/-0)
反码:正数的反码是原码,负数是将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:正数的反码是原码,负数反码+1得到补码。
总所周知,计算机只能存二进制数。那么整数要想在计算机中存储,肯定是要转化成二进制存储。
那么直接在内存里面存储原码不就可以了吗?
但设计的时候还要考虑运算问题和符号位的处理问题,如果用原码,符号位要单独拿出来处理。增加设计的复杂度。而且原码和补码的相互转换的,它们的运算过程也是相同的。
int a = 3;
int b = -5;
// 3
//原码 00000000 00000000 00000000 0000 0011
//反码 00000000 00000000 00000000 0000 0011
//补码 00000000 00000000 00000000 0000 0011
//-5
//原码 10000000 00000000 00000000 0000 0101
//反码 11111111 11111111 11111111 1111 1010
//补码 11111111 11111111 11111111 1111 1011
//补码取反 10000000 00000000 00000000 0000 0100
//补码取反加一 10000000 00000000 00000000 0000 0101
//
//3 + (-5) = -2
//两个的原码相加
//00000000 00000000 00000000 0000 0011 3
//10000000 00000000 00000000 0000 0101 -5
//10000000 00000000 00000000 0000 1000 -8 != -2,所以原码不可以对符号位进行统一处理
//两个的补码相加
//00000000 00000000 00000000 0000 0011 3
//11111111 11111111 11111111 1111 1011 -5
//11111111 11111111 11111111 1111 1110 结果的补码
//10000000 00000000 00000000 0000 0001 结果的补码取反
//10000000 00000000 00000000 0000 0010 结果的原码 -2 ,所以原码可以对符号位进行统一处理
那么在整形表达式中,是使用的原码还是补码进行计算的呢?
int main()
{
int i = 20;
unsigned int j = -10;
printf("%d\n", i + j);
return 0;
}
//使用的是原码的话
//20
// 00000000 00000000 00000000 00010100
//-10
// 10000000 00000000 00000000 00001010
// 10000000 00000000 00000000 00011110 相加,为-30
//使用的是补码的话
//20
// 00000000 00000000 00000000 00010100
//-10
// 11111111 11111111 11111111 11110110
//100000000 00000000 00000000 00001010 最前面的1越界,所以直接丢掉,转化为原码为10
总结:
- 对于整形来说:数据在内存中存放的是数据的补码
- 整形表达式计算使用的实际上是补码
- 打印和看到的都是原码
浮点型在内存中的存储
根据国际标准IEEE(电气和电子工程协会)754标准,任意一个二进制浮点数V可以表示成下面的形式:
- (-1)^S * M * 2^E
- (-1)^S 表示符号位,当S=0,V为正数; 当S=1时,V为负数
- M表示有效数字,大于等于1,小于2
- 2^E表述指数位
举个例子吧
比如十进制数13.5,转化为二进制就是1101.1 也就是1.1011x2^3
那么按照上面的形式,S = 0 M = 1101.1 E = 3
IEEE 754规定:
对于32位的浮点数,最高的1位是符号位s,接着8位是指数E,剩下的23位为有效数字M。
对于64位的浮点数,最高位是符号位S,接着11位是指数E,剩下的52位为有效数字M。
存储模型和上面类似,这里就不在画模型图了。
IEEE754对有效数字M和指数E,还有一些特别的规定。
前面说过,M大于等于1,M小于2.也就是说,M可以写成1.xxxxxx的形式,其中xxxxxxx表示小数部分。
IEEE754 规定,在计算机内部保存M时,默认这个数的第一位总是1,所以1就可以直接被省略,只保存后面的xxxxxx部分。
比如,保存1.01时,只保存01,等到读取数据的时候,再把第一位的1加上去。这样做的目的,是节省一位有效数字。以32位的浮点数为例,留给M的只有23位
但将第一位的省去之后,就可以保存24位有效数字(存放的23位加被省去的1位)。
不同于M,指数E的情况是比较复杂的。
首先要明确一点,E是一个无符号整数(unsigned int)
那么就是说E只能表示成非负数,并且范围是0~255。但是在科学计数法中,E显然是可以表示成负数的。
所以IEEE 754规定,存入内存时E的真实值必须加一个偏移量,对于8位的E,这个偏移量是127(也就是二进制里的七个1)
同理对于11位的E,这个偏移量是1023。
上图是八位E是有符号数的取值范围
显然,偏移量实际上就是有符号取值的最大正数。
然后,指数E从内存中取出还可以分为三种情况:
E不全为0或不全为1
这时,浮点数就用以下的规则表示,即指数E的计算值减去偏移量,得到其真实值,再将有效数字M前加上第一位的1.
eg:
0.25的二进制形式为0.01.由于规定正整数部分必须为1,即将小数点右移两位,
则为1.00*2^(-2),其阶码为-2+127 = 125 表示为 0111 1101,而尾数1.00去掉整数部分为00,补齐0到23位00000000 00000000 0000000,那么二进制形式表示为
0 01111101 00000000000000000000000
E全为0
这时,浮点数的指数等于1-偏移量,即为真实值
有效数字不再加上第一位的1,而是还原为0.xxxxxxx的小数。这样做是为了表示 ±0,以及接近于0很小的数字。
E全为1
这时,如果有效数字M全为0,表示±无穷大;
浮点数是无法精确保存的,只能说是精确到了多少位。