9.19 – Циклы for-each (циклы на основе диапазона)

Добавлено 9 июня 2021 в 17:17
Глава 9 – Массивы, строки, указатели и ссылки  (содержание)

В уроке «9.3 – Массивы и циклы» мы показали примеры, в которых мы использовали цикл for для перебора всех элементов массива.

Например:

#include <iostream>
#include <iterator> // std::size
 
int main()
{
    constexpr int scores[]{ 84, 92, 76, 81, 56 };
    constexpr int numStudents{ std::size(scores) };
 
    int maxScore{ 0 }; // отслеживаем наш самый большой результат
    for (int student{ 0 }; student < numStudents; ++student)
    {
        if (scores[student] > maxScore)
        {
            maxScore = scores[student];
        }
    }
 
    std::cout << "The best score was " << maxScore << '\n';
 
    return 0;
}

Хотя циклы for предоставляют удобный и гибкий способ перебора массива, их также легко испортить, и они подвержены ошибкам на единицу.

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

Циклы for-each

Синтаксис оператора for-each выглядит следующим образом:

for (объявление_элемента : массив)
   инструкция;

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

Давайте рассмотрим простой пример, в котором цикл for-each используется для печати всех элементов массива с именем fibonacci:

#include <iostream>
 
int main()
{
    constexpr int fibonacci[]{ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };
    for (int number : fibonacci) // перебираем массив fibonacci
    {
       // мы обращаемся к элементу массива в этой итерации через переменную number
       std::cout << number << ' '; 
    }
 
    std::cout << '\n';
 
    return 0;
}

Эта программа печатает:

0 1 1 2 3 5 8 13 21 34 55 89

Давайте подробнее рассмотрим, как это работает. Сначала выполняется цикл for, и переменной number присваивается значение первого элемента, имеющего значение 0. Программа выполняет инструкцию, которая печатает 0. Затем цикл for выполняется снова, и number устанавливается равным значению второго элемента, имеющего значение 1. Инструкция выполняется снова, что выводит 1. Цикл for продолжает итерации по каждому из чисел по очереди, выполняя инструкцию для каждого из них, пока в массиве не останется элементов для повторения. На этом этапе цикл завершается, и программа продолжает выполнение (возвращая 0 операционной системе).

Обратите внимание, что переменная number не является индексом массива. Ей присваивается значение элемента массива для текущей итерации цикла.

Циклы for-each и ключевое слово auto

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

Вот пример модификации программы, приведенной выше, но с использованием auto:

#include <iostream>
 
int main()
{
    constexpr int fibonacci[]{ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };

    // тип auto, поэтому тип number выводится из массива fibonacci
    for (auto number : fibonacci) 
    {
       std::cout << number << ' ';
    }
 
    std::cout << '\n';
 
    return 0;
}

Циклы for-each и ссылки

В следующем примере for-each наши объявления элементов объявляются по значению:

std::string array[]{ "peter", "likes", "frozen", "yogurt" };
for (auto element : array) // element будет копией текущего элемента массива
{
    std::cout << element << ' ';
}

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

std::string array[]{ "peter", "likes", "frozen", "yogurt" };

// Амперсанд делает element ссылкой на фактический
// элемент массива, предотвращая создание копии
for (auto& element: array) 
{
    std::cout << element << ' ';
}

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

И, конечно же, неплохо сделать ссылку константной, если вы собираетесь использовать ее только для чтения:

std::string array[]{ "peter", "likes", "frozen", "yogurt" };

// element является константной ссылкой на текущий итерируемый элемент массива
for (const auto& element: array)
{
    std::cout << element << ' ';
}

Лучшая практика


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

Переписываем пример поиска максимальной оценки с использованием цикла for-each

Вот пример из начала урока, переписанный с использованием цикла for-each:

#include <iostream>
 
int main()
{
    constexpr int scores[]{ 84, 92, 76, 81, 56 };
    int maxScore{ 0 }; // отслеживаем наш самый большой результат
 
    // перебираем массив scores, присваивая каждое
    // значение по очереди переменной score 
    for (auto score : scores) 
    {
        if (score > maxScore)
        {
            maxScore = score;
        }
    }
 
    std::cout << "The best score was " << maxScore << '\n';
 
    return 0;
}

Обратите внимание, что в этом примере нам больше не нужно вручную индексировать массив или получать его размер. Мы можем получить доступ к элементу массива напрямую через переменную score. В массиве должна быть информация о размере. Массив, распавшийся до указателя, нельзя использовать в цикле for-each.

Циклы for-each и немассивы

Циклы for-each работают не только с фиксированными массивами, они работают со многими видами структур, подобных спискам, такими как векторы (например, std::vector), связные списки, деревья и карты. Мы еще не рассмотрели ни одну из них, поэтому не беспокойтесь, если вы не знаете, что это такое. Просто помните, что цикл for-each предоставляет гибкий и универсальный способ перебора не только массивов.

#include <iostream>
#include <vector>
 
int main()
{
    // обратите внимание на использование здесь std::vector,
    // а не фиксированного массива
    std::vector fibonacci{ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 }; 
    // До C++17
    // std::vector<int> fibonacci{ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };
 
    for (auto number : fibonacci)
    {
        std::cout << number << ' ';
    }
 
    std::cout << '\n';
 
    return 0;
}

for-each не работает с указателями на массив

Чтобы выполнить перебор по массиву, циклу for-each необходимо знать размер массива. Поскольку массивы, преобразованные в указатель, не знают своего размера, циклы for-each с ними работать не будут!

#include <iostream>
 
int sumArray(const int array[]) // array - это указатель
{
    int sum{ 0 };
 
    for (auto number : array) // ошибка компиляции, размер массива неизвестен
    {
        sum += number;
    }
 
    return sum;   
}
 
int main()
{
     constexpr int array[]{ 9, 7, 5, 3, 1 };
 
     std::cout << sumArray(array) << '\n'; // здесь массив раскладывается в указатель
 
     return 0;
}

Точно так же по той же причине с циклами for-each не будут работать и динамические массивы.

Могу ли я получить индекс текущего элемента?

Циклы for-each не предоставляют прямого способа получить индекс текущего элемента массива. Это связано с тем, что многие структуры, с которыми могут использоваться циклы for-each (например, связные списки), напрямую не индексируются!

Начиная с C++20, циклы for на основе диапазона могут использоваться с инициализирующей инструкцией такой же, как инициализирующая инструкция в операторах for. Мы можем использовать эту инициализирующую инструкцию для создания счетчика индекса вручную, не загрязняя функцию, в которой помещен цикл for.

Инициализирующая инструкция помещается прямо перед переменной цикла:

for (инициализирующая_инструкция; объявление_элемента : массив)
   инструкция;

В следующем коде у нас есть два массива, которые коррелированы по индексу. Например, студент с именем в names[3] набирает балл, равный scores[3]. Каждый раз, когда обнаруживается студент с новым рекордом, мы печатаем его имя и разницу в баллах с предыдущим рекордом.

#include <iostream>
 
int main()
{
    std::string names[]{ "Alex", "Betty", "Caroline", "Dave", "Emily" }; // Имена студентов
    constexpr int scores[]{ 84, 92, 76, 81, 56 };
    int maxScore{ 0 };
 
    for (int i{ 0 }; auto score : scores) // i - индекс текущего элемента
    {
        if (score > maxScore)
        {
            std::cout << names[i] << " beat the previous best score of " << maxScore 
                      << " by " << (score - maxScore) << " points!\n";
            maxScore = score;
        }
        
        ++i;
    }
 
    std::cout << "The best score was " << maxScore << '\n';
 
    return 0;
}

Вывод программы:

Alex beat the previous best score of 0 by 84 points!
Betty beat the previous best score of 84 by 8 points!
The best score was 92

int i {0}; – это инициализирующая инструкция, она выполняется только один раз, при запуске цикла. В конце каждой итерации мы инкрементируем i, как в обычном цикле for. Однако если бы мы использовали continue внутри цикла, была бы возможность пропустить ++i, что привело бы к неожиданным результатам. Если вы используете continue, вам нужно убедиться, что i инкрементируется до того, как встретится continue.

До C++20 индексную переменную i нужно было объявить вне цикла, что могло привести к конфликтам имен, если бы мы хотели позже определить в функции другую переменную с именем i.

Заключение

Циклы for-each обеспечивают превосходный синтаксис для итерации по массиву, когда нам нужно получить доступ ко всем элементам массива в прямом последовательном порядке. Если его можно использовать, то он должен быть предпочтительнее стандартного цикла for. Чтобы предотвратить создание копий каждого элемента, в идеале объявление элемента должно быть ссылкой.

Небольшой тест

Это должно быть легко.


Вопрос 1

Объявите фиксированный массив со следующими именами: Alex, Betty, Caroline, Dave, Emily, Fred, Greg и Holly. Попросите пользователя ввести имя. Используйте цикл for-each, чтобы увидеть, есть ли в массиве имя, введенное пользователем.

Enter a name: Betty
Betty was found.
Enter a name: Megatron
Megatron was not found.

Подсказка: используйте std::string_view в качестве типа массива.

#include <iostream>
#include <string>
#include <string_view>
 
int main()
{
    constexpr std::string_view names[]{ "Alex", "Betty", "Caroline",
                                        "Dave", "Emily", "Fred",
                                        "Greg", "Holly" };
	
    std::cout << "Enter a name: ";
    std::string username{};
    std::cin >> username;
 
    bool found{ false };
 
    for (const auto name : names)
    {
        if (name == username)
        {
            found = true;
            break;
        }
    }
 
    if (found)
        std::cout << username << " was found.\n";
    else
        std::cout << username << " was not found.\n";
 
    return 0;
}

Теги

arrayC++ / CppforeachLearnCppДля начинающихМассивОбучениеПрограммированиеЦикл

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.