我一直有一个印象,如果可能的话,永远不要使用goto。
然而,在前几天阅读libavcodec(它是用C编写的)时,我惊讶地注意到它的多种用法。
在支持循环和函数的语言中使用goto是否有优势?如果有,为什么?请提供一个具体的例子,清楚地说明使用goto的理由。
我一直有一个印象,如果可能的话,永远不要使用goto。
然而,在前几天阅读libavcodec(它是用C编写的)时,我惊讶地注意到它的多种用法。
在支持循环和函数的语言中使用goto是否有优势?如果有,为什么?请提供一个具体的例子,清楚地说明使用goto的理由。
当前回答
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。
其他回答
盲目地遵循最佳实践并不是最佳实践。避免将goto语句作为流控制的主要形式是为了避免产生难以阅读的意大利面条代码。如果在正确的地方谨慎使用,它们有时可以成为表达想法的最简单、最清晰的方式。Walter Bright, Zortech c++编译器和D编程语言的创造者,经常使用它们,但很明智。即使使用了goto语句,他的代码仍然完全可读。
底线:为了避免去那里而避免去那里是毫无意义的。您真正需要避免的是生成不可读的代码。如果充满goto的代码是可读的,那么它就没有任何问题。
我发现do{} while(false)的用法完全令人反感。它可能会让我相信它在某些奇怪的情况下是必要的,但从来没有让我相信它是干净合理的代码。
如果必须执行这样的循环,为什么不显式地依赖标志变量呢?
for (stepfailed=0 ; ! stepfailed ; /*empty*/)
我遇到过goto是一个很好的解决方案的情况,我在这里或其他地方都没有看到过这个例子。
我有一个开关的情况下,所有的情况都需要调用相同的函数在最后。我有其他的情况,最后都需要调用不同的函数。
这看起来有点像这样:
switch( x ) {
case 1: case1() ; doStuffFor123() ; break ;
case 2: case2() ; doStuffFor123() ; break ;
case 3: case3() ; doStuffFor123() ; break ;
case 4: case4() ; doStuffFor456() ; break ;
case 5: case5() ; doStuffFor456() ; break ;
case 6: case6() ; doStuffFor456() ; break ;
case 7: case7() ; doStuffFor789() ; break ;
case 8: case8() ; doStuffFor789() ; break ;
case 9: case9() ; doStuffFor789() ; break ;
}
我没有给每个case一个函数调用,而是用goto代替了break。goto跳转到一个标签,这也是在开关的情况下。
switch( x ) {
case 1: case1() ; goto stuff123 ;
case 2: case2() ; goto stuff123 ;
case 3: case3() ; goto stuff123 ;
case 4: case4() ; goto stuff456 ;
case 5: case5() ; goto stuff456 ;
case 6: case6() ; goto stuff456 ;
case 7: case7() ; goto stuff789 ;
case 8: case8() ; goto stuff789 ;
case 9: case9() ; goto stuff789 ;
stuff123: doStuffFor123() ; break ;
stuff456: doStuffFor456() ; break ;
stuff789: doStuffFor789() ; break ;
}
案例1到3都必须调用doStuffFor123(),类似的案例4到6必须调用doStuffFor456()等。
在我看来,如果你正确使用它们,gotos是非常好的。最后,任何代码都像人们写的那样清晰。有了gotos,你可以写出意大利面代码,但这并不意味着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: 当需要从不同位置的函数返回时,并且在返回之前需要进行一些初始化:
non-goto版本:
int doSomething (struct my_complicated_stuff *ctx)
{
db_conn *conn;
RSA *key;
char *temp_data;
conn = db_connect();
if (ctx->smth->needs_alloc) {
temp_data=malloc(ctx->some_size);
if (!temp_data) {
db_disconnect(conn);
return -1;
}
}
...
if (!ctx->smth->needs_to_be_processed) {
free(temp_data);
db_disconnect(conn);
return -2;
}
pthread_mutex_lock(ctx->mutex);
if (ctx->some_other_thing->error) {
pthread_mutex_unlock(ctx->mutex);
free(temp_data);
db_disconnect(conn);
return -3;
}
...
key=rsa_load_key(....);
...
if (ctx->something_else->error) {
rsa_free(key);
pthread_mutex_unlock(ctx->mutex);
free(temp_data);
db_disconnect(conn);
return -4;
}
if (ctx->something_else->additional_check) {
rsa_free(key);
pthread_mutex_unlock(ctx->mutex);
free(temp_data);
db_disconnect(conn);
return -5;
}
pthread_mutex_unlock(ctx->mutex);
free(temp_data);
db_disconnect(conn);
return 0;
}
goto版本:
int doSomething_goto (struct my_complicated_stuff *ctx)
{
int ret=0;
db_conn *conn;
RSA *key;
char *temp_data;
conn = db_connect();
if (ctx->smth->needs_alloc) {
temp_data=malloc(ctx->some_size);
if (!temp_data) {
ret=-1;
goto exit_db;
}
}
...
if (!ctx->smth->needs_to_be_processed) {
ret=-2;
goto exit_freetmp;
}
pthread_mutex_lock(ctx->mutex);
if (ctx->some_other_thing->error) {
ret=-3;
goto exit;
}
...
key=rsa_load_key(....);
...
if (ctx->something_else->error) {
ret=-4;
goto exit_freekey;
}
if (ctx->something_else->additional_check) {
ret=-5;
goto exit_freekey;
}
exit_freekey:
rsa_free(key);
exit:
pthread_mutex_unlock(ctx->mutex);
exit_freetmp:
free(temp_data);
exit_db:
db_disconnect(conn);
return ret;
}
当您需要更改释放语句中的某些内容时(每个语句在代码中使用一次),第二个版本使它变得更容易,并减少了在添加新分支时跳过其中任何一个语句的机会。在函数中移动它们在这里不会有帮助,因为可以在不同的“级别”进行释放。