首页 > 代码库 > 类型强转和地址强转
类型强转和地址强转
1 例子
最近在干一个很复杂的事—读4w多行的项目源码,头都大了有木有!其中有一步是加载一个二进制文件,为了更好的理解代码的含义,我需要一点一点解析二进制文件。在解析到某个位置的时候,有个读浮点数的操作,对应的二进制值为:…CAF249F1…。非常好奇这个值对应的浮点数是多少,所以写代码去求解一下。这里我没有犯一个错误:原始的文件中是按照从低字节到高字节排序的,所以实际的数应该为:0xF149F2CA。但是还是犯了很多错误,最初的代码为:
void test1() { unsigned int a=0xf149f2ca; float b=a; printf("a is %u,b is %f\n",a,b); }
想法是通过隐式类型强转获得对应的浮点数,但输出结果为:a is 4048155338,b is 4048155392.000000,完全不是想象中的样子。隐式的类型强转只是将一个int型的数变成一个和原值接近的浮点数,而不是将原始的int值按照地址解析成浮点数。后来一想也对,要不这样强转还有什么意义。注意强转的结果并不和原值一样,这是由float类型的精度造成的,后面我们会详细介绍其中的原理。
上述方法不行,我又想到利用字符串进行转换,代码如下:
void test2() { char* a="caf249f1"; float b=(float)atof(a); printf("a is %s,b is %f\n",a,b); }
想法也很简单,将连续四个字节利用atof函数解析成浮点数,输出结果为:a is caf249f1,b is 0.000000,再一次不正确!后来仔细查看atof的解释才明白,原来atof函数只能解析符合浮点数书写格式的数,例如0.0314或者3.14E-2。如果字符串完全无法被转换为数字,则返回0。
连续两次都没有解决,我怒了!想了一种操作地址的方式,代码如下:
void test3() { unsigned int a=0xf149f2ca; float b=*(float*)&a; printf("a is %u,b is %f\n",a,b); }
代码的思想是首先获得int型的地址,然后将该地址强转成float型地址,然后再获得float的值,这样应该不会错了,输出的结果为:a is 4048155338,b is -1000000015047466219876688855040.000000。这个结果可能不太直观,但是我知道这就是我想要的结果。b输出了一个很大的负数,如果将其转换成科学记数法则为-1E30,正好在代码中有个LOG_ZERO的宏定义为该值,正好对应起来了。该浮点数的含义是对某个变量赋初值,初值为log(0)的近似定义。
2 类型强转
通过上面的例子我们能有什么收获呢?在第一次尝试中,我们虽然没有得到正确的结果,但是却发现了一些问题:将一个int型的值转换成float类型之后,值变得和转换之前不一样,精度损失了不少,为什么会这样?要理解造成差异的原因,需要对浮点数的格式非常了解。在计算机中,一个浮点数(真实值)应该用下述形式来表示:
其中,s决定这个数是负数(s=1)还是正数(s=0);M表示尾数,是一个二进制小数,范围是或者
;e是阶码,作用是对浮点数加权,权重是2的e次幂。所以在将一个整数表示成浮点数的时候,我们需要先将整数表示成以2为底的科学计数型,然后按照下面的格式填入相应值即可。
符号 | 阶码 | 尾数 |
如果是单精度(float)类型,上述格式的符号部分为1位,阶码部分为8位,尾数部分为23位;如果是双精度(double)类型,符号部分为1位,阶码部分为11位,尾数部分为52位。
根据阶码的不同,被编码的值可以分为三种情况(最后一种情况有两个变种)。此外,要注意下面的表是IEEE的浮点数表示,而不是原始浮点数的表示。
1. 规格化的
对上述IEEE浮点数进行实际值转换的时候,阶码和尾数都有变化。IEEE标准中的阶码和我们想象中稍有不同,因为它对我们理解中的阶码进行一个偏置操作。针对规格化的浮点数,阶码字段被解释为以偏置形式表示的有符号整数。也就是说,浮点数真实的阶码值e=E-Bias,其中E就是IEEE给出的阶码值,Bias是一个等于(单精度是127,双精度是1023)的偏置值。尾数和我们想象中也稍有不同,我们在表示一个浮点数的尾数时,总会表示成1.
形式,由于第1位总是1,我们可以将其省略,从而获得一个额外的精度位,因而M真实值是1.
,但表现出来却是0.
。
2. 非规格化的
在这种情况下,真实值阶码值e=1-Bias,而不是-Bias。尾数则不包含隐含的1,即M=0.。
非规格化数有两个用途。首先,它提供了一种表示数值0的方法,因为使用规格化数,我们必须总是使>=1,因此我们就不能表示0。当阶码和尾数都为0时就表示0,不过符号位的不同会产生+0.0与-0.0。其次,非规格化可以表示非常接近于0.0的数。
3a.无穷大
3b.NaN
在《深入理解计算机系统》P72有一个很详细的例子,建议大家仔细阅读。
讲了这么多,我们现在应该明白为什么整数强转成浮点数之后会有精度损失。这是因为float在计算机表示中尾数(float的有效位)只有23位,而int整数有32位,这就导致在将int转换成float的时候,int的低位就会被truncate,最大可能会产生511的误差。
当然并不是所有的强转都会导致精度损失,这也和整数值的有效位数相关,具体指的是前置1和后置1之间的部分。下面给出一个例子:
void test() { int a=0x100010; int b=0x8000008; printf("a is %d, type cast of a is %f, b is %d, type cast of b is %f\n",a,(float)a,b,(float)b); }
上述代码的输出为:a is 1048592, type cast of a is 1048592.000000, b is 134217736, type castof b is 134217728.000000。可以看到a在强转的时候没有精度损失,但是b在强转时产生了精度损失。这是因为a的有效位数小于23位,在强转时float的尾数可以容纳;但是b的前置和后置1之间的位数为23位,这样在强转的时候最低位的1就会被truncate,从而结果产生了8的误差。
上述误差的产生是由尾数位数过少造成的,针对float类型的阶码部分,我们能有什么收获呢?利用类型强转将int转成float之后,阶码部分就保存了这个整数的最高位次,因而我们可以用阶码来求整数的前置1位置,我会在后面的博客中详细介绍整数前置1的位置求法。
3 地址强转
地址强转算是一项比较高级的操作,直接将原始地址的类型进行改变。语法也比较复杂,首先通过&符号获得原类型的地址,然后通过(type*)将地址类型进行强转,最好再利用*取值获得新类型的值。这种强转在大多数情况下都没有意义,但是一种情况例外,即在将float的地址强转成int地址时会有特殊意义。
由前面的介绍我们知道浮点数在计算机中是通过符号位、阶码和尾数三部分来表示的。给定一个数学上的浮点数x,我们可以将其表示成
类型强转和地址强转