Один из ключей к пониманию того, как использовать контейнеры стандартной библиотеки шаблонов C ++ (STL), - это понимание того, как работают итераторы. В этой статье я продемонстрирую, как итераторы работают на высоком уровне (цикл range for), а затем как использовать их более непосредственно для управления перемещением по контейнеру STL.

Итераторы и диапазон цикла

Большинство экспертов по программированию теперь рекомендуют при рассмотрении вопроса о хранении данных для своих программ использовать гибкие контейнеры, такие как вектор, вместо более негибких контейнеров, таких как встроенный массив (класс array в STL - лучший вариант, если вы действительно нужен массив).

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

Шаблон синтаксиса для диапазона for loop выглядит так:

for (элемент типа данных: контейнер) {
body;
}

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

#include <iostream>
#include <vector>
using namespace std;
int main()
{
  vector<int> numbers{1,2,3,4,5};
  for (int number : numbers) {
    cout << number << " "; // displays 1 2 3 4 5
  }
  return 0;
}

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

Имея это в виду, мы готовы увидеть, как использовать итераторы напрямую для доступа к элементам контейнера.

Непосредственная работа с итераторами

Цикл range for - это косвенное использование итераторов, поскольку итераторы используются за кулисами. Давайте посмотрим, как мы можем использовать итераторы напрямую для просмотра содержимого контейнера.

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

тип-контейнера ‹data-type› :: итератор имя-итератора;

Вот фрагмент кода, демонстрирующий, как объявить итератор для целочисленного вектора:

vector<int>::iterator iter;

После объявления итератора его необходимо инициализировать. У контейнеров STL есть функции для этого. Если мы хотим создать стандартный итератор для перемещения от начала до конца контейнера, эта функция называется begin. Вот как им пользоваться:

vector<int> numbers{1,2,3,4,5};
vector<int>::iterator iter;
iter = numbers.begin();

Указание типа данных для итератора немного длинно, поэтому большинство программистов используют ключевое слово auto для объявления и инициализации итератора в одном операторе. Вот как это сделать, используя вектор из приведенного выше примера:

vector<int> numbers{1,2,3,4,5};
auto iter = numbers.begin();

Итератор iter теперь указывает на первый элемент вектора. Мы можем получить доступ к значению, на которое указывает итератор, разыменовав итератор с помощью оператора разыменования (*), например:

cout << "first element: " << *iter; // displays first element: 1

Чтобы перейти к следующему элементу, вы используете оператор приращения, чтобы переместить итератор к следующему элементу:

iter++;
cout << "second element: " << *iter;//displays second element: 2

Теперь вы можете увидеть, как перемещаться по контейнеру с помощью итераторов. Но как узнать, когда остановиться? У контейнеров STL есть еще одна функция, end, которая обозначает конец контейнера. Функция возвращает истинное значение, если вы являетесь концом контейнера, и возвращает ложное значение в противном случае.

Вот как вы используете функцию end в цикле для обхода всех элементов вектора:

#include <iostream>
#include <vector>
using namespace std;
int main()
{
  vector<int> numbers{1,2,3,4,5};
  auto iter = numbers.begin();
  while (iter != numbers.end()) {
    cout << *iter << " "; // displays 1 2 3 4 5
    iter++;
  }
  return 0;
}

Перемещение по контейнеру назад

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

Функция для создания обратного итератора - rbegin. Функция, определяющая, находитесь ли вы в начале контейнера, - rend. Вот пример, в котором они используются для перемещения назад по вектору:

#include <iostream>
#include <vector>
using namespace std;
int main()
{
  vector<int> numbers{1,2,3,4,5};
  auto iter = numbers.rbegin();
  while (iter != numbers.rend()) {
    cout << *iter << " "; // displays 5 4 3 2 1
    iter++;
  }
  return 0;
}

Использование цикла for с итераторами

В приведенных выше примерах использовался while цикл для обхода вектора, но нет причин, по которым вы не можете использовать for loop, и цикл for может даже быть предпочтительным методом для итераторов. Вот два приведенных выше примера, переписанных с for циклами:

#include <iostream>
#include <vector>
using namespace std;
// forward iterator
int main()
{
  vector<int> numbers{1,2,3,4,5};
  for (auto iter = numbers.begin(); iter != numbers.end();
       iter++) {
    cout << *iter << " "; // displays 1 2 3 4 5
  }
  return 0;
}
#include <iostream>
#include <vector>
using namespace std;
// reverse iterator
int main()
{
  vector<int> numbers{1,2,3,4,5};
  for (auto iter = numbers.rbegin(); iter != numbers.rend();
       iter++) {
    cout << *iter << " "; // displays 5 4 3 2 1
  }
  return 0;
}

Разные методы итераторов

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

#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
using namespace std;
int main()
{
  vector<int> numbers;
  srand(time(0));
  for (int i = 1; i <= 20; i++) {
    numbers.push_back(rand() % 100 + 1);
  }
  for (auto iter = numbers.begin(); iter != numbers.end();
       iter += 2) {
    cout << *iter << " ";
  }
  return 0;
}

Другой способ написать это - использовать функцию advance. Эта функция берет текущий итератор и номер и продвигает итератор на несколько позиций в контейнере. Приведенный ниже фрагмент кода демонстрирует, как работает функция advance:

vector<int> numbers {1,2,3,4,5};
auto iter = numbers.begin();
advance(iter, 2);
cout << *iter; // displays 3

Следующая функция возвращает итератор в следующую позицию в контейнере после местоположения текущего итератора. Вот пример:

vector<int> numbers {1,2,3,4,5};
auto iter = numbers.begin();
auto next_iter = next(iter);
cout << *next_iter; // displays 2

Этот последний пример - не столько техника итераторов, сколько использование итераторов. Алгоритм STL sort использует итераторы, чтобы определить, какую часть контейнера нужно отсортировать по порядку. Например, если вы хотите отсортировать весь контейнер, вы должны использовать функции begin и end , чтобы указать весь контейнер. Вот пример того, как работает функция sort:

#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <algorithm>
using namespace std;
int main()
{
  vector<int> numbers;
  srand(time(0));
  for (int i = 1; i < 20; i++) {
    numbers.push_back(rand() % 100 + 1);
  }
  for (int num : numbers) {
    cout << num << " ";
  }
  sort(numbers.begin(), numbers.end());
  cout << endl;
  for (int num : numbers) {
    cout << num << " ";
  }
  return 0;
}

Результат одного запуска этой программы:

62 35 56 62 7 74 8 46 4 53 1 26 7 94 27 64 31 63 100
1 4 7 7 8 26 27 31 35 46 53 56 62 62 63 64 74 94 100

Важность итераторов

Функция сортировки - не единственная функция в STL, которая требует использования итераторов. Итераторы лежат в основе всех контейнеров STL, и если вы хотите иметь возможность использовать контейнеры STL в полной мере, вам необходимо ознакомиться с тем, как работают итераторы и как их использовать.

Спасибо, что прочитали эту статью, и пишите мне с комментариями и предложениями.