Цикл, который всегда выполняется хотя бы раз
Цикл while сначала проверяет, а потом действует, поэтому может не выполниться ни разу. Обычно это правильно. Иногда это ровно наоборот: нельзя понять, нужно ли повторять запрос, пока вы его не показали, и нельзя оценить ввод пользователя, пока что-то не прочитали. do-while переносит проверку в конец, поэтому тело выполняется первым, а условие решает только, стоит ли пройти ещё раз.
Условие i < 5 ложно с самого начала. Цикл while выполняется ноль раз; do-while — ровно один. В этом и состоит вся разница между ними.
Синтаксис и та самая точка с запятой
do {
// тело - всегда выполняется хотя бы один раз
} while (условие);
Порядок читается так же, как выполняется: сделай тело, пока условие ещё выполняется — сделай снова.
Завершающая точка с запятой после закрывающей скобки обязательна. do ... while (усл); — это одна инструкция, а инструкции в C заканчиваются точкой с запятой. Забудьте её — и компилятор продолжит читать дальше, а потом сообщит о синтаксической ошибке где-то ниже; именно поэтому настоящая ошибка часто находится за несколько строк от той, на которую указывает компилятор.
Это же единственное место в C, где while (усл); с точкой с запятой — правильно, а не баг. В обычном цикле while та же пунктуация создаёт пустое тело и, как правило, бесконечный цикл; здесь она закрывает do.
Фигурные скобки не обязательны для тела из одной инструкции, но, как и с любой другой управляющей конструкцией C, пишите их всё равно.
Шаблон проверки ввода
Ради этого do-while и придуман. Вы обязаны что-то прочитать, прежде чем сможете понять, годится ли это, поэтому чтение должно идти до проверки:
Цикл читает, пока значение не пройдёт проверку, и то же самое условие, которое отвергает плохое значение, запрашивает следующий проход. Если написать это через while, чтение пришлось бы указать дважды — один раз до цикла, чтобы задать условие, и один раз в конце тела; именно это дублирование и убирает do-while.
С реальным вводом в теле вызывался бы scanf:
int age;
do {
printf("Введите ваш возраст (1-120): ");
if (scanf("%d", &age) != 1) {
while (getchar() != '\n') { } /* выбросить испорченную строку */
age = -1; /* заставить пройти ещё раз */
}
} while (age < 1 || age > 120);
Обратите внимание на вложенный while, который дочитывает остаток строки. Если scanf не смог преобразовать ввод, проблемный текст так и остаётся в буфере, и каждое следующее чтение спотыкается о него — получается цикл, вечно печатающий приглашение. Очистка буфера — часть этого шаблона.
Шаблон меню
Второе классическое применение: показать варианты, прочитать выбор, выполнить действие и повторять, пока пользователь не выйдет. Меню должно появиться раньше, чем появится выбор для проверки.
break внутри switch завершает switch, а не цикл — об этом стоит говорить точно, ведь используется одно и то же ключевое слово. Здесь цикл заканчивается потому, что choice != 4 становится ложным. Если бы вы действительно захотели выйти из цикла изнутри switch, помог бы return или переменная-флаг; одним break этого не сделать.
Гарантированный первый проход может быть багом
Раз тело выполняется до всякой проверки, do-while с удовольствием обработает данные, которых нет:
/* БАГ: печатает a[0], даже когда n равно 0 */
int i = 0;
do {
printf("%d\n", a[i]);
i++;
} while (i < n);
При n == 0 это читает a[0] в пустом массиве — неопределённое поведение. Любому циклу по коллекции, которая может оказаться пустой, нужен while или for, у которых проверка идёт первой.
Правило большого пальца: используйте do-while только тогда, когда ноль итераций невозможен по самой природе задачи, а не просто маловероятен на практике. Запрос к пользователю, броски кубика до нужного результата, генерация и проверка кандидата — их действительно нельзя оценить до первого прохода. Обход списка на такое претендовать не может.
break и continue в do-while
Оба работают, и у continue есть нюанс, о котором стоит знать: он переходит к проверке условия, а не к началу тела.
i++ в начале тела выполняется на каждом проходе, включая те, что обрывает continue, поэтому цикл продолжает двигаться. Поставьте обновление после continue — и цикл зависнет; та же ловушка есть и у циклов while.
break немедленно выходит из цикла и полностью пропускает проверку условия.
Выбор между тремя циклами
Все три цикла в C одинаково выразительны; выбор — о том, какой из них лучше выражает намерение.
for счётчик, его предел и его шаг идут вместе - "n раз"
while повторять, пока не изменится условие, возможно ни разу - "пока не"
do-while то же самое, но первый проход гарантирован - "хотя бы один раз"
На практике do-while встречается заметно реже остальных. Большинство циклов либо считают, либо должны корректно обработать пустой случай, а и то и другое требует проверки сверху. Тянитесь к do-while, когда ловите себя на дублировании инструкции прямо перед циклом while, чтобы задать его условие, — это дублирование и есть сигнал.
Часто задаваемые вопросы
Как написать цикл do-while в C?
do { тело } while (условие); — сначала идёт тело, затем проверка, и всё выражение заканчивается точкой с запятой. Тело всегда выполняется хотя бы один раз, а затем повторяется, пока условие ненулевое.
В чём разница между while и do-while в C?
В том, где стоит проверка. Цикл while проверяет условие до первого прохода, поэтому может не выполниться ни разу. do-while проверяет после, поэтому выполняется хотя бы один раз. Выбирайте do-while только тогда, когда этот гарантированный первый проход действительно нужен по сути задачи.
Почему циклу do-while нужна точка с запятой в конце?
Потому что do ... while (усл); — это одна инструкция, а любая инструкция в C завершается точкой с запятой. Если её не поставить, вы получите непонятную синтаксическую ошибку, указывающую на следующую строку, ведь компилятор продолжает читать оставшуюся часть инструкции.
Когда стоит использовать цикл do-while?
Когда тело должно выполниться прежде, чем вы вообще сможете понять, нужно ли повторять: показать меню перед чтением выбора, повторно запрашивать ввод, пока он не станет корректным, или сгенерировать значение и затем проверить его. Если условие можно проверить заранее, цикл while выражает это яснее.