1 ,变量在内存中的存储
Microsues 发的微薄:
C++ 中,把负值赋给 unsigned 对象是合法的,其结果是该负数对该类型的取值个数求模后的值。例如:把 -1 赋给 8 位的 unsigned char ,因为 -1 对 256 求模后的值为 255 ,所以结果是 255 。
我的回复:
c/c++ 中赋值很随意的,只需要做好 " 合法 " 的类型转换。本质上是数 / 对象如何在机器中表示的问题,一个内存中的数,可以是 char, int, pointer, double.... 任何类型。你的例子中 , 内存中写入了一个 0xFF 字节,而
0xFF=-1(char)=255(u char)=3.57331e-043(float)=......
为此我写了两行代码
float f = 3.57331e-043f; cout << f << '/t' << (int)*(char*)&f << '/t' << (unsigned int)*(unsigned char*)&f << '/t' << std::hex << *(unsigned int*)&f << endl;
这两行代码的输出是:
3.57331e-043 -1 255 ff
也就是说 0x000000FF 表示浮点数的 3.57331e-043 ,看起来有点不可思议,这是怎么来的呢?
2 :浮点数在内存中的表示
IEEE 规定,浮点标准用
形式来表示一个数,其中:
S 是符号 (sign) , M 是有效数 (significand) ,指数2 的 E 次幂,注意是2 不是 10 。
浮点数的位分成三个区域表示这些值:
1)s=1 个符号位 s ,直接编码 S
2) k 位的指数编码 E
3) n 位小数编码 M
在 C 语言中, float 类型对应 s=1, k = 8, n=23 ,表示 32 位的浮点数, double 类型对应 s=1,k=11,n=52 的 64 位浮点数。
表1. 数的存储
符号域 | 指数域 | 小数域 | Bias | |
---|---|---|---|---|
单精度 |
1 [31] | 8 [30-23] | 23 [22-00] | 127 |
双精度 | 1 [63] | 11 [62-52] | 52 [51-00] | 1023 |
以 float 为例详细说明:
符号位很好理解, 0 表示正, 1 表示负,指数位 E 和有效 M 位比较绕了, IEEE 规定了 3 种情况:
1、 规格化:当 E 的二进制位不全为 0, 也不全为 1 时, V 为规格化形式。此时指数为被解释为表示偏置( biased )形式的整数:
E = e - Bias , e 是无符号数,e=ek-1 ...e0 也就是指数位的2 进制数,而Bias = 2k-1 - 1
因此 float 的 bias 是 127 ,取值范围 -126~+127 , double 的 bias 是 1023 ,取值范围 -1022~1023
有效位M=1+f ,其中f=fn--1 ....f1 f0 就是 n 位的小数编码 ,也就是M= 1.fn--1 ....f1 f0 ,整数部分1 是隐含的第一位,省略掉,因为这样可以获得额外的 1 位精度。总之:
(公式1 ) E = e - Bias, M = 1 + f, Bias = 2k-1 - 1
2、 非规格化:当 E 的二进制位全部为 0 时, V 为非规格化形式。此时 E 和 M 的计算都非常简单:
(公式2 ) E = 1 - Bias, M = f, Bias = 2k-1 - 1
此时小数点左侧的隐含位为 0 。
为什么 E 会等于 (1-bias) 而不是 (-bias) ,这主要是为规格化数值、非规格化数值之间的平滑过渡设计的。有了非规格化形式,我们就可以表示 0 了。把符号位 S 值 1, 其余所有位均置 0 后,我们得到了 -0.0; 同理,把所有位均置 0, 则得到 +0.0 。非规格化数还有其他用途,比如表示非常接近 0 的小数,而且这些小数均匀地接近 0, 称为“逐渐下溢 (gradually underflow) ”属性。
3 、特殊数值:当 E 的二进制位全为 1 时为特殊数值。此时,若 M 的二进制位全为 0 ,则 n 表示无穷大,若 S 为 1 则为负无穷大,-INF,若 S 为 0 则为正无穷大,+INF ; 若 M 的二进制位不全为 0 时,表示 NaN(Not a Number) ,表示这不是一个合法实数或无穷,或者该数未经初始化。
NaN有两种,QNaN和SNaN,QNaN表示一个不确定的运算,而SNaN表示一个错误的运算。例如INF-INF是一个不确定的运算,而1/0是一个错误的运算。
3: 无穷/NaN的运算和判断
在数值计算中非常重要.
表2. 关于无穷的运算可以总结成如下表格:
运算 | 结果 |
---|---|
n ÷ ±Infinity | 0 |
±Infinity × ±Infinity | ±Infinity |
±nonzero ÷ 0 | ±Infinity |
Infinity + Infinity | Infinity |
±0 ÷ ±0 | NaN |
Infinity - Infinity | NaN |
±Infinity ÷ ±Infinity | NaN |
±Infinity × 0 | NaN |
在C语言中,可以用float.h中的
int _isnan(double x),返回布尔值
int _finite(double x),返回布尔值
int _fpclass(double x)函数来判断
fpclass的返回结果代表意义分别为
_FPCLASS_SNAN (Signaling NaN)
_FPCLASS_QNAN (Quiet NaN)
_FPCLASS_NINF (Negative Infinity, –INF)
_FPCLASS_PINF (Positive Infinity, +INF)
在C++中,可以用STL中的limits类:
numeric_limits::quiet_NaN()
numeric_limits::signaling_NaN()
numeric_limits::infinity()等方法判断
4. 练习
1)现在回到本文最初提出的问题, 计算 0x000000FF 表示的浮点数:
因此s=0, 是一个正数
指数位 e 的每位都是 0 ,是非规格化表示,使用公式 2 ,
这就是所求的结果。
2)那么现在,我们把指数的最低位置成 1 ,也就是从左边数的第 9 位置为 1 ,这样就变成了规格化表示:
用公式 1 计算 :
有趣的是,这里的 E 同样也是 126 ,但是由于 M 不同,得到的 V 也不同,而且相差非常大,竟然有105 数量级。
3)对于特殊的情况,我们除了使用输出f之外,再使用C runtime library中的几个函数如_fpclass做一个判断
A. 现在把所有的指数位都置成1,也就是从左边的第2位到第9位都置为1,这样就是第三种特殊情况:
0x7F8000FF16 =011111111000000000000000111111112
这个数的M位不全为0,因此应该是一个 NaN(Not a Number),
B. 然后我们让所有的小数位都为0,也就是
0x7F80000016 =011111111000000000000000000000002
C. 再把符号位设置为1,
0xFF80000016 =111111111000000000000000000000002
D. 最后我们做几个特殊运算0/0, INF/0, 0/INF, 看看是什么效果
全部代码如下:
#include
在 MS VC++2008下,运行结果如下:
f is 3.57331e-043 f is 1.17553e-038 f is 1.#QNAN , it is a NaN? 1 , fpclass is 2 f is 1.#INF , it is a finite number? 0 , fpclass is 512 f is -1.#INF , it is a finite number? 0 , fpclass is 4 0/0 is -1.#IND , it is a NaN? 1 , fpclass is 2 INF/0 is 1.#INF , it is a finite number? 0 , fpclass is 512 0/INF is 0 , it is a finite number? 1 , fpclass is 64
其中的 fpclass ,请对照float.h中的定义自行理解.
5. 关于浮点数的总结:
根据以上说明,可以总结成如下的表格:
表3 . 全部的总结
符号位 | 指数位(e ) | 小数位(f ) | 数值 |
---|---|---|---|
0 | 00..00 | 00..00 | +0 |
0 | 00..00 | 00..01 : 11..11 |
Positive Denormalized Real 0.f × 2(-b +1) |
0 | 00..01 : 11..10 |
XX..XX | Positive Normalized Real 1.f × 2(e -b ) |
0 | 11..11 | 00..00 | +Infinity |
0 | 11..11 | 00..01 : 01..11 |
SNaN |
0 | 11..11 | 10..00 : 11..11 |
QNaN |
1 | 00..00 | 00..00 | -0 |
1 | 00..00 | 00..01 : 11..11 |
Negative Denormalized Real -0.f × 2(-b +1) |
1 | 00..01 : 11..10 |
XX..XX | Negative Normalized Real -1.f × 2(e -b ) |
1 | 11..11 | 00..00 | -Infinity |
1 | 11..11 | 00..01 : 01..11 |
SNaN |
1 | 11..11 | 10..00 : 11.11 |
QNaN |
通过使用公式1和公式2,可以计算得到:
表4 . 浮点数表示的范围
非规格化 | 规格化 | 表示数的范围 |
|
---|---|---|---|
单精度 |
± 2-149 to (1-2-23 )×2-126 | ± 2-126 to (2-2-23 )×2127 | ± ~10-44.85 to ~1038.53 |
双精度 |
± 2-1074 to (1-2-52 )×2-1022 | ± 2-1022 to (2-2-52 )×21023 | ± ~10-323.3 to ~10308.3 |
6:字节顺序
需要强调的一个问题是字节顺序,在此不做详细说明,只是说明本文的例子。
在存储器中,不同的操作系统有两种顺序,按照以下方法记忆就可以:
1) 小端法 little endian ,就是同从左到右的书写顺序“相反”,这里的相反是按找字节相反,而不是全部逆序
2) 大端法 big-endian 就是同从左到右的书写顺序相同
采用什么方法并无明确规定,就像吃鸡蛋先从大头吃还是从小头吃都是一样的, Intel 的机器采用小头法,最高字节在最右边,最低字节在最左边,正好是反的,比如上例中:
0x000000FF ,在内存中存储为 FF 00 00 00
而 0x008000FF ,在内存中存储为 FF 00 80 00 , 注意这里 80 的位置
在这种平台上, 如果从内存中读取数值时,无论是什么数据类型,一定要先反向排列,然后再进行计算
7. 参考文献
关于浮点数的话题还有很多很多,尤其是在 浮点数 计算的时候,会有一些让人惊讶的表现, 一些编译器的也许会有bug.更深入的资料有:
1. Computer Systems, A Programmer’s Perspective (CSAPP) ,中译本叫深入理解计算机系统
2. M. Overton. Numerical Computing with IEEE floating Point Arithmetic
3 . IEEE 754: Standard for Binary Floating-Point Arithmetic ,http://steve.hollasch.net/cgindex/coding/ieeefloat.html http://grouper.ieee.org/groups/754/