C语言学习指南:从规范编程到专业级开发
上QQ阅读APP看书,第一时间看更新

7.8 善用goto语句以实现无条件跳转

goto语句会让程序立刻且无条件地跳转到函数体内的指定标签处,也就是说,goto会让程序从那个标签开始往下执行。目前的C语言跟早期的版本不同,它现在已经不允许我们通过goto语句跳转到函数体之外了,因此,我们既不能通过goto进入别的函数,也不能通过该语句跳转到另一个程序里面(这两种用法在当年并不少见)。

goto语句有两个要素。首先,你必须声明一个标签,这个标签(例如叫作label_identifier)可以独占一行,比方说可以写成label_identifier:,也可以跟它所标注的语句(例如叫作statement)合起来写在同一行里,比方说可以写成label_identifier:statement。

第二,你需要把这个标签的名称(例如刚才说的label_identifier)写在goto关键字的右侧以构成一条goto语句。该语句的语法是:

为什么有人不建议使用goto语句呢?这是因为早期还没有结构化编程(structured programming)这一概念,后来业界才提出了这种只有一个入口点与一个出口点的编程范式。今天我们已经不再提这个问题了,因为目前的程序员所受的训练比那时要好,而且从C语言开始,各种编程语言都变得比较规范,所以不再需要通过goto语句来实现某些逻辑了,因而也就很少出现滥用goto的问题。当年推行结构化编程是想反制那种比它更早的意大利面条式代码(spaghetti code),那时由于缺乏规范的跳转机制,因此开发者会频繁使用goto语句,这有时会让代码彻底失控。那种程序可以从一个地方随意跳到另一个地方,这会让阅读程序代码的人很难看懂代码的含义,因而也就很难修改这些代码(goto语句让人很难把程序有可能出现的执行路径全都掌握清楚)。那时的开发者经常会问:我是怎么跑到这个地方来的?程序是沿着哪条路径执行到这个地方的?这些问题很难说清,而且有些情况下根本说不清。C语言以及从它衍生出来的其他语言已经限制了goto语句的用法,让这种语句变得更加规范。

C语言的设计者觉得我们偶尔还是需要用到goto的,因此尽管这种用法比较少见,但他们还是把goto保留在了C语言之中。C语言的goto语句是严格受到限制的,因为它“只能”在限定的范围内跳转。我们不能像以前那样通过goto跳转到其他函数之中的某个标签那里。我们既不能让goto跳转到当前函数之外,也不能让它跳转到别的程序,乃至运行时库(runtime library)与系统代码(system code)里面。以前有人那么写只是为了省事,他们并没有考虑到那种做法会给以后的代码维护工作带来哪些困难。当时有很多问题都是由于滥用goto而产生的,但这些问题现在已经不会再出现了,至少对于goto来说是如此。

目前的C语言只允许你用goto跳转到当前函数的某个标签那里。如果你想从多层嵌套的if...else...结构或循环结构里面跳出来,那么采用goto语句来做会相当方便。我们确实会遇到这样的情况,然而它毕竟是极其罕见的。另一种需要用到goto的场合是在执行高性能计算的时候。考虑到这两项需求,我们还是有必要了解goto这个语句的。

C语言虽然限制了goto语句的用法,但同时提供了另外两个相当有用而且相当规范的语句,让我们可以改用那两种语句来实现以前需要用goto去做的某些功能,这个问题会在下一节讲解。

在本节接下来的内容里,我们要学习怎样以结构化的方式使用goto语句,以实现前面学过的那几种循环结构。在用goto实现早前学过的每种结构时,笔者都会安排两个标签以标注起始位置(begin_loop:)与结束位置(end_loop:),这两个位置之间的代码就相当于那种循环结构的循环体。

第一个例子是用goto实现do...while()循环。这种情况下其实没必要写出结束标签,但为了让代码清晰一些,笔者还是把这个标签写了出来。下面这个sumNviaGoto_Do()函数写在gauss_goto.c程序文件里面:

以前我们学习do...while()循环时所写的那些范例代码,全都出现在了这个sumNviaGoto_Do()函数里面。begin_loop:标签所标注的是循环体的起点,end_loop:标签所标注的则是循环体的终点,这两部分之间的代码就相当于do...while()的循环体。按照现在的这种写法,程序在首次判断num<N这个循环表达式之前肯定会把循环体先执行一遍。以上就是如何用goto语句来实现等效的do...while()循环。

接下来我们可能会考虑怎么用goto语句实现出等效的while()...循环。下面就是实现代码,这个sumNviaGoto_While()函数写在gauss_goto.c程序文件里面:

请注意,我们改写时要稍微调整一下循环的条件,也就是要把它写得跟等效的while()...循环恰好相反。这样的话,如果循环条件为true,那么程序就会执行goto end_loop;语句,以跳出循环。为了跳出这个循环逻辑,我们必须这样写,这就好比在使用while()...循环结构时,要想跳出循环,我们必须设法让条件表达式变为false。

最后,我们用goto语句来实现等效的for()...循环。这个sumNviaGoto_For()函数也写在gauss_goto.c程序文件中:

这次我们需要添加一个名为i的局部变量,并把它初始化为0。我们每次都要判断它当前的值是否小于N,如果是,那就执行循环体,然后还要记得让这个局部变量自增,最后我们通过goto语句,无条件地跳转到循环体的开头,也就是begin_loop:标签那里。大家应该能够看出这种写法里面的相关代码与for()...语句中的相关部分之间的联系。

汇编语言是一种几乎可以跟机器语言直接对应的编程语言,在那种语言里面,开发者没有for()...、while()...与do...while()等循环语句可用,他们只能使用类似goto这样的跳转指令。因此,我们刚才用goto写出来的那些逻辑能够比较整齐地对译为汇编语言乃至机器语言的代码。但我们的重点并不在这里,笔者之所以要讲这些内容,只是想让大家知道怎样在各种循环结构与等效的goto实现方案之间转换而已。

下面给出gauss_goto.c程序的main()函数:

请创建名为gauss_goto.c的文件,并把main()函数以及刚才那三个sum函数录入该文件。然后用cc gauss_goto.c -o gauss_goto命令编译这份文件,最后运行程序。你应该会看到类似下面这样的输出信息。

从这张截图可以看出,用这三种办法实现出的goto循环也能计算出1~100的各整数之和,而且计算结果与早前相符。

现在的问题是:goto语句确实能实现出循环效果,但我们在什么样的情况下才需要这么做呢?

答案很明确:无论在什么样的情况下,我们都不需要这么做。应该优先考虑用for()...、while()...与do...while()这样的循环语句来实现。

我们可以用这三种成熟的循环语句写出清晰的代码,因此不需要再用goto去模拟循环效果了。就算这些语句所表达的依然是跳转逻辑,那也应该由编译器替我们去把它转化成相应的跳转指令,而不需要由我们自己拿goto模拟。因此,对于一般的编程需求来说,很少需要用到goto。只有在实现某些对性能要求比较高的计算任务时,我们才会考虑使用goto。

总之,大家要记住,滥用或误用goto会导致程序出现大问题。一定要明智地使用goto。