条件语句,换句话说,if语句,if else和else if等等。现在我知道你们很多人,你可能认为你已经知道了一切,了解我们的if语句和所有的C++中的分支语句。但是我还鼓励你继续看下去。因为你可能了解到新的东西。实际上我要深入一点,看它在C++中如何工作。
条件语句、if语句、分支语句等,都是什么意思呢?有些时候,我们写程序需要一个特定的条件进行评估,然后根据评估结果,来决定接下来我们想要执行什么样的代码。举个例子,假设我们有一个变量x等于5,我们想做这样一个事情,确定这个变量是否等于5,这个就是condition(条件)的本质。这里的条件就是x等于5,在此基础上,我们可以进行适当的分支。有两种情况会发生,当我们运行我们写的if语句时,先是对实际condition(条件)的评估,然后就是基于这个条件语句评估后的分支语句。换句话说,如果条件为真,我们需要跳到我们源代码的某一部分,如果条件为假,我们需要跳到我们源代码的另一部分。当然这里我说的是源代码。但在运行的应用中,他实际上是机器指令。也就是说,我们分支到机器代码,我们的CPU指令一个区域,或我们分支到CPU指令的另一部分。当我们开始一个应用程序时,整个应用程序及其所有模块被加载到内存中。基本上所有这些指令组成了我们的程序,现在都存储在内存中。当我们有了条件语句所产生的分支,我们基本上是在告诉电脑,跳到我们的这部分内存开始吧,在那里执行我们的指令。正因如此,在内存和分支之间跳跃。实际过程比我说的会更复杂些,这里有相当多的东西值得探索。例如,事实上我们必须检查条件,然后跳转到内存的不同地方,并从这里开始执行指令,意味着if语句和分支通常有比较大的开销,如果你想些快速的代码,你可能决定根本不使用if语句。事实上,许多优化的代码将特别避免分支,避免使用if语句,因为分支会使程序运行慢下来。后面,我们将看到一些优化的例子,比如,删除分支,但还不是现在。记住,我们检查一个条件,就是我们的if语句,如果某件事是真的,我们就去执行一组特定的代码。基本上就是这样。
现在,开始案例
准备一个项目,我们项目中有main.cpp,log.cpp,log.h文件,这是前面文章中的案例项目。内容如下



好,现在我们准备在main.cpp文件的main函数中添加一个基本的if语句。我们给定一个变量x,假设x等于5,int x = 5;
,我们执行打印Hello World
,我们需要校验x是不是等于5,我们实际上需要执行一个叫做比较的操作,换句话说,我们要比较一个值和另外一个值,比较的结果是一个bool类型,给定一个变量来存储比较的结果bool comparisonResult = x == 5
,这里的运算符==
,被称为等于(equality)运算符
。它是为了校验x是否等于5。如果它等于5,比较的结果返回true,如果它不等于5,返回的结果false。现在这个==
操作符是这样工作的原因是因为它在C++标准库中被重载了。这就像写一个函数,接受两个参数,然后检查这两个整数的内存以确保它们是相等的,如果相等,将返回true,不相等将返回false。所有这些你在C和C++中看到的操作符,他们不是魔术或别的什么,而是标准库中有确定的应用方式。比如在整数或者在大多数原始的数据结构中,如果你要检查两个数,比如两个整数是否相等,你基本上是在获取他们的四个字节的内存,比较每个字节。为了让这两个整数是相等的,内存中的每一位都必须相同。我们可以在if后面的括号里放入比较的结果变量comparisonResult,这里有两种写法,我们可以写if(comparisonResult == true)
,但是,这是没有必要的,因为我们将if(comparisonResult)
放到条件中与if(comparisonResult == true)
是一个意思。通过后面接大括号的方式,创建分支。

如果comparisonResult为真将打印Hello World
,comparisonResult为假将跳过这个分支,不打印Hello World
。
我们按下F5运行程序

可以看到控制台打印了Hello World
。
但是,如果我把x变成6,按下F5运行程序

可以看到什么也没打印出来。
我们在int x = 6;
加上断点

按下F5运行程序。

按下F10执行下一行。

鼠标悬停在变量x上,发现x实际上是等于6。然后比较一下,x是不是等于5,显然x不等于5

我们继续按下F10,comparisonResult就会被设置为false。

继续按下F10,if语句会计算if语句的值。

可以看到直接跳到std::cin.get();
这行了,跳过了分支。Log("Hello World");
这行代码从未被调用。
让我们更深入的了解幕后的真实情况,到底发生了什么,CPU运行了哪些指令?
我们可以将代码编译为汇编代码并检查,不过,我们还可以做另外一件事,作为调试的一部分,查看正在运行代码的反汇编。
我们F5重新运行程序

我们可以在黄色箭头指示的那行,鼠标右键点击
转到反汇编(D)


可以看到跳进的这个视图。视图中有我们的源代码,以及相关的汇编指令。这些汇编指令就是上面的源代码编译来的


实际上,我们可以一行一行的看这些汇编代码,甚至可以看到,我们的CPU中的寄存器实际的值。
这个反汇编的视图在你debug时非常的有用,一方面,它可以用于源代码无法找到错误原因只能求助于调试CPU指令,顺便说一下,这绝对是噩梦,尽量避免这种情况。但它很有用,可以快速看到编译器实际生成的代码。而不是把所有的东西都输出到一个文件中,不用那么麻烦的生成文件,我们可以直接查看反汇编(disassembly)视图。
回到反汇编视图。
我们可以看到这里发生的是,加载具有特定值的寄存器,这个mov指令,就是move的意思,mov dword ptr [x],6
这意思是我们将值6
move
到这个寄存器ptr [x]
,所以你可以把它看成是变量x被设置为6。

按下F10,走到下一个指令

我们所做的是把5加载到同一个寄存器ptr [x]
,cmp
意思是比较值,这很有趣,因为编译器对我们的代码做一些漂亮的事情。
我们继续F10
下一条指令是jne
,意思是jump not equal
,也就是说x == 5
这个比较失败了,现在的比较就是比较这两个值5和6。如果它们不相等,我们就跳到这个内存地址main+37h (0605FA7h)
,内存地址的实际值是0605FA7

jne main+37h (0605FA7h)
你看地址的后几位,可以看到它实际上指向的是这行代码00605FA7 mov dword ptr [ebp-0DCh],0

如果这两个值不相等,将跳到这行代码00605FA7 mov dword ptr [ebp-0DCh],0
这行代码。现在,我们知道不相等。
如果继续F10,可以看到我们的指令指针,这个黄色箭头指向了
00605FA7 mov dword ptr [ebp-0DCh],0
这行。

这个黄色箭头,告诉了我们CPU正在执行什么。
它跳转到那个内存地址来执行下一个指令,00605FA7 mov dword ptr [ebp-0DCh],0
mov 它会移动一个特定的值,这种情况下,是将0移动到这个寄存器[ebp-0DCh]
,这个寄存器是EBP,这个实际的寄存器减去一定的偏移量,这里我们把值0加载给它。
现在,我们回来说bool值到底是什么,我们之前在C++变量那篇中有讲到过,bool值本质上是1个字节的数据类型,和其他任何数据类型一样,这里面没有true或false的概念。那么bool是怎么起作用的呢?基本上,如果值是0,那么它是false,如果值不是0,那就是true。在强调一次,我们只是在处理数字,计算机在这里处理数字。这里我们有一个巨大的1byte字节的内存地址空间,如果我们创建了一个布尔值,实际上会占用内存的一个字节,我们不一定要确定是哪个比特位被设置为1,只要有东西在这个byte字节里面,而且不是0,那么它就是true。如果我们只处理1个比特,当然我们只有两个可能的值,0和1,如果它是0,那么就是false,否则就是true。然而,既然我们有这么大范围的数字(1个byte字节的范围),我们可以有把握的说,即使值假设是100,这也是true,只有0是false。也就是说,这里实际发生的是将0加载到寄存器中,所以我们把0赋值给这个bool变量,所以false等于0,这点要记住。
我们继续F10,会有一些其它类型的代码,我们并不关心。
最后是if指令

我们在if指令中做的是,我们只是将某些值加载到EAX寄存器中
然后测试EAX寄存器是否通过我们的条件,这个测试指令test
将基本上执行对这两个寄存器进行逻辑与运算,这里不再深入。

但基本上,如果这个00605FBE test eax,eax
操作成功,可以看到下一行00605FC0 je main+5Fh (0605FCFh)
,je
也就是jump equal
,也就是说这个test
操作成功,我们实际上要跳转0605FCF
这个地址,也就是这样00605FCF mov esi,esp
,所以,我们跳过了log函数。如果这个test
失败,我们就不做je
指令。所以这个je
不是一个普通的jump跳跃指令,和普通的jmp
跳跃指令不同,它是一个条件跳转语句。换句话说,如果这个test失败了,就不跳转,而是继续执行下一行00605FC2 push offset string "Hello World" (0608B44h)
,然而我们知道test将会执行成功。因为比较的结果是false的。。

好了,我们继续按下F10

可以看到,确实发生了跳跃。
我们了解到了新的知识,实际上发生在新语句的背后的事情。现在请记住,我正在编译的这个运行是在调试模式下,这意味着编译器完全不会优化我们的代码。
如果我们回头看一下我们的代码,所有的这些都可以被简化。

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

但编译器为什么要在运行时进行条件检查?这需要额外的时间,这本来是不需要的,记住如果你想看看它在汇编下是如何工作的,请确保你的程序出于debug调试模式并确保已经尽可能多得关闭了优化,因为如果你不这样做,编译器会执行它的一些魔法,你就搞不清楚了。
回到if,我们知道这个比较结果是布尔值,bool值实际上是一个整数,如果是0就是false,如果是其他就是true。那么if语句到底在做什么呢?它只是在检查这个值是数字0吗?如果它是0,他不会执行if语句。但是,如果它不是0,它会跳到if语句内。这就是为什么这整个式子if (comparisonResult)
我们不需要做等号if (comparisonResult == true)
之类的事情,因为我们不需要检查它是否等于1或者等于0。我们可以直接说,这个值是不是0,这就是它所做的。看看comparisonResult的内存,看看这个结果是不是0,如果不是0,就执行if语句,如果是0,就不执行。这就是一个if语句的实质。
举个例子,我们在if条件里直接写1,去掉断点。

F5运行程序

你会看到Hello World
被打印了。
或者直接在if的条件里写0

F5运行程序

就不会打印Hello World
很简单,我们已经知道它是这样工作的。
这里这个comparisonResult
变量用来存储比较结果,其实我们不需要这样做,这里这样做的原因是想通过这样来告诉你if后面的这个条件其实是个bool类型,你可以直接在if后面的条件中写x == 5
。

这将大大简化你的代码。
如果我们的if语句只有一行代码,我们可以省去大括号。

我们这样写也是可以的。
甚至可以写成一行

但是不建议放在一行,因为如果你想调试时,在这行加断点

按下F5运行程序

你会看到运行到这一行时,我不知道,如果我按下F10,它是做if比较呢,还是在做log函数。我不知道发生了什么。
但是我分开两行

按下F5运行程序

按下F10

可以明显的看到log函数没有执行。
接下来,因为bool只是数值,而这个if语句只是对数值进行检查,你可以用if语句做一些很有趣的事情。
例如,我们在if后面的条件中直接写x

然后F5运行我们的代码。

它基本上还是在求值。x的值不是0
如果我们按下F10,程序将会跳进if语句里面。

继续F10,将打印Hello World
到控制台。

对于指针使用这个技巧也很常见。如果我们想检验指针是否为空,也就是null,null当然是0。我们可以把那个指针放到一个if语句的条件当中,看看它是不是null。
如下,我们有一个指针const char* ptr
修改代码如下

F5运行代码

可以看到ptr被设置了值,它不是null,因此,我们可以把它打印到控制台。
按下F5继续

可以打印hello
。
如果指针等于null,它可以是0或nullptr

按下F5运行

可以看到,这个log不会被执行。
我们会在代码的任何地方用到if语句,编程实际上分为2种,一种数学编程,另一种是逻辑编程。一部分的编程就像在做数学运算,实际上,大多数快速的代码本质上都是在做数学运算。另一部分是所谓的逻辑编程,这都是关于逻辑的,如果这个是这个,就去干什么什么,如果这个不是这个,就该干哪些,这很有用,没有哪个应用或游戏编写时没有使用if语句或类似的东西,但是在未来当我们学会写更好代码的时候,你会在很多情况下需要用到if语句,这时,你应该想着试着做些什么,用一些数学计算代替,而不是做一个比较通过分支语句来处理,因为这样做,实际上会降低程序的速度相当多,这被认为是糟糕的代码,虽然写这样的语句,可能更加合理。