
ТЕМА КОНТЕЙНЕРЫ / ТЕМА 8 Контейнеры
.pdf
ООП-2013 |
Тема 8 Контейнеры |
|
|
8.Контейнеры
8.1.Общие сведения
Вреальных задачах обычно требуется обрабатывать группы данных довольно большого объема. Например, посчитать зарплату для всех сотрудников университета или транслировать программу, состоящую из нескольких тысяч строк исходного текста. Поэтому в любом языке программирования, в том числе и в С++, существуют средства объединения данных в группы — обычно это массивы. Однако одних массивов как средств объединения однородных данных явно недостаточно. В процессе развития языков программирования, и С++ в частности, программистское сообщество выработало более общую конструкцию объединения однородных данных в группу —
контейнер.
В языке С++ (да и в любом другом) контейнер — это набор однотипных элементов. Здесь не случайно используется слово «набор», а не «множество», так как множество — это тоже контейнер. По этому определению массив — это контейнер. Каталог файлов на диске — тоже контейнер.
Каждый контейнер характеризуется, в первую очередь, своим именем и типом входящих в него элементов. Имя контейнера — это имя переменной в программе, которое подчиняется правилам видимости С++. Как объект, контейнер должен обладать временем жизни в зависимости от места и времени создания, причем время жизни контейнера в общем случае не зависит от времени жизни его элементов.
Тип контейнера складывается из типа самого контейнера и типа входящих в него элементов. Тип контейнера — это не тип его элементов. Как правило, тип контейнера определяет способ доступа к элементам. Тип элементов может быть либо встроенным, либо реализованным. В том числе элементами контейнера могут быть контейнеры. Например, элементами некоторого каталога могут быть каталоги. Список строк тоже может служить примером контейнера контейнеров, так как отдельную строку можно считать контейнером символов.
Размер контейнера. Размер контейнера может быть либо определен при объявлении, либо не задан. В первом случае получаем контейнер фиксированной длины. Однако в общем случае количество элементов контейнера с заданной фиксированной длиной может изменяться от нуля до объявленного количества.
Если же размер контейнера не задается, то, естественно, количество элементов контейнера изменяется во время работы программы. Элементы добавляются в контейнер и удаляются из него. Такой контейнер является контейнером переменного размера.
8.2.Доступ к элементам контейнера. Итератор
Одной из важнейших характеристик контейнера является доступ к его элементам. Обычно различают прямой, последовательный и ассоциативный доступ.
Прямой доступ к элементу — это доступ по номеру (или, еще говорят, по индексу) элемента. Именно таким образом мы обращаемся к элементам массива, например:
v[7]
Это выражение означает, что мы хотим оперировать элементом контейнера v, имеющим номер (индекс) 7. Нумерация элементов может начинаться, вообще говоря, с
1
ООП-2013 |
Тема 8 Контейнеры |
|
|
любого числа, однако в С++ принято нумерацию начинать с нуля, так как для встроенных массивов (которые являются частным случаем контейнера) принята именно такая нумерация.
Последовательный доступ отличается тем, что мы не имеем в распоряжении индексов элементов, зато можем перемещаться последовательно от элемента к элементу. Можно считать, что существует невидимая «стрелка»-индикатор, которую перемещают по элементам контейнера с помощью некоторого множества операций. Тот элемент, на который в данный момент «стрелка» показывает, называется текущим.
Обычно набор операций для последовательного доступа включает операции:
перехода к первому элементу;
перехода к последнему элементу;
перехода к следующему элементу;
перехода к предыдущему элементу;
перехода на n элементов вперед (от первого в сторону последнего элемента контейнера);
перехода на n элементов назад (от конца к началу контейнера);
получения (изменения) значения текущего элемента.
Эти операции могут быть представлены в функциональной форме, например:
next(v); |
// перейти к |
следующему |
|
prev(v); |
// перейти к |
предыдущему |
|
first(v); |
// перейти |
к первому |
|
last(v); |
// перейти к последнему |
||
current(v); |
// получить текущий |
||
forward(v, n);// перейти |
на n элементов вперед |
||
back(v, n); |
// перейти |
на n элементов назад |
Операция изменения текущего элемента — это, естественно, операция присваивания, например:
current(v) = value;
В этом случае функция current() должна возвращать ссылку на элемент контейнера.
Те же операции, реализованные как методы класса (контейнера), можно представить следующим образом:
v.nextO; |
// перейти к |
следующему |
v.prevQ; |
// перейти к предыдущему |
|
v.firstO; |
// перейти к первому |
|
v.last(); |
// перейти к последнему |
|
v.currentO; |
// получить текущий |
|
v.skip(n); |
// перейти на п элементов вперед |
|
v.skip(-n); |
// перейти на п элементов назад |
Однако в С++ «стрелку»-индикатор удобнее представить в виде некоторого объекта, связанного с контейнером. Если этот объект имеет имя iv, то те же операции могут быть реализованы и так:
iv = v.beginO;// |
перейти к первому |
|
iv = v.last();// |
перейти к последнему |
|
++iv; |
// перейти к следующему |
|
--iv; |
// |
перейти к предыдущему |
iv+=n; |
// |
перейти на n элементов вперед |
iv-=n; |
// |
перейти на n элементов назад |
*iv |
// |
получить значение текущего элемента |
Не правда ли, очень похоже на указатель?! Однако это не указатель! — в практике объектно-ориентированного программирования такой объект называется
итератором.
2

ООП-2013 Тема 8 Контейнеры
Итератор — это объект, обеспечивающий последовательный доступ к элементам контейнера. Так же как контейнер представляет собой более общую концепцию, чем массив, так и итератор является более общей концепцией, чем указатель.
Ассоциативный доступ похож на прямой, однако основан не на номерах элементов, а на содержимом элементов контейнера. Например, в банковской системе контейнер может содержать записи о счетах клиентов. Обязательным элементом записи является поле, содержащее фамилию клиента, например:
class TAccount |
|
{ string Family; |
// фамилия |
unsigned long Count; // номер счета
Date Open; // дата открытия счета public: // . . . }:
Если контейнер v содержит такие объекты, то выражение представляет собой счет, открытый на имя Стенли Липпмана:
v["Lippman"]
Такое выражение в С++ является вполне корректным, так как операция индексирования может быть перегружена для аргумента любого типа.
Поле, с содержимым которого ассоциируется элемент контейнера, называется ключом (полем доступа). Элемент контейнера, соответствующий некоторому значению ключа, обычно так и называется значением. Ассоциативный контейнер, таким образом, состоит из множества пар «ключ-значение». Как правило, ассоциативный контейнер упорядочен некоторым образом по ключу. В данном случае контейнер, содержащий счета клиентов, отсортирован по полю Family, поэтому элементом, предшествующим записи с фамилией Lippman, может быть запись с фамилией Kupaev, а следующим — запись с фамилией Martin.
Методы доступа к контейнеру — настолько важная характеристика, что в стандартной библиотеке различают контейнеры последовательные и ассоциативные. Последовательными контейнерами, которые обеспечивают и прямой, и последовательный варианты доступа, являются контейнеры vector и deque, а ассоциативным — контейнер map. Контейнер, в котором доступ только последовательный, — это list.
8.3.Операции с контейнером
Все операции с контейнером можно разделить на несколько групп:
операции доступа к элементам, включая операцию замены значений элементов;
операции добавления и удаления отдельных элементов или групп элементов;
операции поиска элементов и групп элементов;
операции с контейнером как объектом; в частности, важными являются операции
объединения контейнеров;
прочие (специальные) операции, зависящие от вида контейнера.
Операции доступа мы уже рассмотрели. Операции добавления и удаления элементов работают только для контейнера переменного размера. Очевидно, что эти операции с элементами можно выполнять самыми разными способами:
1)добавлять и удалять элементы в начале контейнера;
2)то же самое делать в «хвосте» контейнера;
3)вставлять элементы перед текущим элементом или после него, удалять текущий элемент;
3
ООП-2013 |
Тема 8 Контейнеры |
|
|
4)делать вставки в соответствии с некоторым порядком сортировки элементов контейнера, в этом случае обязательно выполняется операция поиска;
5)удалять элемент, содержимое которого равно заданному, в этом случае «за кадром» тоже работает операция поиска.
Первые три операции обычно применяются к последовательным контейнерам. Способ вставки и удаления определяет вид последовательного контейнера. Если вставка и удаление выполняются только на одном конце контейнера, то такой контейнер называется стеком, а работает он в соответствии с дисциплиной обслуживания LIFO (Last In First Out — последним вошел, первым вышел). Говорят, что текущий элемент находится на вершине стека. Если же элементы добавляются на одном конце, а удаляются из другого, контейнер называется очередью. Очередь работает в соответствии с дисциплиной обслуживания FIFO (First In First Out — первым пришел, первым ушел). Можно выполнять и вставку, и удаление на обоих концах контейнера — такой контейнер называется деком (от английского термина deque, который является аббревиатурой от «double ended queue», то есть «очередь с двумя концами»). Таким образом, дек представляет собой обобщение очереди и стека.
Если же контейнер ассоциативный, то он упорядочен по полю доступа, поэтому операции вставки и удаления всегда выполняются в последних вариантах. Но и последовательный контейнер может быть отсортирован, поэтому операция вставки тоже может вставлять «по порядку». Примером отсортированного последовательного контейнера является приоритетная очередь priority_queue — один из последовательных контейнеров стандартной библиотеки.
Все операции, которые мы рассматривали до сих пор, — это операции с отдельными элементами контейнера. Но и с самим контейнером или его частью можно выполнять некоторые операции. Наиболее часто используется операция объединения двух контейнеров с получением нового контейнера, которая может быть реализована в различных вариантах:
простое сцепление двух контейнеров, в новый контейнер попадают все элементы и первого, и второго контейнеров; операция не коммутативна;
объединение упорядоченных контейнеров, называемое слиянием, в новый контейнер попадают все элементы первого и второго контейнеров; объединенный контейнер упорядочен; операция коммутативна;
объединение двух контейнеров как объединение множеств, в новый контейнер попадают только те элементы, которые есть хотя бы в одном контейнере; операция коммутативна;
объединение двух контейнеров как пересечение множеств, в новый контейнер попадают только те элементы, которые есть в обоих контейнерах; операция коммутативна.
Одной из операций с контейнером является извлечение из него части элементов и создание из них нового контейнера. Часто эту операцию выполняет конструктор, а требуемая часть контейнера задается двумя итераторами.
Отдельно необходимо сказать о контейнерах-множествах. Множество — это контейнер, в котором каждый элемент единственный. Помимо операций объединения и пересечения, для контейнеров-множеств реализуется операция вычитания множеств: в контейнер-результат попадают только те элементы первого контейнера, которых нет во втором; операция не коммутативна. Очень часто с множествами выполняется операция проверки включения, которая фактически является операцией поиска (как отдельного элемента, так и подмножества элементов).
4
ООП-2013 |
Тема 8 Контейнеры |
|
|
Тип элементов оказывает существенное влияние на то, какие операции могут выполняться с контейнером. Например, для строк операция сортировки обычно не нужна, а для числовых контейнеров или для списка счетов в банке такая операция может быть не только полезной, но и необходимой. Для числовых контейнеров, очевидно, часто необходимы операции поиска минимума и максимума, суммы и произведения элементов.
8.4.Реализация контейнеров
Контейнеры, как правило, реализуются с помощью указателей и динамической памяти. Использование указателей и динамических переменных в классах в сочетании с перегрузкой операций представляет собой удивительно мощный механизм создания новых типов данных — контейнеры стандартной библиотеки тому наглядный пример.
При реализации контейнеров нам предстоит решить несколько важных вопросов. Во-первых, каким образом выделяется память?
Выделение памяти операцией new [ ] обеспечивает выделение непрерывной области памяти. Количество элементов обычно задается выражением, вычисляемым во время работы программы. Такая форма практически всегда используется для реализации динамических. Способ выделения памяти настолько важен, что даже в стандарте указано, что контейнер vector, входящий в стандартную библиотеку С++, должен быть реализован с помощью операции new[].
Второй способ распределения памяти — выделение одиночного элемента операцией new. Выделение памяти для одиночного элемента обычно требуется для реализации контейнеров с переменным количеством элементов. В этом случае не только память для элемента выделяется динамически, но и в состав самого элемента входят один или несколько (чаще всего два) указателей для связи элементов друг с другом. Обычно такие контейнеры либо последовательные, либо ассоциативные.
Как правило, выделением памяти занимается конструктор контейнера. Если память выделяется, то надо ее возвращать — иначе в программе возникает утечка памяти. Решение этой проблемы и составляет ответ на второй вопрос: каким образом возвратить память системе. Обычно эту работу «возлагают» на деструктор. Как мы знаем, операции возврата памяти являются «парными» для операций выделения памяти. Если память выделялась операцией new, то возвращать память нужно операцией delete. Если же память выделялась массивом (операцией new[]), то и возвращать ее нужно соответствующей операцией delete[].
И наконец, третий вопрос связан с копированием и присваиванием. Для динамических классов, в которых используются указатели, предлагаемые по умолчанию операции совершенно не подходят, требуется явная реализация.
8.5.Последовательный контейнер
Последовательный доступ реализуется в цикле последовательным изменением индекса. Чтобы разобраться, каким образом выполняется последовательный доступ с помощью итераторов, реализуем контейнер-дек с помощью двусвязного списка. Типом контейнера по сложившейся уже традиции пусть будет TDeque. Не задумываясь пока об общности, опять задействуем дробные числа типа double.
8.5.1Контейнер - дек
Простейшая структура элемента (обычно элемент списка называют узлом (node))
последовательного контейнера очевидна: struct Elem {
double item; // информационная часть
5
ООП-2013 |
|
Тема 8 Контейнеры |
|
|
|
// связующая часть |
// |
следующий элемент |
Elem *next; |
||
Elem *prev; |
// |
предыдущий элемент |
};
Чтобы обеспечить инициализацию создаваемого элемента, добавим конструктор инициализации:
Elem(const double &а):item(a){}
Отметим, что программа-клиент, которая собирается использовать дек, ничего не должна знать о внутренней структуре узла. Чтобы это обеспечить, инкапсулируем узел в классе-контейнере и реализуем его в приватной части класса TDeque в качестве.
8.5.1.1.Вложенные классы
Вложенным называется класс, объявленный внутри другого класса. Он является членом объемлющего класса, а его определение может находиться в любой из секций public или private. Уровень вложенности стандартом не ограничивается.
Вложенный класс по умолчанию не имеет доступа к приватным элементам объемлющего класса, так же как объемлющий класс — к приватным элементам вложенного. Чтобы это было возможно, нужно использовать механизм друзей, например:
class Outer { //...
friend class Inner; // дня Inner доступна приватная часть Outer class Inner // вложенный класс
{friend class Outer; // для Outer доступна приватная часть Inner
// . . .
} ; //. . .
} ;
Ни вложенный, ни объемлющий классы не имеют возможности пользоваться методами друг друга непосредственно — как и для обычных невложенных классов необходимо объявить объект (или указатель), для которого вызвать нужный метод. Например, объект объемлющего класса может передаваться методу вложенного класса как параметр:
void Outer::Inner::MethodInner(const Outer &t) { //...
memlnner = t.MethodOuter(); // вызов метода объемлющего класса //...
}
Здесь метод Methodlnner () вложенного класса получает ссылку на объект объемлющего класса и обычным способом вызывает метод MethodOuter () для инициализации своего поля memlnner.
Внутри методов вложенного класса указатель this является указателем на текущий объект вложенного класса.
Если вложенный класс объявлен как public, то его можно использовать в качестве типа во всей программе. Но поскольку класс вложенный, то его имя нужно писать с префиксом — именем объемлющего класса, например:
Outer::Inner *pointer;
Если вложенный класс объявлен в приватной части объемлющего класса, то он доступен только членам объемлющего класса и его друзьям. В этом случае компоненты вложенного класса обычно делают открытыми — тогда нет нужды объявлять объемлющий класс другом, например:
class Outer { //... |
|
friend class Inner; |
// дпя Inner доступна приватная часть Outer |
structure Inner |
// все эпементы доступны в Outer |
{ / / . . . |
|
|
6 |
ООП-2013 |
Тема 8 Контейнеры |
|
|
} ; //. . .
} ;
Естественно, имя вложенного класса должно быть уникально в объемлющем классе, но может совпадать с другими именами вне класса, например:
class А {/*...-*/ }; |
// внешний класс |
class В { //... |
|
class А { / * . . . * / }; |
// вложенный класс |
//. . . |
|
} ; |
|
В этом определении внешний класс А не конфликтует с вложенным классом А.
Методы вложенного класса можно реализовать непосредственно внутри класса. Если же методы вложенного класса определяются вне класса, то определение необходимо писать вне самого внешнего из объемлющих классов — в глобальной области видимости. Естественно, имя метода в таком случае должно иметь два префикса, образованные из имен вложенного и объемлющего классов. Вообще, количество префиксов должно быть равно уровню вложенности классов: первый префикс является именем самого объемлющего класса, второй
— именем вложенного класса первого уровня, третий — именем класса, вложенного во вложенный класс, и т. д. Например, конструктор без аргументов класса Inner может быть определен таким образом:
Outer::Inner::Inner() { / / . . .
}
Первое имя Inner — это имя класса, второе — имя самого конструктора.
В глобальной области видимости вне объемлющего класса можно определить и сам вложенный класс. Подобное определение выглядит отчасти парадоксально, но С++ разрешает это делать — если в объемлющем классе задать объявление класса. Например:
class А / / . . .
class В; I I объявление вложенного класса
/ / . . .
} ;
class А::В { // внешнее определение вложенного класса
//...
} ;
Доступность определенного таким образом класса зависит от того, в какой части объемлющего класса задано объявление: если объявление приватное, то и определение является приватным в объемлющем классе.
Зачем определять вложенный класс внешним образом? Это позволяет написать определение в отдельном файле1 и тем самым обеспечивает повышенный уровень инкапсуляции.
8.5.1.2.Итератор для последовательного контейнера
Для дека нам потребуются конструкторы и деструктор. Минимально необходимое множество операций включает в себя операции добавления элемента в начало и конец дека, операции удаления первого и последнего элементов. Еще необходимы функция проверки, есть ли в контейнере элементы, и функция, выдающая количество элементов в контейнере. Обычно в состав методов включают и методы получения значений первого и последнего
7
ООП-2013 |
Тема 8 Контейнеры |
|
|
элементов контейнера. Рассмотрим более подробно операцию доступа к элементам контейнера.
Так как внутренняя структура элемента скрыта, поскольку определена в приватной части класса-дека, программа-клиент не сможет работать с элементами контейнера посредством указателей. Для перебора элементов контейнер должен обеспечить последовательный доступ к ним по-другому. Это можно сделать одним из двух способов:
1)реализовать методы доступа непосредственно как методы контейнера;
2)инкапсулировать все операции доступа в отдельный класс-итератор.
Второе решение предпочтительней хотя бы потому, что позволяет отделить интерфейс доступа от интерфейса контейнера. Это позволит в дальнейшем иметь один и тот же универсальный интерфейс доступа для разных типов контейнеров. Таким образом, итераторы играют очень важную роль при инкапсуляции информации контейнеров.
Класс-итератор можно реализовать как отдельный независимый класс. Но так как итератор должен иметь доступ к внутренней структуре элемента контейнера, он должен с этим классом «дружить». Очевидно, что итератор очень тесно связан с внутренней организацией контейнера, поэтому лучше его реализовать в качестве вложенного в класс-контейнер класса, как и класс-узел. Поскольку программе-клиенту потребуется создавать объекты-итераторы, этот класс должен быть определен в открытой части класса контейнера.
Назовем вложенный класс-итератор именем iterator. При создании итератора программа-клиент обязана будет указать префикс — имя объемлющего класса, например:
TDeque::iterator it;
Значение итератора представляет собой позицию в контейнере. Набор операций с итераторами фактически уже описан нами (см. ранее раздел «Доступ к элементам контейнера»), однако полезно еще раз на этом остановиться. Итак, класс-итератор должен обеспечивать следующий минимальный набор операций:
получение элемента в текущей позиции итератора (*);
присваивание итератора (=);
проверка совпадения позиций, представленных двумя итераторами (== и ! =);
перемещение итератора к следующему элементу контейнера (++).
Итератор с таким набором операций в стандартной библиотеке называется прямым. Если мы добавим операцию перемещения к предыдущей позиции (декремент --), то получим итератор, который в стандартной библиотеке называется двунаправленным.
Для того чтобы начать перебирать элементы контейнера, итератору надо присвоить первоначальное значение, соответствующее первому элементу контейнера. Обычно для этого в контейнер включают метод begin(), который в качестве результата возвращает итератор, установленный в начало последовательности элементов контейнера.
Метод end () возвращает итератор, установленный в конец последовательности элементов контейнера. Что считать концом последовательности, составляет важный вопрос реализации. По примеру STL (STL — прекрасный пример для подражания!) будем считать, что концом последовательности является позиция за последним элементом последовательности. Таким образом, пара методов, begin(), end(), определяет полуоткрытый интервал, который содержит первый элемент, но выходит за пределы последнего элемента (рис. 6.1).
Полуоткрытый интервал обладает двумя достоинствами:
не нужно специально обрабатывать пустой интервал, так как в пустом интервале значения begi п ()и end () равны;
упрощается проверка завершения перебора элементов контейнера — цикл продолжается до тех пор, пока итератор не достигнет позиции end().
Для реализации полуоткрытого интервала в контейнер обычно добавляют «пустой» фиктивный элемент, не содержащий данных (рис. 6.2).
8
ООП-2013 |
Тема 8 Контейнеры |
|
|
8.5.1.3.Реализация контейнера-дека
Обратимся теперь непосредственно к реализации. Сначала покажем составляющие класса TDeque, а затем — всю его структуру. В листинге 6.1 представлена приватная часть класса.
Листинг 6.1. Приватная часть класса TDeque private: class Elem // элемент дека
{ friend class TDeque; friend class iterator;
Elem(const double &a):item(a){ } Elem(){}
~Elem(){} |
// объявлять |
необязательно |
double item; |
// информационная часть элемента |
|
Elem *next; |
// следующий |
элемент |
Elem *prev; |
// предыдущий элемент |
};
// запрещаем копировать и присваивать деки
TDeque& operator=(const TDeque &); TDeque(const TDeque &);
long count; |
//количество элементов |
Elem *Head; |
// Начало дека |
Elem *Tail; |
// указатель на запредельный элемент |
// для итератора |
|
iterator head; |
|
iterator tail; |
|
Вся внутренность класса Elem закрыта, поэтому классы TDeque и iterator объявлены друзьями, чтобы иметь доступ к конструкторам и указателю. Можно поступить и по-другому — просто сделать все члены класса Elem открытыми:
struct Elem |
|
|
{ |
Elem(const double &a):item(a){ } |
|
|
Elem(){} ' |
|
~Elem(){} |
// объявлять необязательно |
|
double item; |
// информационная часть элемента |
|
Elem *next; |
// следующий элемент |
|
Elem *prev; |
// предыдущий элемент |
|
}; |
|
В этом случае друзей объявлять не требуется — классы TDeque и iterator и так имеют доступ ко всем элементам класса Elem.
Чтобы не отвлекаться от главной задачи — изучения доступа посредством итератора, — мы объявили конструктор копирования и операцию присваивания закрытыми. Да и нет особой необходимости (пока) присваивать деки. Наличие объявления приводит к тому, что компилятор не будет создавать эти функции по умолчанию. Таким образом, мы запретили создавать копии контейнера-дека и присваивать один дек другому. Следствием является также и то, что контейнер нельзя передавать по значению в качестве параметра и возвращать в качестве результата.
Далее объявлены поля класса TDeque: счетчик элементов контейнера, реальные указатели на начало и конец списка. Счетчик увеличивается при каждом добавлении элемента и уменьшается при каждом удалении элемента. Поля-указатели никогда не
9
ООП-2013 |
Тема 8 Контейнеры |
|
|
равны 0, так как даже в пустом контейнере присутствует запредельный фиктивный элемент (см. рис. 6.2).
А вот программе-клиенту указатели недоступны — она работает с итераторами. Следовательно, нужны аналогичные поля для класса-итератора, которые этим классом и инициализируются. Сам класс-итератор (листинг 6.2) определен в открытой части класса TDeque.
Листинг 6.2. Класс iterator class iterator
{ friend class TDeque; iterator(Elem *el):the_elem(el){}
public:
// конструкторы
iterator (): the_elem(0) {}
iterator(const iterator &it):the_elem(it.the_elem){} // присваивание
// итераторов - генерируется по умопчанию
// сравнение итераторов
bool operator==(const iterator &it) const { return (the_elem == it.the_elem); }
bool operator!=(const iterator &it) const { return !(it == *this); }
//продвижение к следующему элементу - только префиксная форма iterator& operator++()
{ if ((the_elem!=0)&&(the_elem->next!=0)) the_elem = the_elem->next; return *this;
}
//продвижение к предыдущему элементу - только префиксная форма iterator& operator--()
{ if ((the_elem!=0)&&(the_elem->prev!=0)) the_elem = the_elem->prev; return *this;
}
//получить ссылку на информационную часть
//работает справа и слева от знака присваивания
double& operator*() const
{if (the_elem != 0) return the_elem->item;
else { cout << "Null iterator!" << endl; abort(); }
}
private:
Elem *the_elem; // вот это итератор скрывает!
};
Единственный закрытый элемент класса — указатель на элемент контейнерадека. Именно этот указатель должен скрывать итератор, предоставляя пользователю более надежный способ доступа. Однако для первоначальной установки указателя нашему деку требуется конструктор с указателем. Поэтому реализован приватный конструктор, и класс-дек сделан другом класса-итератора.
Хотя мы не обрабатываем ошибки, тем не менее операции продвижения не приводят к непредсказуемому поведению программы — при некорректном указателе продвижение просто не выполняется и возвращается текущий итератор.
10