编写程序时,两个浮点数不能直接比较大小(无论单精度float双精度double),这是因为浮点数在计算机内部不能精确的表示。
首先明确定点数和浮点数:定点数就是小数点位置固定不动的数,在计算机中,我们假设小数点在最前面,例如,001表示二进制小数0.001;10表示二进制小数0.10。这样任何小于1且不小于0的小数都可以用定点数来表示。浮点数就是小数点位置不固定的数,这样就需要一定的方法来表示浮点数。
再看一下十进制小数和二进制小数的互相转换:
在计算机内部存储时,浮点数主要分为三部分来存储:符号位、阶码部分(也有叫指数)和尾数部分【32位单精度float符号位占1bit,阶码占8bit,尾数部分占23bit;64位双精度double符号位占1bit,阶码占11bit,尾数部分占52bit】。
其中阶码存储的是实际指数值的移码,尾数部分存储的是规约化后的尾数的补码。移码就是将实际的阶码值加上一个固定的数值而得到的数,在float型数据中规定该固定值/偏置量为127,阶码有正负所以8为二进制表示范围-128—127,double中规定该固定值/偏置量为1023,表示范围-1024—1023。
令 n n n表示阶码部分的长度,这个数就是 e = 2 n − 1 − 1 e=2^{n-1}-1 e=2n−1−1。比如若真实的阶码为2,float类型数据则加上127后为129,阶码形式为1000 0010
。这样做的目的就是即使最小的阶码(比如单精度为-126)存储的时候也会存储为1,方便了阶码的大小比较也省了一个符号位。这样浮点数的计算公式可以表示为 ( − 1 ) s i g n × 2 e x p o n e n t − e × f r a c t i o n 2 (-1)^sign\times2^{exponent-e}\times fraction_2 (−1)sign×2exponent−e×fraction2.
例如十进制的0.5表示为二进制即为 ( 0.1 ) 2 = ( − 1 ) 0 × 2 − 1 × ( 1.0 ) 2 (0.1)_2=(-1)^0\times 2^{-1}\times(1.0)_2 (0.1)2=(−1)0×2−1×(1.0)2,用单精度float存储,则阶码部分用移码表示即-1+127=126,十进制0.5的存储就是:
0 0111 1110 10000000000000000000000
但是因为规定尾数部分的整数部分恒为1,所以表示的时候可以去掉,于是存储为:
0 0111 1110 00000000000000000000000
这样,存储的二进制转化为实际数的计算公式为:
( − 1 ) s i g n × 2 e x p o n e n t − e × ( f r a c t i o n 2 + 1 ) (-1)^{sign}\times2^{exponent-e}\times (fraction_2+1) (−1)sign×2exponent−e×(fraction2+1)
当阶码部分为0,尾数部分不为0时,这个浮点数称为非规约形式浮点数,其阶码的偏移值比规约形式的浮点数大1。比如exponent=1,显然这是规约形式浮点数,其实际指数应该是-126。而exponent=0,这是非规约形式浮点数,(若按照规约形式浮点数计算,其实际指数应为-127(0-127))那么根据前面提到的标准可知这个非规约形式浮点数的实际指数也是-126。所有的非规约浮点数比规约浮点数更接近0。
一般地,float型125.5转化为标准浮点格式:125二进制111 1101
,小数部分二进制为1,则125.5二进制表示为111 1101.1
,由于规定尾数的 整数部分恒为1,则表示为 1.1111011 ∗ 2 6 1.1111011*2^6 1.1111011∗26,阶码为6,加上127为133,表示为1000 0101
,而对于尾数将整数部分1去掉为111 1011
,后面补0达到23位,则为111 1011 0000 0000 0000 0000
,也就是说十进制的125.5二进制表示形式为0 10000101 11110110000000000000000
,内存中存放方式为从低地址到高地址依次为00000000 00000000 11111011 01000010
。根据这个二进制来求浮点数,因为符号位为0所以为正数,阶码133-127=6,尾数111 1011 0000 0000 0000 0000
,则其真实的尾数为1.1111011
,所以大小为 1.1111011 × 2 6 1.1111011\times 2^6 1.1111011×26,小数点右移6位得到111 1101.1
,整数部分125,小数部分0.5。
这样我们也能得到float类型表示的最大范围即1.111 1111 1111 1111 1111 1111*2^127=3.4E+38
,所以范围是-3.4E+38~3.4E+38
。double类型类似1.111111111111111111111(小数点后52个1)*2^1023
,范围是-1.7E-308~1.7E+308
运行验证代码:
#include
using namespace std;
int main(int argc, char *argv[])
{
float a=125.5;
char *p=(char *)&a;
printf("%d\n",*p);
printf("%d\n",*(p+1));
printf("%d\n",*(p+2));
printf("%d\n",*(p+3));
return 0;
}
输出结果0,0,-5,66,因为p和p+1指向的单元转化为十进制都是0,p+2指向的单元,因为是char型指针,带符号的数据类型,因此1111 1011
,符号位为1,则为负数,又因为计算机中二进制以补码形式储存,所以后面的部分应为1000 0101
也就是-5;最后一部分0100 0010
,正数,大小66。
原码:符号位加上真值的绝对值,即第一位表示符号,其余位表示值,比如 [ + 1 ] = [ 00000001 ] 原 , [ − 1 ] = [ 10000001 ] 原 [+1]=[0000 0001]_{原},[-1]=[1000 0001]_{原} [+1]=[00000001]原,[−1]=[10000001]原,所以8位二进制数的取值范围是[1111 1111,0111 1111]即[-127,127]
反码:正数的反码是其本身,负数的反码是在原码的基础上,符号位不变,其余各个位取反,比如 [ − 1 ] = [ 11111110 ] 反 [-1]=[1111 1110]_{反} [−1]=[11111110]反
补码:正数的补码是其本身,负数的补码是在其原码基础上,符号位不变,其余各位取反,最后加1,即反码基础加1,比如[-1]=[1111 1111]_{补}
无符号数无所谓原码、反码、补码。
欢迎扫描二维码关注微信公众号 深度学习与数学 [每天获取免费的大数据、AI等相关的学习资源、经典和最新的深度学习相关的论文研读,算法和其他互联网技能的学习,概率论、线性代数等高等数学知识的回顾]