009 - C++条件与分支(if语句)

今天我们来看看条件语句,换句话说,也就是 if 语句、if elseelse if 等等这写语句。

我知道大家基本上已经非常了解 if 语句和所有 C++ 中的分支语句,但我还是鼓励你们继续看完这一讲,这里可能包含一些新东西。我们还会深入一点看它如何在 C++ 中工作,这些对你理解程序如何运行是有一些帮助的。

01 分支语句能干什么

条件语句if 语句分支语句,这些都是什么意思呢?

有些时候我们写程序的时候,我们需要对一个特定的条件进行评估,然后根据评估的结果,决定我们想要执行什么代码。

举个例子,假设我们有一个变量 x 等于5,我们希望能够编写代码实现判断 这个变量的值是否确定等于5,这就是 条件语句 的本质,这里的条件就是 x 等于5,在此基础上我们可以进行适当的分支。

这样的话,有两种情况会发生,当我们运行我们写的 if 语句时,有两个过程,首先是对实际 条件语句 的评估,然后是基于这个条语句评估后的分支语句。换句话说,如果条件为真,我们需要跳到我们源代码的某一部分,如果值为假,我们需要跳到我们源代码的另一部分。这里我说的是源代码,但在运行的应用程序中,它实际上是指机器指令。换句话说,我们分支到机器代码的一个区域,或我们分支到CPU 指令的另一部分。

当我们开始一个应用程序时,整个应用程序及其所有模块加载到内存中。基本上所有这些指令组成了我们的程序,现在这些指令都存储在内存中,当我们有了条件语句所产生的分支,我们基本上是在告诉电脑,嘿,跳到我们的这部分内存开始吧在那里执行我们的指令。正因为如此在内存和分支之间跳跃,实际的过程会更复杂一点,这里有相当多的东西值得我们探索一下。

例如,我们必须检查条件,然后跳转到内存的不同的地方,并从这里开始执行指令。意味着 if 语句和分支语句通常有比较大的开销。如果你想让你的代码有较强的性能,您可能决定不使用 if 语句,事实上,许多优化的代码将特别避免使用 if 语句,因为这样做会使程序运行速度减慢。

在本系列后面的内容中,我们将会看到一些优化的例子,比如删除分支,但这还为时过早,本期不做讨论。

记住,if 语句就是在检查一个条件,如果这件事是真的,我们就去执行一组特定的代码

02 例子时间

我们看一个例子。

请添加图片描述

先做一些解释。

== 操作符称为 比较运算符,它的作用是比较两边的值是否相等。有点像是一个函数,接受两个参数。它会返回一个布尔值类型的结果。

在整数或者在大多数原始的数据结构中,如果你要检查两个数,比如两个整数是否相等,比较运算符基本上是在获取他们的四个字节的内存,然后比较每个字节,这 2 个整数相等的条件是内存的每一位都必须相同

上面的程序将 x 和 5 比较的结果存储到布尔值的变量 comparisonResult中。

我们使用分支语句做点事情。

请添加图片描述

comparisonResult 这里可以有两种写法。

// 第一种写法
if(comparisonResult)
// 第二种写法
if(comparisonResult == true)

这两种写法其实是一个意思,换句话说,在第一种写法中,如果 comparisonResult 不是 true,这个 if 语句也将不会运行。

然后我们为这个 if 语句写了分支语句,换句话说,就是分支去哪里?条件成立后你想让我运行什么代码?

我们现在已经创建了一个分支,如果 comparisonResulttrue,我们将运行函数 Log ,否则就什么都不做。

即使我们只是写了这么一小段代码,这里依然有很多有趣的事情可以讨论。因为这里发生的事情太多了,我们来运行一下,看看会发生什么,我会一行一行地讲解。

03 发生了什么

如果我现在按 F5 运行程序,你可以看到 Hello World!。如果我把 x 变成 6,然后按下 F5,什么都不会打印出来,当然是这样的。

我们现在设置一个断点。
请添加图片描述

现在在有断点的情况下按 F5,然后按 F10,我们会发现 x 等于 6。

然后比较一下 x 是不是等于 5。显然 x 不等于5,因此如果我按 F10comparisonResult 就会被设置为 false,如果我在这里按下 F10if 语句就会计算括号里面的值,你可以看到,它直接跳到 std:cin.get,没有运行到 log 函数。因为我们的代码的意思就是只有 comparisonResulttrue 时,才会运行那些代码。

让我们更深入地了解幕后的真实情况,看看到底发生了什么,看看CPU 运行了哪些指令?

04 更详细一点

我们可以将其编译为汇编代码并检查,不过作为调试的一部分,我可以先查看正在运行代码的反汇编。

在我们设置了断点后,按 F5,然后右键点击打断点的那行代码,选择 转到反汇编,打开的反汇编视图如下。

请添加图片描述

这里有我们的源代码以及相关的汇编指令,这些汇编指令就是上面的源代码编译来的,我们可以一行一行看这些汇编代码,在这里甚至可以看到 CPU 中的寄存器实际的值。

这个反汇编的视图在你 debug 时非常的有用,一方面,它可以用于在源代码中无法找到错误原因只能求助于调试 CPU 指令,(顺便说一下,这绝对是个噩梦,尽量避免这种情况,但它也很有用),还可以快速看到编译器实际生成的代码,而不是把所有的东西输出到一个文件中。

我们看下这些内容。

你可以看到黄色箭头那里是加载具有特定值的寄存器,这个 mov 指令就是 move 的意思,这意思是我们将值 6 move 到这个寄存器,所以你可以把它看成是变量 x 被设置为 6。

按下 F10 走到下一个指令。这就是我们的布尔语句,我们所做的是把 5 加载到同一个寄存器。

这很有趣,因为编译器将对我们的代码做一些漂亮的事情。

我们继续读下去,下一条指令是 jne,意思是 jump not equal。现在的比较就是比较这两个值 5 和 6,如果它们不相等,我们就跳到这个内存地址,你可以根据这个地址找到它实际上的代码。

现在我们知道它不相等,如果我点击 F10 ,你可以看到我们的指令指针跳转到那个内存地址来执行下一个指令。

请添加图片描述

它会移动到一个特定的值,在这种情况下,是将 0 移动到这个寄存器,这个寄存器是 ebp 这个实际的寄存器减去一定的偏移量,这里我们把值 0 加载给它。

现在我们回来说说,布尔值到底是什么,这个我在 C++ 变量那一期的视频中说过, bool 值本质上是1个字节的数据类型,和其他任何数据类型一样,这里面其实没有 true 或 false 的概念,那么 bool 是怎么起作用的呢?

基本上,如果值是 0,那么它是 false,只要值不是 0,那就是 true

详细的解释是,这里我们有个巨大的 1 byte 内存地址空间,如果我们创造了一个布尔值,实际上会占用内存的一个字节,我们不一定要确定是哪个比特位被设置为1,只要有东西在这个 byte 里面,而且不为零,那么它就是 true。

另外,如果我们显式的加载一个值,可能会是 1,这并没有强迫我们必须设置为 1,它可以是任何东西。

这条 move 指令将 0 加载到内存中,它会把这个布尔值设为 false,因为这个比较失败了。

如果我们继续下去,会有一些其他类型的代码,这些我们并不关心。

最后是 IF 指令。

请添加图片描述

我们在 if 指令中做的是,我们只是将某些数据加载到 eax 寄存器中,测试看 eax 寄存器是否通过我们的条件。

这个 test 指令将基本上执行对这两个寄存器进行 逻辑与 运算,本期对这些流程暂时不会讲太深。

但基本上,如果这个test 操作成功,你可以看到会执行 je 指令,跳转到 007A2636 。换句话说,如果这个测试操作为真,实际上我们也要跳转到这里。跳过了 log 函数。如果这里 test 不成功,我们就不做 je 指令,所以这个 je 不是普通的 jump 跳跃指令,和普通的跳跃指令 jmp 不同,它是一个条件跳转语句,换句话说,如果这个 test 失败了就不跳转,而是我们继续下一行代码。

我们知道这个 test 将会成功,因为比较的结果是 false 的。好了,我们按下 F10,你可以看到, 我们确实在这里跳跃,我们继续像往常一 样运行我们的程序。

好了,这是我们更深入的了解实际上发生在新语句的背后的事情,请记住,现在编译运行在调试模式下,这意味着编译器不会优化我们的代码,如果我们回头看一下我们的代码,很多地方是可以被编译器优化的。

比如,编译器知道这个变量 x 确定等于6,然后我们把它和 5 做对比,编译器自己在编译期就能做到,不需要在程序运行的时候审核再去做。就是所谓的常数折叠,所以这就变成了一个常数变量,因为它是否是一个常变量,在编译的时候就已经知道了,然后优化会去掉 comparisonResult、if 语句等等, 直接跳到 std::cin.get() 语句,它会删除一些行,因为他们永远都不会运行。

但是为什么编译器在运行时还要进行条件检查?这些是需要额外的时间的,是不需要的。

记住如果你想看看它在汇编下时如何工作过的,只要确保你在调试模式下,并且关闭了优化,因为如果你不这样做。编译器会执行它的一些优化操作。

我们知道比较结果是一个布尔值,bool 值实际上是一个整数,如果是 0 就是 false 的,其他就是 true。

那么 if 语句到底在做什么呢?它只是在检查这个数字是不是 0 ,如果它是 0,就不会执行 if 语句,如果它不是 0,它会跳到 if 语句内,这就是为什么这整个式子我们不需要做比较运算之类的事情,因为我们不需要检查它是否等于 1 或者是 0,我们可以直接说这个值是不是不是 0,这就是它所做的。

具体到到代码中,就是看看 comparisonResult 的内存是不是 0 呢?如果不是 0,就执行此代码。 如果它是 0,就不执行,这就是一个 if 语句的实质。

举个例子。

请添加图片描述

运行我的代码。它能很好地编译,你会看到 Hello World 被打印了。

如果我输入 if(0) ,就不会打印了。

上面我们存储比较的结果,是想告诉你们这个条件实际上是布尔类型,你可以直接写 x==5,这将大大简化您的代码。

请添加图片描述

对于 if 语句还有其他的一些操作。

如果我们的 if 语句只有一行代码,我们不需要写花括号,这样写也是可以的。

请添加图片描述

你甚至可以格式化成一行,我个人不会这样做,当它在一行的时候有点烦人,因为如果你想调试,例如,如果我在这里放一个断点进行调试。

请添加图片描述

你会看到它在运行到这一行时,我不知道 它是在做 if 比较呢,还是在做 log 函数,我不知道发生了什么,但是如果我把它分成两行,我们就可以得到我们想要得到信息。

因为 bool 只是数值,而这个 if 语句只是对数值进行检查,所以你可以用 if 语句做一些很有趣的事情。

例如,我可以写 if(x),然后如果我运行代码。

请添加图片描述

它基本上还是在求值,**嘿,x 是不是 0 ?**如果我按 F10,程序会跳进去到 if 语句里面到 log 函数,并执行。

对于指针使用这个技巧也很常见,如果我想检验指针是否为空,我们可以把那个指针放到 if 语句的条件当中,看看它是不是 NULL

请添加图片描述

运行这段代码,你可以看到指针实际上是被设置了某值它不是 NULL,因此,我们可以把它打印到控制台。

如果指针等于NULL,你可以看到这段代码不会被执行。

就写 ptr 而不是 ptr != NULL,这是一种程序风格,有些人喜欢直白表述,因为他们认为可以代码可读性更强,就我个人而言,我喜欢这样简单的处理。如果指针是一个有效值,就运行这行代码,干净整洁,这是你可以在 C++中做的,在其他语言中,比如 Java和 C#,你无法这样做。

再说一下,C++ 语言是比较原生态的语言,可以做任何事情,如果我们想做点别的事情。

比如,如果指针是有效的值,如果不是 NULL,但如果我想运行一些其他代码,在这样的情况下就需要 else 语句了。

请添加图片描述

运行之后你可以看到打印的显然 ptr is NULL。如果使用调试器逐行一步一步地执行程序,发现指针是 NULL0 ,就不会执行 if 下面的指令, 而是会跳转到 else

同样, 我们也可以使用 else if 继续检查条件。

05 else if

请添加图片描述

else if 只会是当 if 语句失败之后才会执行。

上面的代码理论上不会被执行,或者至少这部分代码永远不会被执行。而如果我把这里的代码调整一下,然后按 F5,会发生什么?

请添加图片描述

此时指针不是空的,所以 if 可以用来抓住这个条件。

然后我再按 F10,你可以看到我们一路跳过去,因为这些都不重要,如果要运行下面这些条件语句,除非前面的 if 语句失败了。

但是, 如果我写 if 的东西,没有用 if else,那么你可以看到它实际上还是要检查一下。

请添加图片描述

这也会运行成功,实际上我们打印两个东西到控制台。

else if 表述实际上是一个小技巧,你可能没有意识到这其实是一种巧妙的隐藏语法,如果我们这样修改一下,看看这个,你就会理解了。

请添加图片描述

所以其实没有所谓的 else if 语句,只是将两个语句放在一行而已,和我们之前将 if 语句的内容的放在 if 的同一行是一样的。

else if 只是一个小聪明而已,也就是说如果上面的 if 失败了,就尝试 else if。同样只有在前面的 if 失败后,才会触发 else 语句,才会去比较 ptr 是不是 hello。就是这样。

我记得当我发现的时候,这让我很吃惊,因为我真的没意识到这一点,我觉得 else if 是关键字组合,但是实际上不是,else if 不是 C++ 的关键字,是先 else,然后再 if

基本的东西我已经说得差不多了,if 语句和条件语句,分支和跳转和所有的这些东西,希望你们能明自这是怎么回事,我们会在后面的代码中的很多地方用到 if 语句。

06 后话

编程实际上分成两种,一种数学编程,另一种是逻辑编程。

我喜欢把编程分为这两类,部分的编程就像在做数学运算,实际上,大多数对速度有要求的编程本身就是在做数学运算。编程第二类,所谓的逻辑编程,大部分是在做:如果这个是这个,就去干那个,如果这个,然后那个…,这有点无聊。这些操作当然很有用,没有哪个应用或者游戏偏写时没有使用 if 语句或类似的东西,只是在未来写更好的代码的时候,你会在很多情况下需要用到 if 语句,但我会试着做些别的什么操作,比如用一些数学计算代替,而不是做一个比较,然后通过分支语句来处理,因为如果这样做会降低程序的速度,这样会很糟糕,虽然有可能写这些语句看起来更合理。

第二类,所谓的逻辑编程,大部分是在做:如果这个是这个,就去干那个,如果这个,然后那个…,这有点无聊。这些操作当然很有用,没有哪个应用或者游戏偏写时没有使用 if 语句或类似的东西,只是在未来写更好的代码的时候,你会在很多情况下需要用到 if 语句,但我会试着做些别的什么操作,比如用一些数学计算代替,而不是做一个比较,然后通过分支语句来处理,因为如果这样做会降低程序的速度,这样会很糟糕,虽然有可能写这些语句看起来更合理。

本期就是这些,下期见。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值