Раздел 5 — Циклы for и while

Как повторить действие нужное число раз или пока выполняется условие

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

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

3 академических часа теории и 6 часов практики: практическая работа №5 по циклу for и №6 по циклу while — на портале. Все программы страницы — полные; собирайте их командой g++ -std=c++17 -Wall -Wextra файл.cpp -o файл.exe.
01

Цикл while

Цикл повторяет инструкции, пока выполняется условие. Цикл while — цикл с предусловием из раздела 1: условие проверяется до каждого повторения.

while (условие) {
    инструкции       // тело цикла
}

Если условие истинно — выполняется тело, и проверка повторяется. Как только условие стало ложным — выполнение переходит к инструкции после цикла. Одно выполнение тела называют итерацией. Сумма 1 + 2 + … + n:

// Сумма 1 + 2 + ... + n циклом while
#include <iostream>

int main()
{
    int n;
    std::cout << "n: ";
    std::cin >> n;
    if (n < 1) {
        std::cout << "Error: n must be >= 1\n";
        return 1;
    }

    int sum = 0;
    int i = 1;              // 1. начальное значение
    while (i <= n) {        // 2. условие продолжения
        sum += i;
        ++i;                // 3. изменение переменной цикла
    }
    std::cout << "1 + ... + " << n << " = " << sum << '\n';
    return 0;
}
n: 4                        n: 100
1 + ... + 4 = 10            1 + ... + 100 = 5050

У цикла три части, и ошибка в любой из них — самая частая ошибка раздела: начальное значение переменной цикла (i = 1), условие продолжения (i <= n) и изменение переменной в теле (++i). Проверить цикл без компьютера помогает трассировка — таблица значений переменных после каждой итерации. Для n = 4:

Проверкаi до телаi <= nsum после телаi после тела
11true12
22true33
33true64
44true105
55falseвыход из цикла

Условие проверяется на одну итерацию больше, чем выполняется тело: последняя проверка — та, что вернула false. После цикла i равно 5, а не 4.

Бесконечный цикл. Если забыть ++i, условие i <= n останется истинным навсегда: программа «зависнет», ничего не выводя. Компилятор об этом не предупреждает. Остановить программу в консоли — Ctrl+C. Правило курса: в каждом цикле должно быть видно, какая переменная приближает его к концу. Если это нельзя показать — цикл пишется заново.
02

Итерационная модель: цикл, число повторений которого неизвестно

В разделе 1 для вклада построена итерационная модель: 100 000 ₽ под 12 % годовых с ежемесячной капитализацией — через сколько месяцев сумма впервые достигнет 110 000 ₽? Её блок-схема — цикл с предусловием. Теперь это код:

// Через сколько месяцев вклад достигнет цели — итерационная модель из раздела 1
#include <iostream>
#include <iomanip>

int main()
{
    double principal, rate, target;
    std::cout << "Sum, rate %, target: ";
    std::cin >> principal >> rate >> target;
    if (principal <= 0 || rate <= 0 || target <= principal) {
        std::cout << "Error: need sum > 0, rate > 0, target > sum\n";
        return 1;
    }

    double sum = principal;
    int months = 0;
    while (sum < target) {
        sum *= 1 + rate / 1200;     // проценты за месяц: годовая ставка / 12 / 100
        ++months;
    }
    std::cout << std::fixed << std::setprecision(2)
              << "Months: " << months << ", sum: " << sum << '\n';
    return 0;
}
Sum, rate %, target: 100000 12 110000
Months: 10, sum: 110462.21

Число повторений здесь заранее неизвестно — его и ищет программа. Переменная months — счётчик: она не управляет циклом, а считает итерации. Управляет условие sum < target.

Почему цикл закончится. При rate > 0 сумма каждый месяц растёт не меньше чем на principal · rate / 1200 — значит, рано или поздно превысит любую цель. При rate = 0 сумма не меняется, и цикл был бы бесконечным. Поэтому проверка rate <= 0 стоит в программе не «для порядка»: она гарантирует конец цикла. Для каждой итерационной модели в разделе 1 требовалось доказать, что повторение закончится; в программе это доказательство становится проверкой входных данных. Строго говоря, бесконечным цикл сделает и ставка 1e-20: 1 + 1e-22 в double равно 1 (раздел 3), и сумма не растёт. Поэтому в практических работах у ставки есть нижняя граница, а не только «больше нуля».
03

Цифры числа и цикл, который не выполнился ни разу

Операции % 10 и / 10 из раздела 3 дают последнюю цифру целого числа и число без неё. Повторяя их, пока от числа что-то осталось, можно пройти все цифры:

// Количество и сумма цифр числа
#include <iostream>

int main()
{
    int number;
    std::cout << "Number: ";
    std::cin >> number;
    if (number < 0) {
        number = -number;
    }

    int count = 0;
    int digit_sum = 0;
    int rest = number;
    while (rest > 0) {
        digit_sum += rest % 10;     // последняя цифра
        rest /= 10;                 // отбросить последнюю цифру
        ++count;
    }
    std::cout << number << ": digits " << count << ", sum " << digit_sum << '\n';
    return 0;
}
Number: 30572  ->  30572: digits 5, sum 17
Number: -408   ->  408: digits 3, sum 12
Number: 0      ->  0: digits 0, sum 0       <- неверно: у нуля одна цифра

Цикл идёт по копии rest, а не по number: после цикла от числа остаётся 0, а в выводе нужно исходное. Для 30572 rest принимает значения 30572, 3057, 305, 30, 3, 0 — пять итераций, по одной на цифру.

Для нуля условие rest > 0 ложно с первой же проверки: тело цикла с предусловием может не выполниться ни разу. Иногда это правильно — сумма цифр нуля действительно 0, — а иногда нет, как с количеством цифр. Исправляется отдельной проверкой до цикла (if (number == 0)) или циклом с постусловием, у которого тело выполняется хотя бы раз, — он в разделе 6.

У любого цикла проверяйте три случая: тело не выполняется ни разу, выполняется один раз, выполняется много раз. Для цифр это 0, 7 и 30572. Ошибки циклов чаще всего сидят в первых двух.
04

Ввод до признака конца

Сколько чисел введёт пользователь, заранее неизвестно; конец ввода обозначается особым значением — признаком конца, например нулём. Сам признак не обрабатывается:

// Сумма и среднее чисел, ввод до нуля
#include <iostream>

int main()
{
    std::cout << "Numbers, 0 - end: ";
    int value;
    std::cin >> value;              // первое чтение — до цикла

    int count = 0;
    int sum = 0;
    while (value != 0) {
        sum += value;
        ++count;
        std::cin >> value;          // следующее — в конце тела
    }

    if (count == 0) {
        std::cout << "No numbers\n";
    } else {
        std::cout << "Count " << count << ", sum " << sum
                  << ", average " << static_cast<double>(sum) / count << '\n';
    }
    return 0;
}
Numbers, 0 - end: 5 8 -3 10 0      ->  Count 4, sum 20, average 5
Numbers, 0 - end: 7 2 0            ->  Count 2, sum 9, average 4.5
Numbers, 0 - end: 0                ->  No numbers

Чтение стоит в двух местах — перед циклом и последней инструкцией тела. Это чтение с упреждением: к моменту проверки условия очередное число уже прочитано, и признак конца не попадает в сумму. Числа можно вводить в одну строку через пробел или по одному в строке — >> пропускает пробелы и переводы строки.

Частые ошибки: чтение только в начале тела — тогда 0 успевает прибавиться к сумме и увеличить счётчик; деление на count без проверки — при пустом вводе деление на ноль; sum / count без приведения к double — целое деление, 9 / 2 = 4 (раздел 3). Если вместо числа ввести букву, чтение не удастся, в value запишется 0 — и цикл закончится, будто введён признак конца: на вводе 5 8 x 3 0 программа выведет Count 2, sum 13 и не заметит ошибки. Как отличить неверный ввод от признака конца — раздел 6.
05

Цикл for

Когда число повторений известно до начала цикла, три части цикла удобно собрать в одну строку. Это цикл for:

for (начальное значение; условие; изменение) {
    инструкции
}

Порядок выполнения: начальное значение — один раз; затем проверка условия, тело, изменение, снова проверка — и так, пока условие истинно. Любой for можно переписать через while и наоборот; сумма из пункта 01 выглядит так:

int sum = 0;
for (int i = 1; i <= n; ++i) {
    sum += i;
}

Переменная, объявленная в заголовке for, существует только внутри цикла. После цикла её нет — это защищает от случайного использования «последнего значения»:

forscope.cpp:9:30: error: 'i' was not declared in this scope

Если значение после цикла нужно, переменная объявляется до цикла. Шаг может быть любым, в том числе отрицательным: for (int i = 10; i > 0; i -= 3) даёт 10, 7, 4, 1. Степени двойки:

// Степени двойки циклом for
#include <iostream>

int main()
{
    int n;
    std::cout << "n (0..30): ";
    std::cin >> n;
    if (n < 0 || n > 30) {
        std::cout << "Error: n must be 0..30\n";
        return 1;
    }

    long long power = 1;
    for (int i = 0; i <= n; ++i) {
        std::cout << "2^" << i << " = " << power << '\n';
        power *= 2;
    }
    return 0;
}
n (0..30): 4
2^0 = 1
2^1 = 2
2^2 = 4
2^3 = 8
2^4 = 16

Почему long long, если выводится не больше 230? Умножение стоит после вывода, и на последней итерации power становится 231 — это уже больше максимума int (раздел 3). Значение не выводится, но переполнение int — неопределённое поведение, даже когда результат не используется.

ЗадачаЦикл
число повторений известно до начала: от a до b, n раз, для каждого из n чиселfor
повторять, пока не выполнится условие: цель вклада, цифры числа, ввод до признака конца, поискwhile
Правила курса для for: счётчик — целый; переменная цикла меняется только в заголовке, не в теле; фигурные скобки — у тела даже из одной инструкции; имена i, j, k — только для счётчиков, у остальных переменных имена по смыслу (row, col, month).
06

Сумма, количество, среднее, максимум

Большинство задач с циклом сводится к нескольким типовым алгоритмам. У каждого есть переменная-«накопитель» и правило, каким значением её начинать:

АлгоритмНачальное значениеВ теле цикла
суммаsum = 0sum += value;
произведениеproduct = 1product *= value;
количество подходящихcount = 0if (условие) { ++count; }
среднеесумма и количествопосле цикла: static_cast<double>(sum) / count, если count > 0
максимумпервое значениеif (value > max_value) { max_value = value; }
// n чисел: сумма, среднее, максимум и его номер, количество положительных
#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: ";
    int value;
    std::cin >> value;                      // первое число — до цикла
    long long sum = value;
    int max_value = value;
    int max_pos = 1;
    int positive = (value > 0) ? 1 : 0;

    for (int i = 2; i <= n; ++i) {
        std::cin >> value;
        sum += value;
        if (value > max_value) {
            max_value = value;
            max_pos = i;
        }
        if (value > 0) {
            ++positive;
        }
    }
    std::cout << "Sum " << sum << ", average " << static_cast<double>(sum) / n << '\n'
              << "Max " << max_value << " (number " << max_pos << ")\n"
              << "Positive: " << positive << '\n';
    return 0;
}
How many numbers: 6                 How many numbers: 3
Numbers: 4 -2 9 0 9 -5              Numbers: -7 -2 -9
Sum 15, average 2.5                 Sum -18, average -6
Max 9 (number 3)                    Max -2 (number 2)
Positive: 3                         Positive: 0
Произведение растёт быстрее всего. Факториал n! = 1 · 2 · … · n: 12! = 479 001 600 ещё помещается в int, 13! = 6 227 020 800 — уже нет, и программа с int молча выводит 1932053504. Компилятор не предупреждает: он не знает, сколько раз выполнится цикл. В long long помещается до 20! = 2 432 902 008 176 640 000. Для каждого накопителя оценивайте наибольшее значение, как в разделе 3, и ограничивайте ввод так, чтобы оно поместилось в тип.
07

Поиск: условие цикла останавливает перебор

Поиск перебирает кандидатов, пока не найдёт подходящего. Удобно вписать в условие цикла обе причины остановки: «кандидаты кончились» и «найдено». Простое ли число n — есть ли у него делитель от 2 до √n:

// Простое ли число; если нет — разложение с наименьшим делителем
#include <iostream>

int main()
{
    int n;
    std::cout << "n (2..2000000000): ";
    std::cin >> n;
    if (n < 2 || n > 2000000000) {
        std::cout << "Error: n must be 2..2000000000\n";
        return 1;
    }

    int divisor = 2;
    while (divisor * divisor <= n && n % divisor != 0) {
        ++divisor;
    }

    if (divisor * divisor > n) {
        std::cout << n << " is prime\n";
    } else {
        std::cout << n << " = " << divisor << " * " << n / divisor << '\n';
    }
    return 0;
}
n (2..2000000000): 97    ->  97 is prime
n (2..2000000000): 91    ->  91 = 7 * 13
n (2..2000000000): 2     ->  2 is prime
Остановить цикл из середины тела можно инструкцией break — она в разделе 6. Условие поиска в заголовке цикла от неё не устаревает: по нему сразу видно, когда цикл заканчивается.
08

Вложенные циклы

Тело цикла может содержать другой цикл. Внутренний цикл выполняется целиком на каждой итерации внешнего. Таблица умножения — внешний цикл по строкам, внутренний по столбцам:

// Таблица умножения n x n — вложенные циклы
#include <iostream>
#include <iomanip>

int main()
{
    int n;
    std::cout << "n (1..12): ";
    std::cin >> n;
    if (n < 1 || n > 12) {
        std::cout << "Error: n must be 1..12\n";
        return 1;
    }

    for (int row = 1; row <= n; ++row) {
        for (int col = 1; col <= n; ++col) {
            std::cout << std::setw(4) << row * col;
        }
        std::cout << '\n';
    }
    return 0;
}
n (1..12): 5
   1   2   3   4   5
   2   4   6   8  10
   3   6   9  12  15
   4   8  12  16  20
   5  10  15  20  25

std::setw(4) из <iomanip> задаёт ширину поля для следующего выводимого значения: оно дополняется пробелами слева до 4 символов, и столбцы выравниваются. В отличие от setprecision, setw действует один раз — поэтому стоит внутри цикла перед каждым числом. Перевод строки — после внутреннего цикла, в теле внешнего.

Граница внутреннего цикла может зависеть от переменной внешнего. Треугольник: в строке с номером row — row звёздочек.

for (int row = 1; row <= 4; ++row) {           *
    for (int k = 1; k <= row; ++k) {         **
        std::cout << '*';                   ***
    }                                           ****
    std::cout << '\n';
}
Число итераций перемножается. Таблица n × n — это n² выполнений тела внутреннего цикла: для n = 1000 — миллион, для n = 100 000 — десять миллиардов, программа будет работать минуты. Прежде чем вкладывать цикл, оцените произведение границ. У вложенных циклов — разные переменные: если внутренний цикл тоже использует row, он испортит счётчик внешнего.
09

Табулирование функции: целый счётчик вместо вещественного

Табулировать функцию — вывести таблицу её значений на отрезке [a; b] с шагом h. Напрашивается вещественный счётчик:

for (double x = 0; x <= 0.3; x += 0.1) {
    std::cout << x << '\n';
}
0
0.1
0.2

Точки 0.3 нет. 0.1 в двоичной записи — бесконечная дробь (раздел 3), и после трёх сложений x равно 0.30000000000000004 — чуть больше 0.3, условие ложно. С другими a, b и h лишней может оказаться и точка за концом отрезка. Надёжный способ — заранее посчитать число шагов и вести целый счётчик, а x вычислять от начала отрезка:

// Таблица значений y = x^2 - 2x на отрезке [a; b] с шагом h
#include <iostream>
#include <iomanip>

constexpr double EPS = 1e-9;

int main()
{
    double a, b, h;
    std::cout << "a, b, h: ";
    std::cin >> a >> b >> h;
    if (h <= 0 || b < a) {
        std::cout << "Error: need h > 0 and a <= b\n";
        return 1;
    }

    int steps = static_cast<int>((b - a) / h + EPS);   // число шагов, с допуском
    std::cout << std::fixed;
    for (int i = 0; i <= steps; ++i) {
        double x = a + i * h;
        double y = x * x - 2 * x;
        std::cout << std::setprecision(2) << std::setw(6) << x
                  << std::setprecision(4) << std::setw(10) << y << '\n';
    }
    return 0;
}
a, b, h: 0 0.3 0.1                  a, b, h: 0 1 0.25
  0.00    0.0000                      0.00    0.0000
  0.10   -0.1900                      0.25   -0.4375
  0.20   -0.3600                      0.50   -0.7500
  0.30   -0.5100                      0.75   -0.9375
                                      1.00   -1.0000

(b − a) / h для первого примера равно 2.9999999999999996, а не 3: преобразование в int отбрасывает дробную часть и дало бы 2 шага. Допуск EPS возвращает «почти целое» к целому. Если h не укладывается в отрезок целое число раз (0 1 0.3), последняя точка — 0.9, за конец отрезка таблица не выходит. Погрешность при таком способе не накапливается: каждое x получается одним умножением и одним сложением.

10

Ловушки циклов

Точка с запятой после заголовка. for (…); — цикл с пустым телом: он пять раз ничего не делает, а строка под ним выполняется один раз, как бы она ни была сдвинута. g++ замечает это только по отступу:

for (int i = 0; i < 5; ++i);
    std::cout << "*";                  // выведет одну звёздочку, а не пять
emptyfor.cpp:6:5: warning: this 'for' clause does not guard... [-Wmisleading-indentation]

С while (i <= n); хуже: пустое тело не меняет i, и цикл бесконечный.

Беззнаковый счётчик вниз до нуля. У unsigned нет отрицательных значений: после 0 идёт 4 294 967 295 (раздел 3), условие k >= 0 истинно всегда.

for (unsigned k = 3; k >= 0; --k) {     // 3 2 1 0 4294967295 4294967294 ...
unsigneddown.cpp:6:28: warning: comparison of unsigned expression in '>= 0' is always true [-Wtype-limits]
ОшибкаПризнакПредупреждениеКак избежать
; после заголовкатело выполняется один раз или цикл бесконечныйтолько по отступускобки у тела, открывающая — на строке заголовка
переменная цикла не меняетсябесконечный циклнетнайти в теле строку, которая приближает конец
ошибка «на единицу»: < вместо <=, начало с 0 вместо 1на одну итерацию меньше или большенеттрассировка первой и последней итерации
вещественный счётчиктеряется или добавляется последняя точканетцелый счётчик, x = a + i * h (пункт 09)
unsigned вниз до 0бесконечный цикл-Wtype-limitsсчётчик int
переполнение накопителястранные или отрицательные результатынетоценить максимум, long long, ограничить ввод
накопитель не обнулён перед цикломмусор в результате-Wuninitialized (раздел 3), не всегданачальное значение — в строке объявления
Половину ошибок циклов компилятор не видит вообще. Их находят трассировкой и тестами: цикл не выполнился ни разу, выполнился один раз, много раз; значения на границах условия; наибольшие допустимые данные — для переполнения и времени работы.

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

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

  • while проверяет условие до каждой итерации; тело может не выполниться ни разу
  • У цикла три части — начальное значение, условие, изменение; в каждом цикле видно, что приближает его к концу
  • for — когда число повторений известно заранее, while — когда повторять до условия; один переписывается через другой
  • Сумма начинается с 0, произведение с 1, максимум — с первого значения; накопителю нужен тип с запасом
  • Ввод до признака конца — чтение перед циклом и в конце тела; признак не обрабатывается
  • Поиск: в условии цикла — и «кандидаты кончились», и «найдено»; после цикла выясняется, что из двух
  • Внутренний цикл выполняется целиком на каждой итерации внешнего; итерации перемножаются
  • Счётчик цикла — целый; вещественное x вычисляется от него
Связи раздела. Опирается на раздел 1 — итерационная модель, цикл с предусловием в блок-схеме и доказательство, что повторение закончится; раздел 3 — % и / для цифр, диапазоны типов и переполнение, погрешность double, ++; раздел 4 — условия, проверка входных данных, тесты на границах. Нужен для: раздела 6 — цикл с постусловием, break и continue, проверка ввода в цикле; раздела 8 — обход массивов и матриц, сортировка; раздела 10 — обработка строк; разделов 16–19 — циклы ожидания и обработки сообщений в Windows API.
Практические работы №5 и №6 — на учебном портале. Задания по вариантам, критерии оценивания и сдача — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 4: Ветвления Практика на портале