LEARNING · STAGE 00 · PROGRAMMING BASICS

Циклы: 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 — проверяет его перед каждым повтором заново.

main.cppC++
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 — то же самое, но условие проверяется после блока, а не до. Из-за этого тело цикла выполнится хотя бы один раз, даже если условие изначально ложно:

main.cppC++
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 существует именно затем, чтобы для самого частого случая («заранее известно, сколько раз повторять») эти три части не приходилось разносить — собирает их в одну строку:

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

Три части в скобках, разделённые ;: int i = 1 — инициализация, выполняется один раз перед первым повтором; i <= 5 — условие, проверяется перед каждым повтором, как в while; i++ — шаг, выполняется после каждого повтора (i++ — сокращение для i = i + 1, инкремент). Тот же цикл, что выше, через while, выглядел бы так:

Тот же цикл через whileC++
int i = 1;
while (i <= 5)
{
    std::cout << i << "\n";
    i++;
}

Логика одна и та же — for просто держит все три части рядом, что снижает риск забыть шаг изменения счётчика (частая причина зацикливания, см. ниже).

06while, do-while и for — таблица

whiledo-whilefor
Когда проверяется условиедо каждого повторапосле каждого повторадо каждого повтора (как 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 в прошлом уроке. Программа-персонаж это хорошо иллюстрирует: отряд из нескольких участников, и для каждого нужно смоделировать несколько раундов боя — внешний цикл идёт по участникам отряда, внутренний — по раундам боя для текущего участника.

main.cppC++
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";
    }
}
консоль (иллюстрация)
./main Участник 1, раунд 1: HP = 15 Участник 1, раунд 2: HP = 10 Участник 1, раунд 3: HP = 5 Участник 2, раунд 1: HP = 35 Участник 2, раунд 2: HP = 30 Участник 2, раунд 3: HP = 25 Участник 3, раунд 1: HP = 55 Участник 3, раунд 2: HP = 50 Участник 3, раунд 3: HP = 45 Три участника, по три раунда урона у каждого.

При каждом повторе внешнего цикла (member) переменная hp объявляется заново — со своим стартовым значением для этого участника — и внутренний цикл (round) выполняется полностью, от начала до конца, прежде чем внешний перейдёт к следующему member. Именно поэтому hp у второго участника начинается с 40, а не продолжает уменьшаться от первого — она объявлена внутри тела внешнего цикла, а значит создаётся заново на каждом его повторе (та же область видимости, что в прошлом уроке). Для трёх участников по три раунда тело внутреннего цикла выполнится 3 × 3 = 9 раз — с ростом размеров вложенных циклов число повторов растёт как произведение, а не сумма, это стоит держать в голове.

08Хуже → лучше: границы цикла

Задача — вывести числа от 1 до 5 включительно, ровно пять штук. Все три версии ниже синтаксически корректны и компилируются без единой ошибки.

Хуже — на одно число больше, чем нужноC++
for (int i = 0; i <= 5; i++)
{
    std::cout << i << " ";
}
консоль (иллюстрация)
./main 0 1 2 3 4 5 Шесть чисел вместо пяти, и первое — 0, а не 1.

Счётчик начинается с 0 (а нужно с 1), да ещё и условие i <= 5 включает само число 5 — суммарно шесть повторов вместо пяти. Классический off-by-one, разобранный ниже в «Типичных ошибках».

Лучше — верные границыC++
for (int i = 1; i <= 5; i++)
{
    std::cout << i << " ";
}
консоль (иллюстрация)
./main 1 2 3 4 5 Ровно пять чисел, от 1 до 5.

Правильно по количеству и по значениям — но вывод заканчивается лишним пробелом после последнего числа (незаметно в консоли, но не идеально для чистого форматирования).

Лучше всего — без лишнего разделителя в концеC++
for (int i = 1; i <= 5; i++)
{
    std::cout << i;
    if (i < 5)
    {
        std::cout << ", ";
    }
}
std::cout << "\n";
консоль (иллюстрация)
./main 1, 2, 3, 4, 5 Аккуратный список, разделитель только между числами.

if (i < 5) внутри цикла — из прошлого урока: разделитель ", " печатается только между числами, а не после последнего. Небольшая деталь, но именно так формируют списки в реальном выводе.

09break и continue

Оба работают внутри любого цикла (while, do-while, for) и меняют его обычный ход:

ОператорЧто делает
break;немедленно завершает цикл целиком — выполнение продолжается сразу после него, будто условие уже стало false
continue;пропускает оставшуюся часть текущего повтора и переходит к следующей проверке условия (в for — перед этим ещё выполняется шаг)
main.cppC++
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 никогда не меняется — условие всегда истинно
}
консоль (иллюстрация)
./main 0000000000000000000000... Программа не останавливается — нужен Ctrl+C.

В for эта ошибка встречается реже именно потому, что шаг записан прямо рядом с условием и его сложнее забыть.

Лишняя точка с запятой сразу после условия
for (int i = 0; i < 5; i++);   // ; здесь — пустое тело цикла
{
    std::cout << i;
}
консоль (иллюстрация)
./main 5 Ожидалось "01234", получено одно число — и то не то.

; сразу после ) — пустая инструкция (expression statement без содержимого, см. второй урок), именно она и становится телом цикла; цикл пять раз выполнит «ничего», а блок { } ниже выполнится один раз уже после цикла, с i, равным 5. Компилятор не считает это ошибкой — стопроцентно валидный синтаксис, просто не то, что имелось в виду.

Off-by-one — на одну итерацию больше или меньше

Полный разбор с примером вывода — в разделе «Хуже → лучше» выше. Полезная привычка: для «ровно N повторов, начиная с 0» — i < N; для «включая последнее значение N» — i <= N, начиная обычно с 1.

12Проверь себя

Три вопроса: предсказание количества повторов, диагностика бесконечного цикла и разница между break и continue.

Предскажи результат
Сколько раз выполнится тело цикла и с какими значениями i?
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 или continue?
Нужно искать первое число больше 100 в последовательности и сразу остановиться, как только нашли. Что использовать?
Показать объяснение

break — цикл нужно завершить целиком, дальнейшие числа уже не важны. Для сравнения: задачу «пройти по всем числам от 1 до 20 и вывести все, кроме кратных 3» решает continue — цикл должен продолжаться до конца, просто некоторые повторы пропускаются без вывода, а не прерывают весь перебор.

13Практика

★ Выведи числа от 1 до 5 включительно, каждое на новой строке, с помощью for.

★★ Выведи только чётные числа от 1 до 10 включительно: пройди циклом for по всем числам, а нечётные пропускай через continue (используй % для проверки чётности).

★★★ Смоделируй бой для отряда из 3 участников с помощью вложенных циклов for, как в разделе «Вложенные циклы» выше: внешний цикл — участники (member от 1 до 3, стартовое HP = member * 20), внутренний — 3 раунда урона (damage = 5 за раунд), вывод HP участника после каждого раунда.

14Термины

ТерминЗначит
Цикл (loop)конструкция, повторяющая блок кода, пока выполняется условие
Итерацияодин повтор тела цикла
Счётчик циклапеременная, отслеживающая номер текущего повтора (например, i в for)
Off-by-oneошибка на одну итерацию больше или меньше из-за неверной границы условия
Вложенный циклцикл, тело которого содержит ещё один цикл

15Что ты теперь умеешь

До этого урока каждая повторяющаяся строка кода писалась руками. Теперь повторение — это одна конструкция:

  • Ты можешь выбрать между while, do-while и for под конкретную задачу — известно ли заранее число повторов и нужно ли гарантированно хотя бы одно выполнение.
  • Ты можешь предсказать, сколько раз выполнится тело цикла и с какими значениями счётчика, включая нестандартный шаг.
  • Ты можешь найти в чужом коде причину бесконечного цикла — отсутствие изменения переменной, влияющей на условие.
  • Ты можешь использовать break и continue осознанно, понимая разницу между «остановить весь цикл» и «пропустить один повтор».
  • Ты можешь вложить один цикл в другой и объяснить, почему число повторов внутреннего тела растёт как произведение, а не сумма.

16Кратко

  • while (условие) { ... } — повторяет блок, пока условие истинно; проверка до повтора.
  • do { ... } while (условие); — то же самое, но проверка после — минимум одно выполнение гарантировано.
  • for (инициализация; условие; шаг) { ... } — компактная запись для известного числа повторов.
  • break; — выйти из цикла (или switch) целиком; continue; — пропустить остаток текущего повтора.
  • Забытый шаг изменения счётчика — самая частая причина зависшей программы.

Циклы повторяют один и тот же код — но пока этот код обязан быть написан прямо внутри main(). Следующий урок — функции: способ вынести повторяющуюся логику в отдельный именованный блок и вызывать его откуда угодно, а не только внутри одного цикла.

🎮 А как это связано с Unreal Engine?

while/for работают в Unreal C++ без изменений — обходить массив предметов инвентаря или список игроков в комнате ты будешь именно так:

Пример (Unreal C++, для примера)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), которая сама вызывается движком каждый кадр — по сути, это «внешний» цикл, который ты не пишешь руками (условие «пока игра запущена» и повтор скрыты внутри движка), но пишешь код, который выполняется на каждом его повторе:

Пример (Unreal C++, для примера)C++
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 так же, как обычный вывод.