Раздел 6 — Цикл do while и инструкции перехода

Цикл, тело которого выполняется хотя бы раз, проверка ввода и досрочный выход из цикла

Прогресс курса Раздел 6 из 20

Что вы освоите в этом разделе

3 академических часа теории и 7 часов практики: практическая работа №7 по циклу do while и №8 по инструкциям перехода — на портале. Раздел завершает блок А «Основы C++»: после него на портале — тест по разделам 2–6. Все программы страницы — полные; собирайте их командой g++ -std=c++17 -Wall -Wextra файл.cpp -o файл.exe.
01

Цикл 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 (…);.
02

Повтор ввода до допустимого значения

До сих пор программа при неверном вводе выводила сообщение и завершалась с кодом 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, когда прочитать число не удалось.

03

Что происходит с cin при неверном вводе

std::cin — не просто «клавиатура», а поток с состоянием. Чтение std::cin >> month пропускает пробелы и переводы строки и берёт символы, пока они складываются в число. Дальше возможны три исхода:

ВводЧто в переменнойСостояние потокаЧто осталось во вводе
55всё в порядкеперевод строки
12x12всё в порядкеx и перевод строки — их получит следующее чтение
abc0ошибка (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 — и цикл станет бесконечным.

Конец ввода в консоли Windows — Ctrl+Z в начале строки и Enter (в Linux — Ctrl+D). Он же наступает, когда ввод берётся из файла (программа.exe < input.txt) и файл кончился. После конца ввода читать больше нечего: программа, которая переспрашивает, зависнет навсегда. Её нужно завершить.
04

Проверка ввода: 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
Этот блок — 20 строк на одну переменную. В программе с тремя вводами он повторится три раза с разницей в имени и диапазоне. Так и есть в практической работе №7 — и это главный мотив раздела 7: блок станет функцией, которую вызывают одной строкой.
05

Меню: 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
06

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): в теле ровно одно место выхода, и по комментарию к заголовку видно, почему он наступит.
07

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 оправдан, когда он стоит в начале тела и отсекает неподходящие значения, — как в программе выше. Если после него в теле остаётся одна-две строки, проще обратить условие.

08

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 из функции завершает только её, и поиск во вложенных циклах станет отдельной функцией — это лучшая замена всему, что описано в следующем пункте.
09

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.
10

Ловушки раздела

ОшибкаПризнакСообщение 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 labelcase N: { … break; }
goto через объявлениене собираетсяjump to label, crosses initializationне использовать goto, кроме выхода из вложенных циклов
Проверяйте программу с вводом не только на правильных числах. Минимальный набор: число вне диапазона, буквы, 12x, пустая строка (просто Enter — >> её пропускает и ждёт дальше), Ctrl+Z. Программа не должна ни зависать, ни молча принимать неверные данные.

Ключевые выводы раздела

Запомните главное

  • Тело do while выполняется хотя бы раз; условие проверяется после тела и видит только переменные, объявленные до do
  • При неверных символах чтение записывает 0 и переводит поток в состояние ошибки; при конце ввода переменная не меняется; пока ошибка не снята, все чтения проваливаются
  • Проверка ввода: fail() → конец ввода — завершить программу, иначе clear() и ignore(…) → переспросить
  • Меню — do while с switch внутри; выход — в условии цикла
  • break выходит из ближайшего цикла или switch; continue в for переходит к изменению, в while и do while — сразу к условию
  • return из main завершает программу; код завершения 0 — успех, другое число — причина неудачи
  • goto — только для выхода из вложенных циклов; без него то же делают break с признаком, а с раздела 7 — return из функции
Связи раздела. Опирается на раздел 1 — цикл с постусловием в блок-схеме; раздел 2 — код завершения и ERRORLEVEL; раздел 3 — область видимости, INT_MAX; раздел 4 — switch, break в нём, проверка ввода с кодом 1; раздел 5 — циклы while и for, поиск, вложенные циклы. Нужен для: раздела 7 — блок проверки ввода и поиск во вложенных циклах становятся функциями; разделов 8–10 — обход и поиск в массивах и строках с break; раздела 14 — код завершения процесса; разделов 16–19 — циклы ожидания и обработки с выходом по условию.
Практические работы №7 и №8 и тест по блоку А — на учебном портале. Задания по вариантам, критерии оценивания и сдача — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 5: Циклы for и while Практика на портале