目前我正在开发一个使用goto语句的项目.goto语句的主要目的是在例程中使用一个清理部分而不是多个return语句.如下所示:
BOOL foo() { BOOL bRetVal = FALSE; int *p = NULL; p = new int; if (p == NULL) { cout<<" OOM \n"; goto Exit; } // Lot of code... Exit: if(p) { delete p; p = NULL; } return bRetVal; }
这使得我们可以更容易地跟踪代码中一个部分的清理代码,即在Exit标签之后.
但是,我已经阅读了许多地方,有goto语句是不好的做法.
目前我正在阅读Code Complete书,它说我们需要使用接近其声明的变量.如果我们使用goto,那么我们需要在第一次使用goto之前声明/初始化所有变量,否则编译器会给出goto语句跳过xx变量初始化的错误.
哪条路对不对?
来自Scott的评论:
看起来使用goto从一个部分跳转到另一个部分是不好的,因为它使代码难以阅读和理解.
但是如果我们只使用goto前进到一个标签那么它应该没问题(?).
我不确定清理代码是什么意思,但在C++中有一个名为" 资源获取是初始化 " 的概念,它应该是你的析构函数清理东西的责任.
(注意,在C#和Java中,这通常通过try/finally解决)
有关详细信息,请查看此页面:http: //www.research.att.com/~bs/bs_faq2.html#finally
编辑:让我清楚一点.
请考虑以下代码:
void MyMethod() { MyClass *myInstance = new MyClass("myParameter"); /* Your code here */ delete myInstance; }
问题:如果你有多个函数退出会发生什么?您必须跟踪每个出口并在所有可能的出口处删除您的对象!否则,你会有内存泄漏和僵尸资源,对吧?
解决方案:使用对象引用,因为当控件离开作用域时它们会自动清理.
void MyMethod() { MyClass myInstance("myParameter"); /* Your code here */ /* You don't need delete - myInstance will be destructed and deleted * automatically on function exit */ }
哦,是的,使用std::unique_ptr
或类似的东西,因为上面的例子显然是不完美的.
我从来没有在C++中使用goto.永远.EVER.如果有这种情况应该使用它,这是非常罕见的.如果你实际上正在考虑将goto作为逻辑的标准部分,那么某些东西已经飞离了轨道.
关于gotos和你的代码,人们基本上有两点要点:
转到很糟糕. 遇到一个你需要的地方是非常罕见的,但我不建议完全打击它.虽然C++有足够聪明的控制流程来使goto很少适合.
你的清理机制是错误的:这一点更为重要.在C中,使用内存管理不仅可以,而且通常是最好的方法.在C++中,您的目标应该是尽可能避免内存管理.你应该尽可能避免内存管理.让编译器为您完成.而不是使用new
,只需声明变量.您真正需要内存管理的唯一时间是您事先不知道数据的大小.即使这样,你也应该尝试使用一些STL
集合.
如果你合法地需要内存管理(你还没有提供任何证据),那么你应该通过构造函数将内存管理封装在一个类中,以分配内存和解构器来释放内存.
从长远来看,你的回答是你的做事方式要容易得多.首先,一旦你对C++的强烈感觉,这样的构造函数将成为第二天性.就个人而言,我发现使用构造函数比使用清理代码更容易,因为我没有必要仔细注意确保我正确地解除分配.相反,我可以让对象离开范围,语言为我处理它.此外,维护它们比维护清理部分更容易,而且更不容易出现问题.
简而言之,goto
在某些情况下可能是一个不错的选择,但在这种情况下则不是.这只是短期的懒惰.
你的代码非常非惯用,你永远不应该写它.你基本上是在C++中模拟C语言.但其他人已经对此发表评论,并指出RAII是另一种选择.
但是,您的代码将无法按预期工作,因为:
p = new int; if(p==NULL) { … }
不会永远计算为true
(除非你已经超负荷operator new
的不可思议的方式).如果operator new
无法分配足够的内存,它抛出一个异常,它永远不会,永远返回0
,至少不与本组参数; 有一个特殊的placement-new重载,它接受一个类型的实例,std::nothrow
并确实返回0
而不是抛出异常.但是这个版本在普通代码中很少使用.在处理异常的情况太昂贵的情况下,某些低级代码或嵌入式设备应用程序可以从中受益.
对于你的delete
街区来说,类似的东西是正确的,正如哈拉尔德所说:if (p)
前面没有必要delete p
.
另外,我不确定您的示例是否是故意选择的,因为此代码可以重写如下:
bool foo() // prefer native types to BOOL, if possible { bool ret = false; int i; // Lots of code. return ret; }
可能不是一个好主意.
一般而言,从表面上看,如果您只有一个标签,那么您的方法没有任何问题,而且这些标签总是向前发展.例如,这段代码:
int foo() { int *pWhatEver = ...; if (something(pWhatEver)) { delete pWhatEver; return 1; } else { delete pWhatEver; return 5; } }
这段代码:
int foo() { int ret; int *pWhatEver = ...; if (something(pWhatEver)) { ret = 1; goto exit; } else { ret = 1; goto exit; } exit: delete pWhatEver; return ret; }
真的不是彼此都有所不同.如果你能接受另一个,你应该能够接受另一个.
但是,在许多情况下,RAII(资源获取是初始化)模式可以使代码更清晰,更易于维护.例如,这段代码:
int foo() { AutopWhatEver = ...; if (something(pWhatEver)) { return 1; } else { return 5; } }
与前面的两个例子相比,它更短,更易于阅读,更易于维护.
所以,如果可以的话,我建议使用RAII方法.
我认为其他答案(以及他们的评论)已经涵盖了所有重要的观点,但这里有一件事还没有做好:
您的代码应该是什么样子:
bool foo() //lowercase bool is a built-in C++ type. Use it if you're writing C++. { try { std::unique_ptrp(new int); // lots of code, and just return true or false directly when you're done } catch (std::bad_alloc){ // new throws an exception on OOM, it doesn't return NULL cout<<" OOM \n"; return false; } }
好吧,它更短,而且据我所知,更正确(正确处理OOM案例),最重要的是,我不需要编写任何清理代码或做任何特殊的事情来"确保我的返回值被初始化".
我写这篇文章时我真正注意到的代码存在的一个问题是"此时bRetVal的价值到底是什么?".我不知道因为,它被宣布为上面的waaaaay,它最后被分配到何时?在某一点上面.我必须通读整个函数,以确保我理解将要返回的内容.
我如何说服自己内存得到释放?
我怎么知道我们永远不会忘记跳转到清理标签?我必须从清理标签向后工作,找到指向它的每个 goto,更重要的是,找到那些不存在的那些.我需要跟踪函数的所有路径,以确保函数得到正确清理.这对我来说就像意大利面条代码.
非常脆弱的代码,因为每次必须清理资源时,您必须记住复制清理代码.为什么不写一次,需要清理的类型?然后依靠它自动执行,每次我们需要它?
您的示例不是例外安全.
如果您正在使用goto来清理代码,那么如果在清理代码之前发生异常,则完全错过了.如果你声称你没有使用异常那么你就错了,因为new
当它没有足够的内存时会抛出bad_alloc.
此时(当抛出bad_alloc时),您的堆栈将被展开,在调用堆栈的路上错过了每个函数中的所有清理代码,因此无法清理代码.
您需要寻找对智能指针进行一些研究.在上面的情况下,你可以使用一个std::auto_ptr<>
.
另请注意,在C++代码中,不需要检查指针是否为NULL(通常是因为你从未有过RAW指针),但是因为new
它不会返回NULL(它会抛出).
同样在C++中,与(C)不同,在代码中看到早期返回是很常见的.这是因为RAII会自动进行清理,而在C代码中你需要确保在函数末尾添加特殊的清理代码(有点像你的代码).
在我编程的八年里,我经常使用goto,其中大部分是在第一年我使用的是GW-BASIC的一个版本和1980年的一本书,但是没有说清楚goto应该只在某些情况下使用.我在C++中使用goto的唯一一次是当我有如下代码时,我不确定是否有更好的方法.
for (int i=0; i<10; i++) { for (int j=0; j<10; j++) { if (somecondition==true) { goto finish; } //Some code } //Some code } finish:
我知道goto仍在使用的唯一情况是大型机汇编语言,我所知道的程序员确保记录代码跳转的位置和原因.
你应该阅读从Linux内核邮件列表(要特别注意从Linus Torvalds的响应)你在这之前线程摘要形成了一个政策goto
:
http://kerneltrap.org/node/553/2131
通常,您应该设计程序以限制对gotos的需求.使用OO技术"清理"您的返回值.有一些方法可以做到这一点,不需要使用gotos或使代码复杂化.有些情况下gotos非常有用(例如,深度嵌套的范围),但应尽可能避免.
当"尾端逻辑"对于某些但不是全部的情况是常见的时,Goto提供更好的不要重复自己(DRY).特别是在"切换"语句中,当一些交换分支具有尾端通用性时,我经常使用goto.
switch(){ case a: ... goto L_abTail; case b: ... goto L_abTail; L_abTail:break://end of case b case c: ..... }//switch
您可能已经注意到,在您需要在例程的中间进行尾部合并时,引入额外的花括号足以满足编译器的要求.换句话说,你不需要在顶部声明一切; 这确实是可怜的可读性.
... goto L_skipMiddle; { int declInMiddleVar = 0; .... } L_skipMiddle: ;
随着Visual Studio的更高版本检测到未初始化变量的使用,我发现自己总是初始化大多数变量,即使我认为它们可能在所有分支中分配 - 很容易编写"跟踪"语句,该语句引用从未分配的变量因为你的大脑并不认为跟踪语句是"真正的代码",但当然Visual Studio仍会检测到错误.
除了不重复自己,为这样的尾端逻辑分配标签名称甚至似乎通过选择漂亮的标签名称来帮助我的思想保持正确.如果没有有意义的标签,您的评论最终可能会说同样的话.
当然,如果你实际上是在分配资源,那么如果auto-ptr不合适,那么你真的必须使用try-catch,但是当异常安全时,你会经常发生尾端 - 合并 - 不重复 - 自我这不是问题.
总之,虽然goto可用于编码类似意大利面条的结构,但在尾部序列的情况下,这种情况对于某些但不是全部的情况是常见的,那么goto会提高代码的可读性,甚至可维护性如果你原本会复制/粘贴东西,那么很久以后某人可能会更新一个而不是另一个.所以这是另一种情况,对教条狂热可能会适得其反.
正如在Linux内核中使用的那样,当单个函数必须执行可能需要撤消的2个或更多步骤时,用于清理的goto很有效.步骤不必是内存分配.它可能是对一段代码或I/O芯片组寄存器的配置更改.仅在少数情况下才需要Goto,但通常在正确使用时,它们可能是最佳解决方案.他们不是邪恶的.他们是一个工具.
代替...
do_step1; if (failed) { undo_step1; return failure; } do_step2; if (failed) { undo_step2; undo_step1; return failure; } do_step3; if (failed) { undo_step3; undo_step2; undo_step1; return failure; } return success;
你可以用这样的goto语句做同样的事情:
do_step1; if (failed) goto unwind_step1; do_step2; if (failed) goto unwind_step2; do_step3; if (failed) goto unwind_step3; return success; unwind_step3: undo_step3; unwind_step2: undo_step2; unwind_step1: undo_step1; return failure;
应该清楚的是,给出这两个例子,一个比另一个更好.至于RAII人群...这种方法没有任何问题,只要他们可以保证放卷总是以完全相反的顺序发生:3,2,1.最后,有些人不会在代码中使用异常并指示编译器禁用它们.因此,并非所有代码都必须是异常安全的
很好地讨论了GOTO的缺点.我只想补充一点:1)有时你必须使用它们并且应该知道如何最小化这些问题,2)一些公认的编程技术是GOTO-in-confguise,所以要小心.
1)当您必须使用GOTO时,例如在ASM或.bat文件中,请像编译器一样思考.如果你想编码
if (some_test){ ... the body ... }
做编译器做的事情.生成一个标签,其目的是跳过身体,而不是执行任何后续操作.即
if (not some_test) GOTO label_at_end_of_body ... the body ... label_at_end_of_body:
不
if (not some_test) GOTO the_label_named_for_whatever_gets_done_next ... the body ... the_label_named_for_whatever_gets_done_next:
.换句话说,标签的目的不是为了做什么,但到了跳过一些东西.
2)我称之为GOTO-in-伪装的是通过定义几个宏可以转换成GOTO + LABELS代码的任何东西.一个例子是通过使用状态变量和while-switch语句来实现有限状态自动机的技术.
while (not_done){ switch(state){ case S1: ... do stuff 1 ... state = S2; break; case S2: ... do stuff 2 ... state = S1; break; ......... } }
可以变成:
while (not_done){ switch(state){ LABEL(S1): ... do stuff 1 ... GOTO(S2); LABEL(S2): ... do stuff 2 ... GOTO(S1); ......... } }
只需定义几个宏.几乎任何FSA都可以转换为结构化的无转码代码.我更喜欢远离GOTO-in-confguise代码,因为它可以像未经伪装的getos一样陷入相同的意大利面条代码问题.
补充:只是为了让人放心:我认为优秀程序员的一个标志就是承认共同规则何时不适用.