Итераторы в C++: введение

обложка статьи

Всем привет! Изучая контейнеры STL, мы использовали новый вид переменных - итераторы. Так давайте узнаем, зачем ими пользуются?

Что такое итератор

Итератор - это такая структура данных, которая используется для обращения к определенному элементу в контейнерах STL. Обычно из используют с контейнерами set, list , а у вектора для этого применяют индексы.

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

Как создать итератор

Для создания итератора мы должны с самого начала программы подключить библиотеку <iterator>.

#include <iterator>

Далее для его создании нам потребуется использовать вот эту схему:

<контейнер> <его тип> :: iterator <имя итератора>;
  • <контейнер> - указываем требуемый контейнер, на который и будет ссылаться итератор. Например map, vector, list.
  • <его тип> - указываем тип контейнера.

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

Методы начала и конца контейнеров

У каждого контейнера имеются два метода, которые, как указатели передают итератору начало или конец контейнера - begin() и end().

  • Метод begin() отправит итератор на начала контейнера.
  • А метод end() отправит на конец. А если точнее, то на одну ячейку больше последней. Если мы попытаемся вывести эту ячейку у нас появятся проблемы с компилятором :) .

Их мы можем использовать даже без подключения библиотеки <iterator>, что очень удобно.

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

vector <int> i_am_vector;

vector <int> :: iterator it = i_am_vector.begin();

Итератор на vector

Для итератора на vector вы можете:

  • Выполнять операцию разыменования (обращаться к значению элемента на которое указывает итератор), как мы это делали с указателем.
int x = *it;
  • Использовать инкремент (it++, ++it) и декремент (it--, --it).
  • Применять арифметические операции. Так например мы можем сместить итератор на пять ячеек в право, вот так:
it += 5;
  • Сравнивать на равенства.
if (it == it2) { ...
  • Передать переменной разницу итераторов.
int x = it - it2;

Но о использовании арифметических и сравнительных операциях (>, <, ==) с двумя итераторами, вам нужно кое что знать.

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

Есть исключение из правил - если вы создадите два одинаковых итератора на map то при сравнивании они не будут одинаковы.

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

vector <int> vector_first;
vector <double> vector_second;

vector <int> :: iterator it = vector_first.begin();
vector <double> :: iterator it2 = vector_second.begin();

if (it == it2) {  // ошибка!
  cout << "it == it2";
}

Итератор на list, set, map

Для итераторов на list,set,map немного урезан функционал. Так вы не можете:

  • Использовать арифметические операции.
it += 5;  //
it *= 2;  //
it /= 3;  // ошибка
it -= 5;  //
  • Применять операции сравнения (> и <):
if (it > it_second) { ...  // 
                           // ошибка
if (it < it_second) { ...  //

Все остальное можно использовать:

  • Применять инкремент и декремент.
it--;  // все 
it++;  // нормально!
  • Использовать операцию разыменования.
cout << *it;
*it += 5;
  • Сравнивать два итератора на равенство и неравенства:
if (it == it_second) { ...  // 
                            // правильно
if (it != it_second) { ...  //

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

Но вы можете сказать: “Так что мы можем двигать итератор только на один элемент? Это же неудобно!“. Да было бы совсем не гибко со стороны C++ делать вот такое, но они позаботились и создали функцию - advanсe(), она заменяет операции увеличения и уменьшения над итераторами.

Вот как она работает:

advance(<итератор>, <значение>);
  • <итератор> - сюда мы должны указать итератор, который и нужно изменить.
  • <значение> - тут мы должны вписать число на которое должны увеличить или уменьшить итератор.

Если мы увеличиваем итератор, то используем оператор + к числу. Но можно и просто записать число без оператора +.

Если же нужно уменьшить итератор, то мы добавляем оператор -.

advanсe(it, 5);   // сместили на 5 ячеек

Как работают итераторы

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

#include <iostream>
#include <iterator>
#include <vector>

using namespace std;

int main() {
  setlocale(0, "");

  vector  name_vector;

  name_vector.push_back(3);
  name_vector.push_back(4);
  name_vector.push_back(6);

  vector <int> :: iterator it;

  for (it = name_vector.end() - 1; it >= name_vector.begin(); it--) {
    cout << *it << " ";
  }

  system("pause");
  return 0;
}
  • В строке 10: создали вектор name_vector.
  • Дальше в последующих трех строках занимаемся его заполнением.
  • В строке 16: создали итератор под именем it.
  • В цикле for мы написали, что итератор указывает на последнюю ячейку вектора. С помощью вот такой не замысловатой конструкции :
it = name_vector.end() - 1;

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

  • Используя операцию разыменования, в теле цикла, мы вывели все элементы.
cout << *it;

Вы наверняка заметили, что мы выводим элементы задом наперед:

6 4 3
Process returned 0 (0x0) execution time : 0.010 s
Press any key to continue.

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

cout << *(it + 5);

А не вот так;

cout << *it + 5;

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

Все потому что, операция разыменования * происходит быстрее, чем операция присваивание. У этих операций совсем разный приоритет использования. У этой * больше, а у этой + меньше.

Итератор это не указатель

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

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

Да итератор это усовершенствованная версия указателя, которая только работает с контейнерами. В некоторых языках указатель называют итератором, но это не значит, что он и есть указатель в C++.

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

vector  i_am_vector;
vector <int> :: iterator it;

int c = 10;

int *ykaz = &c;

it = ykaz;  // ошибка!

cout << *it; 

Как видим, если итератор был бы указателем он стал бы равен адресу указателя. И смог бы еще вывести этот адрес на экран. А вместо этого мы только словили две ошибки от компилятора.

Мы надеемся, что вы смогли узнать информацию, которая вас интересовала. Если у вас есть вопрос, который мы не обсудили в статье, то можете его написать в комментарии. Удачи!

« Список list в C++

Функция copy в C++ »

Обсуждение