【问题标题】:while(1) .. break instead of gotowhile(1) .. break 而不是 goto
【发布时间】:2010-11-07 14:05:33
【问题描述】:

我在一个 C 程序中找到了以下代码:

while (1)
{
    do_something();
    if (was_an_error()) break;

     do_something_else();
     if (was_an_error()) break;

     [...]

     break;
}
[cleanup code]

这里while(1) 用作“finally”的本地模拟。您也可以使用gotos 编写此代码:

do_something()
if (was_an_error()) goto out;

do_something_else()
if (was_an_error()) goto out;

[...]
out:
[cleanup code]

我认为 goto 解决方案是一个惯用语。我已经在内核源代码中看到过多次出现这种习惯用法,并且在 Diomidis Spinellis 的“代码阅读”一书中也提到了它。

我的问题是:什么解决方案更好?使用while(1) 解决方案有什么具体原因吗?

问题943826 没有回答我的问题。

【问题讨论】:

  • 为此使用 while(1) 会使阅读代码的人期望它“无限”运行...为什么不使用 do { } while(0) ?
  • @Tal Pressman - 为什么会有不同?
  • @Earwicker,使用该选项很明显它至少会运行一次,并且不会再次运行,因为条件是“while (0)”。不会出现令人困惑的“while(1)”。
  • 但是两者都使用循环结构,其中没有任何循环的意图。两者都具有误导性、模糊性等。
  • @Earwicker:我同意,但更糟糕的是它暗示了无限循环。但是,对于一开始就不是循环的事物使用循环是糟糕设计的明显标志。

标签: c goto


【解决方案1】:

对 GOTO 的普遍反感很大程度上是由于 Edsger Dijkstra 的信“Go To Statement被认为是有害的”。

如果你确定不使用 goto,类似

do {
    ...
while(0);

可能比 while(1) { ... } 更安全,因为它保证您不会无意循环(如果您无意循环,使用 while(1) 您可能会无意中无限循环)。

(ab)为此目的使用 do/break/while 或 while/break 比 goto 的一个优点是,您可以保证不会在构造之上跳转 -- goto 可以是用于在同一函数中跳转到更早的标签。

do/break/while 等与 goto 相比的缺点是您被限制在一个退出点(紧接在循环之后)。在某些情况下,您可能需要分阶段清理:例如,当您打开文件句柄时,malloc 一些内存,从文件中读取......如果读取失败,您需要清理 malloc。如果 malloc 失败,你不需要清理它,但你仍然需要清理文件句柄。使用 goto,您可以在每个清理阶段使用一个标签,并根据错误发生的位置准确地跳转到正确的点。

在我看来,因为普遍对 GOTO 的普遍仇恨而盲目地避免 GOTO 比在个案的基础上仔细推理一个案例的使用更具破坏性。我使用的一个经验法则是“Linux 内核能做到吗?如果是,它就不会那么 糟糕”。用任何其他现代软件工程的好例子代替 linux 内核。

【讨论】:

  • 其实不光是Linux内核,MS的Windows Drivers示例全是goto的,Solaris unix内核驱动也用,BSD内核代码等等……底线goto用的很广主要是因为人们知道自己在做什么。
  • 我见过使用do { ... break ... } while(0); 技巧的代码,我可以证明它会妨碍可读性,但不会给你买任何东西。如果您正在做goto - 并且您正在 在这里做 - 那么至少要明确说明。 C 代码中的 goto 用于错误处理/清理目的并没有错。
  • +1 但我几乎没有因为最后两句话。 Linux 内核充满了一些非常糟糕的编码习惯......
【解决方案2】:

将代码放入单独的函数中,并使用return 提前退出是另一种方法,其优点是易于集成指示故障性质的返回代码。

【讨论】:

  • 问题不是提前退出,是清理。在多步初始化中,您最终需要清理/取消分配已成功分配的内容。
  • 这就是我要建议的。如果做不到这一点,我会使用 goto 而不是 while(1),因为 while(1) 给人的误解是代码循环,而实际上它没有。
  • @shodanex:可能只是这种语言的表达力不足以原生表达您想要做的事情(即,像 java 中的“finally”子句)。所以任何绕过这个限制的黑客都会有一些与之相关的缺点。
  • 一个方法调用可能会因增加堆栈以及可能的堆栈溢出异常而对性能产生影响。这是您在驱动程序和内核代码中看到它们的原因之一。
  • 编译器将在有意义的情况下内联方法调用,从而消除性能损失。
【解决方案3】:

我知道我的风格可能不是最酷的,但我更喜欢它,因为它不需要任何特殊结构,而且简洁且不难理解:

错误 = (!error) && do_something1(); 错误 = (!error) && do_something2(); 错误 = (!error) && do_something3(); // 清理代码

【讨论】:

  • 如果do_something1() 返回1 然后第二行将error 再次更改为0 并执行do_something3()。这可能不是你想要的。
  • 对于丑陋的加分,#define FOO error+=!error&& 然后FOO do_something1(); ... ;-)
  • 即使使用+=,您也无法确定是哪个do_something 导致错误,只能确定发生错误的数量。
  • 原理相同,但更简洁:error = do_something1() && do_something2() && do_something3()。将 do_somethings 放在单独的行中以提高可读性(无法在评论中弄清楚如何做到这一点)。
【解决方案4】:

尽管通常不鼓励使用 goto,但在像您这样的一些罕见情况下,最佳实践并不是最好的。

所以,如果 goto 的代码最清晰,我会使用它。使用 while(true) 循环来模拟 goto 是不自然的。您真正需要的是 goto!

【讨论】:

  • +1 使用 GOTO 在嵌入式系统中很常见,用于在发生故障后进行清理。
  • +1 不鼓励使用 goto “仅仅因为”。不鼓励避免意大利面条式代码并提高清晰度。真正的规则是函数应该清晰简单,因此使用更难掌握的 goto 替代方案不是 IMO 的好选择。任何涉及一次性循环的东西都属于这一类。
【解决方案5】:

为什么不使用一系列if 语句?我通常这样写,因为我发现它比循环更清晰:

bool ok = true;

do_something();
if (was_an_error()) ok = false;

if (ok)
{
    do_something_else();
    if (was_an_error()) ok = false;
}

if (ok)
{
    do_something_else_again();
    if (was_an_error()) ok = false;
}

[...]

[Cleanup code]

此外,如果您要遵循严格的编码标准,是的,goto 可能会被禁止,但 breakcontinue 通常也是如此,因此循环不一定是解决此问题的方法。

【讨论】:

  • 可以压缩一点:if(ok) {do_something(); ok = was_an_error();}
  • 在实时系统中,这可能会降低性能。您正在执行多个条件跳转,而不仅仅是一个。
  • @Matthew,优化编译器可以发现,如果一个 if (ok) 测试失败,那么所有剩余的测试也会失败,因此它可以直接跳转到退出点。
【解决方案6】:

"break" 理解块作用域的语义,而"goto" 忽略它。换句话说,“while-break”可以翻译成像Lisp这样带有尾递归的函数式语言,“goto”不能。

【讨论】:

    【解决方案7】:

    通常,GOTO 被认为是糟糕的,但在某些只有通过 GOTO 向前跳转的地方,它们并没有那么糟糕。人们避免像瘟疫一样避免 GOTO,但深思熟虑地使用 GOTO 有时是一个更好的解决方案恕我直言。

    【讨论】:

      【解决方案8】:

      我认为 goto 的这种使用(用于资源管理)是可以的。

      【讨论】:

        【解决方案9】:

        如果由于某种原因无法使用 goto,请使用它

        • 在您的项目约定中被禁止
        • 被您的 lint 工具禁止

        我也认为这也是宏不邪恶的情况之一:

        #define DO_ONCE for (int _once_dummy = 0; _once_dummy < 1; _once_dummy++)
        

        【讨论】:

        • 在 C89 中,不允许在循环中声明变量。在 C99 中是允许的。
        【解决方案10】:

        “do while”和“goto out”在这些方面是不同的:

        1.局部变量初始化

        void foo(bool t = false)
        {
            if (t)
            {
                goto DONE;
            }
        
            int a = 10; // error : Goto bypass local variable's initialization 
        
            cout << "a=" << a << "\n";
        DONE:
        }
        

        可以在 do ... while(0) 块中就地初始化局部变量。

        void bar(bool t = false)
        {
            do{
                if (t)
                {
                    break; 
                }
        
                int a = 10;  // fine
        
                cout << "a=" << a << "\n";
            } while (0);
        
        }
        

        2 宏的区别。 “do while”稍微好一点。宏中的“goto DONE”并非如此。 如果退出代码比较复杂,可以这样看:

        err = some_func(...);
        if (err)
        {
            register_err(err, __LINE__, __FUNC__);
        #if defined (_DEBUG)
            do_some_debug(err)
        #endif
            break;
        }
        

        如果你一次又一次地编写这段代码,你可能会将它们放入一个宏中。

        #define QUIT_IF(err)                     \
        if (err)                                       \
        {                                              \
            register_err(err, __LINE__, __FUNC__);     \
            DO_SOME_DEBUG(err)                         \
            break; // awful to put break in macro, but even worse to put "goto DONE" in macro.  \
        }
        

        代码变成:

        do
        {
            initial();
        
            do 
            {
                err = do_step1();
                QUIT_IF(err);
        
                err = do_step2();
                QUIT_IF(err);
        
                err = do_step3();
                QUIT_IF(err);
        
                ....
            } while (0);
            if (err) {     // harder for "goto DONE" to get here while still using macro.
                err = do_something_else();
            }
            QUIT_IF(err);
            .....
        } while (0);
        

        3.do... while(0) 使用相同的宏处理不同级别的退出。代码如上所示。 goto ... 不是 Macro 的情况,因为您需要不同级别的不同标签。

        这么说,我两个都不喜欢。我更喜欢使用异常方法。如果不允许异常,那么我使用“do ... while(0)”,因为整个块是缩进的,实际上它比“goto DONE”样式更容易阅读。

        【讨论】:

          【解决方案11】:

          我喜欢 while(1) 方法。我自己用它。特别是当循环可能通过 continue 重复时,例如当一个元素在这样的循环中被处理时,它是通过多种方法完成的。

          【讨论】:

            【解决方案12】:

            永远不要使用具有永久正确条件的条件循环。既然条件总是为真,为什么要使用条件循环呢?

            永久正确的条件最直接地由 goto 表示。

            【讨论】:

            • 我发现 while(1)+break+continue 对我来说完全可读.. 与 goto 相反,这是 IMO 不可读的。
            • @emg-2 我可以同时阅读。但是“while(1)”向我传达了一个关于无限循环的错误想法(我在其他情况下看到过),而不是一个只运行一次的简单函数。 “goto”更适合这个目的。所有函数都是序列、循环或条件。
            • 所以你是说应用程序的消息循环最好用转到消息处理程序的开头而不是 while(1) 或 for(;;) 来表示?
            • 他的意思是,循环应该有一个终止条件。特别是如果——就像这里的情况——你根本不打算循环。对于消息处理循环,您确实打算在某个时候离开它,对吗?那么为什么不将终止条件构建到循环条件中呢?
            • @emg-2: 读者容易阅读或不易阅读代码的能力与该代码的语义含义无关 - 在这里你是在争论使用语义不合适的代码的基础上可读性 - 这是矛盾的
            【解决方案13】:

            虽然在错误处理情况下使用“goto”相当普遍,但我仍然更喜欢“while”解决方案(或“do while”)。在“goto”的情况下,编译器可以保证的事情要少得多。如果您在标签名称中打错字,编译器将无法帮助您。如果有人在该块中使用另一个 goto 到另一个标签,则很有可能不会调用清理代码。当您使用更结构化的流控制结构时,您始终可以保证在循环结束后将运行哪些代码。

            【讨论】:

            • 编译器可以轻松捕获标签拼写错误:temp.c:9: error: label 'blarg4h' used but not defined temp.c:7: warning: label 'blargh' defined but not used跨度>
            猜你喜欢
            • 1970-01-01
            • 2014-01-14
            • 1970-01-01
            • 1970-01-01
            • 2016-06-01
            • 1970-01-01
            • 1970-01-01
            • 2012-11-12
            • 1970-01-01
            相关资源
            最近更新 更多