8-7 goto 语句

接下来要介绍的控制流语句是无条件跳转。无条件跳转会使程序执行跳转到代码中的另一个位置。“无条件”意味着跳转总是发生(不同于if语句或switch语句,它们的跳转仅在表达式结果满足条件时才发生)。

在C++中,无条件跳转通过goto语句goto statement实现,跳转目标通过语句标签statement label标识。与switch case标签类似,语句标签通常不缩进。

以下是goto语句与语句标签的示例:

#include <iostream>
#include <cmath> // for sqrt() function

int main()
{
    double x{};
tryAgain: // this is a statement label
    std::cout << "Enter a non-negative number: ";
    std::cin >> x;

    if (x < 0.0)
        goto tryAgain; // this is the goto statement

    std::cout << "The square root of " << x << " is " << std::sqrt(x) << '\n';
    return 0;
}

在此程序中,系统要求用户输入一个非负数。但若输入负数,程序将通过goto语句跳转至tryAgain标签处,再次要求用户输入新数值。通过这种方式,程序可持续请求用户输入直至获得有效数值。

以下是该程序的运行示例:

image


语句标签具有函数作用域

在关于对象作用域的章节(第7章)中,我们介绍了两种作用域:局部(块)作用域和文件(全局)作用域。语句标签则采用第三种作用域:函数作用域function scope,这意味着标签在函数中始终可见,甚至在其声明点之前即可被识别。goto语句及其对应的语句标签必须出现在同一个函数中。

虽然上例展示了回退跳转jumps backwards(跳至函数前部)的goto语句,但goto语句同样支持前向跳转jump forward

#include <iostream>

void printCats(bool skip)
{
    if (skip)
        goto end; // jump forward; statement label 'end' is visible here due to it having function scope

    std::cout << "cats\n";
end:
    ; // statement labels must be associated with a statement
}

int main()
{
    printCats(true);  // jumps over the print statement and doesn't print anything
    printCats(false); // prints "cats"

    return 0;
}

输出

image

除了向前跳转之外,上述程序中还有几个值得一提的有趣之处。

首先,请注意语句标签必须与语句相关联(因此得名:它们标记语句)。由于函数末尾没有语句,我们不得不使用空语句来创建可标记的对象。其次,尽管尚未声明 end 标签,我们仍能跳转至该标签标记的语句,这是因为语句标签具有函数作用域。语句标签无需前置声明。第三,需特别指出上述程序存在编程缺陷——相较于使用goto跳过打印语句,采用if语句跳过会更优雅。

跳转操作存在两大核心限制:只能在单个函数内部跳转(不可跨函数跳转),且向前跳转时,若目标位置仍处于作用域内的变量初始化阶段,则无法跳过该变量初始化。例如:

int main()
{
    goto skip;   // error: this jump is illegal because...
    int x { 5 }; // this initialized variable is still in scope at statement label 'skip'
skip:
    x += 3;      // what would this even evaluate to if x wasn't initialized?
    return 0;
}

image

请注意,你可以跳过变量初始化语句,当该初始化语句被执行时,该变量将被重新初始化。


避免使用goto

在C++(以及其他现代高级语言)中,goto语句的使用受到排斥。著名计算机科学家Edsger W. Dijkstra在其题为《goto语句有害论》的著名但艰深难懂的论文中阐述了避免使用goto的理由。goto的主要问题在于它允许程序员在代码中随意跳转。这会形成俗称的“意大利面代码”——其执行路径如同缠绕扭曲的意大利面,导致代码逻辑极难追踪。

迪杰斯特拉曾半开玩笑地指出:“程序员的质量与其代码中goto语句的密度呈反比关系”。

几乎所有使用goto语句编写的代码,都能通过C++的其他结构(如if语句和循环)更清晰地重构。唯一值得注意的例外是需要退出嵌套循环但不必退出整个函数的情况——此时跳转到循环刚结束的位置可能是最简洁的解决方案。

对于进阶读者

以下是一个刻意设计的示例,展示如何使用goto语句在不退出函数的情况下跳出嵌套循环:

#include <iostream>

int main()
{
    for (int i = 1; i < 5; ++i)
    {
        for (int j = 1; j < 5; ++j)
        {
            std::cout << i << " * " << j << " is " << i*j << '\n';

            // If the product is divisible by 9, jump to the "end" label
            if (i*j % 9 == 0)
            {
                std::cout << "Found product divisible by 9.  Ending early.\n";
                goto end;
            }
        }

        std::cout << "Incrementing the first factor.\n";
    }

end:
    std::cout << "And we're done." << '\n';

    return 0;
}

作者注
来自我们的朋友xkcd
image

最佳实践
避免使用goto语句(除非替代方案会显著降低代码可读性)。


翻译问题(不权威,仅起标识指代作用)

空间角度 时间角度
Backward jump 向上跳转 回退跳转(回跳)
Forward jump 向下跳转 前向跳转(前跳)
代码文件(从上到下 = 从前往后):

─────────────────────────────────────

前跳图示(跳过代码):
    goto end;  ───────────┐
    [被跳过的代码]          │
end:  ◄───────────────────┘

回跳图示(循环结构):
start:  ◄──────────────────┐
    [代码]                  │
    goto start;  ──────────┘
posted @ 2026-02-27 08:14  游翔  阅读(3)  评论(0)    收藏  举报