JavaRush /Курсы /C++ SELF /Цикл for: счётчик, границы

Цикл for: счётчик, границы

C++ SELF
4 уровень , 2 лекция
Открыта

1. Зачем нужен for, если есть while

Если смотреть на программирование глазами новичка, кажется: “Ну есть же while, он повторяет — и ладно”. Но через пару задач вы замечаете повторяющийся сюжет: вы заводите счётчик, задаёте границу, в теле делаете полезную работу и обязательно увеличиваете счётчик. И так снова, и снова, и снова — как сериал, который можно было бы сократить до фильма.

Цикл for как раз и придумали, чтобы этот шаблон выглядел честно и компактно: “вот старт, вот условие, вот шаг”. То есть for — это не новая магия, а более удобная упаковка очень типичного while.

Сравним на примере: вывести числа от 1 до 5.

Вариант с while:

#include <iostream>

int main() {
    int i = 1;

    while (i <= 5) {
        std::cout << i << '\n';
        i = i + 1;
    }
}

Вариант с for (то же самое, но короче и “собраннее”):

#include <iostream>

int main() {
    for (int i = 1; i <= 5; i = i + 1) {
        std::cout << i << '\n';
    }
}

Обратите внимание на ощущение: в for вся механика повторения видна в одной строке. И мозг перестаёт искать “а где же тут увеличивается i?” — потому что шаг прямо в заголовке.

2. Синтаксис for: инициализация, условие, шаг

Когда вы впервые видите for (что-то; что-то; что-то), хочется спросить: “Это пароль от Wi‑Fi или цикл?” На самом деле всё очень логично. Заголовок for состоит из трёх частей, разделённых точками с запятой, и каждая часть отвечает за свою роль: старт, проверка, продвижение.

Общая форма:

for (инициализация; условие; шаг) {
    // тело цикла
}
Общая форма цикла for

Смысл частей такой.

Инициализация выполняется один раз перед началом цикла. Обычно там создают и задают стартовое значение счётчика, например int i = 0.

Условие проверяется перед каждой итерацией (как в while). Пока условие истинно — выполняется тело.

Шаг выполняется после тела на каждой итерации. Обычно там увеличивают счётчик, например i = i + 1.

Вот простой пример “пять раз поздороваться”, но так, чтобы было видно, что мы именно “считаем итерации”:

#include <iostream>

int main() {
    for (int i = 1; i <= 5; i = i + 1) {
        std::cout << "Hello #" << i << '\n';
    }
}

Если вы видите for, полезно научиться читать его буквально: “начни с i = 1, пока i <= 5, каждый раз делай i = i + 1”.

И да, точки с запятой внутри круглых скобок — обязательны. Это частая “ошибка новичка №1”, но сегодня мы её добьём знанием (а не слезами).

Порядок выполнения for

В for нет скрытой телепатии: он выполняется по очень конкретному порядку. Но из-за того, что всё записано компактно, новичок может перепутать, что происходит когда. Поэтому сейчас мы разложим for на шаги. Это не для того, чтобы вы всегда так думали, а чтобы у вас была понятная модель на случай сомнений.

Порядок выполнения такой:

  1. выполняется инициализация (один раз)
  2. проверяется условие
  3. если условие истинно — выполняется тело
  4. выполняется шаг
  5. возвращаемся к проверке условия

Это можно представить в виде простой блок-схемы:

flowchart TD
    A[Инициализация] --> B{Условие?}
    B -- false --> E[Выход из цикла]
    B -- true --> C[Тело цикла]
    C --> D[Шаг]
    D --> B

И теперь важный практический фокус: если вы сомневаетесь, сколько раз сработает цикл, возьмите бумагу (или заметки) и прогоните первые 23 итерации вручную. Так вы поймаете 80% ошибок с границами.

Например, этот код:

#include <iostream>

int main() {
    for (int i = 0; i < 3; i = i + 1) {
        std::cout << i << '\n';
    }
}

Выведет:

  • 0
  • 1
  • 2

Почему не 3? Потому что условие i < 3 перестаёт быть истинным, когда i становится равным 3, а проверка условия происходит до выполнения тела.

Как превратить for в while

Иногда for кажется слишком компактным, особенно на первых порах. Хорошая новость: вы уже знаете while, поэтому можете в любой момент мысленно (или прямо в коде) развернуть for в эквивалентный while. Это снимает ощущение магии: оказывается, for — это просто привычный цикл с тремя компонентами в разных местах.

Вот эквивалентность:

for (init; cond; step) {
    body
}

Примерно то же самое, что:

init;
while (cond) {
    body
    step;
}

Давайте сравним на реальном коде: увеличим значение на 10 три раза.

#include <iostream>

int main() {
    int a = 0;
    for (int i = 0; i < 3; i = i + 1) {
        a = a + 10;
    }

    int b = 0;
    int j = 0;
    while (j < 3) {
        b = b + 10;
        j = j + 1;
    }

    std::cout << a << ' ' << b << '\n'; // 30 30
}

И теперь важная деталь про читаемость: хотя for и while могут быть эквивалентны, в реальном коде обычно выбирают то, что “лучше рассказывает историю”.

Если повторение естественно описывается счётчиком (1n, 0n-1, ровно n раз) — for обычно понятнее.

Если повторение естественно описывается условием без явного “счётчика” (например “пока введённое число не подходит”) — while обычно понятнее.

3. Границы цикла и off-by-one

Есть два вида программистов: те, кто уже ловил off-by-one, и те, кто пока думает, что это название рэп-группы. off-by-one означает, что цикл выполнился на одну итерацию больше или меньше, чем нужно. Ошибка вроде маленькая, но последствия бывают неприятные: лишний ввод, лишний вывод, неправильная сумма — и вы смотрите на результат с выражением “я же всё правильно… вроде”.

Чаще всего эта ошибка появляется из-за путаницы между “включительной” и “невключительной” границей.

Сравним два очень похожих цикла:

for (int i = 0; i < 5; i = i + 1) { ... }   // 0,1,2,3,4  (5 НЕ включаем)
for (int i = 0; i <= 5; i = i + 1) { ... }  // 0,1,2,3,4,5 (5 включаем)

Чтобы не гадать, удобно держать в голове два “стиля” диапазонов.

Что хотим сделать Самый частый стиль Как читать
“ровно n раз”
for (int i = 0; i < n; i = i + 1)
i принимает 0..n-1, итераций ровно n
“от 1 до n включительно”
for (int i = 1; i <= n; i = i + 1)
i принимает 1..n, итераций ровно n

Обратите внимание на психологию: когда вы хотите “ровно n раз”, особенно удобно начинать с 0 и ставить < n. Это даёт красивое правило: количество итераций равно n - 0, то есть n. А когда вы хотите “от 1 до n”, естественно начинается с 1 и заканчивается <= n.

Давайте проверим на примере “посчитать сумму от 1 до n”. Здесь n должно включаться:

#include <iostream>

int main() {
    int n = 0;
    std::cin >> n;

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

    std::cout << sum << '\n';
}

Если случайно написать i < n, сумма станет “от 1 до n-1” — и это классический off-by-one.

Есть ещё одна полезная привычка: проговаривать границу вслух. Например, i < n читаем как “i строго меньше n, значит n не попадает”. А i <= n читаем как “i меньше или равно n, значит n попадает”. Звучит просто, но это реально работает, особенно когда мозг устал, а задача “на пять минут” уже идёт третий час.

4. Мини‑приложение: StepTracker и ввод N значений

Чтобы for не остался просто “красивым синтаксисом”, встроим его в маленькое приложение. Пусть это будет StepTracker — программа, которая спрашивает количество дней, затем просит ввести шаги за каждый день и печатает итог.

Почему это хороший пример для for: мы заранее знаем, сколько раз повторять ввод — ровно days. Это прямо идеальная задача для “счётчика”.

Начнём с заготовки: читаем число дней и делаем цикл по дням.

#include <iostream>

int main() {
    int days = 0;
    std::cout << "How many days? ";
    std::cin >> days;

    for (int day = 1; day <= days; day = day + 1) {
        std::cout << "Day " << day << ": ";
        int steps = 0;
        std::cin >> steps;
    }
}

Обратите внимание на границу: day идёт от 1 до days включительно. Это читабельно для пользователя (“день 1”, “день 2”, …), и здесь очень легко поймать off-by-one, если перепутать < и <=.

Сумма и среднее

Теперь добавим накопление суммы — классический шаблон “переменная-накопитель”.

#include <iostream>

int main() {
    int days = 0;
    std::cout << "How many days? ";
    std::cin >> days;

    int total = 0;

    for (int day = 1; day <= days; day = day + 1) {
        int steps = 0;
        std::cout << "Day " << day << ": ";
        std::cin >> steps;

        total = total + steps;
    }

    std::cout << "Total steps: " << total << '\n';
}

Дальше хочется среднее. Но у нас пока только int, поэтому сделаем “среднее целое” и остаток (это честно и полезно, и мы уже знаем %).

#include <iostream>

int main() {
    int days = 0;
    std::cout << "How many days? ";
    std::cin >> days;

    int total = 0;
    for (int day = 1; day <= days; day = day + 1) {
        int steps = 0;
        std::cout << "Day " << day << ": ";
        std::cin >> steps;
        total = total + steps;
    }

    int avg = total / days;
    int rem = total % days;

    std::cout << "Average (integer): " << avg << '\n';
    std::cout << "Remainder: " << rem << '\n';
}

Тут есть важный момент: если days окажется равным 0, мы получим деление на ноль (а это уже не “ошибка компиляции”, а очень неприятная ошибка выполнения). Полноценную обработку ошибок ввода мы ещё не проходили, но базовую проверку через if мы умеем, и это как раз “по уровню”.

Сделаем простой предохранитель:

#include <iostream>

int main() {
    int days = 0;
    std::cout << "How many days? ";
    std::cin >> days;

    if (days <= 0) {
        std::cout << "Days must be positive.\n";
        return 0;
    }

    int total = 0;
    for (int day = 1; day <= days; day = day + 1) {
        int steps = 0;
        std::cout << "Day " << day << ": ";
        std::cin >> steps;
        total = total + steps;
    }

    std::cout << "Total steps: " << total << '\n';
}

Минимум и максимум без массивов

Теперь добавим минимум и максимум. Без массивов мы не можем “хранить все дни”, но нам и не надо: минимум/максимум можно обновлять на лету. Самая частая проблема тут — как инициализировать minSteps. Мы можем сделать “флаг первого значения”.

#include <iostream>

int main() {
    int days = 0;
    std::cin >> days;

    int total = 0;
    int minSteps = 0;
    int maxSteps = 0;
    bool first = true;

    for (int day = 1; day <= days; day = day + 1) {
        int steps = 0;
        std::cin >> steps;

        total = total + steps;

        if (first) {
            minSteps = steps;
            maxSteps = steps;
            first = false;
        } else {
            if (steps < minSteps) minSteps = steps;
            if (steps > maxSteps) maxSteps = steps;
        }
    }

    std::cout << "Min: " << minSteps << ", max: " << maxSteps << '\n';
}

Да, тут два if подряд — и это нормально для текущего уровня. Главное, что логика не врёт: первый ввод задаёт начальные значения min/max, дальше мы только обновляем.

И вот где for реально удобен: весь контроль количества повторений (“прочитать ровно days чисел”) находится в заголовке цикла, а внутри мы занимаемся смыслом: суммой, минимумом, максимумом. Это именно то, ради чего for любят: он убирает “служебную болтовню” из тела.

5. Типичные ошибки при работе с for

Ошибка №1: перепутали < и <=, и цикл сделал на один проход больше/меньше.
Это классический off-by-one. В таких случаях помогает привычка явно выписывать первые и последние значения счётчика: “мне нужно 1,2,3,4,5” — значит старт 1 и условие <= 5. А если нужно “ровно 5 раз” — удобнее i = 0; i < 5.

Ошибка №2: шаг цикла не ведёт к завершению.
Иногда в for пишут правильное условие, но забывают менять счётчик (или меняют не туда). Например, for (int i = 0; i < n; i = i - 1) почти гарантирует бесконечный цикл при n > 0, потому что i уходит в минус, а условие i < n остаётся истинным. Перед запуском цикла полезно задать себе один вопрос: “что в моём цикле меняется так, чтобы условие однажды стало ложным?”

Ошибка №3: случайно поставили ; после заголовка for.
Запись вида for (... ); { ... } превращает цикл в “пустой”: он крутится сам по себе, а блок в фигурных скобках выполнится ровно один раз после него. Это очень похоже на ситуацию “код как будто правильный, но программа ведёт себя странно”. Лечится дисциплиной: в учебных примерах всегда пишем тело с { ... } сразу после for и внимательно смотрим на лишние ;.

Ошибка №4: меняют счётчик и в шаге, и внутри тела.
Например, for (int i = 0; i < n; i = i + 1) { i = i + 1; } — и вот уже половина элементов “пропала”, потому что счётчик прыгает на 2. Обычно лучше договориться: счётчик изменяется либо в шаге for, либо в теле — но не одновременно (в рамках нашего уровня почти всегда достаточно шага в заголовке).

Ошибка №5: неверно понимают область видимости переменной-счётчика.
Если вы написали for (int i = 0; ... ), то i существует только внутри цикла. Это чаще плюс, чем минус: вы не можете случайно использовать старое значение i ниже по коду. Но иногда новичок удивляется: “почему компилятор не знает i после цикла?” — потому что переменная закончила жизнь вместе с циклом, и это нормальная “защита от случайных ошибок”.

1
Задача
C++ SELF, 4 уровень, 2 лекция
Недоступна
Маршрут по улицам
Маршрут по улицам
1
Задача
C++ SELF, 4 уровень, 2 лекция
Недоступна
Табличка для тренировки умножения
Табличка для тренировки умножения
1
Задача
C++ SELF, 4 уровень, 2 лекция
Недоступна
Сумма первых N “нечётных наград”
Сумма первых N “нечётных наград”
1
Задача
C++ SELF, 4 уровень, 2 лекция
Недоступна
Отчёт по шагам за несколько дней
Отчёт по шагам за несколько дней
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ