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 (ініціалізація; умова; крок) {
// тіло циклу
}
Зміст цих частин такий.
Ініціалізація виконується один раз перед початком циклу. Зазвичай тут створюють лічильник і задають йому початкове значення, наприклад 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 на кроки. Не для того, щоб ви завжди мислили саме так, а щоб у вас була зрозуміла модель на випадок сумнівів.
Порядок виконання такий:
- Виконується ініціалізація (один раз).
- Перевіряється умова.
- Якщо умова істинна, виконується тіло циклу.
- Виконується крок.
- Повертаємося до перевірки умови.
Це можна уявити як просту блок-схему:
flowchart TD
A[Ініціалізація] --> B{Умова?}
B -- ні --> E[Вихід із циклу]
B -- так --> C[Тіло циклу]
C --> D[Крок]
D --> B
А тепер важливий практичний прийом: якщо ви сумніваєтеся, скільки разів спрацює цикл, візьміть папір або нотатки й пройдіть перші 2–3 ітерації вручну. Так ви впіймаєте 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 можуть бути еквівалентними, у реальному коді зазвичай обирають ту форму, яка краще передає задум.
Якщо повторення природно описується лічильником (1…n, 0…n-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 разів» | |
i набуває значень 0..n-1, ітерацій рівно n |
| «від 1 до n включно» | |
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 після циклу?» — бо змінна припинила існування разом із циклом. І це цілком нормальний захист від випадкових помилок.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ