如何在C ++中避免使用GOTO

时间:2013-08-03 16:36:29

标签: c++ c goto basic

我读过GOTO不好,但我该如何避免呢?我不知道如何在没有GOTO的情况下进行编程。在BASIC中,我使用GOTO作为一切。我应该在C和C ++中使用什么?

我在BASIC中使用了GOTO,如下所示:

MainLoop:
INPUT string$   
IF string$ = "game" THEN   
GOTO game
ENDIF

9 个答案:

答案 0 :(得分:11)

考虑下面的C ++代码:

void broken()
{
    int i = rand() % 10;
    if (i == 0) // 1 in 10 chance.
        goto iHaveABadFeelingAboutThis;

    std::string cake = "a lie";

    // ...
    // lots of code that prepares the cake
    // ...

iHaveABadFeelingAboutThis:
    // 1 time out of ten, the cake really is a lie.
    eat(cake);

    // maybe this is where "iHaveABadFeelingAboutThis" was supposed to be?
    std::cout << "Thank you for calling" << std::endl;
}

最终,“goto”与C ++的其他流控制关键字没有太大区别:“break”,“continue”,“throw”等;从功能上讲,它引入了一些与范围相关的问题,如上所示。

依靠goto会教你生成难以阅读,难以调试和难以维护的代码的坏习惯,并且通常会导致错误。为什么?因为goto是以最糟糕的方式自由形式的,它允许你绕过语言中内置的结构控件,例如范围规则等。

很少有替代方案特别直观,其中一些可以说与“goto”一样含糊不清,但至少你是在语言的结构中运作 - 再回到上面的例子,做什么要困难得多我们在上面的示例中做了除goto之外的任何事情(当然,使用指针时,你仍然可以使用for / while / throw射击自己。)

您可以选择避免它并使用语言的自然流控制结构来保持代码的可读性和可维护性:

  • 将代码分解为子程序。

不要害怕小的,离散的,有名的函数,只要你不是永远拖着大量的参数列表(如果你是,那么你可能想看看用类封装)。

许多新手使用“goto”,因为他们编写了可笑的长函数,然后发现他们想要从3000行函数的第2行到第2998行。在上面的代码中,goto创建的bug更难创建如果将函数拆分为两个有效负载,逻辑和功能。

void haveCake() {
    std::string cake = "a lie";

    // ...
    // lots of code that prepares the cake
    // ...

    eat(cake);
}

void foo() {
    int i = rand() % 10;
    if (i != 0) // 9 times out of 10
        haveCake();
    std::cout << "Thanks for calling" << std::endl;
}   

有些人将此称为“吊装”(我将所有需要将'cake'作为范围提升到haveCake函数中的东西)。

  • 一次性循环。

对于程序员来说,这些并不总是显而易见的,它说它是for / while / do循环,但它实际上只打算运行一次。

for ( ; ; ) { // 1-shot for loop.
    int i = rand() % 10;
    if (i == 0) // 1 time in 10
         break;
    std::string cake = "a lie";
    // << all the cakey goodness.

    // And here's the weakness of this approach.
    // If you don't "break" you may create an infinite loop.
    break;
}

std::cout << "Thanks for calling" << std::endl;
  • 例外。

这些可能非常强大,但它们也可能需要大量的锅炉板。另外,您可以抛出异常以进一步备份调用堆栈或根本不执行(并退出程序)。

struct OutOfLuck {};

try {
    int i = rand() % 10;
    if (i == 0)
        throw OutOfLuck();
    std::string cake = "a lie";
    // << did you know: cake contains no fat, sugar, salt, calories or chemicals?
    if (cake.size() < MIN_CAKE)
        throw CakeError("WTF is this? I asked for cake, not muffin");
}
catch (OutOfLuck&) {} // we don't catch CakeError, that's Someone Else's Problem(TM).

std::cout << "Thanks for calling" << std::endl;

在形式上,你应该尝试从std::exception派生你的异常,但我有时会偏向于抛出const char *字符串,枚举和偶尔结构化Rock。

try {
    if (creamyGoodness.index() < 11)
        throw "Well, heck, we ran out of cream.";
} catch (const char* wkoft /*what kind of fail today*/) {
    std::cout << "CAKE FAIL: " << wkoft << std::endl;
    throw std::runtime_error(wkoft);
}

这里最大的问题是异常是为了处理错误,就像上面两个例子中的第二个一样。

答案 1 :(得分:6)

使用goto有几个原因,主要是:条件执行,循环和“退出”例程。

条件执行通常由if / else管理,而且应该足够了

循环由forwhiledo while管理;并由continuebreak

进一步加强

最困难的是“退出”例程,在C ++中,但它被析构函数的确定性执行所取代。因此,为了让您在退出函数时调用例程,您只需创建一个对象,该对象将在其析构函数中执行您需要的操作:直接的优点是您在添加一个return时不会忘记执行操作即使存在例外,它也会工作。

答案 2 :(得分:3)

通常像forwhiledo while这样的循环和函数或多或少地处理了使用GOTO的需要。了解如何使用这些,并在几个例子后,你将不再考虑goto。  :)

答案 3 :(得分:3)

Edsger Dijkstra发表了一封名为Go To Statement Considered Harmful的着名信件。你应该读一读,他主张structured programming。该维基百科文章描述了您需要了解的有关结构化编程的内容。你可以用goto编写结构化程序,但现在这不是一个流行的观点,因为这个视角读了Donald Knuth的Structured Programming with goto Statements

答案 4 :(得分:1)

goto本质上并不坏,它有其用途,就像任何其他语言功能一样。您可以完全避免使用gotoexceptionstry/catch和循环以及相应的if/else构造。

然而,如果你意识到自己极度不受欢迎,只是为了避免它,那么使用它可能会更好。

我个人使用goto实现具有单个入口和出口点的函数,这使得代码更具可读性。这是我仍然发现goto有用的唯一的东西,实际上改善了代码的结构和可读性。

举个例子:

int foo()
{
    int fd1 = -1;
    int fd2 = -1;
    int fd3 = -1;

    fd1 = open();
    if(fd1 == -1)
        goto Quit:

    fd2 = open();
    if(fd2 == -1)
        goto Quit:

    fd3 = open();
    if(fd3 == -1)
        goto Quit:

     ... do your stuff here ...

Quit:
   if(fd1 != -1)
      closefile();

   if(fd2 != -1)
      closefile();

   if(fd3 != -1)
      closefile();
}

在C ++中,您会发现,如果您正确实现封装对资源的访问的类,则可能会大大减少对此类结构的需求。例如,使用smartpointer就是这样一个例子。

在上面的示例中,您将在C ++中实现/使用文件类,这样,当它被破坏时,文件句柄也会被关闭。使用类还有一个优点,它可以在抛出异常时起作用,因为编译器可以确保所有对象都被正确地破坏。所以在C ++中你绝对应该使用带有析构函数的类来实现这一点。

当你想用C编码时,你应该考虑额外的块也会增加代码的复杂性,这反过来会使代码更难理解和控制。我希望随时都可以在一系列人工if / else子句中放置一个好的goto来避免它。如果您以后必须重新访问代码,您仍然可以理解它,而无需遵循所有额外的块。

答案 5 :(得分:1)

goto现已被其他编程结构取代,例如forwhiledo-while等,这些结构更易于阅读。但是goto仍有它的用途。我在函数中的不同代码块(例如,涉及不同的条件检查)具有单个退出点的情况下使用它。除了这个用于其他任何事情之外,你应该使用适当的编程结构。

答案 6 :(得分:0)

BASIC最初是interpreted language。它没有结构,因此它依赖于GOTO来跳转特定的行,就像你跳转汇编一样。通过这种方式,程序流很难遵循,使调试更加复杂。

Pascal,C和所有现代高级编程语言,包括Visual Basic(基于BASIC)都是strongly structured,其中“命令”分组为块。例如,VB有Do... LoopWhile... End WhileFor...Next。甚至像Microsoft QuickBASIC一样old derivatives of BASIC support structures

DECLARE SUB PrintSomeStars (StarCount!)
REM QuickBASIC example
INPUT "What is your name: ", UserName$
PRINT "Hello "; UserName$
DO
   INPUT "How many stars do you want: ", NumStars
   CALL PrintSomeStars(NumStars)
   DO
      INPUT "Do you want more stars? ", Answer$
   LOOP UNTIL Answer$ <> ""
   Answer$ = LEFT$(Answer$, 1)
LOOP WHILE UCASE$(Answer$) = "Y"
PRINT "Goodbye "; UserName$
END

SUB PrintSomeStars (StarCount)
   REM This procedure uses a local variable called Stars$
   Stars$ = STRING$(StarCount, "*")
   PRINT Stars$
END SUB

Visual Basic .NET中的另一个示例

Public Module StarsProgram
   Private Function Ask(prompt As String) As String
      Console.Write(prompt)
      Return Console.ReadLine()
   End Function

   Public Sub Main()
      Dim userName = Ask("What is your name: ")
      Console.WriteLine("Hello {0}", userName)

      Dim answer As String

      Do
         Dim numStars = CInt(Ask("How many stars do you want: "))
         Dim stars As New String("*"c, numStars)
         Console.WriteLine(stars)

         Do
            answer = Ask("Do you want more stars? ")
         Loop Until answer <> ""
      Loop While answer.StartsWith("Y", StringComparison.OrdinalIgnoreCase)

      Console.WriteLine("Goodbye {0}", userName)
   End Sub
End Module

类似的东西将在C ++中使用,例如ifthenfordowhile ......它们共同定义了程序流程。您无需使用goto跳转到下一个语句。在特定情况下,如果它使控制流更清晰,您仍然可以使用goto,但通常不需要它

答案 7 :(得分:0)

也许不是

if(something happens) 
   goto err;

err:
   print_log()

可以使用:

do {

    if (something happens)
    {
       seterrbool = true;       
       break;  // You can avoid using using go to I believe
    } 

} while (false) //loop will work only one anyways
if (seterrbool)
   printlog();

它可能看起来不友好,因为在上面的示例中只有一个goto但如果有很多“goto”则会更具可读性。

答案 8 :(得分:0)

上面这个函数的实现避免了使用goto。注意,这不包含循环。编译器将对此进行优化。我更喜欢这种实现。

使用'break'和'continue',goto语句可以(几乎?)始终避免。

int foo()
{
    int fd1 = -1;
    int fd2 = -1;
    int fd3 = -1;

    do
    {
        fd1 = open();
        if(fd1 == -1)
            break;

        fd2 = open();
        if(fd2 == -1)
            break:

        fd3 = open();
        if(fd3 == -1)
            break;

         ... do your stuff here ...
    }
    while (false);

   if(fd1 != -1)
      closefile();

   if(fd2 != -1)
      closefile();

   if(fd3 != -1)
      closefile();
}