После условных операторов для управления потоком можно разобрать циклы.
Без циклов было бы необходимо дублировать код, если мы бы хотели повторять действие (либо использовать оператор 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.