我应该如何跳出循环?

3
每当我需要从C++的 for(unsigned int i=0;i<bound;++i) 表达式中跳出时,我会简单地将索引变量 i=bound,就像这个 答案 中描述的那样。我倾向于避免使用 break 语句,因为说实话,我并不是很明白它到底是做什么。
比较一下这两个指令:
for(unsigned int i=0;i<bound;++i) {
    if (I need a break) {
    break;
    }
}

并且

for(unsigned int i=0;i<bound;++i) {
    if (I need a break) {
    i=bound;
    }
}

我猜测第二种方法会多做一次变量设置和一次ibound的比较,因此从性能角度来看,它看起来更昂贵。那么调用break是否更便宜,而不是执行这两个测试呢?编译后的二进制文件有什么不同吗?在任何情况下,第二种方法会中断吗?我可以安全地选择这两种替代方案中的任何一种吗?
相关:`break`语句只适用于`for`、`while`、`do-while`、`switch`和`if`语句吗?

在没有`break`语句的情况下退出循环[C]


3
“break” 的意思是程序执行会跳到最内层循环后的第一条语句。 - M.M
7
为了回答你的性能问题,比较两个版本(启用优化)生成的汇编代码。 - M.M
6
第二种方法似乎很危险,因为有一天循环限制变量将从“bound”更改为“size”,你可能会忘记替换第二个“bound”。 - atturri
3
“我并不是特别理解它到底是做什么的。” 这很奇怪,因为这是最简单直接的结构之一。它只是立即跳出循环。 - Matteo Italia
4
  1. 使用更清晰表达意图的结构(并学习“break”语句的行为——对大多数程序员来说可能更清晰)。
  2. 可能没有任何重要的性能差异。
  3. 可能存在行为差异(特别是如果循环控制变量不是局部变量),请注意这点。
  4. 正如其他人所指出的,这可能会影响未来代码维护。
- Michael Burr
5个回答

7

使用 break 会更具有未来性和逻辑性。

考虑以下示例,

for (i = 0; i < NUM_OF_ELEMENTS; i++)
{
     if(data[i] == expected_item)
         break;
} 

printf("\n Element %d is at index %d\n", expected_item, i);

但是第二种方法在这里不会有用。

7
此时考虑性能是过早的优化。你应该选择更清晰的方式,使用 break 关键字即可。 - Benjamin Barenblat
很有见地。我想这又是一种个人偏好和编码风格,我不会在循环范围之外移动索引变量。 - Matsmath
与另一个答案中的此评论所问的相同问题。 - Sourav Ghosh
@MatteoItalia:这取决于您是否需要稍后使用该索引。如果不需要,您应该在循环本身中声明它,以保证严格的局部性。 - too honest for this site
@MatteoItalia:抱歉,我不明白你所说的“keep”和“pull out”的意思。 - too honest for this site
显示剩余2条评论

5

我想到了三个主要的技术差异:

  • as other have stated, if your index variable is not confined to the for scope break leaves it intact, while your method destroys its content; when you are searching e.g. an array with break the code is more concise (you don't have to keep an extra variable to write down where you stopped);
  • break quits the loop immediately; your method requires you to execute the rest of the body. Of course you can always write:

    for(int i=0; i<n; ++i) {
        if(...) {
            i=n;
        } else {
            rest of the loop body
        }
    }
    

    but it adds visual and logical clutter to your loop;

  • break is almost surely going to be translated to a simple jmp to the instruction just following the loop (although, if you have block-scoped variables with a destructor the situation may be more complicated); your solution is not necessarily recognized by the compiler as equivalent.

    You can actually see it here that gcc goes all the way to generate the code that moves n into i, while in the second case it jumps straight out of the loop.

在风格方面:

  • 我认为“your way”过于复杂,不符合惯用语法 - 如果我在实际代码中遇到它,我会问自己“为什么他不直接使用break?”,然后检查两次以确保这不是我错过了一些副作用,而意图实际上只是跳出循环;
  • 正如其他人所说,你内部赋值存在失去与实际循环条件同步的风险;
  • 当循环条件变得比简单的范围检查更加复杂时,它不具有可扩展性,无论是在逻辑方面(如果循环条件很复杂,那么欺骗它可能会变得更加复杂),还是在性能方面(如果循环条件很昂贵,并且您已经知道要退出,则不希望再次检查它);这也可以通过添加额外的变量来解决(通常在缺乏break的语言中完成),但这又是从您的算法实际执行的任务中分心的额外干扰;
  • 它在基于范围的循环中根本不起作用。

非常感谢这个汇编输出,它确实让我朝着正确的方向前进。 - Matsmath
一个更好的例子是两个具有相同行为的函数,因此如果编译器“看穿”这个奇怪的习惯用法,它们可以编译成相同的汇编代码。我在你的例子中将ret += arr[i]移动到了if上面。结果,gcc 6.1使用cmov来处理if,这将把循环计数器依赖链的长度从1个周期增加到3个周期(Intel pre-Broadwell)或2个周期(Broadwell及以后)。没有展开的情况下,“正常”版本可以每2个周期运行一次,但i=c版本只能每3个周期运行一次(pre-Broadwell)。 - Peter Cordes
@PeterCordes:糟糕,这绝对不是我的意图;我会尽快修复链接。 - Matteo Italia
这是一个为什么'i=c'惯用语很糟糕的完美例子!它已经导致了在回答中谈论它的简单代码中的一个错误。当您更新链接时,应该省略'-ansi'。如果您指定了'-std=c++11',那么这是没有意义的。(我使用了'-xc -std=c11',因为Godbolt不直接提供C编译器)。'-m32'也是不必要的。64位ABI将参数传递给寄存器,所以噪音较小。 - Peter Cordes

3
使用break是惯用的做法,除非有某种原因可以使用相对晦涩难懂的替代方案来为下面的逻辑设置舞台。即使如此,我仍然更喜欢在循环结束后进行变量设置,以便更清晰地接近其使用。
我无法想象出性能足以引起担忧的场景。也许一个更加复杂的例子可以证明这一点。正如所述,答案几乎总是“测量,然后调整”。

3

我更喜欢使用break;,因为它可以保留循环变量的值。
在搜索时,我经常使用这种形式:

int i;
for(i=0; i<list.size(); ++i)
{
    if (list[i] == target) // I found what I'm looking for!
    {
        break;  // Stop searching by ending the loop.
    }
}

if (i == list.size() ) // I still haven't found what I'm looking for -U2
{
    // Not found.
}
else
{
    // Do work with list[i]. 
}

编译后的二进制文件是否不同?
几乎肯定是不同的。
(尽管优化器可能会识别您的模式,并将它们缩减到几乎相同)
break; 语句很可能是一个汇编“跳转”语句,用于跳转到列表之外的下一条指令,同时保持控制变量不变。

在非优化代码中对变量进行赋值将导致对控制变量的赋值、测试该变量,然后跳转以结束循环。

正如其他人所提到的那样,在将变量分配为其最终值时,如果将来您的循环条件发生更改,则不太具有未来性。

总的来说,当你说:
"我对它实际上做了什么没有好的理解。(所以我使用了解决方法)",

我会回答:
"花时间学习它到底是什么!作为程序员的主要职责之一就是学习知识."


你确定你可以在循环外部访问变量 i 吗? - Sourav Ghosh
@SouravGhosh 在上面的代码中,他不能这样做。但是你可以在循环之前定义i。int i = 0; for(i = 0... - Fred
@abelenky 对,但如果我们按照OP的代码,这个答案有什么意义呢?(总的来说,你是非常正确的) - Sourav Ghosh
非常感谢您的回答。尽管从您的帖子中并不清楚无条件指令跳转在性能上是便宜还是昂贵。那就是我想学习的东西。 - Matsmath
你认为如果break;在语言中具有巨大的性能成本,它会被包括作为关键字吗?每个单独的汇编指令的成本应该被认为是非常便宜的。昂贵的代码来自于将指令组合成极其低劣的方式(例如冒泡排序,大规模复制内存,已排序数据的线性搜索等)。成本不是来自单个指令。 - abelenky
这样的结构让我想在C语言中使用Pythonic的for ... else - too honest for this site

1

除了使用break语句退出for或[do] while循环外,还允许使用goto语句来退出嵌套循环,例如:

    for (i=0; i<k; i++) {
        for (j=0; j<l; j++) {
            if (someCondition) {
                goto end_i;
            }
        }
    }
  end_i:

的确,我曾经遇到过一个非常棘手的算法,在没有其他(简单)方法的情况下,只能依赖老式的“goto”调用。这启发了我思考这个相关问题 - Matsmath
@Matsmath,针对你提供的状态机参考,我会编写一个C程序来解析状态定义并生成一个C语言状态机。由于这是机器生成的代码,其中充满了goto语句并不重要(但你必须证明生成器是正确的)。 - Paul Ogilvie
Perl在这方面添加了语法糖:你可以给外层循环加上标签并使用last LABEL。但无论如何,如果你理解goto的概念,那么break就等同于跳转到循环外的标签。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接