关于goto语句的见解
序言
我其实对goto语句一直抱有一定的疑惑,我记得刚开始学C的时候,老师布置作业,是有含着多重循环的某个数组内容匹配查找,我当时灵光一闪,用了goto迅速从多重循环里跳出来到末尾打印结果,结果被老师骂了一顿。
为什么呢?即使以我现在的浅薄水平来看,依然并无任何的问题存在,也很清晰,大概是这样的,我忘了以前的了。
QString func()
{
const int judgeValue = 3;
const int array[2][4] = {{5,6,7,8},{1,2,3,4}};
for (int i = 0; i < 2; i++)
{
for (int j = 0; j < 4; j++)
{
if (array[i][j] == judgeValue)
{
goto FIND_OUT;
}
}
}
return "ERROR";
FIND_OUT:
qDebug()<<"find out value";
return "OK";
}
说起老师的理由也是挺奇葩的,不是因为我写得不好,而是因为我用了goto语句!?
我最近问了一次群友对goto的看法,结果存在相当一部分是看到用了就骂,我十分不赞同,所以我写下这篇。
goto为什么这么令人讨厌
其实我认为工具的好坏取决于使用者,goto不可能是真的不好,其应该存在其该存在的理由,不然甚至都不会还保留着。
逻辑顺序混乱
下文程序来源于invalid s分享的basic程序,充斥着goto语句,这种情况下,如果不缩行,你看得有清晰条理可言吗?
作者:invalid s
链接:https://www.zhihu.com/question/20259336/answer/1779133478
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
10 let int i = 1
20 do sth
30 do other
40 goto 190
50 other
60 gosub 180
70 gosub 170
80 gosub 160
90 XX
100 goto 240
...
160 let int call_flag = 1
170 print("hello world!")
180 let int call_flag2 = 2
190 what the fuck?
200 if call_flag = 1 goto 90 '不return了,将来有伏笔!
210 if call_flag2 = 2 goto 230
220 goto 40 '只有第40行会直接goto 190,直接goto回去就好了
230 goto 70
240 do other
'注意这里利用了“伏笔”,所以千万别直接goto 160~180,会崩!
250 if call_flag2 = 2 return '本质上,这是把主程序中的若干行代码拿来当函数体内代码用了,因此其中引用的部分变量可能来自函数内部;return后回归本源
260 print("still alive?")
270 goto 10
你能够很快理解其中的功能吗?你只能从开头顺着一步步往下看,跳来跳去,理解都费劲。
当然,C/C++的goto不支持行号,如果支持行号,随便一更改程序就会全部乱套了。
容易越过不能越的代码区
最简单的,我随便写了一点程序代码,当代码足够多时,你可能会想着直接跳到最后返回,一般来说做法挺好的,但是如果存在一些不能跳的代码你忽略了呢?或许你会觉得这么简单的不可能忽略呢?
不要小看人的遗忘能力,看了信息忘了回都是常有的操作,况且我也只是简单的写一下代码。
23 {
24 Test memory = new Test();
...
27 for (...; ...; ...)
28 {
29 goto PACKAGE_ERROR;
30 }
...
57 if (...)
56 delete memory; //跳过了!导致内存泄漏!!!!!
...
85 PACKAGE_ERROR:
86 return "OK";
87 }
当某个功能需要编程跳到某行,可能就会直接想了一下,哦,用goto就行了,结果以前这写了什么代码也不太记得了,bug百家争鸣。
.
使用goto的可行方案
这是个人对goto的想法可行方案,非行业规范和标准,如有相似,实属同道中人。
使用goto时,要注意4点关键
1、只在函数里使用
这点很容易理解,当goto和标记在同一个函数里使用时,看这段代码,不需要顺着goto跳到其他地方看,会相对容易看一些,最好函数代码不要超过80行。
2、尽量或最好只作为事件反馈功能
比如说在某个位置判断出了事件所要知道的结果,已经不必要继续往下运行了,可以直接跳到函数的末尾进行函数收尾处理。
...
if (array[i][j] == judgeValue)
{
goto FIND_OUT;
}
...
FIND_OUT:
qDebug()<<"find out value";
//释放栈内存等操作,或者调用一些比如FFmpeg的释放编解码器之类的函数
return "OK";
其实也可以使用智能指针避免内存泄漏操作。
3、为避免带偏见的人不爽,每个goto语句留下注释
如上,不喜欢用goto的,总会有所偏见,认为用goto会到处跳来跳去,对于这些人是没那么容易改变的,所以,最好写上注释,注明两部分:①为什么要跳;②跳去哪。
比如下面那点的代码,写清楚为什么要跳,要跳到末尾,自然就很快看到跳在哪里。
4、函数划分要划好逻辑块
我随便写一点函数,非正常使用功能函数,有些人喜欢按逻辑功能划块,有些人喜欢按代码功能划块。
都可以,但是一定要清晰明了,让人快速明白你的函数分布,不至于跳过一些不能跳的地方。
QString func()
{
//第一部分,放定义块
const int judgeValue = 3;
const int array[2][4] = {{5,6,7,8},{1,2,3,4}};
int *test_memory = new int(5);
//第二部分,放函数代码逻辑块
if (test_memory == nullptr)
{
goto MEMORY_NULL; //申请内存失败(虽然此处申请无意义,但演示嘛),跳转末尾返回
}
for (int i = 0; i < 2; i++)
{
for (int j = 0; j < 4; j++)
{
if (array[i][j] == judgeValue)
{
goto FIND_OUT; //找到该数,跳转至末尾返回
}
}
}
//第三部分,统一放返回值,函数结束时需解决的事件如内存释放等
delete test_memory;
return "can't find";
MEMORY_NULL:
return "memroy is null";
FIND_OUT:
delete test_memory;
return "find out";
}
.
为什么要使用goto,有什么好处?
总的来说就是因为便捷,但是会有所影响可读性和带偏见人的骂欲。
虽然可以从繁杂的多重循环一次跳出来的好处功能,但是,你能使用繁杂多重循环自然也说明了你的逻辑有点杂了,无论用不用goto,其实你都可以有方法解决优化的,所以不要轻易使用goto,除非你不在乎多重循环里可能存在的数据处理功能。
goto是一种捷径,这会很快写好混乱的代码,即便这是你以为的清晰,正常的代码也足以构建好很好的代码。
犹如建造高楼,goto在正常逻辑处理中相当于一个长木棍,你通过它爬上了天空,却以为你已经完成了高楼。
所以,为了可读性,最好只像上面那种用法,统一管理返回,而尽量或最好不要在逻辑处理中去使用goto语句。
指引统集return
每个函数都有return,长长的函数中可能有N次的判断返回,很容易不知道哪里就返回了,因为什么返回。所以使用goto统一在末尾返回,可以清楚得看到返回的地方和原因,返回的条件,这是一个便于读代码的好方法。
注:为什么我不说try catch捕获异常用goto的情况呢?因为我觉得try catch捕获自己定义的“异常”属实有点鸡肋,所以我一般不用它。
完~有错误望请指正谢谢