从循环中调用的函数中跳出循环

Breaking out of a loop from within a function called in that loop

我目前正在尝试找出一种方法来从 for 循环中调用的函数中跳出该循环。我知道有可能只让函数 return 有一个值,然后检查一个特定的值然后中断,但我想直接从函数内部进行。

这是因为我正在为特定硬件使用内部库,该库要求我的函数的函数签名如下所示:

void foo (int passV, int aVal, long bVal)

我知道不使用 return 值是 非常糟糕的做法,但是环境迫使我这样做,所以请多多包涵。

考虑以下示例:

#include <stdio.h>

void foo (int a) {
    printf("a: %d", a);
    break;
}

int main(void) {
    for (int i = 0; i <= 100; i++) {
        foo(i);
    }
    return 0;
}

现在无法编译。相反,我得到如下编译错误:

prog.c: In function 'foo': prog.c:6:2: error: break statement not within loop or switch break;

我知道这是什么意思(编译器说 foo() 中的中断不在 for 循环内)

现在,我可以从标准中找到关于 break 语句的内容是:

The break statement causes control to pass to the statement following the innermost enclosing while, do, for, or switch statement. The syntax is simply break;

考虑到我的函数是从 for 循环中调用的,为什么 break 语句没有跳出所述 for 循环?此外,是否可以在没有函数 return 的情况下实现这样的功能?

break 是在编译时解析的语句。因此,编译器必须在同一个函数中找到合适的 for/while 循环。请注意,不能保证无法从其他地方调用该函数。

break,和goto一样,只能在同一个函数内进行局部跳转,但是如果实在不行,可以使用setjmplongjmp

#include <stdio.h>
#include <setjmp.h>

jmp_buf jump_target;

void foo(void)
{
    printf("Inside foo!\n");
    longjmp(jump_target, 1);
    printf("Still inside foo!\n");
}

int main(void) {
    if (setjmp(jump_target) == 0)
        foo();
    else
        printf("Jumped out!\n");
    return 0;
}

longjmp 的调用将导致跳回到 setjmp 调用。来自 setjmp 的 return 值显示它是在设置跳转目标后 returning,还是从跳转开始 returning。

输出:

Inside foo!
Jumped out!

非本地跳转在正确使用时是安全的,但有许多事情需要仔细考虑:

  • 由于 longjmp 跳转 "through" setjmp 调用和 longjmp 调用之间的所有函数激活,如果这些函数中的任何一个期望能够执行额外的操作在当前执行位置之后工作,该工作将根本无法完成。
  • 如果调用 setjmp 的函数激活已终止,则行为未定义。任何事情都有可能发生。
  • 如果尚未调用 setjmp,则未设置 jump_target,并且行为未定义。
  • 调用 setjmp 的函数中的局部变量在某些情况下可以具有未定义的值。
  • 一个词:线程。
  • 其他事情,例如可能不会保留浮点状态标志,以及对可以放置 setjmp 调用的位置有限制。

如果您对非本地跳转在机器指令和 CPU 寄存器级别上的作用有很好的理解,那么其中的大部分内容自然而然,但除非您有这些, 已阅读 C 标准所做的和不保证的内容,我建议您谨慎一些。

你不能这样使用 break;,它必须出现在 for 循环体中。

有几种方法可以做到这一点,但都不推荐:

  • 您可以使用exit()函数退出程序。由于循环是 main() 中的 运行 并且您在它之后不做任何事情,因此可以通过这种方式实现您想要的,但它是一种特殊情况。

  • 您可以在函数中设置一个全局变量,并在函数调用后的 for 循环中对其进行测试。通常不推荐使用全局变量。

  • 你可以使用setjmp()longjmp(),但这就像试图用锤子压扁一只苍蝇,你可能会破坏其他东西而完全错过苍蝇。我不推荐这种方法。此外,它需要一个 jmpbuf,您必须将其作为全局变量传递给函数或访问。

可接受的替代方法是将 status 变量的地址作为额外参数传递:函数可以设置它以指示需要从循环中中断。

但到目前为止,C 中最好的方法是返回一个值来测试连续性,这是最可读的。

从您的解释来看,您没有 foo() 的源代码,但可以检测到函数中的某些条件,您可以修改这些函数并由 foo() 直接或间接调用:longjmp() 将从它的位置跳转,在 [=19= 的内部深处],可能是调用堆栈的许多级别,到 setjmp() 位置,绕过所有中间调用的常规函数​​退出代码。如果这正是您需要做的以避免崩溃,setjmp() / longjmp() 是一个解决方案,但它可能会导致其他问题,例如资源泄漏、缺少初始化、不一致的状态和其他未定义行为的来源.

请注意,您的 for 循环将迭代 101 次,因为您使用了 <= 运算符。惯用的 for 循环使用 for (int i = 0; i < 100; i++) 来精确迭代显示为上(排除)边界的次数。

我认为这与如何将 break 语句翻译成机器代码有关。 break 语句将被转换为紧跟在循环或切换之后的标签的无条件分支。

mov ECX,5
label1:
  jmp <to next instruction address>  ;break
loop label1
<next instruction>

虽然从循环内部调用 foo() 会产生类似

的结果
mov ECX,5
label1:
  call <foo address>
loop label1
<next instruction>

foo地址

call <printf address>
jmp <to where?> ;break cannot translate to any address.

如果您不能使用 break 指令,您可以在您的模块中定义一个局部变量,并在 for 循环中添加第二个 运行 条件。例如像下面的代码:

#include <stdio.h>
#include <stdbool.h>

static bool continueLoop = true;

void foo (int a)
{
    bool doBreak = true;

    printf("a: %d",a);

    if(doBreak == true){
        continueLoop = false;
    }
    else {
        continueLoop = true;
    }
}
int main(void) {
    continueLoop = true;   // Has to be true before entering the loop
    for (int i = 0; (i <= 100) && continueLoop; i++)
    {
        foo(i);
    }
    return 0;
}

请注意,在此示例中,这不完全是 break 指令,但 for 循环将不会执行另一次迭代。如果你想做一个 break 你必须插入一个 if 条件与变量 continueLoop 导致 break:

int main(void) {
    continueLoop = true;   // Has to be true before entering the loop
    for (int i = 0; i <= 100; i++)
    {
        foo(i);
        if(!continueLoop){
            break;
        }
    }
    return 0;
}

考虑在 for 循环中手动内联您的函数。如果在多次循环中调用此函数,则将其定义为宏:

#define f()\
printf("a: %d", a);\
break;

这是另一个可能可行也可能不可行的想法:保留一个可以将 foo 变成空操作的变量:

int broken = 0;

void foo (int a) {
    if (broken) return;

    printf("a: %d", a);
    broken = 1; // "break"
}

int main(void) {
    for (int i = 0; i <= 100; i++) {
        foo(i);
    }
    return 0;
}

这在功能上是相同的,只是丢失了一些时钟周期(该函数将被调用,但仅执行 if 语句),并且无需更改循环。它不是线程安全的,只在第一次工作(但如果需要,foo 可以将 broken 变量重置为 0,如果使用 a 等于 0 调用的话)。

不太好,但一个尚未提及的想法。

(注意:自从我最初写这篇文章以来,问题已经被编辑过)

由于 C 的编译方式,它必须知道在调用函数时中断到哪里。因为你可以从任何地方调用它,甚至在某个地方 break 是没有意义的,你不能在你的函数中有一个 break; 语句并让它像这样工作。

其他答案提出了糟糕的解决方案,例如设置全局变量,在函数外使用 #define 或 longjumping(!)。这些都是非常糟糕的解决方案。相反,您应该使用您在开头段落中错误忽略的解决方案和 return 函数中的一个值,该值表示您想要在这种情况下触发 break 的状态,并执行如下操作:

#include <stdbool.h>

bool checkAndDisplay(int n)
{
    printf("%d\n", n);
    return (n == 14);
}

int main(void) {
    for (int i = 0; i <= 100; i++) {
        if (checkAndDisplay(i))
            break;
    }
    return 0;
}

试图找到晦涩的方法来实现这样的事情,而不是使用正确的方法来实现相同的最终结果,这是生成可怕质量代码的必经之路,是维护和调试的噩梦。

你提到,隐藏在评论中,你必须使用 void return,这不是问题,只需将 break 参数作为指针传递:

#include <stdbool.h>

void checkAndDisplay(int n, bool* wantBreak)
{
    printf("%d\n", n);
    if (n == 14)
        wantBreak = true;
}

int main(void) {
    bool wantBreak = false;
    for (int i = 0; i <= 100; i++) {
        checkAndDisplay(i, &wantBreak);
        if (wantBreak)
            break;
    }
    return 0;
}

由于您的参数是固定类型,我建议您使用强制转换将指针传递给其中一个参数,例如foo(a, b, (long)&out);

根据您更新的问题清楚地列出限制,我建议您将整个循环移动到您的函数中,然后在该函数中调用具有 return 值的第二个函数,例如

#include <stdbool.h>

bool foo (int x)
{
    return (x==14);
}

void loopFoo(int passV, int aVal, long bVal)
{
   for (int i = 0; i <= 100; ++i)
   {
       if (foo(x))
           break;
   }
}

这避免了任何极端和脆弱的体操来绕过限制。

在这种情况下,请考虑使用 while() 循环和多个用 && 链接的条件语句,而不是 for 循环。尽管您 可以 使用 setjmp 和 longjmp 等函数改变正常的控制流,但它在任何地方都被认为是不好的做法。您不必在此站点上费力搜索即可找出原因。 (简而言之,这是因为它能够创建不利于调试或人类理解的复杂控制流)

也可以考虑这样做:

int foo (int a) {
    if(a == someCondition) return 0;
    else {
        printf("a: %d", a);
        return 1;
    }
}

int main(void) {
    for (int i = 0; i <= 100; i++) {
        if(!foo(i)) break;
    }
    return 0;
}

在这种情况下,循环取决于从 'foo' 返回的真值,如果不满足 'foo' 内的条件,循环将中断。

编辑:我没有明确反对使用 goto、setjmp、longjmp 等。但我认为在这种情况下,有一个更简单、更简洁的解决方案,无需诉诸这些措施!

只需设置一个全局变量并在循环中检查:

#include <stdio.h>

int leave = 0;

void foo (int a) {
    printf("a: %d", a);
    leave = 1;
}

int main(void) {
    for (int i = 0; i <= 100; i++) {
        foo(i);
        if (leave)
          break;
    }
    return 0;
}

您可以在循环内的函数中抛出错误,并在循环外捕获该错误。

#include <stdio.h>

void foo (int a) {
    printf("a: %d", a);
    if (a == 50)
    {
       throw a;
    }
}

int main(void) {
    try {
        for (int i = 0; i <= 100; i++) {
            foo(i);
        }
    catch(int e) {
    }
    return 0;
}

如果您无法处理 return 值,您能否至少向该函数添加一个参数: 我可以想象这样的解决方案:

void main (void)
{
  int a = 0;

  for (; 1 != a;)
  {
    foo(x, &a);
  } 
}

void foo( int x, int * a)
{
  if (succeeded)
  {
    /* set the break condition*/
    *a = 1;
  }
  else
  {
    *a = 0;
  }
}

这是我的第一个post,所以,如果我的格式有问题,请原谅我:)

这个问题已经得到解答,但我认为值得深入研究在 C++ 中退出循环的所有可能选项。基本上有五种可能:

  • 使用循环条件
  • 使用 break 条件
  • 使用 return 条件
  • 使用例外
  • 使用goto

在下文中,我将使用 c++14 描述这些选项的用例。但是,您可以在早期版本的 c++ 中执行所有这些操作(例外情况除外)。为了简短起见,我将省略 includes 和 main 函数。如果您认为某些部分需要更清楚,请发表评论。

1。使用循环条件

退出循环的标准方法是循环条件。循环条件写在for语句的中间部分,或者while语句的括号之间:

for(something; LOOP CONDITION; something) {
    ... 
}
while (LOOP CONDITION)
    ... 
}
do {
    ... 
} while (LOOP CONDITION);

循环条件决定是否进入循环,是否重复循环。在上述所有情况下,条件必须为 true,才能重复循环。

举个例子,如果我们想输出从0到2的数字,我们可以使用循环和循环条件来编写代码:

for (auto i = 0; i <= 2; ++i)
    std::cout << i << '\n';
std::cout << "done";

这里的条件是i <= 2。只要此条件的计算结果为 true,循环就会保持 运行.

另一种实现方式是将条件放入变量中:

auto condition = false;

for (auto i = 0; !condition; ++i) {
    std::cout << i << '\n';
    condition = i > 2;
}
std::cout << "done";

检查两个版本的输出,我们得到了想要的结果:

0
1
2
done

您将如何在实际应用程序中使用循环条件?

这两个版本都在 c++ 项目中广泛使用。重要的是要注意,第一个版本更紧凑,因此更容易理解。但如果条件更复杂或需要几个步骤来评估,通常使用第二个版本。

例如:

auto condition = false;
for (auto i = 0; !condition; ++i)
    if (is_prime(i))
        if (is_large_enough(i)) {
            key = calculate_cryptographic_key(i, data);
            if (is_good_cryptographic_key(key))
                condition = true;
        }

2。使用 break 条件

另一种退出循环的简单方法是使用break关键字。如果在循环内部使用,会停止执行,在循环体之后继续执行:

for (auto i = 0; true; ++i) {
    if (i == 3)
        break;
    std::cout << i << '\n';
}
std::cout << "done";

这将输出当前数字,并将其递增 1,直到 i 达到值 3。这里的 if 语句是我们的 break 条件。如果条件是 true,则循环被中断(注意 !)并继续执行下一行,打印 done

进行测试,确实得到了预期的结果:

0
1
2
done

重要的是,这只会停止代码中最内层的循环。因此,如果您使用多个循环,可能会导致不良行为:

for (auto j = 0; true; ++j)
    for (auto i = 0; true; ++i) {
        if (i == 3)
            break;
        std::cout << i << '\n';
    }
std::cout << "done";

使用此代码,我们希望获得与上述示例相同的结果,但我们却得到了一个无限循环,因为 break 仅停止了 i 的循环,而不是一比 j!

正在做测试:

0
1
2
0
1
2
...

您将如何在实际应用程序中使用 break 条件?

通常break仅用于跳过部分内循环,或添加额外的循环出口。

例如,在测试素数的函数中,一旦发现当前数字不是素数的情况,您将使用它跳过其余的执行:

auto is_prime = true;
for (auto i = 0; i < p; ++i) {
    if (p%i == 0) { //p is dividable by i!
        is_prime = false;
        break; //we already know that p is not prime, therefore we do not need to test more cases!
    }

或者,如果您要搜索字符串向量,通常会将数据的最大大小放在循环头中,如果您确实找到了要搜索的数据,则使用附加条件退出循环。

auto j = size_t(0);
for (auto i = size_t(0); i < data.size(); ++i)
    if (data[i] == "Hello") { //we found "Hello"!
        j = i;
        break; //we already found the string, no need to search any further!
    }

3。使用 return 条件

return 关键字退出当前作用域并returns 到调用函数。因此它可用于退出循环,此外,还可以将一个号码返回给调用者。一个常见的情况是使用 return 退出循环(及其功能)并 return 结果。

例如我们可以重写上面的is_prime函数:

auto inline is_prime(int p) {
    for (auto i = 0; i < p; ++i)
        if (p%i == 0) //p is dividable by i!
            return false; //we already know that p is not prime, and can skip the rest of the cases and return the result
    return true; //we didn't find any divisor before, thus p must be prime!
}

关键字return也可以用来退出多个循环:

auto inline data_has_match(std::vector<std::string> a, std::vector<std::string> b) {
    for (auto i = size_t(0); i < a.size(); ++i)
        for (auto j = size_t(0); j < a.size(); ++j)
            if (a[i] == b[j])
                return true; //we found a match! nothing to do here
    return false; //no match was found
}

您将如何在实际应用程序中使用 return 条件?

在较小的函数中,return 常用于退出循环并直接得到 return 结果。此外,在较大的函数中,return 有助于保持代码清晰易读:

for (auto i = 0; i < data.size(); ++i) {
    //do some calculations on the data using only i and put them inside result
    if (is_match(result,test))
        return result;
    for (auto j = 0; j < i; ++j) {
        //do some calculations on the data using i and j and put them inside result
        if (is_match(result,test))
            return result;
    }
}
return 0; //we need to return something in the case that no match was found

这比以下更容易理解:

auto break_i_loop = false;
auto return_value = 0;
for (auto i = 0; !break_i_loop; ++i) {
    //do some calculations on the data using only i and put them inside result
    if (is_match(result,test)) { //a match was found, save the result and break the loop!
        return_value = result;
        break;
    }
    for (auto j = 0; j < i; ++j) {
        //do some calculations on the data using i and j and put them inside result
        if (is_match(result,test)) { //a match was found, save the result, break the loop, and make sure that we break the outer loop too!
            return_value = result;
            break_i_loop = true;
            break;
        }
    }
    if (!break_i_loop) //if we didn't find a match, but reached the end of the data, we need to break the outer loop
        break_i_loop = i >= data.size();
}
return return_value; //return the result

4。使用例外

异常是在代码中标记异常事件的一种方式。例如,如果你想从文件中读取数据,但由于某种原因该文件不存在!异常可用于退出循环,但编译器通常会生成大量样板代码,以便在处理异常时安全地继续执行程序。因此,不应将异常用于 return 值,因为它非常低效。

您将如何在实际应用程序中使用异常?

异常用于处理真正异常的情况。例如,如果我们想计算数据的倒数,我们可能会尝试除以零。然而这对我们的计算没有帮助,因此我们写:

auto inline inverse_data(std::vector<int>& data) {
    for (auto i = size_t(0); i < data.size(); ++i)
        if (data[i] == 0)
            throw std::string("Division by zero on element ") + std::to_string(i) + "!";
        else
            data[i] = 1 / data[i];
}

我们可以在调用函数中处理这个异常:

while (true)
    try {
        auto data = get_user_input();
        inverse = inverse_data(data);
        break;
    }
    catch (...) {
        std::cout << "Please do not put zeros into the data!";
    }

如果data为零,则inverse_data会抛出异常,break永远不会执行,需要用户重新输入数据。

对于这种错误处理还有更多高级选项,以及其他错误类型,...,但这是另一天的话题。

** 你永远不应该做的事! **

如前所述,异常会产生显着的运行时开销。因此,它们只应在真正例外的情况下使用。虽然可以写出下面的函数,但是请不要!

auto inline next_prime(int start) {
    auto p = start;
    try {
        for (auto i = start; true; ++i)
            if (is_prime(i)) {
                p = i;
                throw;
            }
   }
   catch (...) {}
   return p;
 }

5。使用 goto

大多数程序员都讨厌 goto 关键字,因为它使代码更难阅读,并且可能产生意想不到的副作用。但是,它可用于退出(多个)循环:

for (auto j = 0; true; ++j)
    for (auto i = 0; true; ++i) {
        if (i == 3)
            goto endloop;
        std::cout << i << '\n';
    }
endloop:
std::cout << "done";

这个循环将结束(不像第二部分的循环),并输出:

0
1
2
done

您将如何在实际应用程序中使用 goto

在 99.9% 的情况下不需要使用 goto 关键字。唯一的例外是嵌入式系统,如 Arduino 或非常高性能的代码。如果您正在使用这两者之一,您可能希望使用 goto 来生成更快或更高效的代码。然而,对于日常程序员来说,缺点比使用 goto.

的好处要大得多

即使您认为您的情况是 0.1% 之一,您也需要检查 goto 是否真的提高了您的执行力。通常情况下,使用 breakreturn 条件会更快,因为编译器更难理解包含 goto.

的代码