一.浮点数与0比较
1.现象及原因
相信很多初学C语言的朋友一定犯过这样的错误:
double x = 1.0;
double y = 0.9;
if (x - y == 0.1) {
printf("You can see me!\n");
}
else {
printf("oops!\n");
}
理论上,1.0 - 0.9 == 0.1是毫无疑问的,但是当我们执行这段程序的时候却发现和我们预计的恰好相反,程序输出了 oops! 原因是:浮点数在计算机中存储时会有精度损失! 让我们来看一下,上例子x-y在计算机中存储结果是多少: 没错,计算机存储的是这样一串非常接近0.1的数字。 于是,我们得出结论: 千万不可用“==”进行浮点数的比较!!
2.解决方案
既然如此,那我们怎么进行浮点数的比较呢~ 很简单,我们只需控制一个精度,当误差在精度范围内时,我们就可以认为两个浮点数相等。 就像这样:
#define EPS 0.000001 #自定义精度
int main()
{
double x = 1.0;
double y = 0.9;
if (x - y > 0.1 - EPS && x - y < 0.1 + EPS) {
printf("You can see me!\n");
}
else {
printf("oops!\n");
}
return 0;
}
结果就符合我们的预期了。其中EPS为我们自定义精度,可根据实际需要自定义。 我们也可以使用系统帮我们定义的精度:DBL_EPSILON 或 FLT_EPSILON。只需引入float.h的头文件即可。
#include <stdio.h>
#include <float.h>
int main()
{
double x = 1.0;
double y = 0.9;
if (x - y > 0.1 - DBL_EPSILON && x - y < 0.1 + DBL_EPSILON) {
printf("You can see me!\n");
}
else {
printf("oops!\n");
}
return 0;
}
这样的话,浮点数和0比较,就比较简单了。
#include <stdio.h>
#include <float.h>
#include <math.h>
int main()
{
double x = 0.0000000000001;
if (fabs(x) < DBL_EPSILON) {
printf("You can see me!\n");
}
else {
printf("oops!\n");
}
return 0;
}
只需要将所判断的数夹在-精度到+精度之间就可以了,这里使用绝对值函数fabs()可使代码更为简洁。 这里还有个小细节需要注意:
if (fabs(x) <= DBL_EPSILON)
让我们来看系统对于DBL_EPSILON的定义:
#define DBL_EPSILON 2.2204460492503131e-016
这是系统对于DBL_EPSILON的定义,我们要理解 smallest such that 1.0+DBL_EPSILON != 1.0的含义: 1.0加上大多浮点数都不等于1.0,但DBL_EPSILON是最小的那个。换言之,DBL_EPSILON是能使0.1+DBL_EPSILON被计算机识别的不等于0.1的最小值。 所以,如果我们加上了=号,上例中考虑极端情况,当x==DBL_EPSILON时,x是x+0 != 0表达式成立的最小值,也就是说,这时候x != 0 ,与条件矛盾。 结论:当使用系统精度进行浮点数判断是,应该用’>‘或’<’,而不要使用’>=‘或’<=’。
二. 布尔类型与0比较
C语言一度被认为是没有真正的布尔类型的,一般使用0表示假非0表示真,但最新标准C99中引入了布尔类型,需引入stdbool.h即可使用。 我们看定义:
#define bool _Bool
#define false 0
#define true 1
其中_Bool是C99标准的关键字,表示布尔类型。 我们可以看到,false定义为0, true为1。 看以下3份代码:
bool flag = true;
if (flag){}
if (flag == true){}
if (flag != 0){}
当我们进行布尔类型判断时,应使用那种写法较好呢? 答案是第一种! 因为布尔类型本身就是真假结果,而if判断正好判断的是真假结果,所以无需画蛇添足写成 flag==true,更不要写成flag!=0这样还会让人误解flag为整形变量。 结论: 使用布尔值判断时应使用如下格式:
bool flag = false;
if (flag)
{ }
else
{}
if (!flag)
{ }
else
{ }
三. NULL 与 0 比较
NULL是C语言的空指针,一般指针变量初始化时可以赋值为NULL。 我们来看NULL的定义:
#define NULL ((void *)0)
0强制转化为void*类型。 这里要正确的理解强制类型转化。 比如想通过字符串"123456"得到整形123456,你可千万不能写出int i = (int)"123456"这样的代码来,因为强制类型转化只是对于同一个二进制序列换了一种解释方式,而原二进制序列在内存中的存储形式是不发生任何改变的。 比如这样一段代码:
char c = '0';
int a = (int)c;
printf("%c %d\n", c, a);
输出结果为 0 48,其实不管是c还是a在内存的存储的二进制序列是一样的,只不过前者将这段序列理解为字符,后者理解为整形而已。 所以,我们的NULL在本质上,其实就是0。 那么我们就有着相同的问题。
int* p = NULL;
if (p) {}
if (NULL == p) {}
if (0 == p) {}
那种判断形式好? 答案是:第二种。 原因是:第一种会让人以为p为布尔类型变量,而第三种会让人误解p为整形变量。 结论: 使用NULL判断时应使用如下格式:
if (NULL == p)
{}
else
{}
if (NULL != p)
{}
else
{}
之所以将NULL写在等式左侧,是为了避免写出 if (p = NULL) 而造成程序错误(p会被赋值为NULL)。
四. 各色各异的0值
C语言中有着各种各样的零值,比如0, ‘\0’, 以及上面说的NULL。 首先要阐述很重要一点 char c = ‘0’,这个被单引号括起来的0,以及char* s = “abcdef0”,这个在双引号里的零,是字符0,你可以把它们理解为是假装的0,就像披着狼皮的羊一样,它们只是看起来和0一样罢了,其实他们对于的整形是48,就像
char c = '0';
int a = (int)c;
printf("%c %d\n", c, a);
这个例子一样。大家可以自行去了解一下ASCll值的知识。 除了这个‘0’伪装的零以外,0,’\0’,NULL可都是货真价实的零,下面逐一介绍。 0:就是最本质的整形 0。 ‘\0’: 常作为字符串的结束标志,大家可以理解为这个伪装的‘0’,被转义字符 \ 转义为真正的0,用途是放在字符串末尾做结束标志。当你定义 char* s = "abcdef"时系统会自动在后面添加’\0’标记字符串的结束。切记,’\0’本质就是0 NULL: 上面已经讲过,是0被强转为void * 类型,本质也是0。 既然如此,可能大家会问,既然他们的本质都是0,那为什么还要搞出这么多花样,直接全部用 0 不就行了吗? 我的答案是: 在计算机底层实现的角度是是完全没有任何问题的,你可以把你代码中的’\0’,NULL全部换成0,但是那样对于程序员或者编译器来说实在是太过不友好。 比如:
int a = 0;
int a = '\0';
int a = NULL;
上面三份代码在计算机实现时没有任何区别,都是将一串二进制全0的序列放在了整形变量a中,但是后两种写法,但凡学过计算机语言的人都会觉得无比怪异,甚至让编译器也捉摸不透眼前的这个程序员到底想搞啥子,从而可能有一些完全没有必要的警告。 结论: 我们在使用零值时应尽量保证对应使用,整形就用0,字符串就用’\0’,指针就用NULL。但我们要清楚,这三者的本质其实都是零。
|