任何现代编程语言中的存在goto
很大程度上都是退化的,有点像人类的附录。它的功能已被条件和循环控制结构(if-then-else、for/while/loops、switch/case 语句等)取代。它存在于 C 和 C++ 等语言中,因为在某些极端情况下它仍然非常有用,例如打破深度嵌套的循环:
for (...)
{
for (...)
{
for(...)
{
...
// hit a fatal error, need to break out to outermost scope
goto whoopsiedoodle;
}
}
}
whoopsiedoodle:
...
但是,不鼓励使用它的原因有很多:因为它可以在函数中分支任意方向,它会破坏通过简单检查来调试代码的能力。例如,给定以下代码段:
i = 1;
label: printf("i = %d\n", i);
打印什么值i
?该打印语句将执行多少次?除非您考虑到 的每个实例goto label;
,否则您无法知道。现在,想象一下代码的结构如下:
i = 0;
goto label;
foo: ...
...
i = 1;
label: printf("i = %d\n", i);
...
goto foo;
...
现在,想象一下上面片段中的每一个都有几十(甚至几百)行代码...
。还可以想象散布在各处的 10 或 11 个其他标签,相关的goto
陈述大部分是随机分布的。这是根据我在职业生涯早期遇到的一些真实代码建模的。像这样调试代码的唯一方法是逐行跟踪执行,goto
并在整个过程中考虑到每一个。这段代码一开始就写得很糟糕(一个单一的、5000 行的main
函数,实际上有数百个单独的变量,一些在文件范围内声明,一些是本地的main
,以及其他暴行),但是使用goto
是一个力量放大器,它把糟糕的代码变成了无法维护的污泥。这段代码定义了“脆”;我们真的无法在不破坏其他内容的情况下更改其中的一行。
过度使用goto
也会阻碍编译器优化代码的能力。现代编译器非常聪明,可以利用结构化编程技术进行有效的分支预测或展开循环以获得真正的性能提升。编译器几乎不可能优化上述代码。上面的代码片段模仿的是真实世界的代码吗?我们尝试在打开优化标志的情况下编译它(gcc -O1)。编译器吃掉了所有可用的 RAM,然后吃掉了所有可用的交换空间,导致内核恐慌。
我们告诉客户,他们要么需要购买更快的硬件,要么允许我们从头开始重写整个事情。他们最终购买了更快的硬件。
goto
只要您遵守以下规则,就可以有效地使用:
- 只向前分支。
- 永远不要分支到控制结构的主体中(即不要绕过
if
、for
、while
或switch
条件)。
- 避免在大段代码上进行分支。