4.9 选择(判断)结构
计算机本质上就是能够执行运算和做出逻辑判断的机器,它的这种能力可以通过编程语言中的选择结构(即判断结构)表现出来。C++/C有3种基本的选择结构:if结构(单选择)、if/else结构(双重选择)和switch结构(多重选择)。
if结构和if/else结构的语法分别如下:
C++/C也支持下面的if/else结构:
if(…){…} else if(…){…} else if(…){…} else{…} 因为它相当于如下的嵌套结构: if (…) { … } else { if (…) { … } else { if (…) { … } else { … } } }
因为每一个else分支里面仅有一条if语句,故省略了{}。
【建议4-2】: 在if/else结构中,要尽量把为TRUE的概率较高的条件判断置于前面,这样可以提高该段程序的性能。
按理说,if语句是C++/C语言中比较简单和常用的语句,然而很多程序员用隐含错误的方式书写if语句,最常见的就是变量与零值的比较。
4.9.1 布尔变量与零值比较
假设布尔变量名称为flag,它与零值比较的标准if语句如下:
if(flag) // 表示flag为真 if(!flag) // 表示flag为假
【提示4-16】: 根据布尔类型(boolean)的语义,0为“假”,任何非0值都是“真”。可用true和false来表示“真”和“假”两个概念。语言实现必须通过可比的值来区分二者,比如0和非0就可以担当此任务。但是TRUE的值究竟是什么并没有统一的标准,不同的语言可能采用不同的方案。具体到C++语言,标准化以前的某些实现并不支持bool这种内置类型,也没有true/false这两个内置常量。if语句判断其条件表达式的真假并不是通过把它的计算结果转换为布尔类型的临时变量来进行的,而是将其结果直接和0进行相等性比较,如果不等于0则表示真,否则为假。许多语言都采用了这个比较通用的做法。标准化后的某些C++实现可能对if语句的处理做了增强或者调整,因为现在bool是标准类型了,并且增加了true和false这两个常量。由于历史的原因,Visual C++ 将TRUE定义为1,而Visual Basic将TRUE定义为-1。
标准C++规定的bool类型常量和整数、指针等之间的转换规则如下:
false→0, true→1; 0→false, 任何非0值→true;
但是不同的实现对true的表示可能不同(可能不符合标准),因此下面这样的语句:
int flag = -1; if ( flag == true ) {} else{}
在不同的实现下行为可能不一致。但是false的值是确定的,因此应该总是和false比较。
不要将布尔变量flag直接与true或者1、-1、0等进行比较。下列if语句都属于不良用法:
if(flag!=true) // 错误用法 if(flag==true) // 错误用法 if(flag==1) // 错误用法 if(flag!=1) // 错误用法 if(flag==0) // 不良用法,让人误以为flag是整数 if(flag!=0) // 不良用法,让人误以为flag是整数
4.9.2 整型变量与零值比较
假设整型变量为value,它与零值比较的标准if语句如下:
if(value==0) if(value!=0)
不可以模仿bool变量的风格而写成:
if(value) // 会让人误以为value是布尔变量 if(!value)
4.9.3 浮点变量与零值比较
计算机表示浮点数(float或double类型)都有一个精度限制。对于超出了精度限制的浮点数,计算机会把它们的精度之外的小数部分截断。因此,本来不相等的两个浮点数在计算机中可能就变成相等的了。例如:
float a = 10.222222225, b = 10.222222229;
在数学上a和b是不相等的,但是在32位计算机中它们就是相等的。
【提示4-17】: 如果两个同符号浮点数之差的绝对值小于或等于某一个可接受的误差(即精度),就认为它们是相等的,否则就是不相等的。精度根据具体应用要求而定。不要直接用“==”或“!=”对两个浮点数进行比较,虽然C++/C语言支持直接对浮点数进行==和!=的比较操作,但是由于它们采用的精度往往比我们实际应用中要求的精度高,所以可能导致不符合实际需求的结果甚至错误。
假设有两个浮点变量x和y,精度定义为EPSILON = 1e-6,则错误的比较方式如下:
if(x==y) // 隐含错误的比较 if(x!=y) // 隐含错误的比较
应该转化为正确的比较方式:
if(abs(x-y)<=EPSILON) //x等于y if(abs(x-y)>EPSILON) //x不等于y
同理,x与零值比较的正确方式为:
if(abs(x)<=EPSILON) //x等于0 if(abs(x)>EPSILON) //x不等于0
从数学意义上讲,两个不同的数字之间存在着无穷个实数。计算机只能区分至少1bit不同的两个数字,并且使用较少的位(32或64位)来表示一个很大范围内的数字,因此浮点表示只能是一种近似结果。在针对实际应用环境编程时,总是有一个精度要求,而直接比较一个浮点数和另外一个值(浮点数或者整数)是否相等(==)或不等(!=)可能得不到符合实际需要的结果。
例如:假设在某光学精密仪器制造工业应用中,要求该仪器各零部件尺寸精度达到1μm,即10-6m。那么下面两个数从数学意义上来说应该是不相等的:
d1=1.123456 2(m) d2=1.123456 8(m)
但是在精度要求为10-6m的情况下,它们应该被视为相等。而如果使用==和!=对d1和d2直接进行比较,结果将可能如下:
if(d1==d2)…… //FALSE if(d1!=d2)…… //TRUE
原因就是这样直接比较的精度比我们要求的10-6m要高。
同样道理,一个浮点数和一个整数之间的直接判等也存在类似的偏差。例如:
d3=1.000000 1(m) d4=1(m)
同样在精度要求为10-6m的情况下,它们应该被视为相等。而如果使用==和!=对d3和d4直接进行比较,结果可能恰恰相反。
所以,在实际应用环境下,如果直接比较浮点数和另一个数(整数或浮点数)是否相等(==)或不等(!=),可能会产生错误的结果,进而导致软件出错。
直接比较浮点数和另一个数(整数或浮点数)是否相等(==)或不等(!=),其结果可能依赖于具体的编译环境和平台(如操作系统和硬件系统结构),因为每一个编译平台都有自己默认的精度,对浮点数直接进行==和!=的比较采用的就是这个默认精度,而不是按照内存中两个数仅有某个bit不同(其他所有bit都相同)来判断两个数是否相等的。
例如:
float x = 0.0f, y = 0.0f; … if (abs(x - y) <= numeric_limits<float>::epsilon()) { // x == y } else { // x != y } if (abs(x) <= numeric_limits<float>::epsilon()) { // x == 0 } else { // x != 0 }
虽然不建议直接使用==和!=比较浮点数,但是可以直接比较浮点数谁大谁小,即可将“<”和“>”直接应用于浮点数之间的比较及浮点数和整数的比较。但是,对内置类型来说,“!(a>b) && !(a<b)”与“a==b”的语义是等价的,因此在针对实际应用环境编程时也不建议使用“!(a>b) && !(a<b)”来判断浮点数相等与否。
4.9.4 指针变量与零值比较
指针变量的零值是“空值”(记为NULL),即不指向任何对象。尽管NULL的值与0相同,但是两者意义不同(类型不同):
#ifdef __cplusplus #define NULL 0 #else #define NULL ((void*)0) #endif
假设指针变量的名字为p,它与零值比较的标准if语句如下:
if(p==NULL) //p与NULL显式比较,强调p是指针变量 if(p != NULL)
而不要写成:
if(p==0) // 容易让人误以为p是整型变量 if(p != 0)
或者:
if(p) // 容易让人误以为p是布尔变量 if(!p)
4.9.5 对if语句的补充说明
有时候我们可能会看到if (NULL == p) 这样古怪的格式。不是程序写错了,而是程序员为了防止将if (p == NULL) 误写成if (p = NULL),有意把p和NULL颠倒。编译器认为if (p = NULL) 是合法的,但是会指出if (NULL = p)是错误的,因为NULL不能被赋值。类似的还有if ( 100 == i )等。
程序中有时会遇到if/else/return的组合,应该将如下不良风格的程序:
if(condition) return x; return y; 改写为: if(condition) { return x; }else { return y; }
或者改写成更加简练的:
return condition?x:y;
4.9.6 switch结构
有了if/else语句,为什么还要switch语句?
switch是多分支选择语句,而if/else语句只有两个分支可供选择。虽然可以用嵌套的if语句来实现多分支选择,但那样的程序冗长难读,更重要的是可能在匹配到某一个分支前执行多次无谓的比较。相反,switch的效率比if/else结构高,这正是switch语句存在的理由。
switch语句的基本格式是:
switch (表达式) { case常量表达式1: 语句序列 break; case常量表达式2: 语句序列 break; … default : … break; }
【提示4-18】: (1)switch没有自动跳出的功能,每个case子句的结尾不要忘了加上break,否则当表达式与某一个case子句匹配并执行完它的语句序列后,将接着执行下面case子句的语句序列,这就导致了多个分支重叠(除非有意让多个分支共享一段代码)。break语句只是一个“jmp”指令,其作用就是跳到switch结构的结尾处。
(2)不要忘记最后那个default子句。即使程序真的不需要default处理,也应该保留语句default : break;,这样做并非多此一举,而是为了防止别人误以为你忘了default处理,以及出于清晰性和完整性的考虑。
我们举一个选择结构的例子(见示例4-10):输入一个年份,判断是否为闰年。判断闰年的方法是:如果该年能被4整除但不能被100整除,或者能被400整除,就是闰年。
示例4-10
#include <stdio.h> int main(int argc, char* argv[]) { unsigned long year; printf("Input a year : "); scanf("%lu", &year); if ( ( year % 4 == 0 && year % 100 != 0 ) || ( year % 400 == 0 ) ) { printf("Year %lu is a leap year.\n", year); }else { printf("Year %lu is not a leap year.\n", year); } return 0; }