0:00
Итераторы, на самом деле,
есть не только у вектора, как мы сейчас увидели, но и у других контейнеров.
Например, можно взять строчку.
Конкретный язык программирования — тот же самый C++, который лежит вот здесь.
И у строчки тоже есть begin.
Давайте я сохраню в переменную it begin(land).
Могу вывести *it и увидеть, что у меня получится.
Вот этот вот я уберу.
И у
меня что-то нескомпилировалось, давайте я посмотрю.
Я взял строчку lang из langs[1], begin(lang), вывел *it.
Кажется, я не подключил библиотеку string.
И все еще что-то не так.
Давайте посмотрим.
Да, дело в том, что все-таки у меня это структура,
и я должен обратиться к полю name, конечно же.
Отлично, мы исправились.
Компилируем.
Теперь все скомпилировалось.
Запускаем и видим, что begin у строки указывает на
ее первый символ, точнее нулевой символ.
Мы назвали эту штуку итераторами,
она называется итераторами, но пока что мы использовали итераторы только для того,
чтобы указывать на конкретную позицию в контейнере.
Но из названия следует, что с помощью них еще можно и итерироваться по контейнеру,
то есть у итератора есть не только операция *, но и, например, операция ++.
Я могу вот так подвинуть итератор на следующий элемент контейнера.
Давайте я выведу теперь *it.
Скомпилирую, запущу и вижу,
что у меня еще вывелся + дополнительно.
То есть я вывел сначала нулевой символ строки,
потом первый символ строки с помощью одного и того же итератора.
Ну раз мы говорим, что можно итерироваться, наверное,
давайте все-таки действительно напишем честный цикл for и проитерируемся.
Кроме того, нам пора бы узнать, что такое end.
Итак, давайте я уберу весь этот вывод и просто проитерируюсь по вектру langs.
Итак, сначала итератор указывает на begin.
Затем условие окончания цикла
for: мы продолжаем итерироваться пока итератор не равен end(langs).
И на каждом шаге мы делаем ++it.
Вот такой вот цикл for.
Конечно, можно было бы написать range based for,
но мы в дальнейшем увидим, что такой цикл на итераторах чуть универсальнее.
Кроме того, когда range based for в языке C++ еще не было,
цикл по контейнеру писали именно так.
Давайте для каждого элемента, например, выведем it ->name через пробел.
Давайте проверим, что этот цикл работает.
И затем поясню, почему условие в нем именно такое.
Да, я вывел все языки.
Итак, давайте разберемся.
Посмотрим на картинку.
Вот снова тот же самый цикл.
Изначально итератор указывает на begin.
Затем мы проверяем, что он еще не end — я пока не говорю,
что такое end, но пока что он не end.
Захожу в цикл.
В этом цикле я ввожу очередной элемент, затем снова проверяю, что у меня не end,
двигаю итератор вправо.
Вывожу, двигаю вправо — вывожу, двигаю — вывожу, двигаю — вывожу.
Вывожу, наконец, последний элемент контейнера.
Теперь у меня итератор указывает на последний элемент.
И, поскольку он еще не равен end,
я двигают его вправо, и теперь он указывает на end.
То есть end — это не указатель, не итератор на последний элемент.
Это как бы итератор сразу за последним элементом.
И благодаря этому у нас цикл, как только у меня итератор попал за конец,
цикл сразу завершается, и итого я вывел все элементы.
Итак, получается, что begin и end — вот эти самые итераторы,
которые мы передавали, в разные алгоритмы мы передавали,
например, или вот здесь мы просто с помощью них проитерировались,
они задают полуинтервал элементов: от begin включительно до end невключительно.
Давайте заметим, например, что вот когда у меня, грубо говоря,
были 6 клеточек — было 5 элементов в массиве и end был как бы шестым,
— у меня полуинтервал из 5 элементов.
Если же я хочу иметь пустой диапазон элементов,
представить его с помощью двух итераторов, у меня эти итераторы должны быть равны.
Если у меня есть какой-то один итератор и он же begin, и он же end,
этот диапазон пустой, куда бы этот итератор не указывал.
Итак, получается, что — давайте переключимся обратно в среду разработки,
— получается, что end указывает в какое-то непонятное место в памяти.
Мы уже пробовали обращаться к begin, конкретным полям по begin,
давайте попробуем обратиться к конкретному полю end и увидеть, что так делать нельзя.
Я беру end(langs) и, скажем, обращаюсь к полю name.
Я это компилирую.
Этот код компилируется, потому что для компилятора нет никакой разницы между
begin и end, а вот когда я код запускаю — вот я его запустил,
и код благополучно упал.
Итак, обращаться к end, смотреть на элемент, который под ним лежит,
нельзя, потому что там может не быть вообще ничего или какая-то чужая память.
Хорошо.
Вот у нас был до этого цикл for, давайте его вернем.
Нам часто понадобится выводить конкретные диапазоны значений по итераторам,
давайте вынесем его в функцию.
Итак, я пишу функцию, которая — давайте она будет возвращать void,
— называется PrintRange.
Эта функция будет универсальная, она будет понимать пару итераторов,
но мы пока не знали, какой тип имеет итератор, давайте же узнаем.
Если у нас речь идет о векторе структур lang, тогда итераторы его имеют
тип vector <Lang>:: iterator.
Вот ровно такой тип у них.
Это как бы подтип с названием iterator у контейнера vector<Lang>.
Соотвественно, эти два названия разделяют два двоеточия.
Вот такой тип имеет итератор begin, давайте я назову его range_ begin,
поставлю перевод строки.
И ровно такой же тип имеет итератор
конца.
И в этой функции я проитерируюсь с помощью того же самого цикла for,
вот я его оставляю: от range_begin до range_end.
Поскольку я хочу написать некий универсальный цикл for
и не хочу при этом переопределять оператор вывода для структур,
я все-таки ограничусь выводом *it.
Вот я вот так вот сделал, и, кажется, функция закончилась.
Давайте я попробую вернуть вектор строк,
уберу возраста языков,
оставлю Python,
C++, C, Java и С#.
И уберу возраста.
Теперь я смогу выводить этот вектор с помощью функции PrintRange.
Давайте я вызову функцию PrintRange.
Я как будто бы написал свой алгоритм.
Смотрите, я передаю в нее begin(langs) и end(langs).
Я компилирую, запускаю,
и у меня вывелись снова мои языки программирования.
Давайте вернемся к фукнции и посмотрим, что нам в ней не нравится.
Нам не нравится длинные названия типа для итератора.
Давайте сделаем синоним для этого названия типа и будем писать коротко.
Вот я напишу вот так: using LangIt =
vector<String> ::iterator.
Я вот так вот написал с помощью using, и теперь я могу везде
использовать LangIt вместо громоздкого названия vector<String>::iterator.
Я вот так пишу: LangIt.
И вот здесь тоже пишу LangIt.
Прекрасно, компилирую, запускаю.
Все работает.
А давайте вспомним, что все-таки итератор есть у разных контейнеров,
у контейнеров бывают разные типы внутри — я и про строчки говорил, и про векторы.
Хочется написать какую-то универсальную функцию.
Казалось бы, цикл for у нас уже достаточно универсален.
Что мы делали, когда мы не хотели писать разные фукнции,
которые делают одно и то же?
Мы писали шаблонную функцию.
Давайте же сделаем функцию шаблонной: template — и у нее будет шаблонный
параметр итератора, назовем его просто it.
И будем принимать два параметра вот ровно такого типа.
Все, я написал эту функцию, компилирую, запускаю, и она все еще работает.
Более того, я могу теперь вызвать функцию PrintRange от строки,
например от строки langs[0].
begin(langs[0]), end(langs[0]).
Компилируем, запускаем, и теперь наряду со списком языков программирования у
меня еще вывелись элементы строки Python через пробел.
Хорошо.
Давайте — наконец, мы увидели,
что у нас есть вот такая вот концепция полуинтервалов,
что у меня диапазон от begin включительно до end невключительно, чем же она удобна.
Давайте рассмотрим такой пример.
Вот у нас есть вектор с названием языков, мы нашли какой-то элемент в этом векторе,
получили итератор на него, и теперь хотим вывести — вот мы нашли, скажем,
язык C и хотим вывести все элементы вектора, которые были до этого языка,
и все, которые были после этого языка, включая его.
Как мы это сделаем?
Мы вызовем нашу функцию PrintRange — от begin и,
собственно, этого итератора, border которого мы получили.
Это будет диапазон от begin включительно до border невключительно,
а когда мы захотим вывести оставшуюся часть,
мы возьмем диапазон от border включительно до конца.
Таким образом, заметьте, мы разбили наш диапазон, наш вектор
на две части с помощью одного итератора border и никак к нему не прибавляли 1,
не вычитали 1 — оно разбилось естественным образом, эти диапазоны не пересекаются.
У нас есть отдельно желтый диапазон, отдельно красный диапазон.
Заодно заметьте, что алгоритмы, вот как наш —
только что свеженаписанный алгоритм, так и алгоритмы те, которые были до этого,
они принимают итераторы, и они обязаны быть и begin, и end — они могут принимать
итераторы, которые указывают где-то в середину контейнера.
Итак, что мы узнали в этом видео?
Мы узнали про понятие итератора, как способ задать позицию в контейнере.
Заодно мы узнали, что begin и end — это тоже итераторы,
и они задают границы полуинтервала: от begin включительно до end невключительно.
Именно эти границы полуинтервалов принимают все известные нам алгоритмы,
и с помощью шаблона функции удобно писать как раз собственные алгоритмы,
например алгоритм PrintRange.