Два способа изменить ход цикла
Условие цикла решает, когда он закончится, но иногда настоящий ответ обнаруживается в середине тела. Вы ищете элемент в массиве и находите его третьим — зачем проверять остальные 997? Или вы обрабатываете список, и одна запись пустая: пропустите её и идите дальше.
В C есть по ключевому слову на каждый случай:
break— выйти из цикла прямо сейчас. Выполнение продолжится с первой инструкции после него.continue— прервать только этот проход. Перейти к следующему.
Первый цикл выводит 1 2 3 4 и останавливается. Второй выводит 1 2 3 4 6 7 8 9 10 — он пропустил одно значение и продолжил работу.
break в цикле
Канонический случай — поиск, который прекращается сразу после успеха:
Поскольку found устанавливается до break, код после цикла может отличить «нашли» от «элементы кончились». Эта пара — переменная результата плюс break — и есть стандартная идиома поиска в C. Сам счётчик цикла после него использовать нельзя, если он объявлен в заголовке: он выходит из области видимости на закрывающей скобке.
break всегда выходит из одного уровня: ближайшего охватывающего for, while, do-while или switch.
continue в цикле
continue нужен для записей, которые следует пропустить, а не прерывать из-за них работу:
Куда именно переходит continue, зависит от вида цикла:
for -> к части обновления в заголовке (счётчик всё равно двигается)
while -> к проверке условия (больше ничего в теле не выполняется)
do-while -> к проверке условия внизу
Эта разница не академическая. В цикле for continue не может пропустить увеличение счётчика, потому что оно стоит в заголовке. В цикле while — может:
/* БАГ: continue пропускает i++, поэтому всё зависает на первом чётном числе */
int i = 0;
while (i < 10) {
if (i % 2 == 0) {
continue;
}
printf("%d ", i);
i++;
}
Либо перенесите i++ выше continue, либо перепишите всё как цикл for, где заголовок делает такую ошибку невозможной.
break в switch — это не break в цикле
break завершает ближайший охватывающий цикл или switch — что из них ближе. Поместите switch внутрь цикла, и каждый break в нём будет принадлежать switch:
Команда q печатает своё сообщение, а цикл продолжает работать и доходит до cc. Если выход должен был завершить цикл, этот break своей задачи не выполнил. Решения — флаг, goto или вынос цикла в функцию с return; все три показаны ниже.
Выход из вложенных циклов
Один break покидает один уровень. Внутри двух вложенных циклов он завершает только внутренний, а внешний начинает следующий проход, как будто ничего не случилось.
В C нет ни break 2, ни break с меткой. Есть три честных варианта:
Флаг. Переносимо, очевидно и слегка шумно:
Флаг появляется в двух местах — в условии внешнего цикла и в присваивании, — и это его цена. При трёх уровнях вложенности такой подход становится по-настоящему неприятным.
goto. Многократно обруганный переход, и это тот самый случай, когда опытные C-программисты применяют его без извинений:
Переход goto вперёд, на метку сразу после циклов, делает ровно то же, что break с меткой в других языках, — одной строкой и без флага, который нужно держать в согласованном состоянии. Дурная репутация goto заслужена переходами назад и прыжками внутрь других блоков, которые и правда порождают нечитаемый поток управления. Один переход вперёд из вложенных циклов — не тот случай. Ядро Linux использует тот же приём для путей освобождения ресурсов. Применяйте его осознанно и умеренно, но не позволяйте табу загонять себя в альтернативу с тремя флагами, которую читать сложнее.
Функция и return. Обычно лучший ответ, потому что поиск заслуживает имени:
return разворачивает все циклы функции сразу, не требует ни флага, ни метки, а у кода поиска теперь есть имя и его можно тестировать отдельно. Когда вложенность становится настолько глубокой, что выход из неё превращается в проблему, это часто намёк на то, что блок с самого начала хотел быть функцией.
Когда они помогают, а когда мешают
break и continue оправдывают себя, когда позволяют циклу один раз сказать «этот случай закончен» ближе к началу, вместо того чтобы заворачивать всё тело в if. Сравните:
/* с continue: основная работа не уходит вправо */
for (int i = 0; i < n; i++) {
if (!isValid(a[i])) continue;
if (isDuplicate(a[i])) continue;
process(a[i]);
}
/* без него: каждая проверка добавляет уровень */
for (int i = 0; i < n; i++) {
if (isValid(a[i])) {
if (!isDuplicate(a[i])) {
process(a[i]);
}
}
}
Они мешают, когда их много. У цикла с пятью разбросанными break и тремя continue условие выхода нигде не записано — читателю приходится мысленно прогонять всё тело, чтобы понять, когда цикл остановится. Два правила, которые хорошо себя показывают: держите проверки с continue вместе в начале тела и, если в цикле больше одного-двух break, подумайте, не должно ли было этим заниматься условие в заголовке.
Две ловушки помельче. continue в do-while переходит к проверке внизу, которая всё равно следующая по порядку, — безвредно, но удивляет. А break вне цикла и вне switch — ошибка компиляции, и это единственная ошибка из перечисленных, которую компилятор поймает за вас.
Часто задаваемые вопросы
В чём разница между break и continue в C?
break полностью завершает цикл, и выполнение продолжается после него. continue прерывает только текущий проход и переходит к следующему — к части обновления в цикле for или сразу к проверке условия в while и do-while.
Как выйти из вложенного цикла в C?
Один break покидает только самый внутренний цикл. Чтобы выйти из обоих, либо заведите флаг и проверяйте его в условии внешнего цикла, либо используйте goto для перехода на метку после циклов, либо — обычно это чище всего — вынесите циклы в функцию и используйте return.
break выходит из цикла или только из switch?
Из того, что охватывает его теснее. break внутри switch, который находится внутри цикла, завершает только switch; цикл продолжает работать. Чтобы выйти из цикла изнутри switch, нужен флаг, goto или return.
Почему continue вызывает бесконечный цикл в моём while?
Потому что continue переходит к проверке условия и пропускает остаток тела — в том числе увеличение счётчика, если оно стоит в конце. В цикле for обновление живёт в заголовке, поэтому выполняется всегда; в цикле while перенесите обновление выше continue или используйте for.