float编码杂谈
浮点数的编码转换采用的是IEEE规定的编码标准,float和double 这两种类型的数据的转换原理相同,但是由于范围不一样,编码方式有些区别。IEEE规定的编码会将一个浮点数转换为二进制数。以科学计数法划分,将浮点数拆分成3部分:符号,指数,尾数。
1. float类型的IEEE编码。
Float类型在内存中占4个字节(32位)。最高位用于表示符号:剩余31位中,从右向左取8位表示用于指数,其余为表示尾数。
在进行二进制转换钱,需要对单精度的浮点型进行科学计数法转换。例如,将float类型12.25f转换为IEEE编码,需要12.25f转换为对应的二进制1100.01,整数部分1100,小数部分01,小数部分向左移动1次指数加1,移动到除符号位的最高位1处,停止移动,这里移动3次。在IEEE编码中,由于二进制情况下,最高位时钟为1,为恒值。这里是个正数。所以符号位填0。
12.25转换IEEE后各位情况:
(1)符号位:0
(2)指数位: 十进制3+127,转换为二进制10000010
(3)尾数位: 10001 000000000000000000(不足23位,低位0补齐)
为什么指数位要加127?
由于指数可能为负数,十进制127可表示二进制01111111。IEEE编码方式规定,当指数域小于01111111时为一个负数,反之为一个正数。
12.25f转化后的IEEE编码按二进制拼接0 10000010 10001 000000000000000000转换为16进制为0x41440000,内存中一小端方式排列,故为 00 00 44 41.分析结果如下图:
int *p=(int *)&f;
printf("%08x\r\n",*p);
内存跟踪结果:
Demoe:
浮点数-0。125f转换为0.001,用科学计数法表示为1.0,指数-3.
-0.125f IEEE转换后的各位的情况:
(1)符号位: 1
(2)指数位: 十进制 127+ (-3),转换为二进制是01111100,如果不足8位,则高位0补齐。
(3)尾数位: 23个0
-0.125f转化后的IEEE编码并接为 1 01111100 00000000000000000000000 。转换为16进制为 0XBE000000 ,内存中显示为00 00 00 BE ,分析结果如下:
上面两个例子的小数部分转换为二进制是都是有穷的,如果小数部分为二进制是得到一个无穷值,则会根据尾数部分的长度舍弃多余部分。单精度浮点型1.3f,小数部分转换为二进制位无穷值,依次转换为:0.3,0.6,1.2, 0.4,0.8 ,1.6,1.2 , 0.4 ,0.8…………………\\\\\\\\\
转换为二进制数位:1.01001100110011001100110,到23伟终止,尾数部分无法保存。
1.3f 经IEEE编码后的各位情况为:
(1)符号位 : 0
(2)指数位: 十进制 0+127,转换二进制 01111111
(3)尾数位: 01001100110011001100110
转化为16进制 0X3FA66666,内存显示66 66 A6 3F.
这就是为什么C++在比较浮点型值是否为0时,要做一个区间比较而不是直接等值比较。
Flaot temp=0.0001 f;
If(fFloat>=-temp&&fFloat<=temp)
{
//temp=0
}
2. 各类型指针的工作方式
在C++,各种数据类型都对应的指针类型。指针保存时个地址,为什么需要类型作为修饰呢?
因为需要类型区解释这个地址中的数据。每种数据类型在内存中所占的空间不同,指针中只是保存存放数据的首地址,而没有指明在那里结束。这时候就需要对应的类型来解释数据的结束地址。
例如.同一地址,使用不同类型指针进行访问取出的内容不一样。
: int nVar=0x12345678;
00401268 mov dword ptr [ebp-4],12345678h
8: int *pnVar=&nVar;
0040126F lea eax,[ebp-4]
00401272 mov dword ptr [ebp-8],eax
9: char *pcVar=(char*)&nVar;
00401275 lea ecx,[ebp-4]
00401278 mov dword ptr [ebp-0Ch],ecx
10: short *psnVar=(short*)&nVar;
0040127B lea edx,[ebp-4]
0040127E mov dword ptr [ebp-10h],edx
11: printf("%08x\r\n",*pnVar);
00401281 mov eax,dword ptr [ebp-8]
00401284 mov ecx,dword ptr [eax]
00401286 push ecx
00401287 push offset string "%08x\r\n" (0043101c)
0040128C call printf (00408300)
00401291 add esp,8
12: printf("%08x\r\n",*pcVar);
00401294 mov edx,dword ptr [ebp-0Ch]
00401297 movsx eax,byte ptr [edx]
0040129A push eax
0040129B push offset string "%08x\r\n" (0043101c)
004012A0 call printf (00408300)
004012A5 add esp,8
13: printf("%08x\r\n",*psnVar);
004012A8 mov ecx,dword ptr [ebp-10h]
004012AB movsx edx,word ptr [ecx]
004012AE push edx
004012AF push offset string "%08x\r\n" (0043101c)
004012B4 call printf (00408300)
004012B9 add esp,8
在变量nVar中在内存中的数据位 78 56 43 21,首地址78 开始。
整形4个字节,char 1个字节 8位取两个;Short 2个字节 16为 取4个。
所有类型的指针对于地址的解释都是取自于自身指针的类型。
所有的指针类型只支持加法和减法。指针式用于保存数据的地址,解释地址而存在的。
1:float类型强制转换为int,在编译器中是如何进行的?
由于float特殊的编码方式,决定这一切和别的不一样的。
VC6.0反汇编跟踪结果:
18: float f=134.2225f;
004010D8 mov dword ptr [ebp-4],430638F6h//IEEE编码后的结果0012FF44 F6 38 06 43内存小端表示方式
19: //int a=ftol(f);
20: int a=(float)f;
FLD是浮点指令 表示浮点型In压入ST(0)中。
004010DF fld dword ptr [ebp-4]
004010E2 call __ftol (00401098)//这里是关键,这个干神马的?call在这里明显搞的一个库函数
004010E7 mov dword ptr [ebp-8],eax
21:
22: printf("%d\n",a);
004010EA mov eax,dword ptr [ebp-8]
004010ED push eax
004010EE push offset string "%d\n" (0042601c)
004010F3 call printf (00401130)
004010F8 add esp,8
MS-VC6.0通过库函数_ftol将float型转化为int 型。
下面是根据现有对float编码认识,写的一个ftol函数
int ftol(float f)
{
int a = *(int*)(&f);
int sign = (a>>31); //符号位
int mantissa = (a&((1<<23)-1))|(1<<23);//尾数位
int exponent = ((a&0x7fffffff)>>23)-127;//指数位
int r = ((unsigned int)(mantissa)<<8)>>(31-exponent);//
return ((r ^ (sign)) - sign ) &~ (exponent>>31);
}
//神奇的宏啊
这个宏神奇的在正数和负数的双精度浮点数时都可以正确工作,以四舍五入方式转换为 32 位整数
//来自云风大哥的bolg http://blog.codingnow.com/2006/02/double_to_int_magic_number.html
union luai_Cast { double l_d; long l_l; };
#define lua_number2int(i,d) { volatile union luai_Cast u; \
u.l_d = (d) + 6755399441055744.0; (i) = u.l_l; }
2.基本浮点数的指令。
浮点寄存器是通过栈结构来实现的,由于ST(0)--ST(7)共八个栈空间组成,每个浮点型寄存器占8个字节。每次调用都是是率先调用ST(0),而不能超越ST(0)直接使用ST(1)
浮点型寄存器的使用就是压站,出栈的过程。
IN 表示操作数入栈,OUT表示 操作数出栈。
(1) FLD FLD In 将浮点型In压入St(0)中
(2)FILD FILD In 将整数IN压入ST(0)中。
(3)FILDZ FILDZ 将 0.0压入ST(0)中
(4)FILD1 FILD1 将1.0压入ST(0)中。
(5)FST FST Out ST(0)中的数据以浮点形式存入Out地址中。
(6)FSTP FSTP Out 和FST差不多,但会多执行一次出栈操作。
(7)FIST FIST Out ST(0)以整数形式存入Out地址中。
(8)FISTP FISTP Out 同上,多执行一次出栈的操作。
(9)FCOM FCOM IN 将In地址数据域ST(0)进行实数比较,影响对于的标记位。
(10)FTST FTST 比较ST(0)是否为0.0,影响对应的标记位。
在使用浮点指令时候,都先利用ST(0)进行用算。当ST(0)有值时候,变会将ST(0)数据顺序向下存放到ST(1),然后再将数据存入ST(0)中、,如果ST(7)存满后,在添加数据时候舍弃ST(7)中数据信息。
25: float fFloat=(float)argc;
//将地址ebp+8处的整形数据转换为浮点型数据,并放入ST(0)中,对于变量argc
//FILD指令使用
004010D8 fild dword ptr [ebp+8]
//从ST(0)中取出数据以浮点型编码方式放入地址ebp-4中,对于变量fFloat。ebp栈底,esp栈顶。两者的差形成栈帧。
//FST指令使用 存到OUT地址中,及这里的ebp -4
004010DB fst dword ptr [ebp-4]
26: printf("%f",fFloat);
//这里对esp 执行减8操作时由于浮点型作为变参函数printf时候需要转换为双精度型
提前准备8个字节的栈空间。
004010DE sub esp,8
//FSTP 指令 多一次出栈操作。
004010E1 fstp qword ptr [esp]
004010E4 push offset string "%f" (0042612c)
004010E9 call printf (00401130)//C++中默认的调用方式_cdcel,调用方平衡栈,不定参数函数可以使用。
004010EE add esp,0Ch
27: argc=(int)fFloat;
//下面这段已经说过不多此一举。
004010F1 fld dword ptr [ebp-4]
004010F4 call __ftol (00401098)
004010F9 mov dword ptr [ebp+8],eax
28: printf("%d",argc);
004010FC mov eax,dword ptr [ebp+8]
004010FF push eax
00401100 push offset string "%d" (0042601c)
00401105 call printf (00401130)
0040110A add esp,8
29: return 0;
0040110D xor eax,eax
30: }
0040110F pop edi
00401110 pop esi
Float类型的浮点数虽然栈4个字节,但是以8个字节方式处理。当浮点型作为参数时,就不能这届压栈。Push指令只能传入4个字节的到栈中,这样掉失4个字节数据。这就是为什么使用printf函数以整数方式输出浮点型数据时会出错的原因。
浮点型作为返回值时候情况也是如此,同样需要传8个字节数据。
//将浮点数保存到ST(0)中,在返回为浮点型的情况下,无法使用eax,一般用fld..
3.double类型的IEEE编码。
Double类型栈8个字节的内存空间,也同样,最高位用于表示符号位,指数位栈11位,剩余52位表示尾数。
Float中,指数位范围为八位表示,加127,表示用于判断指数的符号。在double中,由于扩大啦精度,指数精度11位整数表示,加1023后可用于指数符号的判断。
由于Float和Double相似。不做一一举例。
双精度、单精度的有效位数
浮点数7位有效数字。(应该是单精度数)
双精度数16位有效数字。
浮点数取值范围:
负数取值范围为 -3.4028235E+38 到 -1.401298E-45,正数取值范围为 1.401298E-45 到 3.4028235E+38。
双精度数取值范围:
负值取值范围-1.79769313486231570E+308 到 -4.94065645841246544E-324,正值取值范围为 4.94065645841246544E-324 到 1.79769313486231570E+308。
C/C++中浮点数的表示遵循IEEE 754标准。
一个浮点数由三部分组成:符号位S、指数部分E(阶码)以及尾数部分M(如下)。
Floating
S--------E-------M
1位-----8位-----23位
Double
S--------E-------M
1位-----11位----52位
十进制数的换算计算公式为(n^m表示n的m次幂,B表示前面的数字是二进制):
S * 2^(E-127) * (1.M)B
浮点数的精度取决于尾数部分。尾数部分的位数越多,能够表示的有效数字越多。
单精度数的尾数用23位存储,加上默认的小数点前的1位1,2^(23+1) = 16777216。因为 10^7 < 16777216 < 10^8,所以说单精度浮点数的有效位数是7位。
双精度的尾数用52位存储,2^(52+1) = 9007199254740992,10^16 < 9007199254740992 < 10^17,所以双精度的有效位数是16位。
单精度和双精度数值类型最早出现在C语言中(比较通用的语言里面),在C语言中单精度类型称为浮点类型(Float),顾名思义是通过浮动小数点来实现数据的存储。这两个数据类型最早是为了科学计算而产生的,他能够给科学计算提供足够高的精度来存储对于精度要求比较高的数值。但是与此同时,他也完全符合科学计算中对于数值的观念:
当我们比较两个棍子的长度的时候,一种方法是并排放着比较一下,一种方法是分别量出长度。但是事实上世界上并不存在两根完全一样长的棍子,我们测量的长度精度受到人类目测能力和测量工具精度的限制。从这个意义上来说,判断两根棍子是否一样长丝毫没有意义,因为结果一定是False,但是我们可以比较他们两个哪个更长或者更短。这个例子很好地概括了单精度/双精度数值类型的设计初衷和存在意义。
float不是按照正规的编码的五个数
+0.0 -0.0 +inf-inf
NaN Not a Number
NaN 用于处理计算中出现的错误情况,比如 0.0 除以 0.0 或者求负数的平方根。由上面的表中可以看出,对于单精度浮点数,NaN 表示为指数为 emax + 1 = 128(指数域全为 1),且尾数域不等于零的浮点数。IEEE 标准没有要求具体的尾数域,所以 NaN 实际上不是一个,而是一族。不同的实现可以自由选择尾数域的值来表达 NaN,比如 Java 中的常量 Float.NaN 的浮点数可能表达为 01111111110000000000000000000000,其中尾数域的第一位为 1,其余均为 0(不计隐藏的一位),但这取决系统的硬件架构。Java 中甚至允许程序员自己构造具有特定位模式的 NaN 值(通过 Float.intBitsToFloat() 方法)。比如,程序员可以利用这种定制的 NaN 值中的特定位模式来表达某些诊断信息。
这里没有介绍IBM浮点型编码规范有兴趣的同学可以自行研究以及两种编码之间的转换。