Спецификации исключительных ситуаций
Иногда возникает необходимость заранее указать, какие исключения могут генерироваться в той или иной функции. Это можно сделать с помощью так называемой спецификации исключительных ситуаций. Это средство позволяет указать в объявлении функции типы исключительных ситуаций, которые могут в ней генерироваться. Синтаксически спецификация исключения является частью заголовочной записи функции и имеет вид:
объявление функции throw(тип1, тип2,…){тело функции}
где тип1, … - список типов, которые может иметь выражение throw внутри функции. Если список типов пуст, то компилятор полагает, что функцией не будет выполняться ни какой throw.
void fun(char c) throw();
Использование спецификации исключительных ситуаций не означает, что в функции не может быть сгенерирована исключительная ситуация некоторого не указанного в спецификации типа. Просто в этом случае программа по умолчанию завершится, так как подобные действия приведут к вызову неожиданного обработчика. Таким образом, когда функция генерирует исключительную ситуацию, не описанную в спецификации, выполняется неожиданный обработчик unexpected().
Задание собственного неожиданного обработчика
Так же как и обработчик terminate(), обработчик unexpected() позволяет перед завершением программы выполнить какие-то действия. Но в отличие от обработчика завершения неожиданный обработчик может сам генерировать исключительные ситуации. Таким образом, собственный неожиданный обработчик может сгенерировать исключительную ситуацию, на этот раз уже входящую в спецификацию. Установка собственного неожиданного обработчика выполняется с помощью функции set_unexpected().
Приведенная ниже программа демонстрирует применение спецификации исключений и перехват неожиданных исключительных ситуаций с помощью собственного обработчика.
#include <iostream.h>
#include <exception>
class first{};
class second : public first{};
class third : public first{};
class my_class{};
void my_unexpected()
{ cout<<"my_unexpected handler"<<endl;
throw third(); // возбуждение исключения типа объект
} // класса third
void f(int i) throw(first) // указание спецификации исключения
{ if(i ) throw second(); //
else throw my_unexpected();
}
void main()
{ set_unexpected(my_unexpected);
try {
f(1);
}
catch(first) {
cout<<"first handler"<<endl;
}
catch(my_class) {
cout<<"my_class handler"<<endl;
}
try{
f(0);
}
catch(first) {
cout<<"first handler"<<endl;
}
catch(my_class) {
cout<<"my_class handler"<<endl;
}
}
Результат выполнения программы.
first handler
my_unexpected handler
first handler
В данной программе вызов функции f() во втором блоке try приводит к тому, что генерируется исключительная ситуация, тип которой не указан в спецификации, поэтому вызывается установленный нами неожиданный обработчик, где происходит генерация исключения, которое успешно обрабатывается.
Иерархия исключений стандартной библиотеки
Вершиной иерархии является класс exceptioh (определенный в заголовочном файле <exception>). В этом классе содержится функция what(), переопределяемая в каждом производном классе для выдачи сообщения об ошибке.
Непосредственными производными классами от класса exception являются классы runtime_error и logic_error ( определенные в заголовочном файле <stdexcept>), имеющие по несколько производных классов.
Производными от exception также являются исключения: bad_alloc, генерируемое оператором new, bad_cast, генерируемое dynamic_cast, и bad_typeid, генерируемое оператором typeid.
Класс logic_error и производные от него классы (invalid_argument, length_error, out_of_range) указывают на логические ошибки (передача неправильного аргумента функции, выход за пределы массива или строки).
Класс runtime_error и производные от него (overflow_error и underflow_error) указывают на математические ошибки переполнения сверху и снизу.
Стандартная библиотека шаблонов (STL)
Общее понятие о контейнере
Стандартная библиотека шаблонов (Standard Template Library, STL) входит в стандартную библиотеку языка C++. В неё включены реализации наиболее часто используемых контейнеров и алгоритмов, что избавляет программистов от рутинного переписывания их снова и снова. При разработке контейнеров и применяемых к ним алгоритмов (таких как удаление одинаковых элементов, сортировка, поиск и т. д.) часто приходится приносить в жертву либо универсальность, либо быстродействие. Однако разработчики STL поставили перед собой задачу: сделать библиотеку одновременно эффективной и универсальной. Для ее решения были использованы такие универсальные средства языка C++, как шаблоны и перегрузка операторов. В последующем изложении будем опираться на реализацию STL, поставляемую фирмой Microsoft вместе с компилятором Visual C++ 6.0. Тем не менее большая часть сказанного будет справедлива и для реализаций STL другими компиляторами.
Основными понятиями в STL являются понятия контейнера (container), алгоритма (algorithm) и итератора (iterator).
Контейнер - это хранилище объектов (как встроенных, так и определённых пользователем типов). Как правило, контейнеры реализуются в виде шаблонов классов. Простейшие виды контейнеров (статические и динамические массивы) встроены непосредственно в язык C++. Кроме того, стандартная библиотека включает в себя реализации таких контейнеров, как вектор (vector), список (list), очередь (deque), ассоциативный массив (map), множество (set) и некоторых других.
Алгоритм - это функция для манипулирования объектами, содержащимися в контейнере. Типичные примеры алгоритмов - сортировка и поиск. В STL реализовано порядка 60 алгоритмов, которые можно применять к различным контейнерам, в том числе к массивам, встроенным в язык C++.
Итератор - это абстракция указателя, то есть объект, который может ссылаться на другие объекты, содержащиеся в контейнере. Основные функции итератора - обеспечение доступа к объекту, на который он ссылается (разыменование), и переход от одного элемента контейнера к другому (итерация, отсюда и название итератора). Для встроенных контейнеров в качестве итераторов используются обычные указатели. В случае с более сложными контейнерами итераторы реализуются в виде классов с набором перегруженных операторов.
Помимо отмеченных элементов в STL есть ряд вспомогательных понятий; с некоторыми из них следует также познакомиться.
Аллокатор (allocator) - это объект, отвечающий за распределение памяти для элементов контейнера. С каждым стандартным контейнером связывается аллокатор (его тип передаётся как один из параметров шаблона). Если какому-то алгоритму требуется распределять память для элементов, он обязан делать это через аллокатор. В этом случае можно быть уверенным, что распределённые объекты будут уничтожены правильно.
В состав STL входит стандартный класс allocator (описан в файле xmemory). Именно его по умолчанию используют все контейнеры, реализованные в STL. Однако пользователь может реализовать собственный класс. Необходимость в этом возникает очень редко, но иногда это можно сделать из соображений эффективности или в отладочных целях.
Остановимся более подробно на рассмотрении введенных понятий.
Контейнеры. Каждый контейнер предоставляет строго определённый интерфейс, через который с ним будут взаимодействовать алгоритмы. Этот интерфейс обеспечивают соответствующие контейнеру итераторы. Важно подчеркнуть, что никакие дополнительные функции-члены для взаимодействия алгоритмов и контейнеров не используются. Это сделано потому, что стандартные алгоритмы должны работать, в том числе со встроенными контейнерами языка C++, у которых есть итераторы (указатели), но нет ничего, кроме них. Таким образом, при создании собственного контейнера реализация итератора - необходимый минимум.
Каждый контейнер реализует определённый тип итераторов. При этом выбирается наиболее функциональный тип итератора, который может быть эффективно реализован для данного контейнера. "Эффективно" означает, что скорость выполнения операций над итератором не должна зависеть от количества элементов в контейнере. Например, для вектора реализуется итератор с произвольным доступом, а для списка - двунаправленный. Поскольку скорость выполнения операции [] для списка линейно зависит от его длины, итератор с произвольным доступом для списка не реализуется.
Вне зависимости от фактической организации контейнера (вектор, список, дерево) хранящиеся в нём элементы можно рассматривать как последовательность. Итератор первого элемента в этой последовательности возвращает функция begin(), а итератор элемента, следующего за последним, - функция end(). Это очень важно, так как все алгоритмы в STL работают именно с последовательностями, заданными итераторами начала и конца.
Кроме обычных итераторов в STL существуют обратные итераторы (reverse iterator). Обратный итератор отличается тем, что просматривает последовательность элементов в контейнере в обратном порядке. Другими словами, операции + и - у него меняются местами. Это позволяет применять алгоритмы как к прямой, так и к обратной последовательности элементов. Например, с помощью функции find можно искать элементы как "с начала", так и "с конца" контейнера.
В STL контейнеры делятся на три основные группы (табл. 2): контейнеры последовательностей, ассоциативные контейнеры и адаптеры контейнеров. Первые две группы объединяются в контейнеры первого класса.
Таблица 2
Контейнерный класс STL | Описание |
Контейнеры последовательностей | |
vector | Динамический массив |
deque | Двунаправленная очередь |
list | Двунаправленный линейный список |
Ассоциативные контейнеры | |
set | Ассоциативный контейнер с уникальными ключами |
multiset | Ассоциативный контейнер, допускающий дублирование ключей |
map | Ассоциативный контейнер для наборов уникальных элементов |
multimap | Ассоциативный контейнер для наборов с дублированием элементов |
Адаптеры контейнеров | |
stack | Стандартный стек |
queue | Стандартная очередь |
priority_queue | Очередь с приоритетами |
Каждый класс контейнера, реализованный в STL, описывает набор типов, связанных с контейнером. При написании собственных контейнеров следует придерживаться этой же практики. Вот список наиболее важных типов:
value_type - тип элемента;
size_type - тип для хранения числа элементов (обычно size_t);
iterator - итератор для элементов контейнера;
key_type - тип ключа (в ассоциативном контейнере).
Помимо типов можно выделить набор функций, которые реализует почти каждый контейнер в STL (табл. 3). Они не требуются для взаимодействия с алгоритмами, но их реализация улучшает взаимозаменяемость контейнеров в программе. STL разработана с тем расчетом, чтобы контейнеры обеспечивали аналогичные функциональные возможности.
Таблица 3
Общие методы всех STL-контейнеров | Описание |
default constructor | Конструктор по умолчанию. Обычно контейнер имеет несколько конструкторов |
copy constructor | Копирующий конструктор |
destructor | Деструктор |
empty | Возвращает true, если в контейнере нет элементов, иначе false |
max_size | Возвращает максимальное число элементов для контейнера |
size | Возвращает число элементов в контейнере в текущее время |
operator = | Присваивает один контейнер другому |
operator < | Возвращает true, если первый контейнер меньше второго, иначе false |
operator <= | Возвращает true, если первый контейнер не больше второго, иначе false |
operator > | Возвращает true, если первый контейнер больше второго, иначе false |
operator >= | Возвращает true, если первый контейнер не меньше второго, иначе false |
operator == | Возвращает true, если сравниваемые контейнеры равны, иначе false |
operator != | Возвращает true, если сравниваемые контейнеры не равны, иначе false |
swap | Меняет местами элементы двух контейнеров |
Функции, имеющиеся только в контейнерах первого класса | |
begin | Две версии этой функции возвращают либо iterator, либо const_iterator, который ссылается на первый элемент контейнера |
end | Две версии этой функции возвращают либо iterator, либо const_iterator, который ссылается на следующую позицию после конца контейнера |
rbegin | Две версии этой функции возвращают либо reverse_iterator, либо reverse_const_iterator, который ссылается на последний элемент контейнера |
rend | Две версии этой функции возвращают либо reverse_iterator, либо reverse_const_iterator, который ссылается на позицию перед первым элементом контейнера |
insert, erase, | Позволяют вставить или удалить элемент(ы) в середине последовательности |
Окончание табл. 3 | |
clear | Удаляет из контейнера все элементы |
front, back | Возвращают ссылки на первый и последний элемент, хранящийся в контейнере |
push_back, pop_back | Позволяют добавить или удалить последний элемент в последовательности |
push_front, pop_front | Позволяют добавить или удалить первый элемент в последовательности |
Итераторы обычно создаются как друзья классов, с которыми они работают, что позволяет выполнить прямой доступ к частным данным этих классов. С одним контейнером может быть связано несколько итераторов, каждый из которых поддерживает свою собственную «позиционную информацию» (табл. 4).
Таблица 4
Тип итератора | Доступ | Разыменование | Итерация | Сравнение |
Итератор вывода (output iterator) | Только запись | * | ++ | |
Итератор ввода (input iterator) | Только чтение | *, -> | ++ | ==, != |
Прямой итератор (forward iterator) | Чтение и запись | *, -> | ++ | ==, != |
Двунаправленный итератор (bidirectional iterator) | Чтение и запись | *, -> | ++, -- | ==, != |
Итератор с произвольным доступом (random-access iterator) | Чтение и запись | *, ->, [] | ++, --, +, -, +=, -= | ==, !=, <, <=, >, >= |
Общее понятие об итераторе
Для структурированных итераций, например, при обработке массивов:
for(i=0;i<size;i++) sm+=a[i];
порядок обращения к элементу управляется индексом i, который изменяется явно. Можно зафиксировать получение следующего элемента в компоненте-функции.
Class vect
{ int *p; // массив чисел
int size; // размерность массива
int ind; // текущий индекс
public:
vect(); // размерность массива const
vect(int SIZE); // размерность массива size
~vect();
int ub(){return size-1;}
int next()
{ if(ind==pv->size)
return pv->p[(ind=0)++];
else
return pv->p[ind++];
}
};
Это соответствует тому, что обращение к объекту ограничивается использованием одного индекса ind. Другая возможность состоит в том, чтобы создать множество индексов и передавать функции обращения к элементу один из них. Это ведет к существенному увеличению числа переменных. Более удобным представляется создание отдельного, связанного с vect класса (класса итераций), в функции которого входит обращение к элементам класса vect. В приведенном ниже примере, для универсальности разрабатываемого класса vect использованы шаблоны классов vect и vect_iterator.
#include <iostream.h>
template<class T> // предварительное template объявление
class vect_iterator; // класса vect_iterator
template<class T>
class vect// шаблон класса vect
{ friend class vect_iterator<T>; // предварительное friend-объявление
T *p; // базовый указатель (массив)
int size; // размерность массива
public:
vect(int SIZE):size(SIZE)
{ p=new T[size];
for(int i=0; i<size; *(p+i++)=(T)i);
}
int ub(){return size-1;} // возвращается размер массива
void add() // изменение содержимого массива
{ for(int i=0; i<size; *(p+i++)+=1);}
~vect(){delete [] p;}
};
template<class T>
class vect_iterator
{ vect<T> *pv; // указатель на класс vect
int ind; // текущий индекс в массиве
public:
vect_iterator(vect<T> &v): ind(0),pv(&v){}
T &next();//возвращается текущее значение из массива (с индекса ind)
};
template<class T> // шаблон класса vect_iterator
T &vect_iterator<T>::next()
{ if(ind==pv->size)
return (T)pv->p[(ind=0)++];
else
return (T)pv->p[ind++];
}
void main()
{ vect<int> v(5);
vect_iterator<int> v_i1(v),v_i2(v); // создано 2 объекта-итератора
// для прохода по объекту vect
cout<<v_i1.next()<<' '<<v_i2.next()<<endl;
v.add(); // модификация объекта v
cout<<v_i1.next()<<' '<<v_i2.next()<<endl;
for(int i=0;i<v.ub();i++)
cout<<v_i1.next()<<endl;
}
Результат работы программы:
0 0
2 2
Полное отсоединение обращения от составного объекта позволяет объявлять столько объектов итераторов, сколько необходимо. При этом каждый из объектов итераторов может просматривать объект vect независимо от других.
Итератор представляет собой операцию, обеспечивающую последовательный доступ ко всем частям объекта. Итераторы имеют свойства, похожие на свойства указателей, и могут быть использованы для указания на элементы контейнеров первого класса. Итераторы реализуются для каждого типа контейнера. Также имеется целый ряд операций (*, ++ и другие) с итераторами, стандартными для контейнеров.
Если итератор a указывает на некоторый элемент, то ++a указывает на следующий элемент, а *a ссылается на элемент, на который указывает a.
Объект типа iterator может использоваться для ссылки на элемент контейнера, который может быть модифицирован, а const_iterator для ссылки на немодифицируемый элемент контейнера.
Еще один пример реализации контейнерного класса vect. В этом случае итерационный механизм реализован в непосредственно в классе vect.
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
template <class T>
class vect// шаблон класса vect
{ int size; // размерность массива
public:
typedef T* vect_iterator;
typedef const T* const_vect_iterator;
vect_iterator vv; // итератор контейнера
const_vect_iterator cv; // константный итератор
vect()
{ vv=new T; //
cv=vv; //
size=0;
}
~vect(){}
vect_iterator begin_v(); // итератор на начало вектора
vect_iterator end_v(); // итератор на конец вектора
void push_b(T); // добавление элемента в конец вектора
void pop_b(); // удаление элемента с конца вектора
void insert_v(T,T*); // добавление элемента в вектор
void erase_v(T*); // удаление элемента с конца вектора
};
template <class T>
void vect<T>::push_b(T a)
{ vect_iterator temp;
temp=new T[++size];
for(int i=0;i<size-1;i++)
*(temp+i)=*(vv+i);
*(temp+size-1)=a;
delete vv;
vv=temp;
}
template <class T>
T* vect<T>::begin_v()
{ return vv; }
template <class T>
T* vect<T>::end_v()
{ return vv+size; }
template <class T>
void vect<T>::pop_b()
{ vect_iterator temp;
temp=new T[--size];
for(int i=0;i<size;i++)
*(temp+i)=*(vv+i);
delete vv;
vv=temp;
}
template <class T>
void vect<T>::insert_v(T zn,T* adr)
{ int k=0;
vect_iterator temp;
temp=new T[++size];
for(vect_iterator i=vv;i<adr;i++)
*(temp+k++)=*i;
*(temp+k)=zn;
for(i=adr;i<vv+size-1;i++)
*(temp+ ++k)=*i;
delete vv;
vv=temp;
}
template <class T>
void vect<T>::erase_v(T* adr) // adr - итератор на удаляемый
{ int k=0; // элемент контейнера
vect_iterator temp;
temp=new T[--size]; // временный вектор
for(vect_iterator i=vv;i<adr;i++)
*(temp+k++)=*i;
for(i=adr+1;i<=vv+size;i++)
*(temp+k++)=*i;
delete vv; // освобождение памяти для старого вектора
vv=temp; // vv указывает нп новый вектор
}
void menu(void)
{ cout<<endl;
cout<<"1 - добавить элемент"<<endl;
cout<<"2 - pop последний элемент"<<endl;
cout<<"3 - erase элемент"<<endl;
cout<<"4 - print содержимое контейнера"<<endl;
cout<<"0 - окончание работы "<<endl;
}
main()
{ int count;
int choice;
int pos;
int val;
vect<int> Vect; // объект контейнер
vect<int>::vect_iterator itr; // итератор
int a;
cout<<"Сколько элементов вводится в вектор?"<<endl;
cin>>count;
for(int i = 0;i < count; i++)
{ cout<<"enter the element number "<<i+1<<endl;
cin>>a; // инициализация контейнера
Vect.push_b(a); // начальными значениями
}
while(1)
{ menu();
cout<<"? ";
cin>>choice;
cout<<endl;
switch(choice)
{ case 1:
cout<<"Введите значение и позицию "<<endl;
cin>>val>>pos;
Vect.insert_v(val,Vect.begin_v()+pos-1);
cout<<endl;
break;
case 2:Vect.pop_b();
cout<<"Элемент удален";
break;
case 3:
cout<<"Введите позицию элемента для удаления "<<endl;
cin>>pos;
Vect.erase_v(Vect.begin_v()+pos-1);
break;
case 4:
for(itr=Vect.begin_v();itr!=Vect.end_v();itr++)
cout<<*itr<<" ";
break;
case 0: return 0;
}
}
return 0;
}
Категории итераторов
Итераторы, как и контейнеры, реализуются в виде шаблонов классов. Итераторы обладают такими достоинствами, как, например, автоматическое отслеживание размера типа, на который указывает итератор, автоматизированные операции инкремента и декремента для перехода от элемента к элементу. Именно благодаря таким возможностям итераторы и являются фундаментом всей библиотеки.
Итераторы можно условно разделить на две категории: основные и вспомогательные.
Основные итераторы
Основные итераторы используются наиболее часто. Они взаимозаменяемы, однако при этом нужно соблюдать иерархию старшинства (рис.8).
Рис. 8. Иерархия итераторов
Итераторы ввода. Итераторы ввода (input iterator) стоят в самом низу иерархии итераторов. Это наиболее простые из всех итераторов STL, и доступны они только для чтения. Итератор ввода может быть сравнен с другими итераторами на предмет равенства или неравенства, чтобы узнать, не указывают ли два разных итератора на один и тот же объект. Можно использовать оператор разыменовывания (*) для получения содержимого объекта, на который итератор указывает. Перемещаться от первого элемента, на который указывает итератор ввода, к следующему элементу можно с помощью оператора инкремента (++). Ниже приведен пример, демонстрирующий некоторые приемы работы с итератором ввода.
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
main(void)
{ int init1[4];
vector<int> v(4);
istream_iterator<int> ii(cin);
for(int j,i=0;i<4;i++)
// v.push_back(*ii++); добавление в конец вектора
// *(v.begin()+i)=*ii++;
// v[i]=*ii++;
copy(v.begin(), v.end(), ostream_iterator<int>(cout, "\n"));
}
Итераторы ввода возвращает только шаблонный класс istream_iterator. Однако, несмотря на то что итераторы ввода возвращаются единственным классом, ссылки на него присутствуют повсеместно. Это связано с тем, что вместо итератора ввода может подставляться любой из основных итераторов, за исключением итератора вывода, назначение которого прямо противоположно итератору ввода.
template <class InputIterator, class Function>
Function for_each (InputIterator first, InputIterator last, Function f)
{
while (first != last) f(*first++);
return f;
}
В примере первые два параметра - итераторы ввода на начало цепочки объектов и на первое значение, находящееся "за пределом" для этой цепочки. Тело алгоритма выполняет переход от объекта к объекту, вызывая для каждого значения, на которое указывает итератор ввода first, функцию. Указатель на нее передается в третьем параметре. Здесь задействованы все три перегруженных оператора, допустимые для итераторов ввода: сравнения (!=), инкремента (++) и разыменовывания (*). Ниже приводится пример использования алгоритма for_each для однонаправленных итераторов.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void Print(int n)
{ cout << n << " " ; }
void main()
{ const int SIZE = 5 ;
typedef vector<int > IntVector ; // создание синонима для vector<int>
typedef IntVector::iterator IntVectorItr;//аналогично для IntVector::iterator
IntVector Numbers(SIZE) ; //вектор, содержащий целые числа
IntVectorItr start, end ; // итераторы для IntVector
int i ;
for (i = 0; i < SIZE; i++) // инициализация вектора
Numbers[i] = i + 1 ;
start = Numbers.begin() ; // итератор на начало вектора
end = Numbers.end() ; // итератор на запредельный элемент вектора
for_each(start, end, Print);
cout << "\n\n" ;
}
Чтобы включить в программу возможность использования потоков, добавляется включаемый файл iostream, а для описания прототипа алгоритма for_each в программу включается заголовочный файл algorithm (algorith для продуктов Borland). Обязательным при использовании STL является использование директивы:
using namespace std,
включающей пространство имен библиотеки STL.
Итераторы вывода. Если итератор ввода предназначен для чтения данных, то итератор вывода (output iterator) служит для ссылки на область памяти, куда выводятся данные. Итераторы вывода можно встретить повсюду, где происходит хоть какая-то обработка информации средствами STL. Для данного итератора определены операторы присвоения (=), разыменовывания (*) и инкремента (++). Однако следует помнить, что первые два оператора предполагают, что итератор вывода располагается в левой части выражений, то есть во время присвоения он должен быть целевым итератором, которому присваиваются значения. Разыменовывание нужно делать лишь для того, чтобы присвоить некое значение объекту, на который итератор ссылается. Итераторы вывода могут быть возвращены итераторами потоков вывода (ostream_iterator) и итераторами вставки inserter, front_inserter и back_inserter (рассмотрены ниже в разделе "Итераторы вставки"). Рассмотрим пример использования итераторов вывода:
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
main(void)
{ int init1[] = {1, 2, 3, 4, 5};
int init2[] = {6, 7, 8, 9, 10};
vector<int> v(10);
merge(init1, init1 + 5, init2, init2 + 5, v.begin());
copy(v.begin(), v.end(), ostream_iterator<int>(cout, "\n"));
}
В примере помимо потоков и алгоритмов использован контейнер vector (представляющий одномерный массив, вектор). У него имеются специальные компоненты-функции begin() и end(). В приведенном нами примере создаются и инициализируются два массива - init1 и init2. Далее их значения соединяются вместе алгоритмом merge и записываются в вектор. А для проверки полученного результата мы пересылаем данные из вектора в поток вывода, для чего вызываем алгоритм копирования copy и специальный итератор потока вывода ostream_iterator. Он перешлет данные в поток cout, разделив каждое пересылаемое значение символом окончания строки. Для шаблонного класса ostream_iterator требуется указать тип выводимых значений. В нашем случае это int.
Если в примере описать еще один вектор vv:
vector<int> vv(10);
то в алгоритме copy вместо выходного итератора можно, например, использовать итератор вектора vv для копирования данных из одного вектора в другой:
copy(v.begin(), v.end(), vv.begin());
Рассмотрим еще один пример использования итераторов ввода-вывода.
#include <iostream>
using namespace std;
#include <iterator>
#define N 2
void main()
{ cout<<"Введите "<<N<<" числа"<<endl;
std::istream_iterator<int> in_obj(cin);
int ms[N],i;
for(i=0;i<N;i++)
{ ms[i]=*in_obj; // аналогично ms[i]=*(in_obj++);
++in_obj;
}
ostream_iterator<int> out_obj(cout);
for(i=0;i<N;i++)
*out_obj=ms[i];
cout<<endl;
}
В инструкциях:
std::istream_iterator<int> in_obj(cin);
и
ostream_iterator<int> out_obj(cout);
создаются итераторы istream_iterator и ostream_iterator для ввода и вывода int значений из объектов cin и cout соответственно.
Использование операции * (разыменовывания) ms[i]=*in_obj приводит к получению значения из потока in_obj и занесению его в элемент массива, а в инструкции *out_obj=ms[i] к получению ссылки на объект out_obj ассоциируемый с выходным потоком, и посылке значения элемента массива в поток. Перегруженная операция ++in_obj перемещает итератор in_obj к следующему элементу во входном потоке. Отметим, что для выходного потока операции разыменование и инкремент возвращают одно значение – ссылку на поток.
ostream_iterator<_U, _E, _Tr>& operator*()
{return (*this); }
ostream_iterator<_U, _E, _Tr>& operator++()
{return (*this); }
Однонаправленные итераторы. Если соединить итераторы ввода и вывода, то получится однонаправленный итератор (forward iterator), который может перемещаться по цепочке объектов в одном направлении, за что и получил такое название. Для такого перемещения в итераторе определена операция инкремента (++). Кроме этого, в однонаправленном итераторе есть операторы сравнения (== и !=), присвоения (=) и разыменовывания (*). Все эти операторы можно увидеть, если посмотреть, как реализован, например, алгоритм replace, заменяющий одно определенное значение на другое:
template <class ForwardIterator, class T>
void replace (ForwardIterator first, ForwardIterator last,
const T& old_value, const T& new_value)
{ while (first != last)
{ if (*first == old_value) *first = new_value;
++first;
}
}
Чтобы убедиться в правильности работы всех операторов однонаправленных итераторов, составим программу, заменяющую в исходном массиве все единицы на нули и наоборот, т. е. произведем инверсию. С этой целью все нули заменяются на некоторое значение, например на двойку. Затем все единицы обнуляются, а все двойки становятся единицами:
#include <algorithm>
#include <iostream>
using namespace std;
main(void)
{
replace(init, init + 5, 0, 2);
replace(init, init + 5, 1, 0);
replace(init, init + 5, 2, 1);
copy(init, init + 5, ostream_iterator<int>(cout, "\n"));
}
Алгоритм replace, используя однонаправленные итераторы, читает значения, заменяет их и перемещается от одного к другому.
Двунаправленные итераторы. Двунаправленный итератор (bidirectional iterator) аналогичен однонаправленному итератору. В отличие от последнего двунаправленный итератор может перемещаться не только из начала в конец цепочки объектов, но и наоборот. Это становится возможным благодаря наличию оператора декремента (--). На двунаправленных итераторах базируются различные алгоритмы, выполняющие реверсивные операции, например reverse. Этот алгоритм меняет местами все объекты в цепочке, на которую ссылаются переданные ему итераторы. Следующий пример был бы невозможен без двунаправленных итераторов:
#include <algorithm>
#include <iostream>
using namespace std;
main(void)
{
int init[] = {1, 2, 3, 4, 5};
reverse(init, init + 5);
copy(init, init + 5, ostream_iterator<int>(cout, "\n"));
}
Итераторы двунаправленного доступа возвращаются несколькими контейнерами STL: list, set, multiset, map и multimap.
Итераторы произвольного доступа. Итераторы этой категории - наиболее универсальные из основных итераторов. Они не только реализуют все функции, свойственные итераторам более низкого уровня, но и обладают большими возможностями. Глядя на исходные тексты, в которых используются итераторы произвольного доступа, можно подумать, что имеешь дело с арифметикой указателей языка C++. Реализованы такие операции, как сокращенное сложение и вычитание (+= и -=), сравнение итераторов (<, >, <= и >=), операция обращения к заданному элементу массива ([]), а также и некоторые другие операции.
Как правило, все сложные алгоритмы, требующие расширенных вычислений, оперируют итераторами произвольного доступа. Ниже приводится пример, в котором мы используем практически все операции, допустимые для них. Исходный текст разбит на части, к каждой из которых приведены комментарии. Сначала нужно включить требуемые заголовочные файлы и определить константу пробела:
#include <algorithm>
#include <iostream>
#include <vector>
#define space " "
Затем следует включить использование STL:
using namespace std;
В функции main мы описываем массив числовых констант и вектор из пяти элементов:
int main(void)
{
const int init[] = {1, 2, 3, 4, 5};
vector<int> v(5);
Создаем переменную типа "итератор произвольного доступа". Для этого берем итератор и на его основе создаем другой, более удобный:
typedef vector<int>::iterator vectItr;
vectItr itr ;
Инициализируем вектор значениями из массива констант и присваиваем адрес его первого элемента итератору произвольного доступа:
copy(init, init + 5, itr = v.begin());
Далее, используя различные операции над итераторами, последовательно читаем элементы вектора, начиная с конца, и выводим их на экран:
cout << *( itr + 4 ) << endl;
cout << *( itr += 3 ) << endl;
cout << *( itr -= 1) << endl;
cout << *( itr = itr - 1) << endl;
cout << *( --itr ) << endl;
После этого итератор, претерпев несколько изменений, снова указывает на первый элемент вектора. А чтобы убедиться, что значения в векторе не были повреждены, и проверить оператор доступа ([]), выведем в цикле значения вектора на экран:
for(int i = 0; i < (v.end() - v.begin()); i++)
cout << itr[i] << space;
cout << endl;
}
Операции с итераторами произвольного доступа реализуются таким образом, чтобы не чувствовалось разницы между использованием обычных указателей и итераторов.
Итераторы произвольного доступа возвращают такие контейнеры, как vector и deque.
Вспомогательные итераторы
Вспомогательные итераторы названы так потому, что они выполняют вспомогательные операции по отношению к основным.
Реверсивные итераторы. Некоторые классы-контейнеры спроектированы так, что по хранимым в них элементам данных можно перемещаться в заданном направлении. В одних контейнерах это направление от первого элемента к последнему, а в других - от элемента с самым большим значением к элементу, имеющему наименьшее значение. Однако существует специальный вид итераторов, называемых реверсивными. Такие итераторы работают "с точностью до наоборот", то есть если в контейнере итератор ссылается на первый элемент данных, то реверсивный итератор ссылается на последний. Получить реверсивный итератор для контейнера можно вызовом метода rbegin(), а реверсивное значение "за пределом" возвращается методом rend(). Следующий пример использует нормальный итератор для вывода значений от 1 до 5 и реверсивный итератор для вывода этих же значений, но в обратном порядке:
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
main(void)
{
const int init[] = {1, 2, 3, 4, 5};
vector<int> v(5);
copy(init, init + 5, v.begin());
copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));
copy(v.rbegin(), v.rend(), ostream_iterator<int>(cout, " ")); }
Итераторы потоков. Важную роль в STL играют итераторы потоков, которые делятся на итераторы потоков ввода и вывода. Практически во всех рассмотренных примерах имеется итератор потока вывода для отображения данных на экране. Суть применения потоковых итераторов в том, что они превращают любой поток в итератор, используемый точно так же, как и прочие итераторы: перемещаясь по цепочке данных, он считывает значения объектов и присваивает им другие значения.
Итератор потока ввода - это удобный программный интерфейс, обеспечивающий доступ к любому потоку, из которого требуется считать данные. Конструктор итератора имеет единственный параметр - поток ввода. А поскольку итератор потока ввода представляет собой шаблон, то ему передается тип вводимых данных. Вообще-то должно передаваться четыре типа, но последние три имеют значения по умолчанию. Каждый раз, когда требуется ввести очередной элемент информации, используйте оператор ++ точно так же, как с основными итераторами. Считанные данные можно узнать, если применить разыменовывание (*).
Итератор потока вывода весьма схож с итератором потока ввода, но у его конструктора имеется дополнительный параметр, которым указывают строку-разделитель, добавляемую в поток после каждого выведенного элемента. Ниже приведен пример программы, читающей из стандартного потока cin числа, вводимые пользователем и дублирующие их на экране, завершая сообщение строкой - "last entered value". Работа программы заканчивается при вводе числа 999:
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
main(void)
{ istream_iterator<int> is(cin);
ostream_iterator<int> os(cout, " – введенное значение \n");
int input;
while((input = *is) != 999)
{ *os++ = input;
is++ ;
}
}
Потоковые итераторы имеют одно существенное ограничение: в них нельзя возвратиться к предыдущему элементу. Единственный способ сделать это - заново создать итератор потока.
Итераторы вставки. Появление итераторов вставки (insert iterator) было продиктовано необходимостью. Без них просто невозможно добавить значения к цепочке объектов. Так, если в массив чисел, на которые ссылается итератор вывода, вы попытаетесь добавить пару новых значений, то итератор вывода попросту запишет новые значения на место старых и они будут потер<