我一直有一个印象,如果可能的话,永远不要使用goto。

然而,在前几天阅读libavcodec(它是用C编写的)时,我惊讶地注意到它的多种用法。

在支持循环和函数的语言中使用goto是否有优势?如果有,为什么?请提供一个具体的例子,清楚地说明使用goto的理由。


当前回答

这些年来,我写了不少汇编语言。最终,每一种高级语言都被编译成gotos。好吧,叫它们“分支”或“跳跃”或其他什么,但它们是gotos。有人能写无goto汇编器吗?

当然,你可以向Fortran、C或BASIC程序员指出,gotos的泛滥就像意大利肉酱面一样。然而,答案不是避免它们,而是小心地使用它们。

刀可以用来准备食物,解救某人,或者杀死某人。我们会因为害怕后者而没有刀吗?同样,“后向”:不小心使用它会碍事,小心使用它会有所帮助。

其他回答

goto不好的一个原因是,除了编码风格之外,你可以用它来创建重叠但非嵌套的循环:

loop1:
  a
loop2:
  b
  if(cond1) goto loop1
  c
  if(cond2) goto loop2

这将创建一个奇怪的,但可能是合法的流控制结构,其中可能有(a, b, c, b, a, b, a, b, b,…)这样的序列,这让编译器黑客不高兴。显然,有许多聪明的优化技巧依赖于这种类型的结构不发生。(我应该检查一下我的龙书……)这样做的结果(使用一些编译器)可能是对包含gotos的代码没有进行其他优化。

如果你知道它只是“哦,顺便说一下”,恰好说服编译器发出更快的代码,那么它可能会很有用。就我个人而言,我更喜欢在使用像goto这样的技巧之前尝试向编译器解释什么是可能的,什么是不可能的,但可以说,我也可能在破解汇编程序之前尝试goto。

有些人说在c++中没有去的理由。有人说99%的情况下都有更好的选择。这不是推理,只是非理性的印象。下面是一个可靠的例子,goto会导致一个很好的代码,比如增强的do-while循环:

int i;

PROMPT_INSERT_NUMBER:
  std::cout << "insert number: ";
  std::cin >> i;
  if(std::cin.fail()) {
    std::cin.clear();
    std::cin.ignore(1000,'\n');
    goto PROMPT_INSERT_NUMBER;          
  }

std::cout << "your number is " << i;

将其与goto-free代码进行比较:

int i;

bool loop;
do {
  loop = false;
  std::cout << "insert number: ";
  std::cin >> i;
  if(std::cin.fail()) {
    std::cin.clear();
    std::cin.ignore(1000,'\n');
    loop = true;          
  }
} while(loop);

std::cout << "your number is " << i;

我看到了这些差异:

需要嵌套的{}块(尽管do{…}而看起来更熟悉) 需要额外的循环变量,在四个地方使用 阅读和理解带有循环的工作需要更长的时间 循环不保存任何数据,它只是控制执行的流程,这比简单的标签更难理解

还有一个例子

void sort(int* array, int length) {
SORT:
  for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
    swap(data[i], data[i+1]);
    goto SORT; // it is very easy to understand this code, right?
  }
}

现在让我们摆脱“邪恶”的goto:

void sort(int* array, int length) {
  bool seemslegit;
  do {
    seemslegit = true;
    for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
      swap(data[i], data[i+1]);
      seemslegit = false;
    }
  } while(!seemslegit);
}

你看,这是使用goto的同一类型,它是结构良好的模式,它不像唯一推荐的方式那样转发goto。你肯定想避免这样的“智能”代码:

void sort(int* array, int length) {
  for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
    swap(data[i], data[i+1]);
    i = -1; // it works, but WTF on the first glance
  }
}

关键是goto很容易被误用,但goto本身不应该受到指责。注意,在c++中,label有函数作用域,所以它不会像纯汇编那样污染全局作用域,在纯汇编中,重叠循环有它的位置,而且非常常见——比如下面8051的代码,其中7段显示连接到P1。该程序循环闪电段周围:

; P1 states loops
; 11111110 <-
; 11111101  |
; 11111011  |
; 11110111  |
; 11101111  |
; 11011111  |
; |_________|

init_roll_state:
    MOV P1,#11111110b
    ACALL delay
next_roll_state:
    MOV A,P1
    RL A
    MOV P1,A
    ACALL delay
    JNB P1.5, init_roll_state
    SJMP next_roll_state

还有一个优点:goto可以作为命名循环、条件和其他流:

if(valid) {
  do { // while(loop)

// more than one page of code here
// so it is better to comment the meaning
// of the corresponding curly bracket

  } while(loop);
} // if(valid)

或者你可以使用等效的goto和缩进,所以如果你明智地选择标签名称,你不需要注释:

if(!valid) goto NOTVALID;
  LOOPBACK:

// more than one page of code here

  if(loop) goto LOOPBACK;
NOTVALID:;

我发现有趣的是,有些人会给出一个可以接受goto的例子列表,说所有其他的用法都是不可接受的。你真的认为你知道每种情况下goto是表达算法的最佳选择吗?

为了说明这一点,我将给你一个还没有人展示过的例子:

今天我在写代码,在哈希表中插入一个元素。哈希表是以前计算的缓存,可以随意重写(影响性能但不影响正确性)。

哈希表的每个桶都有4个槽,当桶满时,我有一堆标准来决定覆盖哪个元素。现在,这意味着在一个桶中最多要经过三次,就像这样:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    goto add;

// Otherwise, find first empty element
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
    goto add;

// Additional passes go here...

add:
// element is written to the hash table here

如果不使用goto,代码会是什么样子?

就像这样:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    break;

if (add_index >= ELEMENTS_PER_BUCKET) {
  // Otherwise, find first empty element
  for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
    if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
      break;
  if (add_index >= ELEMENTS_PER_BUCKET)
   // Additional passes go here (nested further)...
}

// element is written to the hash table here

如果添加更多的遍数,它看起来会越来越糟,而带有goto的版本始终保持相同的缩进级别,并避免使用虚假的if语句,其结果由前一个循环的执行暗示。

所以在另一种情况下,goto使代码更清晰,更容易编写和理解……我相信还有更多的例子,所以不要假装知道所有goto有用的例子,而轻视任何你想不到的好例子。

在c#中,switch语句不允许切换。因此,goto用于将控制转移到特定的开关箱标签或默认标签。

例如:

switch(value)
{
  case 0:
    Console.WriteLine("In case 0");
    goto case 1;
  case 1:
    Console.WriteLine("In case 1");
    goto case 2;
  case 2:
    Console.WriteLine("In case 2");
    goto default;
  default:
    Console.WriteLine("In default");
    break;
}

编辑:有一个例外的“不掉落”规则。如果case语句没有代码,则允许延迟执行。

当然,可以使用GOTO,但是有一件事比代码风格更重要,或者在使用它时,您必须考虑到代码是否可读:其中的代码可能不像您想象的那样健壮。

例如,看看下面的两个代码片段:

If A <> 0 Then A = 0 EndIf
Write("Value of A:" + A)

GOTO的等效代码

If A == 0 Then GOTO FINAL EndIf
   A = 0
FINAL:
Write("Value of A:" + A)

我们首先想到的是这两段代码的结果将是“Value of A: 0”(当然,我们假设执行没有并行性)

这是不正确的:在第一个示例中,A将始终为0,但在第二个示例中(使用GOTO语句)A可能不是0。为什么?

原因是,从程序的另一点,我可以插入一个GOTO FINAL而不控制a的值。

这个例子非常明显,但是随着程序变得越来越复杂,看到这些东西的难度也增加了。

相关材料可以在Dijkstra先生的著名文章“反对GO TO声明的案例”中找到