Цикл, тело которого выполняется хотя бы раз, проверка ввода и досрочный выход из цикла
do while и выбирать между ним и whiledo while и switchbreak и пропускать итерацию инструкцией continuereturn и сообщать результат кодом завершенияgoto и без него — и объяснять, почему второй способ предпочтительнееdo while и №8 по инструкциям перехода — на портале. Раздел завершает блок А «Основы C++»: после него на портале — тест по разделам 2–6. Все программы страницы — полные; собирайте их командой g++ -std=c++17 -Wall -Wextra файл.cpp -o файл.exe.do whileВ разделе 5 количество цифр числа считалось циклом while, и у нуля оказалось 0 цифр: условие rest > 0 ложно с первой проверки, тело не выполнилось ни разу. Здесь нужен цикл, который сначала выполняет тело и только потом проверяет условие, — цикл с постусловием из раздела 1:
do {
инструкции // тело цикла
} while (условие); // точка с запятой обязательна
Тело выполняется, затем проверяется условие; пока оно истинно — тело повторяется. Первая проверка — после первого выполнения тела, поэтому тело выполняется хотя бы один раз:
// Количество цифр числа циклом do while: у нуля одна цифра
#include <iostream>
int main()
{
int number;
std::cout << "Number: ";
std::cin >> number;
if (number < 0) {
number = -number;
}
int count = 0;
int rest = number;
do {
rest /= 10;
++count;
} while (rest > 0);
std::cout << number << ": digits " << count << '\n';
return 0;
}
Number: 30572 -> 30572: digits 5
Number: -408 -> 408: digits 3
Number: 0 -> 0: digits 1
Трассировка для 0: тело выполняется — rest остаётся 0, count становится 1; проверка 0 > 0 ложна — выход. Для 30572 цикл делает те же пять итераций, что и while: разница между циклами видна только тогда, когда условие ложно с самого начала.
while — с предусловием | do while — с постусловием | |
|---|---|---|
| Когда проверяется условие | перед каждой итерацией | после каждой итерации |
| Сколько раз выполнится тело | 0 и больше | 1 и больше |
| Проверок условия | на одну больше, чем итераций | столько же, сколько итераций |
| Когда выбирать | действие может не понадобиться ни разу: ввод до признака конца, поиск | действие нужно выполнить, чтобы было что проверять: ввод с проверкой, меню, цифры числа, итерационный расчёт |
while (…) в do while обязательна — это конец инструкции. Без неё g++ сообщает об ошибке на следующей строке:
dosemicolon.cpp:8:24: error: expected ';' before 'std'
А у обычного while точка с запятой после условия — наоборот ошибка: пустое тело и, скорее всего, бесконечный цикл (раздел 5, пункт 10). Правило курса помогает их различать: у do while закрывающая скобка тела и while — на одной строке: } while (…);.До сих пор программа при неверном вводе выводила сообщение и завершалась с кодом 1 (раздел 4). Пользователю удобнее, когда программа переспрашивает. Спросить нужно хотя бы раз — значит, цикл с постусловием:
// Повтор ввода, пока значение не попадёт в диапазон
#include <iostream>
int main()
{
int month;
do {
std::cout << "Month (1..12): ";
std::cin >> month;
} while (month < 1 || month > 12);
std::cout << "Month " << month << " accepted\n";
return 0;
}
Month (1..12): 13
Month (1..12): 0
Month (1..12): 5
Month 5 accepted
Переменная month объявлена до цикла. Условие do while стоит после закрывающей скобки тела, а переменные, объявленные внутри тела, за скобкой уже не существуют (раздел 3, область видимости):
do {
int answer;
std::cin >> answer;
} while (answer != 0);
doscope.cpp:8:14: error: 'answer' was not declared in this scope
Программа выше надёжна, пока пользователь вводит числа. Введите вместо месяца abc — и она бесконечно печатает приглашение, не давая ввести ничего:
Month (1..12): abc
Month (1..12): Month (1..12): Month (1..12): Month (1..12): Month (1..12): ...
Чтобы понять почему, нужно знать, что делает std::cin, когда прочитать число не удалось.
cin при неверном вводеstd::cin — не просто «клавиатура», а поток с состоянием. Чтение std::cin >> month пропускает пробелы и переводы строки и берёт символы, пока они складываются в число. Дальше возможны три исхода:
| Ввод | Что в переменной | Состояние потока | Что осталось во вводе |
|---|---|---|---|
5 | 5 | всё в порядке | перевод строки |
12x | 12 | всё в порядке | x и перевод строки — их получит следующее чтение |
abc | 0 | ошибка (fail) | abc — непрочитанным |
| Ctrl+Z, Enter — конец ввода | не меняется | ошибка и конец ввода (fail и eof) | ничего |
Главное правило: пока поток в состоянии ошибки, все следующие чтения сразу завершаются неудачей — они ничего не ждут от пользователя и ничего не читают. В программе из пункта 02 после abc в month записан 0, условие month < 1 истинно, цикл повторяется, чтение мгновенно проваливается — и так без конца.
Поэтому признак конца ввода нельзя путать с ошибкой. В программе «сумма до нуля» из раздела 5 буква читается как 0 — и цикл заканчивается, будто пользователь ввёл признак конца:
Numbers, 0 - end: 5 8 x 3 0 -> Count 2, sum 13, average 6.5
Программа молча выдала ответ по неполным данным. Это хуже, чем зависание: ошибку никто не заметит. А если там же нажать Ctrl+Z после 5 8, переменная сохранит 8 — и цикл станет бесконечным.
программа.exe < input.txt) и файл кончился. После конца ввода читать больше нечего: программа, которая переспрашивает, зависнет навсегда. Её нужно завершить.fail, eof, clear, ignoreПосле чтения состояние потока спрашивают функциями самого потока — они вызываются через точку:
| Вызов | Что делает |
|---|---|
std::cin.fail() | true, если последнее чтение не удалось — неверные символы или конец ввода |
std::cin.eof() | true, если ввод закончился |
std::cin.clear() | снимает признак ошибки — после этого поток снова читает |
std::cin.ignore(N, '\n') | выбрасывает символы до перевода строки включительно, но не больше N |
Порядок важен: сначала clear() — иначе ignore, как любое чтение из потока в состоянии ошибки, ничего не сделает, — затем ignore, иначе следующее чтение снова наткнётся на abc. Как N передают наибольшее значение типа std::streamsize — «сколько угодно». Его даёт std::numeric_limits из заголовка <limits>, как INT_MAX из раздела 3 даёт максимум int:
// Ввод целого числа с проверкой: не число, не тот диапазон, конец ввода
#include <iostream>
#include <limits>
int main()
{
int month;
bool ok = false;
do {
std::cout << "Month (1..12): ";
std::cin >> month;
if (std::cin.fail()) {
if (std::cin.eof()) { // Ctrl+Z: ввода больше не будет
std::cout << "\nNo input\n";
return 1;
}
std::cin.clear(); // снять признак ошибки
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // выбросить строку
std::cout << "Not a number\n";
} else if (month < 1 || month > 12) {
std::cout << "Month must be 1..12\n";
} else {
ok = true;
}
} while (!ok);
std::cout << "Month " << month << " accepted\n";
return 0;
}
Month (1..12): 13 Month (1..12): abc
Month must be 1..12 Not a number
Month (1..12): abc Month (1..12): ^Z
Not a number
Month (1..12): 5 No input
Month 5 accepted
ok: по нему сразу видно, когда цикл заканчивается.clear() снял бы признак, следующее чтение снова упёрлось бы в конец — и программа зависла бы, печатая «Not a number». После конца ввода программа завершается с кодом 1.12x принимается как 12, а x остаётся во вводе и достанется следующему чтению. Строгая проверка «в строке ничего, кроме числа» — после раздела 10 (строки).INT_MAX (например, 3000000000) тоже даёт fail(): в int оно не помещается.do while и switchПрограмма с меню показывает список команд, читает номер, выполняет команду — и так, пока пользователь не выберет выход. Показать меню нужно хотя бы раз: цикл с постусловием, а выбор команды — switch из раздела 4:
// Меню: do while + switch. Счёт в копилке
#include <iostream>
#include <limits>
int main()
{
long long balance = 0;
int command;
do {
std::cout << "1 - deposit, 2 - withdraw, 3 - balance, 0 - exit: ";
std::cin >> command;
if (std::cin.fail()) {
if (std::cin.eof()) {
std::cout << "\nNo input\n";
return 1;
}
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
command = -1; // не число — неизвестная команда
}
switch (command) {
case 1: {
int amount;
std::cout << "Amount: ";
std::cin >> amount;
if (std::cin.fail() || amount <= 0) {
std::cout << "Amount must be a positive number\n";
return 1;
}
balance += amount;
break;
}
case 2: {
int amount;
std::cout << "Amount: ";
std::cin >> amount;
if (std::cin.fail() || amount <= 0) {
std::cout << "Amount must be a positive number\n";
return 1;
}
if (amount > balance) {
std::cout << "Not enough money\n";
} else {
balance -= amount;
}
break;
}
case 3:
std::cout << "Balance: " << balance << '\n';
break;
case 0:
std::cout << "Bye\n";
break;
default:
std::cout << "Unknown command\n";
break;
}
} while (command != 0);
return 0;
}
1 - deposit, 2 - withdraw, 3 - balance, 0 - exit: 1
Amount: 500
1 - deposit, 2 - withdraw, 3 - balance, 0 - exit: 2
Amount: 700
Not enough money
1 - deposit, 2 - withdraw, 3 - balance, 0 - exit: 2
Amount: 200
1 - deposit, 2 - withdraw, 3 - balance, 0 - exit: 3
Balance: 300
1 - deposit, 2 - withdraw, 3 - balance, 0 - exit: x
Unknown command
1 - deposit, 2 - withdraw, 3 - balance, 0 - exit: 0
Bye
command = -1 после неудачного чтения — не формальность. Неудачное чтение записало в command 0 (пункт 03), а 0 — команда выхода: без этой строки буква молча завершала бы программу.amount. Метка case — точка входа (раздел 4): без скобок переход на case 2 «перепрыгнул» бы объявление с начальным значением, и компилятор это запрещает:
caseinit.cpp:12:10: error: jump to case label
caseinit.cpp:9:13: note: crosses initialization of 'int amount'
Скобки делают amount локальной для одной ветви.break: досрочный выход из циклаbreak знаком по switch: он выходит из switch. Внутри цикла break так же немедленно завершает цикл — выполнение продолжается с инструкции после него. Он нужен, когда у цикла два конца: «попытки кончились» — в условии, «код верный» — посреди тела:
// PIN-код: не больше трёх попыток, break — при верном коде
#include <iostream>
constexpr int PIN = 4071;
constexpr int MAX_ATTEMPTS = 3;
int main()
{
bool granted = false;
for (int attempt = 1; attempt <= MAX_ATTEMPTS; ++attempt) {
int code;
std::cout << "PIN: ";
std::cin >> code;
if (code == PIN) {
granted = true;
break; // дальше спрашивать незачем
}
std::cout << "Wrong PIN, attempts left: " << MAX_ATTEMPTS - attempt << '\n';
}
if (granted) {
std::cout << "Access granted\n";
return 0;
}
std::cout << "Card blocked\n";
return 2;
}
PIN: 1111 PIN: 1
Wrong PIN, attempts left: 2 Wrong PIN, attempts left: 2
PIN: 4071 PIN: 2
Access granted Wrong PIN, attempts left: 1
PIN: 3
Wrong PIN, attempts left: 0
Card blocked
После цикла программа выясняет, почему он закончился, — как после поиска в разделе 5. Здесь это признак granted. Тот же поиск можно записать без break, внеся признак в условие: attempt <= MAX_ATTEMPTS && !granted. Оба способа верны; break удобнее, когда после проверки в теле есть ещё инструкции, которые при успехе выполнять не нужно, — здесь это сообщение «Wrong PIN».
Бесконечный цикл с break. Если причина выхода вычисляется только в середине тела, пишут while (true) — цикл, который заканчивается только через break (или return):
int k = 0;
long long sum = 0;
while (true) { // наименьшее k, при котором 1 + 2 + ... + k >= n
++k;
sum += k;
if (sum >= n) {
break;
}
}
break выходит только из одного — ближайшего — цикла или switch. Во вложенных циклах break во внутреннем цикле продолжает внешний. А break в ветви switch внутри цикла выходит из switch, но не из цикла: меню, в котором выход написан как case 0: break; внутри while (true), никогда не закончится. Поэтому в пункте 05 выход из меню — в условии do while, а не в switch. Правило курса для while (true): в теле ровно одно место выхода, и по комментарию к заголовку видно, почему он наступит.continue: к следующей итерацииcontinue пропускает остаток тела и переходит к следующей итерации. Удобно, когда часть значений обрабатывать не нужно:
// Сумма положительных из n чисел: continue пропускает остальные
#include <iostream>
int main()
{
int n;
std::cout << "How many numbers: ";
std::cin >> n;
if (n < 1) {
std::cout << "Error: n must be >= 1\n";
return 1;
}
std::cout << "Numbers: ";
long long sum = 0;
int used = 0;
for (int i = 1; i <= n; ++i) {
int value;
std::cin >> value;
if (value <= 0) {
continue; // к следующему числу: ++i, проверка i <= n
}
sum += value;
++used;
}
std::cout << "Positive: " << used << ", sum " << sum << '\n';
return 0;
}
How many numbers: 5 How many numbers: 3
Numbers: 3 -1 0 7 -4 Numbers: -1 -2 -3
Positive: 2, sum 10 Positive: 0, sum 0
Куда именно переходит continue, зависит от цикла — и здесь самая частая ошибка раздела:
| Цикл | После continue | Опасность |
|---|---|---|
for | изменение переменной (++i), затем проверка условия | нет: изменение в заголовке не пропускается |
while | сразу проверка условия | если ++i стоит в теле после continue, он пропускается — бесконечный цикл |
do while | сразу проверка условия | условие проверяется со старыми значениями — например, с 0 после неудачного чтения (пункт 05) |
int i = 1;
int sum = 0;
while (i <= 10) {
if (i % 3 == 0) {
continue; // при i = 3 ++i больше не выполнится: цикл бесконечный
}
sum += i;
++i;
}
g++ об этом не предупреждает даже с -Wall -Wextra. Исправление — for, у которого изменение в заголовке, или условие без continue: if (i % 3 != 0) { sum += i; }. continue оправдан, когда он стоит в начале тела и отсекает неподходящие значения, — как в программе выше. Если после него в теле остаётся одна-две строки, проще обратить условие.
return из main и код завершенияreturn в main завершает программу из любого места — в том числе из середины вложенных циклов и ветвей switch. Число после return — код завершения, который получает запустивший программу (раздел 2). Соглашение: 0 — успех, другое число — что-то пошло не так; какие коды что значат, программа решает сама и сообщает в описании. Программа PIN-кода из пункта 06 различает три исхода:
| Код | Когда |
|---|---|
| 0 | доступ разрешён |
| 2 | карта заблокирована: три неверные попытки |
В курсе принято: 1 — неверный ввод или конец ввода, 2 и дальше — исходы, особые для задачи. Код виден в консоли сразу после запуска:
C:\cpp> pin.exe PS C:\cpp> .\pin.exe
PIN: 1 PIN: 4071
Wrong PIN, attempts left: 2 Access granted
PIN: 2 PS C:\cpp> $LASTEXITCODE
Wrong PIN, attempts left: 1 0
PIN: 3
Wrong PIN, attempts left: 0
Card blocked
C:\cpp> echo %ERRORLEVEL%
2
Слева — cmd, справа — PowerShell: там код хранится в переменной $LASTEXITCODE. По коду завершения скрипты и другие программы решают, что делать дальше, не разбирая текст вывода. В разделе 14 одна программа будет запускать другую и читать её код функцией Windows API.
return из main — самый короткий выход из вложенных циклов, но он завершает всю программу. Если после поиска нужно продолжить работу, он не подходит. В разделе 7 появятся функции: return из функции завершает только её, и поиск во вложенных циклах станет отдельной функцией — это лучшая замена всему, что описано в следующем пункте.goto и выход из вложенных цикловgoto метка; передаёт управление на инструкцию, помеченную метка:, в пределах той же функции. Это безусловный переход — стрелка блок-схемы, ведущая в любое место. Единственный случай, где он встречается в современном коде на C++, — выход сразу из нескольких вложенных циклов, ведь break выходит только из одного:
// n = a^2 + b^2: выход из двух циклов через goto
#include <iostream>
int main()
{
int n;
std::cout << "n (1..1000000): ";
std::cin >> n;
if (n < 1 || n > 1000000) {
std::cout << "Error: n must be 1..1000000\n";
return 1;
}
for (int a = 1; a * a <= n; ++a) {
for (int b = a; a * a + b * b <= n; ++b) {
if (a * a + b * b == n) {
std::cout << n << " = " << a << "^2 + " << b << "^2\n";
goto done;
}
}
}
std::cout << n << ": no such a, b\n";
done:
return 0;
}
n (1..1000000): 25 -> 25 = 3^2 + 4^2
n (1..1000000): 50 -> 50 = 1^2 + 7^2
n (1..1000000): 1000000 -> 1000000 = 280^2 + 960^2
n (1..1000000): 999999 -> 999999: no such a, b
Без goto — break из внутреннего цикла и признак, по которому break выходит из внешнего. Вывод тот же на всех тестах:
bool found = false;
for (int a = 1; a * a <= n; ++a) {
for (int b = a; a * a + b * b <= n; ++b) {
if (a * a + b * b == n) {
std::cout << n << " = " << a << "^2 + " << b << "^2\n";
found = true;
break; // только из внутреннего цикла
}
}
if (found) {
break; // и из внешнего
}
}
if (!found) {
std::cout << n << ": no such a, b\n";
}
Почему goto избегают. В 1968 году Эдсгер Дейкстра опубликовал заметку «О вреде оператора go to»: программу, где управление прыгает по меткам, нельзя читать сверху вниз — чтобы понять строку, надо найти все goto, которые на неё ведут. Циклы и ветвления («структурное программирование») описывают те же алгоритмы, и у каждого участка кода один вход и один выход. Переход назад по метке — это цикл, записанный так, что его не видно; переход вперёд — ветвление, у которого не видно границ. Компилятор к тому же запрещает прыгать через объявление с начальным значением — та же ошибка, что у меток case в пункте 05:
gotoinit.cpp:12:1: error: jump to label 'finish'
gotoinit.cpp:8:14: note: from here
gotoinit.cpp:10:9: note: crosses initialization of 'int square'
goto допустим только для выхода из вложенных циклов — вперёд, на метку сразу после них. Все остальные переходы записываются циклами, break, continue и return. С раздела 7 и этот случай решается функцией: поиск выносится в неё, а выход из всех циклов сразу — return.| Ошибка | Признак | Сообщение g++ | Как избежать |
|---|---|---|---|
нет ; после while (…) в do while | не собирается | expected ';' before … — на следующей строке | } while (…); на одной строке |
переменная условия объявлена в теле do while | не собирается | was not declared in this scope | объявлять до do |
ввод не проверяется на fail() | на букве программа зависает или считает букву нулём | нет | блок проверки из пункта 04 |
clear() после конца ввода | бесконечное «Not a number» | нет | сначала eof() — и завершить программу |
нет ignore после clear() | то же слово читается снова и снова | нет | clear(), затем ignore(…) |
continue в while до изменения переменной | бесконечный цикл | нет | for или условие без continue |
continue в do while после неудачного чтения | программа завершается на букве (в переменной 0) | нет | присвоить значение, которое не заканчивает цикл |
break в switch для выхода из цикла | цикл не заканчивается | нет | выход — в условии цикла |
объявление в ветви switch без скобок | не собирается | jump to case label | case N: { … break; } |
goto через объявление | не собирается | jump to label, crosses initialization | не использовать goto, кроме выхода из вложенных циклов |
12x, пустая строка (просто Enter — >> её пропускает и ждёт дальше), Ctrl+Z. Программа не должна ни зависать, ни молча принимать неверные данные.do while выполняется хотя бы раз; условие проверяется после тела и видит только переменные, объявленные до dofail() → конец ввода — завершить программу, иначе clear() и ignore(…) → переспроситьdo while с switch внутри; выход — в условии циклаbreak выходит из ближайшего цикла или switch; continue в for переходит к изменению, в while и do while — сразу к условиюreturn из main завершает программу; код завершения 0 — успех, другое число — причина неудачиgoto — только для выхода из вложенных циклов; без него то же делают break с признаком, а с раздела 7 — return из функцииERRORLEVEL; раздел 3 — область видимости, INT_MAX; раздел 4 — switch, break в нём, проверка ввода с кодом 1; раздел 5 — циклы while и for, поиск, вложенные циклы. Нужен для: раздела 7 — блок проверки ввода и поиск во вложенных циклах становятся функциями; разделов 8–10 — обход и поиск в массивах и строках с break; раздела 14 — код завершения процесса; разделов 16–19 — циклы ожидания и обработки с выходом по условию.