学习编程的时候有没有一位心灵导师告诉过你,编程的时候千万不要使用goto,否则他会给你带来意想不到的后果。我也是,看过的所有初学者的书上都在贯穿着这一思想,我不明白,为什么大家都不让使用goto语句,如果真的像大家说的那样恐怖,那么为什么goto语句至今还没有被移除出去。
从1969年C语言诞生到现在,经过几十年的发展goto语句不仅没有被标准委员会移除,相反,在许多其他的语言也被继承了下来,如:Java、C#、C ,是不是很奇怪,如果goto真的给我们带来灾难,为什么主流的编程语言里面一直还在使用……
其实,goto语句并不像很多人认为的那么不堪,相反,正确的使用还能够简化程序设计并提升程序的鲁棒性。这一观点,在某个微软的PPT中也有提及。下面将给大家总结下正确使用goto语句的场景。
1 函数只有一个return语句
如果函数中只有一个return语句且中间没有break、continue推荐大家使用goto语句。如下面这段代码,代码来源于GCC源码。
代码语言:javascript复制int process_zones (int cpu)
{
struct zone *zone, *dzone;
int node = cpu_to_node (cpu);
for (zone = (first_online_pgdat ())->node_zones;
zone; zone = next_zone (zone))
{
((zone)->pageset[(cpu)]) =
kmalloc_node (sizeof (struct per_cpu_pageset),
(((gfp_t) 0x10u) | ((gfp_t) 0x40u) | ((gfp_t) 0x80u)),
node);
if (!((zone)->pageset[(cpu)]))
goto bad;
}
return 0;
bad:
XYZZY ();
return -12;
}
2 谁分配、谁释放的时候使用
如下面的代码,在不使用goto语句的情况下,代码写法:
代码语言:javascript复制int main()
{
char *pTest1=nullptr;
char *pTest2=nullptr;
char *pTest3=nullptr;
pTest1 = (char *)malloc(1);
if(pTest1==nullptr) return -1;
pTest2 = (char *)malloc(2);
if(pTest2==nullptr)
{
free(pTest1);
pTest1=nullptr;
return -1;
}
pTest3 = (char *)malloc(3);
if(pTest3==nullptr)
{
free(pTest1);
pTest1=nullptr;
free(pTest2);
pTest2=nullptr;
return -1;
}
//程序退出前的清理
if(pTest1!=nullptr)
{
free(pTest1);
pTest1=nullptr;
return -1;
}
if(pTest2!=nullptr)
{
free(pTest2);
pTest2=nullptr;
return -1;
}
if(pTest3!=nullptr)
{
free(pTest3);
pTest3=nullptr;
return -1;
}
return 0;
}
在上面这段代码中,对资源的释放写了很多重复的代码,但是又不能省略,它们都是必须的,试想下,如果使用goto语句呢?代码可以这么写:
代码语言:javascript复制int main()
{
char *pTest1=nullptr;
char *pTest2=nullptr;
char *pTest3=nullptr;
pTest1 = (char *)malloc(1);
if(pTest1==nullptr) goto CLEAN;
pTest2 = (char *)malloc(2);
if(pTest2==nullptr) goto CLEAN;
pTest3 = (char *)malloc(3);
if(pTest3==nullptr) goto CLEAN;
//程序退出前的清理
CLEAN:
if(pTest1!=nullptr)
{
free(pTest1);
pTest1=nullptr;
}
if(pTest2!=nullptr)
{
free(pTest2);
pTest2=nullptr;
}
if(pTest3!=nullptr)
{
free(pTest3);
pTest3=nullptr;
}
return 0;
}
从上面的例子可以看出,使用goto语句后代码不会产生大量重复且必要的语句,且我们的程序变得更加简洁。写代码都知道。代码越少,bug也就越少。所以从某种程度上说goto语句避免了bug的产生。
3 使用goto语句可以使得程序可读性增强
在上面的代码示例中,给malloc申请资源时,如果出错我们通过if语句进行判断,就已经使代码边的冗余很多。试想,如果再加上else甚至elseif语句会怎么样,我们的代码将会变得越来越多,重复的代码也会成倍的增长,如果工程较小还好,如果我们的工程很大呢?代码中充斥着if、elseif 、else等语句,不得不说,这是件让人头疼的事情。想要知道一个程序的处理逻辑可能要来回切换,给代码阅读带来不便。使用goto句后呢,可以将一些公共的逻辑抽象到固定的位置处理,同时简化了代码的复杂度,代码也变得易读。使用GCC的同学可能知道,如果大括号超过3层,编译器可能就不会再进行优化了。因此在实际编程中也不推荐多层嵌套的使用。
4 尽量将问题消灭在内部
很多时候我们写的接口不是给自己使用,而是提供给别人调用,如果别人在使用我们提供的代码时还要回过头来处理我们的接口抛出的错误将会给别人带来非常的不便。
在实际的编程中,尤其是面向对象的编程,很多人喜欢使用try catch将问题抛给外部处理,不得不说这是一个很不负责任的操作。在大型项目开发中,我们建议每个模块的封装都要具备高度内敛性,在函数返回前,将函数内部产生的异常全部处理掉,不能对其它外部模块产生依赖。因此,我们也建议,编程时少用异常抛出机制,避免因为处理疏漏带来更多的不便。
因此,在提供给外部调用的接口时,我们可以使用goto语句来解决这种问题。让每个接口只有一个返回路径,不管在哪个逻辑出现问题,都通过goto语句跳转到函数最后返回,确保程序不会崩溃。
5 在循环中使用goto可以避免循环问题
在实际编码中,可能使用两层循环,在内层循环满足时我们使用break语句跳出循环。但可能并不是我们的本意。如下面的代码:
代码语言:javascript复制int main()
{
int i=0;
for(i=0;i<100;i )
{
int sum=0;
do{
sum =( i);
if(sum > 50)
{
printf("sum=%d,break",sum);
break;
}
}while(1);
}
return 0;
}
如上面的代码,本意是如果sum大于50就跳出循环,终止程序,但是实际上程序运行时并不像我们设想的那样,因为我们使用break只是跳出了内层循环。跳出后外层循环会继续执行,这种情况下,使用goto会精准地达到我们的目标。如:
代码语言:javascript复制int main()
{
int i=0;
int sum=0;
for(i=0;i<100;i )
{
sum=0;
do{
sum =( i);
if(sum > 50)
{
goto QUIT;
}
}while(1);
}
QUIT:
printf("sum=%d,break",sum);
return 0;
}
从上面的代码可知,goto语句的使用可以减少很多不必要的错误,设想如果在大型项目中,多重循环嵌套下我们如果不能准确跳出循环,结束程序,那么给我们带来的后果将非常严重,不得不投入很大的精力排查问题。
6 goto语句使用法则
前面介绍这么多,大家可以知道,正确地使用goto语句确实会在实际的编码中给我们带来意想不到的效果。在这里,也给大家在使用goto语句时提供一点建议:
- 从内向外跳转,不要从外向内跳。如上面的代码只能从循环内部跳出循环外。
- 向后跳转,不要向前跳转,这可能也是很多书上让我们规避使用goto的原因。实际编码中应该严格遵守。
- 禁止使用goto语句实现死循环或者循环操作。死循环有while(1)循环实现方式更多。
- goto语句后面不准直接使用大括号,否则编译器可能会报错。实际编码时尽量避免。无法避免时可以写上一句无意义的代码。哪怕是一行日志输出语句。