“ goto”语句导致什么样的错误?有历史上重要的例子吗?


103

我知道,除了打破嵌套在循环中的循环之外,其他方法都是这样。该goto语句是易于出错的编程风格,因此一直被人们回避和谴责。

XKCD 替代文字: “尼尔·斯蒂芬森(Neal Stephenson)认为给他的唱片公司定名为“ dengo”很可爱”,
请参见以下原始漫画:http//xkcd.com/292/

因为我很早就学到了 对于什么类型的错误goto实际上导致什么,我真的没有任何见识或经验。那么我们在这里谈论什么:

  • 不稳定吗?
  • 无法维护或无法读取的代码?
  • 安全漏洞?
  • 还有其他东西吗?

实际上,“ goto”语句会导致哪种错误?有历史上重要的例子吗?


33
您是否阅读过Dijkstra关于该主题的原始短文?(这是一个是/否问题,除了明确的即时“是”以外,其他任何答案都是“否”。)
约翰·R·斯特罗姆

2
我假设发生灾难性故障时会发出警报。你永远不会回来的地方。像电源故障一样,设备或文件消失。某些“出错时出错” ...但是,我认为当今大多数“几乎是催化的”错误都可以通过“尝试-捕获”构造来处理。
Lenne

13
还没有人提到计算出的GOTO。回到地球年轻的时候,BASIC就有行号。您可以编写以下代码:100 N = A * 100;
GOTON

7
@ JohnR.Strohm我已经读过Dijkstra的论文。它写于1968年,建议使用包括switch语句和函数之类的高级功能的语言可以减少对gotos的需求。它是为响应语言而编写的,其中goto是流控制的主要方法,可用于跳转到程序中的任何地方。而在撰写本文后开发的语言(例如C)中,goto只能跳转到同一堆栈框架中的位置,并且通常仅在其他选项无法正常工作时使用。(续...)

10
(...续)他的观点在当时是有效的,但不再有意义,无论 goto是否是一个好主意。如果我们想争论一种或另一种方式,则必须提出与现代语言相关的论点。顺便说一句,您是否阅读过Knuth的“使用goto语句进行结构化编程”?

Answers:


2

这是一种查看它的方法,我尚未在其他答案中看到。

它与范围有关。良好编程习惯的主要支柱之一是保持范围狭窄。您需要范围狭窄,因为您缺乏监督和理解的精神能力,而不仅仅是几个逻辑步骤。因此,您要创建小块(每个小块都成为“一件事”),然后使用它们进行构建以创建更大的块,再将其变成一件事,依此类推。这样可以使事情易于管理和理解。

goto有效地将逻辑范围扩大到整个程序。这肯定会打败您的大脑,但是除了跨越几行的最小程序之外。

因此,您将无法判断自己是否犯了一个错误,因为有太多东西需要检查您可怜的小脑袋。这是真正的问题,错误只是可能的结果。


非本地goto?
重复数据删除器

thebrain.mcgill.ca/flash/capsules/experience_jaune03.html <<人类的大脑可以一次平均跟踪7件事。您的基本原理会受到这种限制,因为扩大范围会增加许多需要跟踪的内容。因此,将要引入的错误类型很可能是遗漏和健忘。通常,您还提供了“保持范围紧凑”的缓解策略和良好实践。因此,我接受这个答案。好工作马丁。
Akiva

1
多么酷啊?!在超过200张选票中,只有1张获胜!
Martin Maat

68

并不是说它goto本身就是坏的。(毕竟,计算机中的每个跳转指令都是goto。)问题是存在一种人性化的编程风格,它早于结构化编程(可以称为“流程图”编程)。

在流程图编程(这一代人学习并用于Apollo moon程序)中,您创建了一个图,其中包含用于语句执行的块和用于决策的菱形图,并且可以将它们与遍布各处的线连接起来。(所谓的“意大利面条代码”。)

意大利面条代码的问题在于,您(程序员)可以“知道”它是正确的,但是您如何向自己或其他人证明这一点?实际上,它实际上可能有潜在的不当行为,并且您知道它始终是正确的可能是错误的。

随之而来的是带有start-end块的结构化编程,包括for-while,if-else等。它们的优点是您仍然可以在其中执行任何操作,但是如果您非常谨慎,则可以确保代码正确。

当然,即使没有,人们仍然可以编写意大利面条式代码goto。常见的方法是编写一个while(...) switch( iState ){...,其中不同的情况会将设置iState为不同的值。实际上,在C或C ++中,可以编写一个宏来完成该操作,并将其命名为GOTO,因此说您不使用goto是一种区别,没有区别。

作为一个例子,证明代码可以如何防止不受限制地使用goto,很久以前,我偶然发现了一种可用于动态更改用户界面的控件结构。我称之为差异执行。它是完全图灵万能的,但它的正确性证明依赖于纯粹的结构化程序设计-不gotoreturncontinuebreak,或例外。

在此处输入图片说明


49
除了在最琐碎的程序中,即使使用结构化程序编写它们,也无法证明其正确性。您只能提高信心水平;结构化编程确实可以做到这一点。
罗伯特·哈维

23
@MikeDunlavey:相关:“当心上面代码中的错误;我只证明了它是正确的,没有尝试过。” staff.fnwi.uva.nl/p.vanemdeboas/knuthnote.pdf
Mooing Duck

26
@RobertHarvey 正确性证明仅适用于琐碎的程序并不完全正确。但是,需要比结构化编程更专业的工具。
Mike Haskel

18
@MSalters:这是一个研究项目。这样的研究项目的目的是表明,如果他们有资源,他们可以编写经过验证的编译器。如果查看他们当前的工作,您会发现他们对支持更高版本的C根本不感兴趣,而是对扩展可以证明的正确性感兴趣。因此,它们是否支持C90,C99,C11,Pascal,Fortran,Algol或Plankalkül完全无关紧要。
约尔格W¯¯米塔格

8
@martineau:我对那些“玻色子短语”持怀疑态度,程序员在某种程度上同意它的好坏,因为如果不这样做,他们会坐下来。事情必须有更基本的原因。
Mike Dunlavey

63

为什么goto危险?

  • goto本身不会引起不稳定。尽管大约有100,000 gotos,Linux内核仍然是稳定的模型。
  • goto本身不应引起安全漏洞。但是,在某些语言中,将其与try/ catch异常管理块混合使用可能会导致漏洞,如本CERT建议中所述。主流C ++编译器会标记并防止此类错误,但不幸的是,较早或更旧的编译器不会这样做。
  • goto导致无法读取和无法维护的代码。这也称为意大利面条代码,因为像意大利面条盘一样,当有太多的goto时,很难遵循控制流程。

即使您设法避免使用意大利面条式的代码,并且仅使用了几个goto,它们仍然会带来类似bug和资源泄漏的问题:

  • 使用具有清晰嵌套的块和循环或开关的结构编程的代码很容易理解;它的控制流程非常可预测。因此,更容易确保不变量得到尊重。
  • 通过goto声明,您可以打破那种直截了当的流程,并打破期望。例如,您可能不会注意到仍然必须释放资源。
  • 许多goto不同地方的人都可以将您带到一个goto目标。因此,确定到达此位置时所处的状态并不明显。因此,做出错误/毫无根据的假设的风险很大。

附加信息和报价:

C提供了无限滥用的goto语句和要跳转到的标签。形式上goto从来没有必要,实际上,没有它,编写代码几乎总是容易的。(...)
尽管如此,我们将建议goto可能会找到位置的一些情况。最常见的用途是放弃某些深层嵌套结构中的处理,例如一次脱离两个循环。(...)
尽管我们对此并不拘谨,但似乎应该尽量少使用goto语句

  • James Gosling和Henry McGilton在他们1995年的Java语言环境白皮书中写道:

    没有更多的Goto语句
    Java没有goto语句。研究表明,goto被(误用)的原因不只是“因为它存在”而已。消除goto导致了语言的简化(...)对大约100,000行C代码的研究确定,大约90%的goto语句纯粹用于获得摆脱嵌套循环的效果。如上所述,多级中断并继续删除了对goto语句的大部分需求。

  • Bjarne Stroustrup在他的词汇表中以这些吸引人的术语定义了goto :

    goto-臭名昭著的goto。在机器生成的C ++代码中主要有用。

什么时候可以使用goto?

像K&R一样,我也不是固执己见。我承认,在某些情况下,goto可能会减轻您的生活。

通常,在C语言中,goto允许多级循环退出,或错误处理要求到达适当的退出点,以释放/解锁到目前为止分配的所有资源(即,顺序进行多次分配意味着多个标签)。 本文量化了Linux内核中goto的不同用法。

我个人比较喜欢避免这种情况,并且在使用C语言的10年中,我最多使用10个goto。我更喜欢使用嵌套ifs,我认为它更易读。当这会导致嵌套太深时,我会选择将函数分解为较小的部分,或者在级联中使用布尔值指示器。如今的优化编译器足够聪明,可以生成几乎与相同的代码goto

goto的使用很大程度上取决于语言:

  • 在C ++中,正确使用RAII会使编译器自动销毁超出范围的对象,从而无论如何都将清除资源/锁,并且不再需要goto。

  • 在Java中有没有需要跳转(请参阅Java的作者引用上面这个优秀的堆栈溢出的答案):垃圾收集,清理残局,breakcontinue,和try/ catch异常处理覆盖所有地方的情况goto可能是有用的,但在一个更安全和更好方式。Java的流行证明了可以用现代语言避免goto语句。

放大著名的SSL goto失败漏洞

重要免责声明:鉴于评论中的激烈讨论,我想澄清一下,我并不假装goto语句是导致此错误的唯一原因。我不假装没有goto就不会有bug。我只想表明goto可能与严重的错误有关。

我不知道goto编程历史中涉及多少个严重的错误:细节通常无法传达。但是,有一个著名的Apple SSL错误削弱了iOS的安全性。导致此错误的goto语句是错误的语句。

有人认为,错误的根本原因不是goto语句本身,而是错误的复制/粘贴,误导性的缩进,条件块周围缺少花括号或开发人员的工作习惯。我也无法确认其中任何一个:所有这些论点都是可能的假设和解释。没人真正知道。(同时,鉴于同一功能中的其他缩进不一致,合并的假设如注释中所建议的那样出错了,这似乎是一个很好的选择)。

唯一客观的事实是重复 goto导致过早退出该功能。查看代码,唯一可能导致相同结果的其他语句将是返回值。

文件中的函数SSLEncodeSignedServerKeyExchange()存在错误:

    if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0)
        goto fail;
    if ((err =...) !=0)
        goto fail;
    if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
        goto fail;
        goto fail;  // <====OUCH: INDENTATION MISLEADS: THIS IS UNCONDITIONDAL!!
    if (...)
        goto fail;

    ... // Do some cryptographic operations here

fail:
    ... // Free resources to process error

确实,条件块周围的花括号可以防止该错误:
它可能导致编译时语法错误(并因此导致更正)或冗余的无害goto。顺便说一句,由于其可选的警告来检测不一致的缩进,GCC 6将能够发现这些错误。

但是首先,使用结构化的代码可以避免所有这些问题。因此,goto至少间接地是导致此错误的原因。至少有两种方法可以避免这种情况:

方法1:if子句或嵌套ifs

与其顺序测试大量条件是否出错,并fail在出现问题的情况下每次发送给标签,if不如选择在-statement中执行加密操作,该声明仅在没有错误的前提条件时才这样做:

    if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0 &&
        (err = ...) == 0 ) &&
        (err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) &&
        ...
        (err = ...) == 0 ) )
    {
         ... // Do some cryptographic operations here
    }
    ... // Free resources

方法2:使用错误累加器

这种方法基于这样一个事实,即几乎所有的语句都调用某个函数来设置err错误代码,并仅在该代码err为0 时才执行其余代码(即,执行的函数没有错误)。一个安全且易读的替代方法是:

bool ok = true;
ok =  ok && (err = ReadyHash(&SSLHashSHA1, &hashCtx))) == 0;
ok =  ok && (err = NextFunction(...)) == 0;
...
ok =  ok && (err = ...) == 0;
... // Free resources

在这里,没有一个转到:没有跳转到故障出口点的风险。而且在视觉上,很容易发现线条未对准或遗忘了ok &&

这种结构更紧凑。这是基于以下事实:在C中,&&仅当逻辑和()的第一部分为true时才对其进行评估。实际上,由优化编译器生成的汇编器几乎等效于使用gotos的原始代码:优化器能够很好地检测条件链并生成代码,该代码在第一个非null返回值时会跳到最后(在线证明)。

您甚至可以设想在功能结束时进行一致性检查,以在测试阶段识别出ok标志和错误代码之间的不匹配。

assert( (ok==false && err!=0) || (ok==true && err==0) );

在调试阶段很容易发现错误,如==0无意中替换为!=0或逻辑连接器错误。

如前所述:我不假装其他结构会避免任何错误。我只想说,他们本来可以使此错误更难以发生。


34
不,Apple goto失败错误是由一个聪明的程序员决定在if语句后不包括花括号引起的。:-)因此,当下一个维护程序员(或同一个,相同的差异)出现并添加了仅当if语句评估为true时才应该执行的另一行代码,该语句每次都运行。Bam,“转到失败”漏洞,这是一个主要的安全漏洞。但这实际上与使用goto语句无关。
克雷格

47
Apple的“ goto失败”错误直接由重复的一行代码引起。该错误的特殊影响是由于该行是goto一个不在括号内的if语句。但是,如果您建议避免goto使用代码,则可以避免代码受到随机重复行的影响,这对您来说是个坏消息。
immibis

12
您正在使用快捷布尔操作符行为来通过副作用执行一条语句,并声称它比一条if语句更清晰吗?娱乐时间。:)
Craig

38
如果不是“ goto失败”,而是有一条语句“ failed = true”,那么将会发生完全相同的事情。
gnasher729

15
该错误是由于开发者草率的不注意自己在做什么而没有阅读自己的代码更改而引起的。仅此而已。好吧,也许在那里也进行一些测试监督。
Lightness Races in Orbit

34

著名的Dijkstra文章是在某些编程语言实际上能够创建具有多个入口和出口点的子例程时编写的 换句话说,您实际上可以跳入函数的中间,然后跳出该函数内的任何位置,而无需实际调用该函数或以常规方式从该函数返回。汇编语言仍然如此。没有人认为这种方法优于我们现在使用的结构化软件编写方法。

在大多数现代编程语言中,功能都是通过一个入口和一个出口点来定义的。 入口点是您为函数指定参数并调用它的地方,出口点是您返回结果值并在原始函数调用之后的指令处继续执行的地方。

在该功能内,您应该能够在合理范围内做任何您想做的事情。如果在函数中添加一两个goto使其更清晰或提高速度,为什么不呢?函数的重点是隔离一些明确定义的功能,这样您就不必再考虑它在内部如何工作了。编写完成后,您就可以使用它。

是的,您可以在一个函数中包含多个return语句;返回的适当函数中始终仍然有一个位置(基本上是该函数的背面)。这与在goto有机会正确返回之前跳出goto函数完全不同。

在此处输入图片说明

因此,这实际上与使用goto无关。这是关于避免滥用它们。每个人都同意,您可以使用gotos编写出令人费解的程序,但是您也可以通过滥用功能来做到这一点(滥用gotos容易得多)。

值得一提的是,自从我从行号样式的BASIC程序毕业到使用Pascal和花括号语言的结构化编程以来,我再也不需要使用goto。我唯一一次尝试使用它的方法是从嵌套循环中进行早期退出(在不支持从循环中进行多级早期退出的语言中),但是我通常可以找到另一种更干净的方法。


2
评论不作进一步讨论;此对话已转移至聊天
maple_shaft

11

“ goto”语句导致什么样的错误?有历史上重要的例子吗?

我曾经在goto孩提时代编写BASIC程序时就使用语句作为获取和while循环的简单方法(Commodore 64 BASIC没有while循环,而且我还不太成熟,无法学习for循环的正确语法和用法)。我的代码经常很琐碎,但是任何循环错误都可以立即归因于我对goto的使用。

我现在主要使用Python,这是一种高级编程语言,它确定它不需要goto。

当1968年Edsger Dijkstra宣布“ Goto认为是有害的”时,他没有举几个例子,可以将相关的错误归咎于goto,相反,他宣称对于高级语言来说goto不必要的,应该避免使用它,而应该使用什么语言。今天我们考虑正常的控制流程:循环和条件。他的话:

毫不费力地使用go to有一个直接的后果,那就是很难找到有意义的坐标来描述过程的进展。
[...]
目前的go语句太原始了;太多地把程序弄得一团糟。

每次他goto在其中调试代码时,他可能都有大量的错误示例。但是他的论文是一个概括的立场声明,goto并辅以高级语言不需要的证明。普遍存在的错误是您可能无法静态分析所讨论的代码。

goto语句静态分析代码要困难得多,特别是如果您跳回到控制流(我曾经做过)或代码的某些不相关部分。代码可以并且是通过这种方式编写的。这样做是为了对非常稀缺的计算资源以及机器的体系结构进行高度优化。

伪经的例子

有一个二十一点程序,维护人员发现该程序经过了非常优雅的优化,但是由于代码的性质,无法对其进行“修复”。它是用很大程度上依赖于gotos的机器代码编程的-所以我认为这个故事很有意义。这是我能想到的最好的规范示例。

反例

但是,CPython的C源代码(最常见的Python参考实现)使​​用goto语句产生了很大的效果。它们用于绕过函数内部无关的控制流以到达函数末尾,从而使函数更有效而又不会损失可读性。这符合结构化编程的理想之一-为功能提供单个出口点。

作为记录,我发现这个反例非常容易静态分析。


5
我遇到的“梅尔故事”是在一个奇怪的体系结构上进行的:(1)每条机器语言指令在其操作后都会执行跳转,并且(2)将指令序列放置在连续的存储单元中效率极低。而且该程序是通过手动优化的程序集编写的,而不是经过编译的语言。

@MilesRout我认为您在这里可能是正确的,但是有关结构化编程的Wikipedia页面说:“在许多语言中,最常见的偏差是使用return语句使子程序提前退出。这会导致多个退出点,而不是结构化编程所需的单个退出点。” -您是在说错吗?您有资料来源吗?
亚伦·霍尔

@AaronHall这不是本来的意思。
Miles Rout

11

棚屋是当前的建筑艺术时,他们的建造者无疑可以为您提供有关棚屋的构造,如何使烟气逸出等实用建议。幸运的是,今天的建筑商可能会忘记大多数建议。

驿马车是当前的运输艺术时,他们的司机无疑会为您提供关于驿马车的马匹,如何防御高速公路工等的切实可行的建议。幸运的是,今天的驾驶者也可以忘记大部分建议。

打孔卡是当前的编程艺术时,他们的从业人员同样可以为您提供关于卡的组织,如何对语句进行编号等实用的实用建议。我不确定今天的建议是否有用。

您是否足够老,甚至不知道短语“ to number statement”?您不需要知道它,但是如果您不知道,那么您就不会对警告的goto主要含义所在的历史背景有所了解。

今天的警告goto并不十分重要。随着而基本训练/ for循环和函数调用,你甚至不会想到发出goto非常频繁。当您想到它时,您可能有一个原因,所以继续吧。

但是可以goto不滥用该声明吗?

答:可以,但是可以将其滥用,但是与更常见的错误(例如使用将用作常量的变量或剪切和粘贴编程)相比,它的滥用在软件工程中是一个很小的问题。被称为忽视重构)。我怀疑你有很大的危险。除非您正在使用longjmp或以其他方式将控制权转移到遥远的代码,否则,如果您认为使用a goto或您只是想尝试乐趣,请继续。你会没事的。

您可能会注意到缺少最近在恐怖故事goto扮演反派。这些故事中的大多数或全部似乎都存在30或40年。如果您认为这些故事大多已过时,则可以站在坚实的基础上。


1
我发现我至少吸引了一位选民。好吧,这是意料之中的。可能会有更多的投票表决。但是,当数十年的编程经验告诉我,在这种情况下,常规答案碰巧是错误的,因此我不会给出常规答案无论如何,这是我的看法。我已经说明了原因。读者可以决定。
2016年

1
您的帖子太好了,太糟糕了,我无法拒绝评论。
Pieter B

7

要在其他出色的答案中添加一件事,使用goto语句可能很难准确地告诉您如何到达程序中的任何给定位置。您可以知道某个特定行发生了异常,但是如果goto代码中没有,则无法在不搜索整个程序的情况下就知道执行了哪些语句导致此异常导致状态。没有调用堆栈,也没有视觉流程。可能有一条语句在1000行之外,这使您处于错误状态goto,对引发异常的行执行了a 。


1
Visual Basic 6和VBA具有痛苦的错误处理,但是,如果使用On Error Goto errh,则可以使用Resume重试,Resume Next跳过有问题的行并Erl知道它是哪一行(如果使用行号)。
Cees Timmerman

@CeesTimmerman我在Visual Basic方面做得很少,我不知道。我将对此添加评论。
qfwfq 16/10/24

2
@CeesTimmerman:如果子例程中没有自己的错误错误块,则发生错误时,“ resume”的VB6语义将令人恐惧。 Resume将从头开始重新执行该功能,并Resume Next跳过该功能中尚未执行的所有操作。
supercat

2
@supercat就像我说的那样,很痛苦。如果您需要知道确切的行,则应该将它们全部编号,或者暂时禁用错误处理On Error Goto 0并手动调试。Resume是指在检查Err.Number并进行必要的调整后使用。
Cees Timmerman

1
我应该注意,在现代语言中,goto仅适用于带有标记的行,而这些行似乎已被带有标记的循环所取代。
Cees Timmerman

7

与其他形式的流量控制相比,goto对于人类而言更难推理。

编写正确的代码很困难。编写正确的程序很困难,确定程序是否正确很难,证明程序正确是困难的。

与编程有关的所有其他方面相比,让代码模糊地执行您想要的事情是容易的。

goto通过获取代码来完成某些程序,以完成您想要的工作。它不能帮助使检查正确性变得容易,而它的替代方法通常可以。

有一些编程风格,其中goto是合适的解决方案。甚至在编程风格中,邪恶的孪生子都是合适的解决方案。在这两种情况下,都必须格外小心,以确保您以可理解的模式使用它,并且要进行大量的手动检查,以确保自己没有做任何事情来确保其正确性。

例如,某些语言有一个称为协程的功能。协程是无线程线程;执行状态,没有线程可对其运行。您可以要求他们执行,他们可以运行自己的一部分,然后中止自己,交还流量控制权。

不使用协程支持的语言(例如C ++ pre-C ++ 20和C)中的“浅”协程可以通过混合使用gotos和手动状态管理来实现。可以使用C 的setjmplongjmp功能来完成“深层”协程。

在某些情况下,协程非常有用,以至于手工和仔细地编写它们是值得的。

对于C ++,人们发现它们足够有用,以至于扩展了语言来支持它们。的gotoS和手动状态管理正被隐藏在零成本的抽象层,从而允许程序员将它们写入,而不具有证明自己的跳转,乱七八糟的难度void**S,人工构造/状态的破坏等是正确的。

goto隐藏在更高层次的抽象(例如whileor forifor)后面switch。这些更高级别的抽象更容易证明正确和检查。

如果缺少某种语言(例如某些现代语言缺少协程),则使用不适合该问题的模式进行修饰或使用goto成为您的替代选择。

在通常情况下,让计算机隐约地执行您想要的操作很容易。可靠地编写健壮的代码很难。Goto对第一个的帮助远大于对第二个的帮助。因此,“ goto认为是有害的”,因为这是表面上“工作”的代码的征兆,其中包含难以发现的错误。通过足够的努力,您仍然可以使带有“ goto”的代码可靠地变得健壮,因此,坚持“绝对”规则是错误的。但是根据经验,这是一个很好的选择。


1
longjmp仅在C中合法,可将堆栈退回到setjmp父函数中的a。(就像突破了多层嵌套一样,但是对于函数调用而不是循环)。 longjjmp从此setjmp函数返回的上下文中返回上下文是不合法的(而且我怀疑在大多数情况下在实践中不会起作用)。我不熟悉协同例程,但是从您对类似于用户空间线程的协同例程的描述中,我认为它将需要自己的堆栈以及保存的注册上下文,并且longjmp只为您提供注册-节省,而不是单独的堆栈。
彼得·科德斯

1
哦,我应该刚刚用Google搜索过:fanf.livejournal.com/105413.html描述了为协程运行提供堆栈空间。(我怀疑这是仅使用setjmp / longjmp的必要条件)。
彼得·科德斯

2
@PeterCordes:不需要实现提供任何方法来使代码可以创建适合用作协程的对jmp_buff。某些实现确实指定了一种方法,即使标准不要求代码提供这种功能,代码也可以通过这种方式来创建代码。我不会描述依赖于诸如“标准C”之类的技术的代码,因为在许多情况下jmp_buff将是不透明的结构,不能保证在不同的编译器之间表现一致。
超级猫

6

http://www-personal.umich.edu/~axe/research/Software/CC/CC2/TourExec1.1.f.html看一下这段代码,它实际上是大型囚徒困境模拟的一部分。如果您看过旧的FORTRAN或BASIC代码,您会意识到它并不稀奇。

C  Not nice rules in second round of tour (cut and pasted 7/15/93)
   FUNCTION K75R(J,M,K,L,R,JA)
C  BY P D HARRINGTON
C  TYPED BY JM 3/20/79
   DIMENSION HIST(4,2),ROW(4),COL(2),ID(2)
   K75R=JA       ! Added 7/32/93 to report own old value
   IF (M .EQ. 2) GOTO 25
   IF (M .GT. 1) GOTO 10
   DO 5 IA = 1,4
     DO 5 IB = 1,2
5  HIST(IA,IB) = 0

   IBURN = 0
   ID(1) = 0
   ID(2) = 0
   IDEF = 0
   ITWIN = 0
   ISTRNG = 0
   ICOOP = 0
   ITRY = 0
   IRDCHK = 0
   IRAND = 0
   IPARTY = 1
   IND = 0
   MY = 0
   INDEF = 5
   IOPP = 0
   PROB = .2
   K75R = 0
   RETURN

10 IF (IRAND .EQ. 1) GOTO 70
   IOPP = IOPP + J
   HIST(IND,J+1) = HIST(IND,J+1) + 1
   IF (M .EQ. 15 .OR. MOD(M,15) .NE. 0 .OR. IRAND .EQ. 2) GOTO 25
   IF (HIST(1,1) / (M - 2) .GE. .8) GOTO 25
   IF (IOPP * 4 .LT. M - 2 .OR. IOPP * 4 .GT. 3 * M - 6) GOTO 25
   DO 12 IA = 1,4
12 ROW(IA) = HIST(IA,1) + HIST(IA,2)

   DO 14 IB = 1,2
     SUM = .0
     DO 13 IA = 1,4
13   SUM = SUM + HIST(IA,IB)
14 COL(IB) = SUM

   SUM = .0
   DO 16 IA = 1,4
     DO 16 IB = 1,2
       EX = ROW(IA) * COL(IB) / (M - 2)
       IF (EX .LE. 1.) GOTO 16
       SUM = SUM + ((HIST(IA,IB) - EX) ** 2) / EX
16 CONTINUE

   IF (SUM .GT. 3) GOTO 25
   IRAND = 1
   K75R = 1
   RETURN

25 IF (ITRY .EQ. 1 .AND. J .EQ. 1) IBURN = 1
   IF (M .LE. 37 .AND. J .EQ. 0) ITWIN = ITWIN + 1
   IF (M .EQ. 38 .AND. J .EQ. 1) ITWIN = ITWIN + 1
   IF (M .GE. 39 .AND. ITWIN .EQ. 37 .AND. J .EQ. 1) ITWIN = 0
   IF (ITWIN .EQ. 37) GOTO 80
   IDEF = IDEF * J + J
   IF (IDEF .GE. 20) GOTO 90
   IPARTY = 3 - IPARTY
   ID(IPARTY) = ID(IPARTY) * J + J
   IF (ID(IPARTY) .GE. INDEF) GOTO 78
   IF (ICOOP .GE. 1) GOTO 80
   IF (M .LT. 37 .OR. IBURN .EQ. 1) GOTO 34
   IF (M .EQ. 37) GOTO 32
   IF (R .GT. PROB) GOTO 34
32 ITRY = 2
   ICOOP = 2
   PROB = PROB + .05
   GOTO 92

34 IF (J .EQ. 0) GOTO 80
   GOTO 90

70 IRDCHK = IRDCHK + J * 4 - 3
   IF (IRDCHK .GE. 11) GOTO 75
   K75R = 1
   RETURN

75 IRAND = 2
   ICOOP = 2
   K75R = 0
   RETURN

78 ID(IPARTY) = 0
   ISTRNG = ISTRNG + 1
   IF (ISTRNG .EQ. 8) INDEF = 3
80 K75R = 0
   ITRY = ITRY - 1
   ICOOP = ICOOP - 1
   GOTO 95

90 ID(IPARTY) = ID(IPARTY) + 1
92 K75R = 1
95 IND = 2 * MY + J + 1
   MY = K75R
   RETURN
   END

这里有很多问题远远超出了这里的GOTO声明。老实说,我认为GOTO声明有点替罪羊。但是,这里的控制流程绝对不清楚,并且代码混合在一起的方式使其非常不清楚正在发生的事情。即使不添加注释或使用更好的变量名,将其更改为没有GOTO的块结构也将使其更易于阅读和遵循。


4
如此真实:-)可能值得一提:传统的FORTAN和BASIC没有块结构,因此,如果THEN子句不能放在一行上,则GOTO是唯一的选择。
Christophe

用文本标签代替数字,或者至少在数字行上添加注释,将大有帮助。
Cees Timmerman

回到我的FORTRAN-IV时代:我限制使用GOTO来实现IF / ELSE IF / ELSE块,WHILE循环以及BREAK和NEXT循环。有人向我展示了意大利面条代码的弊端,以及为什么即使必须使用GOTO实现块结构,也应该构造避免这种结构的代码。后来我开始使用预处理器(RATFOR,IIRC)。Goto本质上不是邪恶的,它只是映射到汇编器分支指令的功能强大的低级构造。如果由于语言不足而必须使用它,请使用它来构建或扩充适当的块结构。
nigel222

@CeesTimmerman:那是各种花园的FORTRAN IV代码。FORTRAN IV不支持文本标签。我不知道该语言的最新版本是否支持它们。标准FORTRAN IV不支持与代码在同一行上的注释,尽管可能存在特定于供应商的扩展来支持它们。我不知道“当前”的FORTRAN标准怎么说:很久以前我放弃了FORTRAN,但我不会错过它。(我见过的最新FORTRAN代码与1970年代早期的PASCAL非常相似。)
John R. Strohm,2016年

1
@CeesTimmerman:特定于供应商的扩展名。总体而言,该代码对注释真的很害羞。另外,有一种约定在某些地方变得很普遍,即仅将行号放在CONTINUE和FORMAT语句上,以使以后添加行更加容易。此代码不遵循该约定。
约翰·斯特罗姆

0

可维护编程的原理之一是封装。关键是您可以使用已定义的接口(仅此接口)与模块/例程/子例程/组件/对象进行接口,并且结果将是可预测的(假设已对单元进行了有效测试)。

在单个代码单元内,适用相同的原理。如果您一贯地应用结构化编程或面向对象的编程原理,则不会:

  1. 通过代码创建意外的路径
  2. 到达代码段中包含未定义或不允许的变量值
  3. 退出带有未定义或不允许的变量值的代码路径
  4. 无法完成交易单元
  5. 在内存中保留实际上无效的部分代码或数据,但不适合进行垃圾清理和重新分配,因为您没有使用显式构造来发出信号释放的信号,该构造在代码控制路径退出单元时调用释放它们

这些处理错误的一些较常见的症状包括内存泄漏,内存ho积,指针溢出,崩溃,数据记录不完整,记录添加/更改/删除异常,内存页面错误等。

这些问题的用户可观察到的表现包括用户界面锁定,性能逐渐降低,数据记录不完整,无法启动或完成事务,数据损坏,网络中断,断电,嵌入式系统故障(由于失去了对导弹的控制)导致空中交通管制系统的跟踪和控制能力丧失,计时器故障等)。目录非常广泛。


3
您回答goto甚至不提一次。:)
罗伯特·哈维

0

goto很危险,因为它通常在不需要的地方使用。使用不需要什么是危险的,但转到更是如此。如果您使用google,会发现很多由goto引起的错误,这并不是不使用它的原因(错误通常在您使用语言功能时发生,因为这在编程中是固有的),但是其中一些显然是高度相关的要转到使用。


使用/不使用goto的原因:

  • 如果需要循环,则必须使用whilefor

  • 如果需要进行条件跳转,请使用 if/then/else

  • 如果需要过程,请调用函数/方法。

  • 如果您需要退出功能,只需return

我可以指望看过的地方goto和正确使用的地方。

  • CPython的
  • libKTX
  • 可能还有几个

在libKTX中,有一个具有以下代码的函数

if(something)
    goto cleanup;

if(bla)
    goto cleanup;

cleanup:
    delete [] array;

现在在这里goto很有用,因为语言是C:

  • 我们在一个函数内
  • 我们无法编写清除函数(因为我们进入了另一个作用域,并且使可访问的调用者函数状态更加繁重)

这个用例很有用,因为在C中我们没有类,因此最简单的清除方法是使用goto

如果我们在C ++中具有相同的代码,则不再需要goto:

class MyClass{
    public:

    void Function(){
        if(something)
            return Cleanup(); // invalid syntax in C#, but valid in C++
        if(bla)
            return Cleanup(); // invalid syntax in C#, but valid in C++
    }

    // access same members, no need to pass state (compiler do it for us).
    void Cleanup(){

    }



}

它会导致什么样的错误?随便 无限循环,执行顺序错误,堆栈螺丝。

一个有案可查的案例是SSL中的一个漏洞,该漏洞使Man可以通过错误使用goto进行中间攻击:这是本文

这是一个错字错误,但有一段时间没有引起注意,如果代码以其他方式构造,则无法正确测试该错误。


2
一个答案有很多注释,这些注释非常清楚地说明了“ goto”的使用完全与SSL错误无关-该错误是由于不小心重复一行代码而导致的,因此它曾经有条件地执行,而无条件地执行了一次。
gnasher729

是的,在我接受之前,我正在等待一个更好的goto错误历史示例。如前所述 那行代码到底什么都没关系;缺少花括号将导致执行该错误并导致错误。不过我很好奇。什么是“堆叠螺丝”?
Akiva

1
我以前在PL / 1 CICS中工作的代码示例。程序弹出一个由单线图组成的屏幕。当屏幕返回时,它找到了已发送的地图数组。使用该元素在一个数组中查找索引,然后将该索引用于数组变量的索引以执行GOTO操作,以便可以处理该单个映射。如果发送的地图数组不正确(或已损坏),则它可能会尝试对错误的标签执行GOTO操作,否则可能会崩溃,因为它在标签变量数组的末尾访问了元素。重写为用例类型语法。
Kickstart's
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.