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 << "Привіт #" << i << '\n';
    }
}

Якщо ви бачите for, корисно вміти читати його буквально: «почни з i = 1, поки i <= 5, щоразу роби i = i + 1».

Так, крапки з комою всередині круглих дужок обовʼязкові. Це типова «помилка новачка № 1», але сьогодні ми її переможемо знанням, а не сльозами.

Порядок виконання for

У for немає прихованої телепатії: він працює в дуже конкретному порядку. Але через те, що все записано компактно, новачок може переплутати, що і коли відбувається. Тому зараз ми розкладемо for на кроки. Не для того, щоб ви завжди мислили саме так, а щоб у вас була зрозуміла модель на випадок сумнівів.

Порядок виконання такий:

  1. Виконується ініціалізація (один раз).
  2. Перевіряється умова.
  3. Якщо умова істинна, виконується тіло циклу.
  4. Виконується крок.
  5. Повертаємося до перевірки умови.

Це можна уявити як просту блок-схему:

flowchart TD
    A[Ініціалізація] --> B{Умова?}
    B -- ні --> E[Вихід із циклу]
    B -- так --> 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 << "Скільки днів? ";
    std::cin >> days;

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

Зверніть увагу на межу: day змінюється від 1 до days включно. Для користувача це зрозуміло («день 1», «день 2», …), і саме тут дуже легко натрапити на off-by-one, якщо переплутати < і <=.

Сума та середнє

Тепер додамо накопичення суми — класичний шаблон зі змінною-накопичувачем.

#include <iostream>

int main() {
    int days = 0;
    std::cout << "Скільки днів? ";
    std::cin >> days;

    int total = 0;

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

        total = total + steps;
    }

    std::cout << "Усього кроків: " << total << '\n';
}

Далі хочеться порахувати середнє. Але в нас поки лише int, тому зробимо ціле середнє й остачу від ділення. Це і чесно, і корисно, адже оператор % ми вже знаємо.

#include <iostream>

int main() {
    int days = 0;
    std::cout << "Скільки днів? ";
    std::cin >> days;

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

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

    std::cout << "Середнє (ціле): " << avg << '\n';
    std::cout << "Остача: " << rem << '\n';
}

Тут є важливий момент: якщо days виявиться рівним 0, ми отримаємо ділення на нуль. А це вже не «помилка компіляції», а дуже неприємна помилка під час виконання. Повноцінну обробку помилок введення ми ще не проходили, але просту перевірку через if уже вміємо робити, і тут її цілком достатньо.

Зробімо простий запобіжник:

#include <iostream>

int main() {
    int days = 0;
    std::cout << "Скільки днів? ";
    std::cin >> days;

    if (days <= 0) {
        std::cout << "Кількість днів має бути додатною.\n";
        return 0;
    }

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

    std::cout << "Усього кроків: " << 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 << "Мінімум: " << minSteps << ", максимум: " << maxSteps << '\n';
}

Так, тут два if підряд — і для нашого поточного рівня це нормально. Головне, що логіка прозора: перше введене значення задає початкові minSteps і maxSteps, а далі ми їх лише оновлюємо.

І ось тут 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 після циклу?» — бо змінна припинила існування разом із циклом. І це цілком нормальний захист від випадкових помилок.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ