c语⾔int超出范围溢出处理_每个C语⾔程序员都应该明⽩,计
算机究竟是如何存储⼩数的?...
浮点型在内存中的存储分布⽅式因机器平台⽽异,完全理解所有机器平台中的浮点型存储⽆疑是⼀件相当⿇烦的事。幸运的是,⼤多机器平台都遵守 IEEE-754 标准,很可能读者和我使⽤的平台正是使⽤的 IEEE-754 标准。
计算机是如何存储浮点数的呢?
IEEE-754是如何存储浮点数的?
IEEE-754浮点(32位)或双精度(64位)有三个部分(在IEEE-854下也有类似的96位扩展精度格式):符号位,表⽰数字是正的还是负的;指数位;以及指定实际数字的尾数位。以C语⾔中的单精度浮点数为例,下⾯是某位浮点数的位布局:
某位浮点数的位布局
该浮点数的值等于尾数乘以 2^x。读者应该注意,上图是⼆进制分数,因此 0.1表⽰ 1/2。为了⽅便理解,我们可以将其与⼗进制的⼩数对应起来:⼗进制的 0.1 等于 1*10^-1,所以⼆进制的 0.1 等于1*2^-1,也即 1/2。
“尾数+指数”模式存储浮点数可能有⼀点问题,例如:2x10^-1=0.2x10^0=0.02x10^1,依此类推。同样⼀个数字可能有多种“尾数+指数”的表⽰⽅法,⽽同时兼顾多种表⽰⽅法势必会造成巨⼤的浪费(也可能使在硬件中实现数学操作变得困难和缓慢)。
所以,“尾数+指数”的存储模式需要⼀个统⼀的标准。事实上,IEEE-754 确实已经有标准了:假设给定⼀个⼆进制的浮点数,那么除⾮这个数是 0,否则总有某个位是 1。将⼩数点移到第⼀个 1 之后,调整指数位,这样⼀来,“尾数+指数”的唯⼀存储⽅式就固定下来了,也即“1.m x 2^n”形式。
既然⼩数点前总是 1,那么上述标准下的“尾数+指数”的存储模式甚⾄都不需要再花费空间存储⼩数点前的 1.
但是如果数字是零呢?IEEE Standards Committee 通过将零作为⼀种特殊情况来解决这⼀问题:如果数字的每⼀位都为零,那么数字就被认为是零。
1.0 似乎是没有办法存储的
现在读者可能⼜有疑问了,因为 1.0 =1.0×2^0,上述存储模式不存储⼩数点前的 1,也即尾数和指数部分都为 0,⽽“如果数字的每⼀位都为零,那么数字就被认为是零”,这样看来,1.0 似乎是没有办法存储的。
当然可以存储 1.0。单精度浮点数的指数部分是“shift-127”编码的,也即实际的指数等于 eeeeee 减去 127,所以 1.0 的表⽰⽅法实际上是 1.0×2^127。同样的道理,最⼩值本应该是 2^-127,按照“shift-127”编码指数部分,也即 2^0,可是这样⼜变成“指数部分和尾数部分都为零”了,因此在该标准下的最⼩值,实际上的写法是 2^1,也即 2^-126。
在我看来,为了表⽰ 0 和 1,舍弃最⼩值(2^-127)是⾮常可取的做法。
零不是唯⼀的“特殊情况”。对于正⽆穷⼤和负⽆穷⼤,⾮数字(NaN),以及没有数学意义的结果(例如,⾮实数,或⽆穷⼤乘以零之类的计算结果)也有表⽰:如果指数的每⼀位都等于1,那么这个数字是⽆穷⼤,如果指数的每⼀位都等于1,并且尾数位也都等于1,那么这个数字就是NaN。符号位仍然区分+/-inf和+/-nan。
现在,读者应该明⽩IEEE-754浮点数的表⽰⽅法了,下⾯是⼏个数字的表⽰⽅法:
⼏个数字的表⽰⽅法
作为程序员,了解浮点表⽰的某些特性是很重要的,下标列出了单精度和双精度IEEE浮点数的⽰例值:
单精度和双精度IEEE浮点数的⽰例值
注意,本⽂中的所有数字都假定为单精度浮点数;上⾯包含双精度浮点数⽤于参考和⽐较。
在C语⾔程序开发中,数值的处理是⼀门值得深究的科学。本⽂不可能将复杂的数值算法以及相关的C语⾔程序开发经验⼀⼀列出。事实上,讨论如何以理想的数值精度进⾏计算,就和讨论如何编写最快的C语⾔程序,如何设计⼀款优秀的软件⼀样,主要取决于程序员本⾝的综合素质。
鉴于此,这⾥将尝试介绍⼀些基础的,我认为每个C语⾔程序员都应该知道的内容。
相等
⾸先,我们应该明⽩C语⾔程序开发中的两个浮点数何时相等。可能读者并不觉得难,因为似乎C语⾔中的 == 运算符就能判断两个浮点数是否完全相等。
然⽽实际上,C语⾔中的 == 运算符是逐位⽐较两个操作数的,⽽两个浮点数的精度总是有限的,在这种场景下,== 运算符的实际使⽤意义就没有那么⼤了。
== 运算符的实际使⽤意义没有那么⼤
读者应该已经明⽩,计算机存储浮点数时,很有可能是需要舍弃⼀些位的(如果该浮点数过长),如果 CPU 或者相应的程序没有按照预期四舍五⼊,那么使⽤ == 运算符判断两个浮点数是否相等可能会失败。
例如,标准C语⾔函数库三⾓函数 cos() 的实现其实只是⼀种多项式近似,也就是说,我们并不能指望 cos(π/2) 结果的每⼀个位都为零。在C语⾔程序开发中,我们甚⾄不能准确的表⽰ π。
看到这⾥,读者应该思考“相等到底是什么意思呢?”,对于⼤多数情况来说,两个数“相等”意味着这两个数“⾜够接近”。本着这种精神,在实际的C语⾔程序开发中,程序员通常定义⼀个很⼩的值模拟“⾜够接近”,并以此判断两个浮点数是否“⾜够接近到相等”,例如:
#define EPSILON 1.0e-7#define flt_equals(a, b) (fabs((a)-(b)) < EPSILON)
宏 flt_equals(a, b) 正是通过判断 a 和 b 的距离是否⼩于 EPSILON(10的-7次⽅),来断定 a 和 b 是否可以被认为“相等”的。这样的近似模拟技术有时候是有⽤的,有时候却可能导致错误结果,读者应该⾃⾏判断它是否符合⾃⼰的程序。
读者应该⾃⾏判断它是否符合⾃⼰的程序
在本例中,EPSILON 可以看作是⼀种⽤于说明程序精度的标尺。应该明⽩,衡量精度的应该是有效数字,纠结 EPSILON 的具体⼤⼩并⽆意义,下⾯是⼀个例⼦。
假设在某段C语⾔程序中有两个数字 1.25e-20 和 2.25e-20,它俩的差值是 1e-20,远⼩于 EPSILON,但是显然它俩并不相等。但是如果这两个数字是 1.2500000e-20和1.2500001e-20,那么就可以认为它们是相等的。也就是说,两个数字距离⾜够接近时,我们还需要关注需要匹配多少有效数字。
溢出
计算机存储空间总是有限的,因此数值溢出是C语⾔程序员最关⼼的问题之⼀。读者应该已经知道,如果向C语⾔中的最⼤⽆符号整数加⼀,该整数将归零,令⼈崩溃的是,我们并不能只通过看这个数字的⽅式获知是否有溢出发⽣,归零的整数看起来和标准零⼀模⼀样。
当溢出发⽣时,实际上⼤多数 CPU 是会设置⼀个标志位的,如果读者懂得汇编,可以通过检查该标志位获知是否有溢出发⽣。
float 浮点数溢出时,我们可以⽅便的使⽤ +/- inf(⽆穷)。+inf(正⽆穷)⼤于任何数字,-inf(负⽆穷)⼩于任何数字,inf+1 等于 inf ,依此类推。因此在C语⾔程序开发中,⼀个⼩技巧是,将整数转换为浮点数,
这样就⽅便判断后续处理是否会造成溢出了。处理完毕后,再将该数转换回整数即可。
将整数转换为浮点数,就⽅便判断后续处理是否会造成溢出了
不过,将整数转换为浮点数判断是否溢出也是要付出代价的,因为浮点数可能没有⾜够的精度来保存整
个整数。32 位的整数可以表⽰任何9 位⼗进制数,但是 32 位的浮点数最多只能表⽰ 7 位的⼗进制数。所以,如果将⼀个很⼤的整数转换为浮点数,可能不会得到期望的结果。
此外,在C语⾔程序开发中,int 与 float 之间的数值类型转换,包括 float 与 double 之间的数值类型转换,实际上是会带来⼀定的性能开销的。
读者应该明⽩,在C语⾔程序开发中,不管是否使⽤整数,都应该⼩⼼避免数值溢出的发⽣,不仅仅是最开始和最终结果数值可能溢出,在⼀些计算的中间过程,可能会产⽣⼀些更⼤的值。⼀个经典的例⼦是“C语⾔数字配⽅”计算复数的幅度问题,极可能造成数值溢出的C语
⾔实现是下⾯这样的:
double magnitude(double re, double im){ return sqrt(re*re + im*im);}
程序员到底是干什么的
假设该复数的实部 re 和虚部 im 都等于 1e200,那么它们的幅度约为 1.414e200,这的确在双精度的允许范围内。但是,上述C语⾔代码的中间过程将产⽣ 1e200 的平⽅值,也即 1e400,这超出了 inf 的范围,此时上⾯的实现函数计算的平⽅根将仍然是⽆穷⼤。
谨防计算中间值溢出
因此,magnitude() 函数的更佳C语⾔实现如下:
double magnitude(double re, double im){ double r; re = fabs(re); im = fabs(im); if (re > im) { r = im/re; return re*sqrt(1.0+r*r); } if (im == 0.0) return 0.0; r = r
magnitude() 函数的更佳C语⾔实现