После условных операторов для управления потоком можно разобрать циклы.

Без циклов было бы необходимо дублировать код, если мы бы хотели повторять действие (либо использовать оператор goto).

Существует несколько типов циклов

  • for - цикл, который применяется когда количество выполнений известно
  • while - цикл, который применяется когда количество выполнений неизвестно
  • do-while - цикл, который применяется когда количество выполнений неизвестно и необходимо пост-условие
  • foreach - цикл применяющийся для коллекций, является формой цикла for

Цикл for

Этот цикл применяется в той ситуации, когда количество его выполнений известно заранее.

В большинстве IDE при написании оператора for вам предложит раскрыть такую конструкцию.

for (size_t i = 0; i < length; i++)
{

}

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

for (/* инициализация */; /* условие продолжения */; /* пост-действие */)
{
}

Таким образом можно описать части заголовка цикла - так называется всё, что идёт в скобочках после for.

Принято в этих частях записывать соответствующие части управления циклом, хотя можно обойтись вообще без них либо управлять другим образом.

Разберём самый простой пример.

for (size_t i = 0; i < 5; i++)
{
	std::cout << i << std::endl;
}
0
1
2
3
4

Тут мы инициализируем переменную цикла i типа size_t значением 0. Её еще называют счётчиком цикла в таких случаях.

После инициализации у нас выполняется проверка условия i < 5. Если оно false, то цикл даже не начнёт своё выполнение.

В нашем случае на первой итерации i = 0 и поток выполнения попадёт в тело цикла.

Итерация - это одно выполнение тела цикла.

Тело цикла - это блок кода следующий за его заголовком.

При попадании на первой итерации цикла в его тело у нас выполняется вывод в консоль 0 и перенос строки.

Далее поток выполнения переходит к пост-действию - инкременту переменной цикла i.

А после этого снова переходит к шагу, который начинается с проверки условия.

Он продолжает своё выполнение до тех пор, пока условие i < 5 не станет ложным.

То есть после итерации, на которой переменная i = 4 выполняется инкремент, переменная принимает значение 5 и условие i < 5 становится ложным.

После этого происходит выход из цикла.

Типичная ошибка применения типа size_t

Один из примитивных примеров выполнения цикла - вывод значения функции на промежутке.

Давайте возьмём функцию квадрата числа и промежуток от 0 до 10.

#include <format> //Не забываем, это C++20
#include <iostream>

int main() {
    for (size_t x = 0; x < 10; x++)
    {
        int y = x * x;
        std::cout << std::format("x = {}, y = {}\n", x, y);
    }
}
x = 0, y = 0
x = 1, y = 1
x = 2, y = 4
...
x = 8, y = 64
x = 9, y = 81

Как видим всё работает корректно, но если мы захотим взять промежуток от -10 до 10 то получим вот это

//C6294	Ill - defined for - loop.Loop body not executed.
for (size_t x = -10; x < 10; x++)
{
    int y = x * x;
    std::cout << std::format("x = {}, y = {}\n", x, y);
}

Вывод в консоли отсутствует.

Это произошло потому что условие цикла не выполнилось даже для первой итерации.

Компилятор выдал нам предупреждение, которое означает что такая конструкция буквально - не здоровая.

То есть выражение формально существует, но имеет проблему с логикой или смыслом.

Вся проблема в типе size_t и его неспособности хранить отрицательные значение, потому что тип беззнаковый.

size_t x = -10;
std::cout << x << std::endl;
18446744073709551606

При такой инициализации мы получаем число, явно большее чем 10 из-за переполнения unsigned типа и поэтому цикл даже не начинает выполнятся.

Таким образом шаблон из IDE может подкинуть новичку неожиданных проблем, хотя и люди с опытом из-за невнимательности могут попасться на это.

Для таких ситуаций, когда переменная цикла должна хранить отрицательные значения необходимо использовать соответствующий тип.

for (int x = -10; x < 10; x++)
{
    int y = x * x;
    std::cout << std::format("x = {}, y = {}\n", x, y);
}
x = -10, y = 100
x = -9, y = 81
x = -8, y = 64
...
x = 8, y = 64
x = 9, y = 81

Конечно мы можем оставить тип size_t и изменить логику

for (size_t x = 0; x < 20; x++)
{
    int signedX = x - 10;
    int y = signedX * signedX;
    std::cout << std::format("x = {}, y = {}\n", signedX, y);
}
x = -10, y = 100
x = -9, y = 81
x = -8, y = 64
...
x = 8, y = 64
x = 9, y = 81

Такая реализация выглядит нелогично и стоит в этом случае применить int и сделать диапазон от -10 до 10.

Тип size_t отлично подходит для индексов массивов и размеров контейнеров, но не для диапазонов, содержащих отрицательные значения.

Нестандартные случаи использования цикла for

Мы можем формировать заголовок цикла for неочевидными способами.

Бесконечный цикл for

for (;;)
{
    std::cout << "hi" << std::endl;
}
hi
hi
hi
...

Таким образом мы реализовали бесконечный цикл.

Гибкость составления заголовка

Переменная цикла может либо вообще отсутствовать либо быть инициализирована раньше

int x = -5;
for (; x++;)
{
    std::cout << x << std::endl;
}
-4
-3
-2
-1
0

Этим примером я хочу сразу показать вам несколько моментов.

Пост-действие так же может полностью отсутствовать, а в условии может быть записано любое логическое выражение.

Это выражение может как зависеть от переменной цикла так и не зависеть от неё.

В этом примере у нас цикл выполняется до тех пор, пока результат приведения переменной x типа int к типу bool не станет равным false.

Это происходит, когда переменная принимает значение 0.

То есть это достаточно гибкий инструмент, который может применяться неочевидными способами.

Объявление переменных в заголовке

Помимо выноса инициализации переменной мы можем выполнить инициализацию или объявление двух и более переменных в заголовке цикла

for (int i = 0, j = 0; i < 3; ++i, ++j) {
	std::cout << std::format("x = {}, y = {}\n", i, j);
}
x = 0, y = 0
x = 1, y = 1
x = 2, y = 2

Интересно то, что если мы захотим использовать разные типы, то есть что-то вроде int i = 0, double j = 0, то мы этого сделать не сможем.

Потому что синтаксис объявления переменных через запятую в C++ - это одно объявление с одним общим спецификатором типа.

Уменьшение переменной цикла

Существует возможность выполнять не ++, а -- по отношению к переменной цикла и инициализировать её не нулём, а положительным числом - чтобы реализовать её уменьшение.

for (int i = 1; i != 0; i--)
{
	std::cout << i << std::endl;
}
1

Область видимости переменных цикла

Обратите внимание - если мы инициализируем переменную цикла в его заголовке, то мы не можем её использовать за его пределами

for (int i = 1; i != 0;)
{
	std::cout << i << std::endl;
    i--;
}
std::cout << i << std::endl; // 'i': undeclared identifier

Гибкость условия, пост-действия и пограничные случаи

Условие цикла не обязательно должно содержать оператор больше или меньше - там может быть любое логическое выражение.

Пост-операцию i-- мы можем вообще вынести из заголовка в конец цикла.

Обратите внимание на то, как сформировано условие - i != 0. Тут мы рассчитываем на то, что i в какой-то момент гарантированно примет значение 0 и выполнится выход из цикла.

В действительности это и происходит, потому что шаг цикла это единица, но вместо i-- мы можем написать i-= 2 и тогда будет шаг 2.

for (int i = 1; i != 0;)
{
	std::cout << i << std::endl;
	i-=2;
}
1
-1
-3
...

С шагом 2 мы не получим истинность условия в ближайшее время выполнения цикла.

Цикл будет продолжать своё выполнение до переполнения переменной, а далее всё зависит от её типа и реализации компилятора соответственно.

Именно поэтому в большинстве случаев предпочтительнее использовать <, <=, > или >=, а не !=, если счётчик изменяется с некоторым шагом.

Отсутствие блока кода как тела цикла

Стоит помнить, что тут по аналогии с оператором if работает момент с последующей инструкцией или блоком кода.

Если блок кода отсутствует - будет выполнена только следующая инструкция.

int i = 1;
for (; i <= 4; ++i) std::cout << i << " ";
std::cout << i << std::endl;
1 2 3 4 5

Заметьте тут при последнем выполнении цикла в любом случае выполняется i++ и поэтому мы имеем в выводе число 5, которое выводится за пределами цикла.

Это актуально так же для остальных типов циклов.

Цикл foreach

До темы массивов и контейнеров нам будет необходимо здесь упомянуть возможность применения for для перебора элементов какой-либо коллекции.

int numbers[] = { 1, 2, 3, 4, 5 };
for (int number : numbers) {
    std::cout << number << " ";
}
std::cout << std::endl;
for (int i = 0; i < 5; i++)
{
    std::cout << numbers[i] << " ";
}
1 2 3 4 5
1 2 3 4 5

Тут как вы можете заметить мы указываем сначала тип элемента, потом - его алиас внутри тела цикла и само название коллекции через :.

Эта же логика может быть реализована через обращение по индексу и стандартную форму записи цикла for.

Сейчас разницы между этими примерами нет - производительность, скорее всего, будет идентичная.

Проблемы могут возникать с тяжёлыми объектами, потому что при такой записи каждый элемент будет скопирован в новую переменную под капотом.

В таких случаях применяется передача по ссылке типа auto& item, если нужно изменять значение, и const auto& name, если значение менять не нужно.

Это нужно разбирать на примерах и с уже имеющимся пониманием работы ссылок.

Цикл while

Цикл while обычно используется в тех случаях, когда количество итераций неизвестно, либо - пока состояние программы не изменится.

Его структура выглядит так

while (/* условие */)
{
}

Он выглядит гораздо проще чем цикл for, но тем не менее мы можем реализовать через него ту же самую логику с известным количеством выполнений и выводом значения переменной цикла.

int i = 0;
while (i < 5)
{
	std::cout << i << std::endl;
	i++;
}
0
1
2
3
4

Мы не можем, как в случае с for оставить его условие пустым, но зато можем реализовать бесконечный цикл просто записав true в условии.

while (true)
{
}

Примером использования цикла while может быть ситуация, когда пользователю нужно угадывать случайное число

Оформите подписку, чтобы иметь доступ ко всем публикациям без ограничений. Вы получите мгновенный доступ к полным версиям материалов, исходному коду и новым публикациям сразу после их выхода.

Я регулярно публикую материал и разборы ситуаций для различных игр, движков, а так же подходов разработки включая Internal, External и DMA.

Эта публикация содержит только часть статьи.

Полные уроки по C++, реверс-инжинирингу и разработке софта вы можете найти тут: