4.1.4  关系操作符

在C++中,除了需要用算术操作符对数据进行加减乘除的算术操作之外,我们有时候还需要对数据之间的关系进行操作,也就是对两个数据进行大小比较,得出它们之间的大小关系。在现实世界中,这种大小关系的比较是非常常见的。例如,这家摊位上的西红柿5元一斤,而另外一家相同的西红柿却只卖3元一斤,5和3一比较,就知道第二家的西红柿更便宜了。我们说,程序是用来抽象和描述现实世界的,为了在程序中表达这种大小关系的比较,C++专门提供了关系操作符,包括“>”(大于)、“>=”(大于或等于)、“<”(小于)、“<=”(小于或等于)、“==”(等于)、“!=”(不等于)。

最佳实践:注意“==”和“=”的区别

这两个符号在形式上非常相似,但是所表达的意义却是千差万别:

“==”是判断两个值是否相等的关系操作符;

“=”则是用其右边的值对左边的变量进行赋值的赋值操作符。

因为两者在形式上的相似性,如果不注意,很容易将“==”误写作“=”,或是将“=”误写作“==”,从而导致代码无法正确地表达设计者的意图,最终导致程序错误。更让人头疼得是,编译器无法发现这种隐秘的语义上的错误,即使在代码中出现了误用,编译器也不会给出错误信息,这使得我们防范这种错误的发生变得更加困难。例如:

int a;
// 程序员本来的意图是将a赋值为1
// 结果却将“=”误写成了“==”,
// 代码意义成了将a和1进行相等比较
a == ; // 程序员本来的意图是将a和1进行相等比较
// 结果却将“==”误写成了“=”,代码意义成了将a赋值为1
if(a = )
{
cout<<"a等于1"<<endl;
}

从这段代码的实际意义上来看,整个都是在胡言乱语,但是从代码的执行结果上来看,却与我们的设计预期相符,成功地输出了“a等于1”。同时,编译器也不会报告任何错误信息,这使得这一错误更具隐秘性。

要杜绝第一种误用,只有靠我们在书写代码时多加注意。另外,如果是给变量赋初始值,最好是在定义变量的时候同时进行。例如:

// 定义变量的同时赋初始值
// 在这种形式下,编译器会帮助发现“int a == 1;”这样的错误
int a = ;

而对于第二种误用,同样需要我们在书写代码时小心谨慎。此外,有一种特殊情况是,当我们在将变量与某个常量进行比较时,我们最好是将常量放在等号的左边。这样,因为不能对常量进行赋值,即使我们将“==”误写成了“=”,编译器也会帮助我们发现这种误用。例如:

// 将常量1同变量a进行相等比较
// 如果误写作“if( 1 = a )”,就成了对常量1进行赋值,
// 而常量是不能被赋值的,编译器会报告一个错误信息,帮助我们发现这种误用
if( == a)
{
cout<<"a等于1"<<endl;
}

此外,虽然编译器对这种误用没有错误信息,但是却有相应的警告信息。在编译代码的时候,打开相应的警告信息开关(使用gcc编译器的-Wall编译选项打开全部警告开关),并注意编译器输出的警告信息,也可以很好地防止这种误用的发生。

关系操作符是二元操作符,其作用是将两个操作数进行关系运算,比较两个操作数的大小或者是否相等,其运算结果是bool类型的true或者false。如果两个操作数的大小关系符合操作符所表达的大小关系,则表达式的结果为true,反之则为false。例如:

// 两个摊位上的西红柿价格
int a = ;
int b = ; // 对a和b的值进行小于比较,但a的值大于b的值,不符合操作符“<”的意义,
// 所以表达式“a < b”的计算结果值为false,然后赋值给bCheap,
// bCheap的值也为false,表示a并不比b便宜
bool bCheap = a < b;

在C++中,我们通常是用关系操作符来判断某个条件是否成立,然后配合稍后我们将要学到的条件结构,决定代码的执行路径,对数据进行不同的处理以获得不同的执行结果。比如,我们要表示网吧禁止未满十八岁的未成年人进入:

int nAge = ;
cout<<"请输入你的年龄:";
// 用户输入年龄
cin>>nAge;
// 用关系操作符“>=”判断输入的年龄是否大于等于18
// 判断是否成年人
if(nAge >= )
{
// nAge的值大于等于18
cout<<"欢迎来到红树林网吧"<<endl;
}
else // nAge的值小于18
{
cout<<"很抱歉,未成年人不能进入"<<endl;
}

在这段代码中,我们首先让用户输入年龄并将其保存到nAge变量中,然后在if条件结构中,用“>=”关系操作符将其与18进行大小比较。如果nAge的值大于等于18,则“nAge >= 18”表达式的值为true,表示是成年人,代码会进入if后面的语句执行,输出欢迎信息。反之,则表示是未成年人,代码会进入else后面的代码执行,将其拒之门外。

最佳实践:不要使用“==”判断两个浮点数是否相等

考虑下面这段代码的输出是什么:

float a = 0.1;
if(a* == 1.0)
{
cout<<"0.1*10等于1.0"<<endl;
}
else
{
cout<<"0.1*10不等于1.0"<<endl;
}

如果我要是告诉你这段代码的输出是“0.1*10不等于1.0”会不会让你大跌眼镜呢?虽然有点意外,可这就是事实。这是因为在C++中,除了某些特殊的浮点数(比如,0.5、0.25等)之外,我们无法精确地表达大多数浮点数,所以比较两个浮点数是否相等是不保险的。虽然在表面上看来,a的值为0.1,a*10等于1.0是确定无疑的,但是因为浮点数a无法精确地表达0.1这个值,存在一个十分微小的误差,当经过一定的运算后,这个误差会累积到一个可被察觉的程度,这时再用“==”将其与它的理论结果进行比较时,其结果有可能是相等,也有可能是不相等。而至于到底是哪一个,则取决于计算机硬件和编译器优化设置。这段代码在某台计算机上输出的结果可能是“0.1*10不等于1.0”,但是在另外一台计算机上输出的结果却又可能是“0.1*10等于1.0”。所以,为了保证代码行为的一致性,不要在代码中使用“==”比较两个浮点数是否相等。

如果确实需要在代码中比较两个浮点数是否相等又该怎么办呢?最简单的方法就是设定一个允许的误差值(根据我们的精度要求而定),当两个浮点数之差的绝对值在这个误差范围内时,就认为两个浮点数相等,反之则认为两个浮点数不相等。例如,上面的代码可以修改为下面的样子,以保证代码行为的一致。

float a = 0.1;
// 相等的误差范围
const float fEpsilon = 0.0001;
// 判断两个浮点数之差的绝对值(用fabs()函数取得)是否在误差范围内
// 如果在,则认为两个浮点数相等
if(fabs(a* - 1.0) < fEpsilon)
{
cout<<"0.1*10等于1.0"<<endl;
}
else // 反之,则认为两个浮点数不相等
{
cout<<"0.1*10不等于1.0"<<endl;
}

经过这样的改写,这段浮点数相等比较的代码在任何计算机上都可以得出一致的正确的结果。

4.1.5  逻辑操作符

在处理复杂事务时,我们往往需要根据多个条件来决定是否执行某项操作。例如,网吧门口贴着这样一张条子:

“只有年满十八岁而且兜里有钱的人才可以进入网吧。”

这里的“而且”,实际上就是对“年满十八岁”和“兜里有钱”这两个条件进行“与”的逻辑运算,只有这两个条件同时满足时,才算是符合条件,才能够执行“进入网吧”的操作。像“而且”这种对两个条件进行逻辑运算的动词,在C++中,我们用逻辑操作符来表达。

C++提供的逻辑操作符包括以下三种。

l  !(非):计算操作数的逻辑非。

l  &&(与):计算两个操作数的逻辑与。

l  ||(或):计算两个操作数的逻辑或。

其中,“!”是一元操作符,只能放在单个bool类型的操作数之前,对其进行取非操作,获得与之相反的逻辑值。例如:

bool bFlag = true;     // 定义一个bool类型的变量bFlag并赋值为true
// 对操作数bFlag进行取非操作,整个表达式的结果为false,
// 与操作数bFlag的值相反
!bFlag;

“&&”和“||”都是二元操作符,它们连接两个bool类型的操作数,并对其进行逻辑运算,所获得的bool类型的结果值作为整个表达式的值。“&&”的作用是计算两个操作数的逻辑与,也就是只有当两个操作数的值都为true时,整个表达式的值才为true,否则为false。“||”的作用是计算两个操作数的逻辑或,只要两个操作数中有一个为true,整个表达式的值就为true,否则为false。

在具体的编程实践中,逻辑操作符常常和关系操作符配合使用,在条件结构中被用来表达比较复杂的条件逻辑判断。例如,我们要根据学生的语文和数学成绩综合评定学生的考核等级,规则是:如果语文和数学成绩都是60分以上,则为“合格”;在“合格”的基础上,只要其中有一门成绩是85分以上,就是“优秀”。在C++中,我们可以这样来表达这个复杂的逻辑判断:

cout<<"请输入学生的语文、数学成绩(例如,82 96):";
// 定义变量保存输入的数据
int nChs = ;
int nMath = ;
// 输入数据,并保存到变量
cin>>nChs>>nMath; // 对变量进行逻辑判断评定等级
// 首先判断两个分数是否都在60分以上,达到“合格”的标准
if((nChs >= )&&(nMath >= ))
{
// 在“合格”的基础上,判断是否有一门的成绩在85以上,达到“优秀”的标准
if((nChs >= )||(nMath >= ))
{
cout<<"优秀"<<endl;
}
else // 如果没有达到“优秀”的标准,那就是“合格”
{
cout<<"合格"<<endl;
}
}
else // 如果没有达到“合格”标准,那就是“不合格”
{
cout<<"不合格"<<endl;
}

在这里,我们用“&&”操作符对“nChs >= 60”和“nMath >= 60”进行了“与”运算,也就是这两个表达式的值同时为true时,最终结果才为true。换句话说,要想让最终结果为true,也就是达到“及格”标准,那就必然要求“nChs >= 60”和“nMath >= 60”这两个表达式的值同时为true,而要想让这两个表达式的值同时为true,自然就要求nChs和nMath的值要同时大于60,这样就表达了“语文和数学成绩都是60分以上”的“合格”标准的逻辑判断。只有满足这个逻辑判断,达到合格标准,才能进入下一级判断是否“优秀”;否则,只要nChs和nMath中的任意一个小于60,就无法满足这个逻辑判断,程序就会进入else分支输出“不合格”的提示。在用“||”操作符对“优秀”条件进行判断时,只要“nChs >= 85”和“nMath >= 85”这两个表达中的任意一个为true,最终结果就为true。换句话说,也就是只要nChs和nMath中任意一个大于85,最终结果就为true,这也就表达了“只要其中有一门成绩是85分以上”的“优秀”标准。

最佳实践:注意“&&”和“||”操作符的“逻辑短路”机制

这两个操作符是用来对多个表达式进行逻辑运算,取得多个表达式经过运算后的最终结果。在进行运算时,如果凭借前面部分表达式的值就已经可以确定整个表达式的最终结果,C++将不再继续对后面剩下的表达式进行运算,而是直接抄近路返回已经得到的整个表达式的值。这种机制被称为“逻辑短路”,也就是走了捷径。

这样的解释还是有点抽象,我们就以上面的例子为例,看看“短路机制”到底是怎么“抄近路”的。

if((nChs >= )&&(nMath >= ))

在这个条件判断中,我们用“&&”操作符对“nChs >= 60”和“nMath >= 60”进行“与”运算,如果nChs的值小于60,也就是第一个条件表达式“nChs >= 60”的值为false时,无论后面的“nMath >= 60”表达式的值为true或者false,我们都已经能够确定整个表达式的值为false,C++将用false作为整个表达式的值,跳过对后面“nMath >= 60” 的判断而直接结束对整个表达式的计算。所以,为了减少计算提高效率,C++就直接跳过对第二个关系表达式的计算,抄了近路。

在使用“||”进行“或”运算时,也同样存在这种“逻辑短路”。例如:

if((nChs >= )||(nMath >= ))

在这个用“||”操作符表达的“或”逻辑运算中,如果nChs的值大于等于85,则第一个关系表达式“nChs >= 85”的值为true,进而可以确定整个表达式的值为true,C++同样也会跳过对第二个关系表达式“nMath >= 85”的计算,而用true作为整个表达式的值,直接结束对整个表达式的计算。

除了减少不必要的计算,在一定程度上提高效率之外,“逻辑短路”更大的意义在于,在某些情况(后一个条件判断以前一个条件成立为前提)下,它可以减少逻辑判断的层次。比如,如果我们要以某个结构体指针的成员变量作为条件,得先判断这个结构体指针是否有效,然后才能对指针的成员变量进行判断。如果没有“逻辑短路”机制,我们的代码要写成:

Human* p = nullptr;

// …

if(nullptr != p)// 先判断指针是否有效
{
// 然后再判断指针的成员变量是否符合条件
// 第二个条件判断以第一个条件成立为前提
if(p->m_nAge >= )
{
// ...
}
}

而如果利用“逻辑短路”机制,这种条件判断就可以简化为:

Human* p = nullptr;
// …
if((nullptr != p) && (p->m_nAge >= ))
{
// ...
}

这样在进行条件判断时,同样会先判断指针p是否为空指针,如果第一个条件满足,才会继续向下判断它的成员变量是否满足条件。实现相同的条件判断,但是却减少了逻辑判断的层次,简化了代码,而这才正是“逻辑短路”机制的主要运用场景。

你好,C++(17)0.1*10不等于1.0——4.1.4 关系操作符4.1.5 逻辑操作符的更多相关文章

  1. C# 语言规范_版本5.0 (第10章 类)

    1. 类 类是一种数据结构,它可以包含数据成员(常量和字段).函数成员(方法.属性.事件.索引器.运算符.实例构造函数.静态构造函数和析构函数)以及嵌套类型.类类型支持继承,继承是一种机制,它使派生类 ...

  2. ceph hammer 0.94.10手动部署方法Ceph Hammer版(0.94.10)手动部署for CentOS 7.x

    Ceph Hammer版(0.94.10)手动部署for CentOS 7.x --lin.wang 20190310 环境有三个节点node-1,node-2,node-3,每个节点三个ssd盘作为 ...

  3. [ubuntu 18.04 + RTX 2070] Anaconda3 - 5.2.0 + CUDA10.0 + cuDNN 7.4.1 + bazel 0.17 + tensorRT 5 + Tensorflow(GPU)

    (RTX 2070 同样可以在 ubuntu 16.04 + cuda 9.0中使用.Ubuntu18.04可能只支持cuda10.0,在跑开源代码时可能会报一些奇怪的错误,所以建议大家配置 ubun ...

  4. TensorFlow2.0(10):加载自定义图片数据集到Dataset

    .caret, .dropup > .btn > .caret { border-top-color: #000 !important; } .label { border: 1px so ...

  5. MySQL与MariaDB核心特性比较详细版v1.0(覆盖mysql 8.0/mariadb 10.3,包括优化、功能及维护)

    注:本文严禁任何形式的转载,原文使用word编写,为了大家阅读方便,提供pdf版下载. MySQL与MariaDB主要特性比较详细版v1.0(不含HA).pdf 链接:https://pan.baid ...

  6. 采用二进制方式安装K8S集群,版本etcd-v3.3.10,flannel-v0.11.0,kubernetes-server-linux-amd64

    官方提供的几种Kubernetes部署方式 minikube Minikube是一个工具,可以在本地快速运行一个单点的Kubernetes,尝试Kubernetes或日常开发的用户使用.不能用于生产环 ...

  7. python中,a=10.0 b=10.0 a is b 为什么输出是false

    >>>a=10.0>>>b=10.0>>>a is bFalse为什么当a=10,b=10时,a is b输出的是True呢? >>& ...

  8. 背水一战 Windows 10 (1) - C# 6.0 新特性

    [源码下载] 背水一战 Windows 10 (1) - C# 6.0 新特性 作者:webabcd 介绍背水一战 Windows 10 之 C# 6.0 新特性 介绍 C# 6.0 的新特性 示例1 ...

  9. jQuery hide()并不等于hide(0)

    在实际使用中,经常用hide()函数来隐藏HTML元素,通常是没有什么问题的,但在一次做二级下拉菜单时遇到了问题,后来才发现有时候“speed”是不能省略的,即使“speed=0”,也就是说hide( ...

随机推荐

  1. uboot mkimage使用详解

    mkimage使用详解uboot源代码的tools/目录下有mkimage工具,这个工具可以用来制作不压缩或者压缩的多种可启动映象文件. mkimage在制作映象文件的时候,是在原来的可执行映象文件的 ...

  2. 解读sample1

    说明 理解被测试代码 理解测试代码 TEST宏 EXPECT_*断言和ASSERT_*断言 检查数值比较结果的断言 检查布尔值的断言 说明 被测试代码文件 sample1.h.sample1.cc 测 ...

  3. Appium 环境搭建

    1.安装nodejs 下载地址: http://nodejs.org/download/ 下载之后一路next就好. 验证是否安装成功: node -v

  4. mysql记录所有执行过的SQL

    前不久,遇见一些问题,要监控一下SQL的执行,看看是不是有哪些SQL是要去掉的之类的 于是我上网找啊找啊,给出来的结果都是一种,修改my.cnf文件 我按着网上的说法去做,结果我直接崩溃了, 也不知道 ...

  5. Unity3D NGUI制作的Button放到场景中,按钮从2D变到3D

    通常我们使用Button都是在UI界面,即NGUI的摄像机下,如果想换到场景中,即不让按钮以UI形式显现,而是和场景中的物体一起随着摄像机移动而缩小,放大. 很简单,把Button从NGUi的摄像机中 ...

  6. 设计模式19---设计模式之状态模式(State)(行为型)

    1.场景模拟 考虑一个在线投票的应用,分为四种情况 正常投票 正常投票以后还继续重复投票 用户恶意投票 黑名单用户 2.不用模式的解决方案 package demo17.state.example1; ...

  7. BitBlt介绍

    设备上下文画图有非常多种方法.比如通过创建位图画刷,利用其填充一个区域来实现图像的绘制.此外,还能够使用CDC类的位图函数来输出位图到设备上下文中. BitBlt 用于从原设备中复制位图到目标设备,语 ...

  8. 8000401a错误解决方式(Excel)

    前一阵子做开发须要用到Excel和Word编程,本人用的是Vista系统,开发环境是VS2005和Office2007,測试无不论什么问题,但是到部署的时候出现了一些令人非常头痛的问题,老是会出现比如 ...

  9. 阅读underscore源码笔记

    本文为原创作品,可以转载,但请添加本文连接,谢谢传阅,本人博客已转移至github,地址为:jruif.github.io underscorejs,一个实用的的Javascript函数库,值得推荐, ...

  10. linux内核--中断处理程序

    一个设备的中断处理程序是它设备驱动程序的一部分--设备驱动程序是用于对设备进行管理的内核代码.中断处理程序与其他内核函数的真正区别在于,中断处理程序是被内核调用来响应中断的,而它们运行于我们称之为中断 ...