我一直有一个印象,如果可能的话,永远不要使用goto。
然而,在前几天阅读libavcodec(它是用C编写的)时,我惊讶地注意到它的多种用法。
在支持循环和函数的语言中使用goto是否有优势?如果有,为什么?请提供一个具体的例子,清楚地说明使用goto的理由。
我一直有一个印象,如果可能的话,永远不要使用goto。
然而,在前几天阅读libavcodec(它是用C编写的)时,我惊讶地注意到它的多种用法。
在支持循环和函数的语言中使用goto是否有优势?如果有,为什么?请提供一个具体的例子,清楚地说明使用goto的理由。
当前回答
如果有,为什么?
C语言没有多级/标记的中断,并不是所有的控制流都可以用C语言的迭代和决策原语轻松建模。Gotos对纠正这些缺陷大有帮助。
有时使用某种类型的标志变量来实现一种伪多级中断更清晰,但它并不总是优于goto(至少goto可以轻松地确定控制的位置,不像标志变量),有时您只是不想为了避免goto而付出旗帜/其他扭曲的性能代价。
Libavcodec是一段性能敏感的代码。控制流的直接表达可能是优先考虑的,因为它往往会运行得更好。
其他回答
我发现有趣的是,有些人会给出一个可以接受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有用的例子,而轻视任何你想不到的好例子。
下面是我所知道的使用“goto”语句的一些原因(有些人已经谈到了这个问题):
干净地退出函数
通常在一个函数中,您可能会分配资源并需要在多个位置退出。程序员可以通过将资源清理代码放在函数的末尾来简化他们的代码,并且函数的所有“出口点”都将进入清理标签。这样,您就不必在函数的每个“退出点”都编写清理代码。
退出嵌套循环
如果处于嵌套循环中,需要跳出所有循环,那么goto可以比break语句和If -checks更简洁。
低水平的性能改进
这只在对性能要求严格的代码中有效,但是goto语句执行得非常快,并且可以在遍历函数时提高性能。然而,这是一把双刃剑,因为编译器通常不能优化包含goto的代码。
注意,在所有这些示例中,gotos都被限制在单个函数的范围内。
有些人说在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:
例子:
// 1
try{
...
throw NoErrorException;
...
} catch (const NoErrorException& noe){
// This is the worst
}
// 2
do {
...break;
...break;
} while (false);
// 3
for(int i = 0;...) {
bool restartOuter = false;
for (int j = 0;...) {
if (...)
restartOuter = true;
if (restartOuter) {
i = -1;
}
}
etc
etc
看看在C语言中编程时何时使用Goto:
Although the use of goto is almost always bad programming practice (surely you can find a better way of doing XYZ), there are times when it really isn't a bad choice. Some might even argue that, when it is useful, it's the best choice. Most of what I have to say about goto really only applies to C. If you're using C++, there's no sound reason to use goto in place of exceptions. In C, however, you don't have the power of an exception handling mechanism, so if you want to separate out error handling from the rest of your program logic, and you want to avoid rewriting clean up code multiple times throughout your code, then goto can be a good choice.
我是什么意思?你可能会有这样的代码:
int big_function()
{
/* do some work */
if([error])
{
/* clean up*/
return [error];
}
/* do some more work */
if([error])
{
/* clean up*/
return [error];
}
/* do some more work */
if([error])
{
/* clean up*/
return [error];
}
/* do some more work */
if([error])
{
/* clean up*/
return [error];
}
/* clean up*/
return [success];
}
This is fine until you realize that you need to change your cleanup code. Then you have to go through and make 4 changes. Now, you might decide that you can just encapsulate all of the cleanup into a single function; that's not a bad idea. But it does mean that you'll need to be careful with pointers -- if you plan to free a pointer in your cleanup function, there's no way to set it to then point to NULL unless you pass in a pointer to a pointer. In a lot of cases, you won't be using that pointer again anyway, so that may not be a major concern. On the other hand, if you add in a new pointer, file handle, or other thing that needs cleanup, then you'll need to change your cleanup function again; and then you'll need to change the arguments to that function.
通过使用goto,它将是
int big_function()
{
int ret_val = [success];
/* do some work */
if([error])
{
ret_val = [error];
goto end;
}
/* do some more work */
if([error])
{
ret_val = [error];
goto end;
}
/* do some more work */
if([error])
{
ret_val = [error];
goto end;
}
/* do some more work */
if([error])
{
ret_val = [error];
goto end;
}
end:
/* clean up*/
return ret_val;
}
这样做的好处是,您的代码可以访问执行清理所需的所有内容,并且您已经成功地减少了更改点的数量。另一个好处是你的函数从多个出口点变成了只有一个;不可能不小心从函数返回而不进行清理。
此外,由于goto仅用于跳转到单个点,因此并不是为了模拟函数调用而创建大量来回跳转的意大利面代码。相反,goto实际上有助于编写更结构化的代码。
总而言之,goto应该谨慎使用,并作为最后的手段——但它是有时间和地点的。问题不应该是“你是否必须使用它”,而应该是“它是使用它的最佳选择”。