Что такое итератор в c
Перейти к содержимому

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

  • автор:

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

Определяет предопределенные итераторы итераторы потоков, примитивы итератора и вспомогательные шаблоны.

Требования

Заголовок.

Пространство имен std :

Замечания

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

В C++20 есть шесть категорий итераторов. Итераторы упорядочены в иерархии возможностей. Их возможности задаются концепциями C++20. Описание различных итераторов и их возможностей см . в разделе «Основные понятия итератора»

Visual Studio добавил расширения в итераторы стандартной библиотеки C++ для поддержки отладки для проверка и не проверка итераторов. Дополнительные сведения см. в разделе Безопасные библиотеки: стандартная библиотека C++.

Участники

Функции

Имя Описание
advance Увеличивает итератор на указанное количество позиций.
back_inserter Создает итератор, может вставлять элементы с обратной стороны указанного контейнера.
begin Извлекает итератор для первого элемента в указанном контейнере.
cbegin Извлекает итератор только для чтения в первый элемент в указанном контейнере.
cend Извлекает итератор только для чтения в элемент, который следует последнему элементу в указанном контейнере.
crbegin Получите обратный итератор только для чтения до начала указанного контейнера.
crend Получите sentinel в конце возвращаемого crbegin() значения.
data Получите указатель на первый элемент в указанном контейнере.
distance Указывает количество приращений между позициями, которые адресуют два итератора.
end Извлекает итератор для элемента, следующего за последним элементом в указанном контейнере.
empty Проверьте, пуст ли указанный контейнер.
front_inserter Создает итератор, может вставлять элементы с передней стороны указанного контейнера.
inserter Адаптер итератора, добавляющий в контейнер новый элемент в указанной позиции.
make_checked_array_iterator Создает объект checked_array_iterator , который можно использовать другими алгоритмами. Примечание. Эта функция — расширение Майкрософт для стандартной библиотеки C++. Код, реализованный с помощью этой функции, не переносимый в среды сборки C++ Standard, которые не поддерживают это расширение Майкрософт.
make_move_iterator Возвращает итератор перемещения, содержащий предоставленный итератор в качестве сохраненного базового итератора.
make_unchecked_array_iterator Создает объект unchecked_array_iterator , который можно использовать другими алгоритмами. Примечание. Эта функция — расширение Майкрософт для стандартной библиотеки C++. Код, реализованный с помощью этой функции, не переносимый в среды сборки C++ Standard, которые не поддерживают это расширение Майкрософт.
next Выполняет итерацию заданное число раз и возвращает новую позицию итератора.
prev Выполняет обратную итерацию заданное число раз и возвращает новую позицию итератора.
rbegin Получите обратный итератор к началу указанного контейнера.
rend Получите обратный итератор в sentinel в конце указанного контейнера.
size Получение количества элементов.

Операторы

Имя Описание
operator!= Проверяет, не равен ли объект итератора слева от оператора итератору справа.
operator== Проверяет объект итератора в левой части оператора на равенство объекту итератора в правой части.
operator< Определяет, верно ли, что объект итератора в левой части оператора меньше объекта итератора в правой части.
operator

Определяет, верно ли, что объект итератора в левой части оператора меньше или равен объекту итератора в правой части.
operator> Определяет, верно ли, что объект итератора в левой части оператора больше объекта итератора в правой части.
operator>= Определяет, верно ли, что объект итератора в левой части оператора больше или равен объекту итератора в правой части.
operator+ Добавление смещения к итератору и возврат нового итератора reverse_iterator , который обращается к вставленному элементу в новой позиции смещения.
operator- Вычитает один итератор из другого и возвращает разницу.

Классы

Имя Описание
back_insert_iterator Шаблон класса описывает выходной итератор объекта. Он вставляет элементы в контейнер типа Container , к которому он обращается через защищенный pointer объект, который он хранит, называется контейнером.
bidirectional_iterator_tag Класс, предоставляющий тип возвращаемого значения для iterator_category функции, представляющей двунаправленный итератор.
checked_array_iterator Класс, который обращается к массиву при помощи проверенного итератора произвольного доступа. Примечание. Этот класс — расширение Майкрософт для стандартной библиотеки C++. Код, реализованный с помощью этой функции, не переносимый в среды сборки C++ Standard, которые не поддерживают это расширение Майкрософт.
forward_iterator_tag Класс, предоставляющий тип возвращаемого значения для iterator_category функции, представляющей итератор пересылки.
front_insert_iterator Шаблон класса описывает выходной итератор объекта. Он вставляет элементы в контейнер типа Container , к которому он обращается через защищенный pointer объект, который он хранит, называется контейнером.
input_iterator_tag Класс, предоставляющий тип возвращаемого значения для iterator_category функции, представляющей входной итератор.
insert_iterator Шаблон класса описывает выходной итератор объекта. Он вставляет элементы в контейнер типа Container , к которому он обращается через защищенный pointer объект, который он хранит, называется контейнером. Он также сохраняет защищенный iterator объект класса Container::iterator , называемый iter .
istream_iterator Шаблон класса описывает входной итераторный объект. Он извлекает объекты класса Ty из входного потока, к которому он обращается через объект, на который он хранит, указателя basic_istream типа.
istreambuf_iterator Шаблон класса описывает входной итераторный объект. Он вставляет элементы класса Elem в выходной буфер потока, к которому он обращается через объект, в котором он хранит тип pointer basic_streambuf .
iterator Шаблон класса используется в качестве базового типа для всех итераторов.
iterator_traits Вспомогательный класс шаблона, предоставляющий критические типы, связанные с разными типами итераторов, на которые они могут ссылаться аналогичным образом.
move_iterator Объект move_iterator содержит итератор произвольного доступа типа RandomIterator . Его поведение аналогично поведению итератора произвольного доступа, кроме случаев отмены ссылки. Результат operator* неявно приводится к value_type&&: , чтобы обеспечить rvalue reference .
ostream_iterator Шаблон класса описывает выходной итератор объекта. Он вставляет объекты класса Type в выходной поток, к которому он обращается через объект, в котором он хранит тип pointer basic_ostream .
ostreambuf_iterator Шаблон класса описывает выходной итератор объекта. Он вставляет элементы класса Elem в выходной буфер потока, к которому он обращается через объект, на который он хранит, указателя basic_streambuf типа.
output_iterator_tag Класс, предоставляющий тип возвращаемого значения для iterator_category функции, представляющей итератор выходных данных.
random_access_iterator_tag Класс, предоставляющий тип возвращаемого значения для iterator_category функции, представляющей итератор случайного доступа.
reverse_iterator Шаблон класса описывает объект, который ведет себя как итератор случайного доступа, только в обратном направлении.
unchecked_array_iterator Класс, который обращается к массиву при помощи непроверенного итератора произвольного доступа. Примечание. Этот класс — расширение Майкрософт для стандартной библиотеки C++. Код, реализованный с помощью этой функции, не переносимый в среды сборки C++ Standard, которые не поддерживают это расширение Майкрософт.

Основные понятия

В пространстве имен определены std следующие понятия. Они применяются к итераторам, а также относятся к категориям итератора для диапазонов, описанных в концепциях.

Концепция итератора Description
bidirectional_iterator C++20 Указывает итератор, который может считывать и записывать как вперед, так и назад.
contiguous_iterator C++20 Указывает итератор, элементы которого являются последовательными в памяти, одинаковым размером и могут быть доступны с помощью арифметики указателя.
forward_iterator C++20 Указывает итератор, который может читать (и, возможно, записывать) несколько раз.
input_iterator C++20 Указывает итератор, который можно прочитать по крайней мере один раз.
input_or_output_iterator C++20 Основа итеромии концепции таксономии.
output_iterator Указывает итератор, в который можно написать.
random_access_iterator C++20 Указывает итератор, который можно читать и записывать по индексу.
sentinel_for C++20 Указывает sentinel для типа итератора.
sized_sentinel_for C++20 Указывает, что итератор и его sentinel можно вычитать (используя — ), чтобы найти их разницу в постоянном времени.

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

Итераторы обеспечивают доступ к элементам контейнера и представляют реализацию распространенного паттерна объектно-ориентированного программирования «Iterator». С помощью итераторов очень удобно перебирать элементы. В C++ итераторы реализуют общий интерфейс для различных типов контейнеров, что позволяет использовать единой подход для обращения к элементам разных типов контейнеров.

Стоит отметить, что итераторы имеют только контейнеры, адаптеры контейнеров — типы std::stack , std::queue и std::priority_queue итераторов не имеют.

Итератор описывается типом iterator . Для каждого контейнера конкретный тип итератора будет отличаться. Так, итератор для контейнера list представляет тип list::iterator , а итератор контейнера vector представляет тип vector::iterator и так далее. Однако общий функционад, который применяется для доступа к элементам, будет аналогичен.

Для получения итераторов контейнеры в C++ обладают такими функциями, как begin() и end() . Функция begin() возвращает итератор, который указывает на первый элемент контейнера (при наличии в контейнере элементов). Функция end() возвращает итератор, который указывает на следующую позицию после последнего элемента, то есть по сути на конец контейнера. Если контейнер пуст, то итераторы, возвращаемые обоими методами begin и end совпадают. Если итератор begin не равен итератору end, то между ними есть как минимум один элемент.

Обе этих функции возвращают итератор для конкретного типа контейнера:

#include #include int main() < std::vectornumbers< 1,2,3,4 >; std::vector::iterator iter = numbers.begin(); // получаем итератор >

В данном случае создается вектор — контейнер типа vector, который содержит значения типа int. И этот контейнер инициализируется набором . И через метод begin() можно получить итератор для этого контейнера. Причем этот итератор будет указывать на первый элемент контейнера.

С итераторами можно проводить следующие операции:

  • *iter : получение элемента, на который указывает итератор
  • ++iter : перемещение итератора вперед для обращения к следующему элементу
  • —iter : перемещение итератора назад для обращения к предыдущему элементу. Итераторы контейнера forward_list не поддерживают операцию декремента.
  • iter1 == iter2 : два итератора равны, если они указывают на один и тот же элемент
  • iter1 != iter2 : два итератора не равны, если они указывают на разные элементы
  • iter + n : возвращает итератор, который смещен от итератора iter на n позиций вперед
  • iter — n : возвращает итератор, который смещен от итератора iter на n позиций назад
  • iter += n : перемещает итератор на n позиций вперед
  • iter -= n : перемещает итератор на n позиций назад
  • iter1 — iter2 : возвращает количество позиций между итераторами iter1 и iter2
  • >, >=,

Стоит отметить, что итераторы не всех контейнеров поддерживают все эти операции.

Итераторы для типов std::forward_list , std::unordered_set и std::unordered_map не поддерживают операции —, -= и -. (поскольку std::forward_list — однонаправленный список, где каждый элемент хранит указатель только на следующий элемент)

Итераторы для типа std::list поддерживают операции инкремента и декремента, но не поддерживаются операции +=, -=, + и -. Те же ограничения имеют итераторы контейнеров std::map и std::set .

Операции +=, -=, +, -, , >= и поддерживаются только итераторами произвольного доступа (итераторы контейнеров std::vector , array и deque )

Получение и изменение элемента контейнера

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

#include #include int main() < std::vectornumbers< 1,2,3,4 >; auto iter < numbers.begin() >; // получаем итератор // получаем элемент, на который указывает итератор std::cout 

После получения итератора он будет указывать на первый элемент контейнера. То есть при выражение *iter возвратит первый элемент вектора.

Прибавляя или отнимая определенное число, можно переместить итератор вперед или назад на определенное количество элементов:

#include #include int main() < std::vectornumbers< 10, 20, 30, 40 >; auto iter < numbers.begin() >; // получаем итератор // переходим на 1 элемент вперед ко 2-му элементу ++iter; std::cout 

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

Перебор контейнера

Например, используем итераторы для перебора элементов вектора:

#include #include int main() < std::vectornumbers< 10, 20, 30, 40 >; auto iter < numbers.begin() >; // получаем итератор while(iter!=numbers.end()) // пока не дойдем до конца < std::cout // аналогичный пример с циклом for for(auto start; start !=numbers.end(); start++ ) < std::cout >

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

Константные итераторы

Если контейнер представляет константу, то для обращения к элементам этого контейнера можно использовать только константный итератор (тип const_iterator ). Такой итератор позволяет считывать элементы, но не изменять их:

const vector numbers; for(auto iter ; iter != numbers.end(); ++iter) < std::cout 

В данном случае итератор iter будет представлять тип std::vector::const_iterator .

Для получения константного итератора также можно использовать функции cbegin() и cend . При этом даже если контейнер не представляет константу, но для его перебора используется константный итератор, то опять же нельзя изменять значения элементов этого контейнера:

#include #include int main() < std::vectornumbers < 1, 2, 3, 4, 5 >; for (auto iter ; iter != numbers.cend(); ++iter) < std::cout >

Стоит отметить, что для типов std::set (множество) и std::map (словарь) доступны только константные итераторы.

Реверсивные итераторы

Реверсивные итераторы позволяют перебирать элементы контейнера в обратном направлении. Для получения реверсивного итератора применяются функции rbegin() и rend() , а сам итератор представляет тип reverse_iterator :

#include #include int main() < std::vectornumbers < 1, 2, 3, 4, 5 >; for (auto iter ; iter != numbers.rend(); ++iter) < std::cout std::cout

В данном случае итератор будет представлять тип std::vector::reverse_iterator . Консольный вывод программы:

5 4 3 2 1

Если надо обеспечить защиту от изменения значений контейнера, то можно использовать константный реверсивный итератор, который представлен типом const_reverse_iterator и который можно получить с помощью функций crbegin() и crend() :

#include #include int main() < std::vectornumbers < 1, 2, 3, 4, 5 >; for (auto iter ; iter != numbers.crend(); ++iter) < std::cout >

Итераторы для массивов

Для массивов в C++ также имеется поддержка итераторов. Для этого в стандартной библиотеке С++ определены функции std::begin() (возвращает итератор на начало массива) и std::end() (возвращает итератор на конец массива):

int data[]; // получаем итератор на начало массива auto iter = std::begin(data); // получаем итератор на конец массива auto end = std::end(data);

Как и контейнеры, массив можно перебрать с помощью итераторов:

#include int main() < int data[]; // перебор массива с помощью итераторов for(auto iter ; iter != std::end(data); iter++) < std::cout >

Но перебор массива вполне можно сделать и другими способами - через индексы, обычные указатели. Но итераторы на массивы могут быть полезны при манипуляции с контейнерами. Например, функция insert() , которая есть у ряда контейнеров, позволяет добавить в контейнер какую-то часть другого контейнера. Для выделения добавляемой части могут применяться итераторы. И таким образом, с помощью итераторов можно добавить в контейнер, например, в вектор какую-то часть контейнера:

#include #include int main() < int data[]; std::vector numbers < 1, 2, 3, 4>; // добавляем в конец вектора numbers из массива data элементы со 2-го по предпоследний (включительно) numbers.insert(numbers.end(), std::begin(data) + 1, std::end(data)-1); for (auto iter ; iter != numbers.end(); ++iter) < std::cout std::cout

numbers.insert(numbers.end(), std::begin(data) + 1, std::end(data)-1);

Добавляет в вектор numbers, начиная с позиции, на которую указывает итератор numbers.end() (то есть в самый конец вектора), диапазон элементов массива data. Начало этого диапазона задается выражением std::begin(data) + 1 (то есть со 2-го элемента), а конуц — выражением std::end(data)-1 (то есть по предпоследний элемент включительно). Консольный вывод:

Итераторы

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

Итераторы можно использовать явным образом с помощью функций-членов и глобальных функций, таких как и end() операторы, такие как — begin() ++ и перемещение вперед или назад. Кроме того, можно использовать итераторы неявно с диапазоном для цикла или (для некоторых типов итератора) подстрочного оператора [] .

В стандартной библиотеке С++ началом последовательности или диапазона является первый элемент. Конец последовательности или диапазона всегда определяется как элемент, следующий за последним элементом. Глобальные функции begin и возвращают итераторы end в указанный контейнер. Типичный цикл явных итераторов, включающий все элементы, выглядит следующим образом:

vector vec< 0,1,2,3,4 >; for (auto it = begin(vec); it != end(vec); it++) < // Access element using dereference operator cout

Того же можно достичь более простым способом, с помощью цикла range-for:

for (auto num : vec) < // no dereference operator cout

Существует пять категорий итераторов. Ниже описаны категории в порядке возрастания силы.

  • Выход. Выходной итератор X может выполнять итерацию по последовательности с помощью ++ оператора и может записывать элемент только один раз с помощью * оператора.
  • Ввод. Входной итератор X может выполнять итерацию по последовательности с помощью ++ оператора и читать элемент в любое количество раз с помощью * оператора. Вы можете сравнить входные итераторы с помощью == операторов и != операторов. После увеличения любой копии входного итератора ни одна из других копий не может быть безопасно сравниваема, разыменовывается или увеличивается после этого.
  • Переслать. Переадресация итератора X может выполнять итерацию по последовательности с помощью оператора ++ и может считывать любой элемент или записывать элементы, отличные от const, с помощью * оператора. Вы можете получить доступ к элементам элемента с помощью -> оператора и сравнения переадресации итераторов с помощью == операторов и != операторов. Вы можете сделать несколько копий однонаправленного итератора, каждая из которых может быть разыменована и для нее может быть выполнено независимое приращение. Итератор пересылки, который инициализирован без ссылки на любой контейнер, называется итератором пересылки null. Пустые однонаправленные итераторы всегда равны.
  • Двунаправленный. Двунаправленный итератор может занять место итератора X вперед. Однако можно также уменьшать двунаправленный итератор, как и в --X , X-- или (V = *X--) . Получить доступ к членам элементов и сравнить двунаправленные итераторы можно так же, как и однонаправленные итераторы.
  • Произвольный доступ. Итератор случайного доступа может занять место двунаправленного итератора X . С помощью итератора случайного доступа можно использовать оператор [] подстрока для доступа к элементам. Вы можете использовать + - += операторы и -= операторы для перемещения вперед или назад указанного количества элементов и вычисления расстояния между итераторами. Вы можете сравнить двунаправленные итераторы с помощью == , , , < , >и = . !=

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

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

итератор вывода
—> переадресация итератора
—> двунаправленный итератор
—> итератор случайного доступа

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

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

итератор ввода
—> переадресация итератора
—> двунаправленный итератор
—> итератор случайного доступа

Итератор ввода является самым слабым по всем категориям в этом смысле.

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

итератор пересылки
—> двунаправленный итератор
—> итератор случайного доступа

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

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

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

Вы можете избежать явного использования итераторов с помощью циклов range-for. Дополнительные сведения см. в разделе "Диапазон" для инструкции.

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

Основы C++: Указатели и Итераторы

Знакома ли вам ситуация, когда вы внезапно оказываетесь совершенно не в состоянии объяснить какой-нибудь базовый элемент языка, с которым работаете? Вам задают простой вопрос, а вы только и можете, что сказать «ээээээээ, ну я точно не помню, мне нужно освежить знания, извините».

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

Сегодня мы с вами обсудим указатели и итераторы.

Указатели

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

Что такое указатель?

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

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

Как можно инициализировать указатель?

В нашем распоряжении есть сразу три способа!

  • Взять адрес другой переменной:
#include int main()
  • Указать на переменную в куче
#include int main()< int* p = new int ; >
  • Или просто взять значение другого указателя
#include int main()< int* p = new int ; int* p2 = p; >
Значения указателей и значения, на которые они указывают

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

#include int main()< int* p = new int ; int* p2 = p; std::cout /* 0x215dc20 42 0x215dc20 42 0x7fff77592cb0 0x7fff77592cb8 */

В данном примере мы видим, что p и p2 хранят один и тот же адрес в памяти, а это значит, что они указывают на одно и то же значение. Но если мы воспользуемся оператором & , то мы увидим, что адреса у этих указателей разные.

Деаллокация памяти

Если выделение памяти (аллокация) происходит с помощью оператора new, другими словами, если вы выделяете память в куче, то кто-то должен впоследствии высвободить (деаллоцировать) выделенную память. Это можно сделать с помощью оператора delete . Если забыть это сделать, то, когда указатель выйдет за пределы области видимости, произойдет утечка памяти (memory leak).

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

#include int main() < int* p = new int ; std::cout

Если вы попытаетесь получить доступ к указателю уже после удаления или же попытаетесь удалить его во второй раз, то это вызовет неопределенное поведение и, скорее всего, вы наткнетесь на core dump.

Подобные ошибки часто возникают в легаси-коде, например, в таких сценариях:

#include int main()< int* p = new int ; std::cout // . delete p; >

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

Простейший способ перестраховаться от такой ситуации — сразу после удаления присвоить p nullptr . Если попытаться удалить указатель еще раз, то это не даст никакого эффекта, так как удаление nullptr является no-op.

#include int main()< int* p = new int ; std::cout // . delete p; p = nullptr; >

Еще один важный момент — всегда проверяйте пригодность (валидность) указателя перед обращением к нему. Даже если не брать в расчет мороку с потокобезопасностью, мы не можем расслабляться. Что если указатель уже был удален и не установлен в nullptr ? Неопределенное поведение, потенциальные краши. Или еще хуже.

#include int main()< int* p = new int ; if (p != nullptr) < std::cout delete p; // we forget to set it to nullptr if (p != nullptr) < // we pass the condition std::cout > /* 0x22f3c20 42 0x22f3c20 0 */

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

#include int main()< int* p = new int ; int* p2 = p; if (p != nullptr) < std::cout delete p; // we forget to set it to nullptr p = nullptr; if (p != nullptr) < // p is nullptr, we skip this block std::cout if (p2 != nullptr) < // we pass the condition and anything can happen std::cout > /* 0x1133c20 42 0x1133c20 0 */

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

Итерация по массивам

Еще один важный момент, связанный с указателями, — это операции, которые можно выполнять над ними. Мы часто называем их арифметикой указателей, потому что указатели можно инкрементировать или декрементировать (выполнять сложение и вычитание). Но на самом деле можно складывать и вычитать любые целые числа. Благодаря возможности инкремента/декремента, указатели можно использовать для итерации по массивам и доступа к любому их элементу.

#include int main()< int numbers[5] = ; int* p = numbers; for(size_t i=0; i < 5; ++i) < std::cout for(size_t i=0; i < 5; ++i) < std::cout std::cout /* 1 2 3 4 5 5 4 3 2 1 4 */

Это конечно хорошо, но стоит ли использовать указатели для итерации по массивам в 2023 году?

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

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

Не используйте голые указатели!

На самом деле, на сегодняшний день уже вообще нет особого смысла использовать голые/сырые указатели (raw pointers). Особенно это касается указателей, которые выделяются с помощью new, и указателей, владеющих ресурсами. Передача ресурсов через голый указатель — это еще куда ни шло, но владение этими ресурсами, использование указателей в качестве итераторов или для выражение того, что значение может быть, а может и не быть — это то, чего не следует допускать в коде.

В нашем распоряжении есть средства более подходящие для этих задач.

Прежде всего, для замены владеющих ресурсом голых указателей мы можем использовать умные указатели (smart pointers).

Мы можем использовать невладеющие (non-owning) указатели, мы можем использовать ссылки, если что-то не может быть nullptr , или если мы хотим выразить, что что-то может присутствовать или не присутствовать, мы можем попробовать std::optional . Но я расскажу об этом подробнее в другой раз.

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

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

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

  • алгоритмы ( std::rotate , std::find_if и т.д.)
  • контейнеры ( std::vector , std::list и т.д.)
  • функциональные объекты ( std::greater , std::logical_and и т.д.)
  • итераторы ( std::iterator , std::back_inserter и т.д.)

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

Упоминание контейнеров STL также не спроста — итераторы нельзя использовать с C-массивами. И это нормально, в 2023 году мы вообще уже не должны использовать массивы в стиле C.

Пять категорий итераторов

По сути, итераторы можно разделить на пять категорий:

  • итераторы ввода (input iterators)
  • итераторы вывода (output iterators)
  • однонаправленные итераторы (forward iterators)
  • двунаправленные итераторы (bidirectional iterators)
  • итераторы произвольного доступа (random access iterators)

Итераторы ввода являются простейшей формой итераторов. Они поддерживают операции чтения и могут двигаться только вперед. Итераторы ввода можно использовать для сравнения на (не)равенство, и они могут быть инкрементированы. Хорошим примером может послужить итератор std::list .

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

Однонаправленные итераторы представляют собой комбинацию итераторов ввода и вывода. Они позволяют нам как получать доступ к значениям, так и изменять их. Например, такие итераторы используются в std::replace . Однонаправленные итераторы являются DefaultConstructible и могут обращаться/разыменовывать одни и те же позиции несколько раз.

Двунаправленные итераторы подобны однонаправленным итераторам, но они могут быть еще и декрементированы, поэтому могут двигаться как вперед, так и назад. std::reverse_copy использует такие итераторы, поскольку ему нужно и обращать значения контейнера (декрементировать), и помещать результаты в новый контейнер один за другим (инкрементировать).

Итераторы произвольного доступа могут делать все то же, что и двунаправленные итераторы. Кроме того, они могут не только инкрементироваться или декрементироваться, но и изменять свою позицию на любое значение. Другими словами, они поддерживают операторы + и - . Различные итераторы произвольного доступа можно также сравнивать с помощью различных операторов сравнения (а не только с помощью равенства/неравенства). Произвольный доступ означает, что к контейнерам, принимающим такие итераторы, можно просто обращаться с помощью оператора сдвига. Алгоритм, которому нужны итераторы с произвольным доступом, — это std::random_shuffle() .

Использование итераторов

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

  • через функции-члены, такие как std::vector::begin() или std::vector::end()
  • или через свободные функции, такие как std::begin() или std::end()

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

Как и указатели, итераторы можно инкрементировать или декрементировать, что позволяет использовать их в циклах. Правда, до появления C++11 они были достаточно громоздки:

#include #include int main() < std::vectorv ; for (std::vector::const_iterator it=v.begin(); it != v.end(); ++it) < std::cout >

С появлением языка C++11 и ключевого слова auto использование итераторов значительно упростилось:

#include #include int main() < std::vectorv ; for (auto it=v.begin(); it != v.end(); ++it) < std::cout >

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

Чем итератор отличается от указателя

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

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

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

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

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

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

Когда использовать одно, а когда другое?

Если требуется выполнить итерацию по стандартному контейнеру, используйте итератор. Так как он был разработан именно для этого, он безопаснее и именно его вы и получите, если вызовете begin() или end() на контейнере. Более того, алгоритмы STL принимают на вход именно итераторы, а не указатели, и именно их они зачастую и возвращают.

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

  • использование цикла for с диапазоном, который действительно предпочтительнее, но под капотом, в большинстве случаев, он все равно использует итераторы
  • использование массив в стиле C. Но в 2023 году вообще нет смысла использовать C-массив, ведь можно использовать std::array или другой STL-контейнер.

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

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

Не используйте (голые) указатели для работы с памятью. Если вам необходимо использовать динамическое выделение памяти, если вам нужна куча, используйте умный указатель вместо обычного указателя — это позволит вам избежать утечек памяти или повторного высвобождения.

Заключение

Мне бы очень хотелось иметь наиболее полное понимание основ языка C++ в начале своей карьеры разработчика я.

Мне бы очень хотелось иметь его и сегодня.

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

Ссылки

  • Apache C++ Standard Library User’s Guide: Varieties of Iterators
  • University of Helsinki: STL Iterators
  • GeeksForGeeks: Difference between Iterators and Pointers in C/C++ with Examples
  • Microsoft: Raw pointers (C++)
  • Stackoverflow: Why should I use a pointer rather than the object itself?

Материал подготовлен в преддверии старта специализации "C++ Developer".

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *