一.浮点数与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) { //fabs(x): 求x的绝对值
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 /* smallest such that 1.0+DBL_EPSILON != 1.0 */
这是系统对于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){} //1
if (flag == true){} //2
if (flag != 0){} //3
当我们进行布尔类型判断时,应使用那种写法较好呢?
答案是第一种!
因为布尔类型本身就是真假结果,而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) {} //1
if (NULL == p) {} //2
if (0 == p) {} //3
那种判断形式好?
答案是:第二种。
原因是:第一种会让人以为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); //输出结果 0 48
这个例子一样。大家可以自行去了解一下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。但我们要清楚,这三者的本质其实都是零。