break、continue的使用

5 阅读6分钟

对于 while 循环,最简单的记忆口诀是:

  • continue:不跑了,直接回起点(判断条件),准备下一圈。
  • break:不玩了,直接跳到终点(循环体外),彻底结束。

为了让你看得更清楚,我结合刚才的 epoll 代码逻辑画个图来演示。


1. continue 的跳转路径

含义:立即停止当前这一次循环体中剩余代码的执行,直接跳转回 while条件判断处,看看是否还要进行下一次循环。

代码场景:刚才代码中处理 epoll_wait 被信号中断(EINTR)的情况。

C++

    // ↓↓↓↓↓↓↓ [跳转点 A]:continue 会跳回这里进行条件判断
    while (true) { 

        // 5) 等待事件
        int nready = ::epoll_wait(epfd, events, 1024, -1);
        
        if (nready < 0) {
            if (errno == EINTR) {
                // 遇到 continue
                // 动作:跳过下面所有的代码,直接飞回 [跳转点 A]
                continue; 
            }
            // 这里的代码被跳过
            perror("epoll_wait");
            break; 
        }

        // 这里的代码也被跳过
        for (int i = 0; i < nready; i++) {
            // ...
        }

    } // while 结束括号
  • 形象比喻:你在操场跑步(循环)。跑到一半鞋带散了(满足 if 条件),你没有退赛,而是停下当前的步伐,直接回到起跑线,重新开始跑下一圈。

2. break 的跳转路径

含义:立即终止整个循环,跳出循环体,程序执行流将来到 while 循环大括号后面的第一行代码。

代码场景:刚才代码中处理 accept 获取新连接的情况(ET 模式循环读取)。

C++

                while (true) {
                    sockaddr_in cli{};
                    socklen_t len = sizeof(cli);
                    int connfd = ::accept(listenfd, (sockaddr*)&cli, &len);

                    if (connfd < 0) {
                        // 如果 accept 返回 -1,且 errno 是 EAGAIN
                        // 说明连接都取完了
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                             // 遇到 break
                             // 动作:直接跳到 [跳转点 B]
                             break;
                        }
                        perror("accept");
                        break; // 这里的 break 也是跳到 [跳转点 B]
                    }

                    // 这里的代码被跳过(当执行了 break 时)
                    set_nonblocking(connfd);
                    // ... 注册 epoll 等

                } // while 结束括号

                // ↓↓↓↓↓↓↓ [跳转点 B]:break 会跳到这里
                // 程序继续向下执行...
                
                continue; // (这是外层 for 循环的 continue)
  • 形象比喻:你在操场跑步。突然下大雨了(满足 break 条件),你决定彻底不跑了,直接离开跑道,去更衣室(循环体外的代码)。

3. 进阶:嵌套循环中的“陷阱”

在你的 epoll 代码中,其实是一个双层循环结构。break 只能跳出当前所在的那一层循环,不能跨层跳出。

我们来看这个结构:

C++

while (true) { // 【外层循环】: epoll 主循环

    int nready = epoll_wait(...);

    for (int i = 0; i < nready; i++) { // 【中层循环】: 遍历事件
        
        if (fd == listenfd) {
            
            while (true) { // 【内层循环】: 处理 accept
                int connfd = accept(...);
                if (connfd < 0) {
                    if (errno == EAGAIN) {
                        break; // 1. 这里的 break 跳出的是【内层循环】
                    }
                }
            } 
            // 2. break 跳到了这里
            
            continue; // 3. 这里的 continue 作用于【中层循环】(for)
                      //    它会跳过本次 for 迭代,让 i++,处理下一个事件
        }
    }
    // ...
}

关键点总结

  1. 内层的 break:只杀死了 accept 的那个 while 循环。程序回到了 for 循环里继续跑。
  2. 外层的 continue(如果写在 for 里):跳过当前的 fd 处理,去处理 events 数组里的下一个 fd
  3. 最外层的 continue(如果写在 while(true) 里):跳回到 epoll_wait 继续等待。

小测验

假设代码如下,输出是什么?

C++

int i = 0;
while (i < 5) {
    i++;
    if (i == 2) {
        continue;
    }
    if (i == 4) {
        break;
    }
    std::cout << i << " ";
}
std::cout << "End";

解析

  1. i=1: 输出 1
  2. i=2: 遇到 continue -> 跳回起点 (不输出)
  3. i=3: 输出 3
  4. i=4: 遇到 break -> 跳到 End (直接结束,不输出 4)
  5. 最终输出: 1 3 End

这样解释清晰吗?如果你在处理多层嵌套循环时想一次性跳出所有循环(比如遇到严重错误直接重启),C++ 里通常会用 goto 或者 return,需要我展开讲讲吗? 这是一个非常实用的工程问题。在像 epoll 服务器这种多层嵌套(While -> For -> While)的结构中,如果最内层检测到了致命错误(比如内存耗尽、配置文件损坏),单纯用 break 只能跳出一层,还得在每一层外做判断,代码会写得非常难看。

以下是三种常见的“逃生”方案,按推荐程度从高到低排列:

1. 使用 return (最推荐,如果代码封装得当)

这是最干净、最现代的做法。

前提:你需要将核心逻辑封装在一个独立的函数中(例如 runServer()),而不是全部堆在 main 函数里。

逻辑:一旦遇到致命错误,直接结束整个函数。

C++

void runServer(int epfd, int listenfd) {
    while (true) { // 第一层
        int n = epoll_wait(...);
        for (int i = 0; i < n; ++i) { // 第二层
            while (true) { // 第三层
                // ... 业务逻辑 ...
                
                if (critical_error) {
                    std::cerr << "Fatal Error! Stopping server." << std::endl;
                    return; // 💥 直接杀掉 runServer 函数,回到 main
                }
            }
        }
    }
}

int main() {
    // ... 初始化 ...
    runServer(epfd, listenfd);
    // ... 资源清理 ...
    return 0;
}
  • 优点:逻辑清晰,自动利用 C++ 的析构函数清理栈上对象(RAII)。
  • 缺点:如果函数结尾还有必须要执行的清理代码(且没用 RAII 封装),return 可能会跳过它们。

2. 使用 goto (在 C/C++ 系统编程中非常常见)

虽然教科书通常告诉我们“不要用 goto”,但在 Linux 内核源码RedisNginx 等底层 C/C++ 项目中,goto 被大量用于统一的错误处理和深层循环跳出。

逻辑:定义一个标签(Label),直接“传送”过去。

C++

int main() {
    // ... 初始化 ...

    while (true) { // 第一层
        // ...
        for (int i = 0; i < n; ++i) { // 第二层
            // ...
            while (true) { // 第三层
                
                if (fatal_error) {
                    // 🚀 启动传送门
                    goto Cleanup_And_Exit;
                }
                
                // 普通错误只用 break
                if (normal_error) break; 
            }
        }
    }

    // 这是一个标签(Label),以冒号结尾
Cleanup_And_Exit:
    std::cout << "Server shutting down due to fatal error..." << std::endl;
    close(epfd);
    close(listenfd);
    return 1;
}
  • 优点:简单粗暴,无需引入额外的标志变量,专门解决深层嵌套跳出。在 C++ 中,向外跳转(Jump out)依然会正确触发局部对象的析构函数,所以是安全的。
  • 缺点:滥用会导致“面条代码”(逻辑混乱)。原则是:只能向下跳(跳去结尾),不能往回跳。

3. 使用 bool 标志位 (最“规范”但最繁琐)

这是某些严格禁止 goto 的编程规范(如某些公司的老旧规范)要求的做法。

逻辑:在每一层循环都检查一个标志位。

C++

bool is_running = true; // 标志位

while (is_running) { // 第一层
    int n = epoll_wait(...);
    
    for (int i = 0; i < n; ++i) { // 第二层
        if (!is_running) break; // 🔍 每一层都要检查

        while (true) { // 第三层
            if (fatal_error) {
                is_running = false; // 1. 修改标志
                break;              // 2. 跳出第三层
            }
        }
        
        // break 出来到了这里
    }
    
    // break 出来到了这里,while(is_running) 检测为 false,终止
}
  • 优点:绝对符合所有编程教科书的规范。
  • 缺点啰嗦。如果嵌套了 4 层,你就要写 4 次 check 和 break,代码阅读体验极差,增加了 CPU 分支预测的开销(虽然很微小)。

总结建议

  1. 首选 return:尽量把你的 while(true) 循环封装在一个函数里(比如 EventLoop::run()),遇到大事直接 return。这是面向对象设计的最佳实践。
  2. 次选 goto:如果你在一个很长的 main 函数里写脚本式的工具代码,或者在写纯 C 风格的底层库,用 goto error_exit 是完全可以接受且专业的。
  3. 不推荐标志位:除非你的项目组明确禁止使用 goto 且无法封装函数。