Циклы: while, do-while, for
В самом первом уроке, чтобы вывести три строки, пришлось трижды написать почти одинаковый std::cout. Для трёх строк это терпимо, для тысячи — нет. Циклы решают именно это: выполнить один и тот же код много раз, не копируя его руками.
01Что ты изучишь
while— повторение, пока условие истинно, проверка перед каждым повторомdo-while— то же самое, но проверка после, минимум одно выполнение гарантированоfor— компактная запись, когда известно, сколько раз повторятьbreak— досрочно выйти из цикла целикомcontinue— пропустить остаток текущего повтора и перейти к следующему
02Основная идея: «повторяй, пока...»
Цикл — это компьютерный аналог инструкции вида «повторяй это, пока не выполнится условие», ровно как в обычных задачах: мой тарелки, пока в раковине есть грязные; отожмись 20 раз; проверяй почту, пока не придёт нужное письмо. В каждом из этих примеров есть действие, которое повторяется, и условие, которое рано или поздно останавливает повторение. Три конструкции этого урока — while, do-while, for — делают ровно это, различаясь только тем, когда именно проверяется условие и насколько компактно записан счётчик повторов.
03while — пока условие истинно
Обратный отсчёт от 3 до Liftoff — три строки, которые различаются только числом. Написать их вручную несложно, а если отсчёт не от 3, а от 100 — уже нет. while берёт то же условие bool, что уже встречалось в if, и просто не отпускает выполнение из блока { }, пока условие остаётся true — проверяет его перед каждым повтором заново.
int countdown = 3;
while (countdown > 0)
{
std::cout << countdown << "\n";
countdown = countdown - 1;
}
std::cout << "Liftoff!\n";
Важно: countdown = countdown - 1; внутри блока — это то, что в итоге сделает условие false. Без этой строки условие countdown > 0 останется истинным навсегда — программа зациклится и не завершится (см. «Типичные ошибки»). Условие проверяется до каждого повтора — если оно ложно с самого начала, блок не выполнится ни разу.
04do-while — сначала выполнить, потом проверить
do-while — то же самое, но условие проверяется после блока, а не до. Из-за этого тело цикла выполнится хотя бы один раз, даже если условие изначально ложно:
int attempts = 0;
do
{
std::cout << "Attempt\n";
attempts = attempts + 1;
}
while (attempts < 3);
Обрати внимание на синтаксис: while (...) здесь идёт после блока и заканчивается ; — в этом легко ошибиться, перепутав с обычным while. На практике do-while используется реже обычного while — только там, где по смыслу нужно выполнить действие как минимум один раз, прежде чем впервые проверять условие (например: показать меню хотя бы раз, даже если пользователь сразу выберет «выход»).
05for — известное число повторов
«Вывести числа от 1 до 5» через while требует переменной-счётчика до цикла, условия в скобках while и шага изменения счётчика где-то внутри блока — три части, логически связанные, но разнесённые по трём разным местам кода. for существует именно затем, чтобы для самого частого случая («заранее известно, сколько раз повторять») эти три части не приходилось разносить — собирает их в одну строку:
for (int i = 1; i <= 5; i++)
{
std::cout << i << "\n";
}
Три части в скобках, разделённые ;: int i = 1 — инициализация, выполняется один раз перед первым повтором; i <= 5 — условие, проверяется перед каждым повтором, как в while; i++ — шаг, выполняется после каждого повтора (i++ — сокращение для i = i + 1, инкремент). Тот же цикл, что выше, через while, выглядел бы так:
int i = 1;
while (i <= 5)
{
std::cout << i << "\n";
i++;
}
Логика одна и та же — for просто держит все три части рядом, что снижает риск забыть шаг изменения счётчика (частая причина зацикливания, см. ниже).
06while, do-while и for — таблица
while | do-while | for | |
|---|---|---|---|
| Когда проверяется условие | до каждого повтора | после каждого повтора | до каждого повтора (как while) |
| Минимум выполнений тела | 0 (если условие сразу false) | 1 (тело выполнится всегда хотя бы раз) | 0 (если условие сразу false) |
| Где живёт счётчик | объявляется отдельной строкой до цикла | объявляется отдельной строкой до цикла | инициализация, условие и шаг — все три части в одной строке |
| Когда удобнее | число повторов заранее не известно (например, «пока не введут правильный пароль») | нужно выполнить действие хотя бы раз перед первой проверкой (например, показать меню) | число повторов известно заранее (например, «ровно 10 раз» или «по всем элементам») |
| Пример | while (x > 0) { ...; x--; } | do { ...; x++; } while (x < 3); | for (int i = 0; i < 5; i++) { ... } |
На практике for выбирают чаще всего именно потому, что счётчик, условие и шаг видны в одном месте — сложнее забыть что-то одно, в отличие от while, где эти три части разбросаны по разным строкам (см. «Живая отладка» ниже про забытый шаг).
07Вложенные циклы
Тело цикла — это блок { }, а внутри блока может быть любая инструкция, включая ещё один цикл — точно тот же принцип вложенности, что был с if внутри if в прошлом уроке. Программа-персонаж это хорошо иллюстрирует: отряд из нескольких участников, и для каждого нужно смоделировать несколько раундов боя — внешний цикл идёт по участникам отряда, внутренний — по раундам боя для текущего участника.
int damage = 5;
for (int member = 1; member <= 3; member++)
{
int hp = member * 20; // у каждого участника отряда своё стартовое HP
for (int round = 1; round <= 3; round++)
{
hp = hp - damage;
std::cout << "Участник " << member << ", раунд " << round << ": HP = " << hp << "\n";
}
}
При каждом повторе внешнего цикла (member) переменная hp объявляется заново — со своим стартовым значением для этого участника — и внутренний цикл (round) выполняется полностью, от начала до конца, прежде чем внешний перейдёт к следующему member. Именно поэтому hp у второго участника начинается с 40, а не продолжает уменьшаться от первого — она объявлена внутри тела внешнего цикла, а значит создаётся заново на каждом его повторе (та же область видимости, что в прошлом уроке). Для трёх участников по три раунда тело внутреннего цикла выполнится 3 × 3 = 9 раз — с ростом размеров вложенных циклов число повторов растёт как произведение, а не сумма, это стоит держать в голове.
08Хуже → лучше: границы цикла
Задача — вывести числа от 1 до 5 включительно, ровно пять штук. Все три версии ниже синтаксически корректны и компилируются без единой ошибки.
for (int i = 0; i <= 5; i++)
{
std::cout << i << " ";
}
Счётчик начинается с 0 (а нужно с 1), да ещё и условие i <= 5 включает само число 5 — суммарно шесть повторов вместо пяти. Классический off-by-one, разобранный ниже в «Типичных ошибках».
for (int i = 1; i <= 5; i++)
{
std::cout << i << " ";
}
Правильно по количеству и по значениям — но вывод заканчивается лишним пробелом после последнего числа (незаметно в консоли, но не идеально для чистого форматирования).
for (int i = 1; i <= 5; i++)
{
std::cout << i;
if (i < 5)
{
std::cout << ", ";
}
}
std::cout << "\n";
if (i < 5) внутри цикла — из прошлого урока: разделитель ", " печатается только между числами, а не после последнего. Небольшая деталь, но именно так формируют списки в реальном выводе.
09break и continue
Оба работают внутри любого цикла (while, do-while, for) и меняют его обычный ход:
| Оператор | Что делает |
|---|---|
break; | немедленно завершает цикл целиком — выполнение продолжается сразу после него, будто условие уже стало false |
continue; | пропускает оставшуюся часть текущего повтора и переходит к следующей проверке условия (в for — перед этим ещё выполняется шаг) |
for (int i = 1; i <= 10; i++)
{
if (i == 5)
{
break; // цикл остановится на i == 5, дальше не пойдёт
}
std::cout << i << "\n"; // выведет 1 2 3 4
}
break в switch из прошлого урока — тот же самый оператор: он точно так же «выходит» из окружающей конструкции, просто там окружающей конструкцией был switch, а не цикл.
10Попробуй изменить
- В примере из раздела «for» замени
i <= 5наi < 5— последнее число в выводе изменится с 5 на 4. - В примере из раздела «break и continue» замени
break;наcontinue;— вместо остановки на 5 цикл пропустит только вывод числа 5 и продолжит до 10. - Убери
attempts = attempts + 1;из примераdo-while— программа зациклится (не делай этого в реальном проекте без способа прервать выполнение). - В примере с отрядом замени внутренний предел
round <= 3наround <= 5— на каждого участника станет приходиться пять раундов урона вместо трёх.
11Живая отладка: типичные ошибки
Как и в прошлых двух уроках, ни одна из этих ошибок не остановит компиляцию.
int i = 0;
while (i < 5)
{
std::cout << i; // i никогда не меняется — условие всегда истинно
}
В for эта ошибка встречается реже именно потому, что шаг записан прямо рядом с условием и его сложнее забыть.
for (int i = 0; i < 5; i++); // ; здесь — пустое тело цикла
{
std::cout << i;
}
; сразу после ) — пустая инструкция (expression statement без содержимого, см. второй урок), именно она и становится телом цикла; цикл пять раз выполнит «ничего», а блок { } ниже выполнится один раз уже после цикла, с i, равным 5. Компилятор не считает это ошибкой — стопроцентно валидный синтаксис, просто не то, что имелось в виду.
Полный разбор с примером вывода — в разделе «Хуже → лучше» выше. Полезная привычка: для «ровно N повторов, начиная с 0» — i < N; для «включая последнее значение N» — i <= N, начиная обычно с 1.
12Проверь себя
Три вопроса: предсказание количества повторов, диагностика бесконечного цикла и разница между break и continue.
for (int i = 2; i <= 8; i = i + 2)
{
std::cout << i << " ";
}Показать объяснение
Четыре раза, выведет 2 4 6 8. Шаг здесь не i++, а i = i + 2 — счётчик увеличивается сразу на 2 за повтор, а не на 1; цикл всё ещё останавливается, когда условие i <= 8 становится ложным (при i = 10).
int lives = 3;
while (lives > 0)
{
std::cout << "Try again\n";
}Показать объяснение
Внутри блока lives никогда не меняется — условие lives > 0 остаётся истинным навсегда (см. §11). Нужна инструкция вроде lives = lives - 1; внутри блока, которая рано или поздно сделает условие ложным.
Показать объяснение
break — цикл нужно завершить целиком, дальнейшие числа уже не важны. Для сравнения: задачу «пройти по всем числам от 1 до 20 и вывести все, кроме кратных 3» решает continue — цикл должен продолжаться до конца, просто некоторые повторы пропускаются без вывода, а не прерывают весь перебор.
13Практика
★ Выведи числа от 1 до 5 включительно, каждое на новой строке, с помощью for.
Три части в скобках for: начальное значение i, условие остановки, шаг — ровно как в разделе 05.
Решение: for (int i = 1; i <= 5; i++) { std::cout << i << "\n"; }
★★ Выведи только чётные числа от 1 до 10 включительно: пройди циклом for по всем числам, а нечётные пропускай через continue (используй % для проверки чётности).
Нечётный остаток от деления на 2 равен 1, а не 0 — значит, условие для пропуска: i % 2 != 0. После continue код ниже него в этом повторе не выполнится.
Решение: if (i % 2 != 0) { continue; } std::cout << i << "\n";
★★★ Смоделируй бой для отряда из 3 участников с помощью вложенных циклов for, как в разделе «Вложенные циклы» выше: внешний цикл — участники (member от 1 до 3, стартовое HP = member * 20), внутренний — 3 раунда урона (damage = 5 за раунд), вывод HP участника после каждого раунда.
Внешний for отвечает за участников — hp объявляется ЗАНОВО внутри его тела на каждом повторе, со своим стартовым значением member * 20.
Внутренний for — целиком внутри тела внешнего — на каждом своём повторе уменьшает hp на damage и выводит текущее значение.
Решение: for (int member = 1; member <= 3; member++) { int hp = member * 20; for (int round = 1; round <= 3; round++) { hp = hp - damage; std::cout << "Участник " << member << ", раунд " << round << ": HP = " << hp << "\n"; } }
14Термины
| Термин | Значит |
|---|---|
| Цикл (loop) | конструкция, повторяющая блок кода, пока выполняется условие |
| Итерация | один повтор тела цикла |
| Счётчик цикла | переменная, отслеживающая номер текущего повтора (например, i в for) |
| Off-by-one | ошибка на одну итерацию больше или меньше из-за неверной границы условия |
| Вложенный цикл | цикл, тело которого содержит ещё один цикл |
15Что ты теперь умеешь
До этого урока каждая повторяющаяся строка кода писалась руками. Теперь повторение — это одна конструкция:
- Ты можешь выбрать между
while,do-whileиforпод конкретную задачу — известно ли заранее число повторов и нужно ли гарантированно хотя бы одно выполнение. - Ты можешь предсказать, сколько раз выполнится тело цикла и с какими значениями счётчика, включая нестандартный шаг.
- Ты можешь найти в чужом коде причину бесконечного цикла — отсутствие изменения переменной, влияющей на условие.
- Ты можешь использовать
breakиcontinueосознанно, понимая разницу между «остановить весь цикл» и «пропустить один повтор». - Ты можешь вложить один цикл в другой и объяснить, почему число повторов внутреннего тела растёт как произведение, а не сумма.
16Кратко
while (условие) { ... }— повторяет блок, пока условие истинно; проверка до повтора.do { ... } while (условие);— то же самое, но проверка после — минимум одно выполнение гарантировано.for (инициализация; условие; шаг) { ... }— компактная запись для известного числа повторов.break;— выйти из цикла (или switch) целиком;continue;— пропустить остаток текущего повтора.- Забытый шаг изменения счётчика — самая частая причина зависшей программы.
Циклы повторяют один и тот же код — но пока этот код обязан быть написан прямо внутри main(). Следующий урок — функции: способ вынести повторяющуюся логику в отдельный именованный блок и вызывать его откуда угодно, а не только внутри одного цикла.
while/for работают в Unreal C++ без изменений — обходить массив предметов инвентаря или список игроков в комнате ты будешь именно так:
for (int32 i = 0; i < InventoryItems.Num(); i++)
{
UE_LOG(LogTemp, Log, TEXT("Item %d: %s"), i, *InventoryItems[i]);
}
// или, идиоматичнее для Unreal-массивов (TArray) — range-based for:
for (const FString& Item : InventoryItems)
{
UE_LOG(LogTemp, Log, TEXT("Item: %s"), *Item);
}
Отдельная параллель появится позже: у каждого Actor в Unreal есть функция Tick(float DeltaTime), которая сама вызывается движком каждый кадр — по сути, это «внешний» цикл, который ты не пишешь руками (условие «пока игра запущена» и повтор скрыты внутри движка), но пишешь код, который выполняется на каждом его повторе:
void AMyActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// этот код выполняется каждый кадр — как тело цикла без видимого while
Health = FMath::Clamp(Health + RegenRate * DeltaTime, 0.0f, MaxHealth);
}
TArray.Num() вместо длины массива и Tick() — Unreal-специфика, подробности в Stage 07+; сами циклы for/while внутри — то же самое, что в этом уроке. Ещё одна деталь, которая иначе осталась бы немым пятном: %d и %s внутри TEXT("...") — это не что-то новое в языке, а старый printf-синтаксис форматирования строк, которым пользуется UE_LOG вместо привычного std::cout <<; %d — «сюда подставь целое число», %s — «сюда подставь строку», а порядок подстановки задают аргументы после самой строки, слева направо. Полный разбор этого формата — в Stage 07+, здесь важно только не спутать его с std::cout и не пытаться писать UE_LOG так же, как обычный вывод.