
- •Учебное пособие
- •Введение
- •Объектно-ориентированный подход
- •Объектно-ориентированное программирование Абстрактные типы данных
- •Базовые принципы объектно-ориентированного программирования
- •Простейший ввод и вывод
- •Объект cout
- •Манипуляторы hex и oct
- •Другие манипуляторы
- •Объект cin
- •Операторы для динамического выделения и освобождения памяти (new и delete)
- •Базовые конструкции объектно-ориентированных программ Объекты
- •Понятие класса
- •Конструктор копирования
- •Конструктор explicit
- •Указатели на компоненты класса
- •Встроенные функции (спецификатор inline)
- •Организация внешнего доступа к локальным компонентам класса (спецификатор friend)
- •Вложенные классы
- •Static-члены (данные) класса
- •Указатель this
- •Компоненты-функции static и const
- •Proxi-классы
- •Параметры ссылки
- •Независимые ссылки
- •Практические приемы ограничения числа объектов класса
- •Наследование (производные классы)
- •Конструкторы и деструкторы при наследовании
- •Виртуальные функции
- •Абстрактные классы
- •Виртуальные деструкторы
- •Множественное наследование
- •Виртуальное наследование
- •Перегрузка функций
- •Перегрузка операторов
- •Перегрузка бинарного оператора
- •Перегрузка унарного оператора
- •Дружественная функция operator
- •Перегрузка оператора []
- •Перегрузка оператора ()
- •Перегрузка операторов new и delete
- •Преобразование типа
- •Явные преобразования типов
- •Преобразования типов, определенных в программе
- •Шаблоны Параметризированные классы
- •Передача в шаблон класса дополнительных параметров
- •Шаблоны функций
- •Совместное использование шаблонов и наследования
- •Шаблоны класса и friend
- •Некоторые примеры использования шаблона класса Реализация smart-указателя
- •Классы поддерживающие транзакции
- •Задание значений параметров класса по умолчанию
- •Пространства имен
- •Ключевое слово using как директива
- •Ключевое слово using как объявление
- •Псевдоним пространства имен
- •Организация ввода-вывода
- •Состояние потока
- •Строковые потоки
- •Организация работы с файлами
- •Организация файла последовательного доступа
- •Создание файла произвольного доступа
- •Основные функции классов ios, istream, ostream
- •Основы обработки исключительных ситуаций
- •Перенаправление исключительных ситуаций
- •Исключительная ситуация, генерируемая оператором new
- •Генерация исключений в конструкторах
- •Задание собственной функции завершения
- •Спецификации исключительных ситуаций
- •Задание собственного неожиданного обработчика
- •Иерархия исключений стандартной библиотеки
- •Стандартная библиотека шаблонов (stl) Общее понятие о контейнере
- •Общее понятие об итераторе
- •Категории итераторов
- •Основные итераторы
- •Вспомогательные итераторы
- •Операции с итераторами
- •Контейнерные классы Контейнеры последовательностей
- •Контейнер последовательностей vector
- •Контейнер последовательностей list
- •Контейнер последовательностей deque
- •Ассоциативные контейнеры
- •Ассоциативный контейнер multiset
- •Ассоциативный контейнер set
- •Ассоциативный контейнер multimap
- •Ассоциативный контейнер map
- •Адаптеры контейнеров
- •Адаптеры stack
- •Адаптеры queue
- •Адаптеры priority_queue
- •Пассивные и активные итераторы
- •Алгоритмы
- •Алгоритмы сортировки sort, partial_sort, sort_heap
- •Алгоритмы поиска find, find_if, find_end, binary_search
- •Алгоритмы fill, fill_n, generate и generate_n
- •Алгоритмы equal, mismatch и lexicographical_compare
- •Математические алгоритмы
- •Алгоритмы работы с множествами
- •Алгоритмы swap, iter_swap и swap_ranges
- •Алгоритмы copy, copy_backward, merge, unique и reverse
- •Примеры реализации контейнерных классов Связанные списки
- •Реализация односвязного списка
- •Реализация двусвязного списка
- •Реализация двоичного дерева
- •Литература
- •Вопросы по курсу ооп
- •220013, Минск, п.Бровки, 6.
Примеры реализации контейнерных классов Связанные списки
Связанные списки - это способ размещения данных, при котором одни данные ссылаются на другие. Списки представляют собой пример контейнерных классов. В общем, список напоминает массив с тем отличием, что его размеры не фиксированы: он может расти и уменьшаться по мере работы с ним. Очередной элемент списка размещается в памяти динамически и связывается с остальной частью списка посредством указателей. В случае если некоторый элемент списка более не нужен, то освобождается память, отведенная под этот элемент. Частным случаем списков являются стеки, очереди и кольца.
Реализация односвязного списка
Односвязный список предполагает организацию хранения данных в памяти, при которой перемещение может быть выполнено только в одном направлении (от начала списка в конец). Расположение в памяти элементов односвязного списка можно изобразить следующим образом (рис. 9).
В настоящем пособии реализация односвязного списка не приводится (предлагается выполнить самостоятельно, основываясь на информации о реализации двусвязного списка, приводимого ниже).
Реализация двусвязного списка
Двусвязный список - это организация хранения данных в памяти, позволяющая выполнять перемещение в обоих направлениях (от начала списка в конец и наоборот). Расположение в памяти двусвязного списка можно изобразить следующим образом (рис. 10).
В приведенном ниже примере для доступа к элементам списка используется разработанный класс, реализующий простые итераторы.
#include <iostream>
#include <cassert>
using namespace std;
template <class T>
class D_List
{private:
class D_Node
{ public:
D_Node *next; // указатель на следующий узел
D_Node *prev; // указатель на предыдущий узел
T val; // поле данного
D_Node(T node_val) : val(node_val) { } // конструктор
D_Node() {} // конструктор
~D_Node(){} // деструктор
// для вывода элементов, тип которых определен пользователем,
// неодходимо перегрузить операцию << operator<<( ).
void print_val( ) { cout << val << " "; }
};
public:
class iterator
{private:
friend class D_List<T>;
D_Node * the_node;
public:
iterator( ) : the_node(0) { }
iterator(D_Node * dn) : the_node(dn) { }
// Copy constructor
iterator(const iterator & it) : the_node(it.the_node) { }
iterator& operator=(const iterator& it)
{ the_node = it.the_node;
return *this;
}
bool operator==(const iterator& it) const
{ return (the_node == it.the_node); }
bool operator!=(const iterator& it) const
{ return !(it == *this); }
iterator& operator++( )
{ if ( the_node == 0 )
throw "incremented an empty iterator";
if ( the_node->next == 0 )
throw "tried to increment too far past the end";
the_node = the_node->next;
return *this;
}
iterator& operator--( )
{ if ( the_node == 0 )
throw "decremented an empty iterator";
if ( the_node->prev == 0 )
throw "tried to decrement past the beginning";
the_node = the_node->prev;
return *this;
}
T& operator*( ) const
{ if ( the_node == 0 )
throw "tried to dereference an empty iterator";
return the_node->val;
}
};
private:
D_Node *head; // указатель на начало списка
D_Node *tail; // указатель на элемент вне списка
D_List & operator=(const D_List &);
D_List(const D_List &);
iterator head_iterator;
iterator tail_iterator;
public:
D_List( )
{ head = tail = new D_Node;
tail->next = 0;
tail->prev = 0;
head_iterator = iterator(head);
tail_iterator = iterator(tail);
}
// конструктор (создание списка, содержащего один элемент)
D_List(T node_val)
{ head = tail = new D_Node;
tail->next = 0;
tail->prev = 0;
head_iterator = iterator(head);
tail_iterator = iterator(tail);
add_front(node_val);
}
// деструктор
~D_List( )
{ D_Node *node_to_delete = head;
for (D_Node *sn = head; sn != tail;)
{ sn = sn->next;
delete node_to_delete;
node_to_delete = sn;
}
delete node_to_delete;
}
bool is_empty( ) {return head == tail;}
iterator front( ) { return head_iterator; }
iterator rear( ) { return tail_iterator; }
void add_front(T node_val)
{ D_Node *node_to_add = new D_Node(node_val);
node_to_add->next = head;
node_to_add->prev = 0;
head->prev = node_to_add;
head = node_to_add;
head_iterator = iterator(head);
}
// добавление нового элемента в начало списка
void add_rear(T node_val)
{ if ( is_empty( ) ) // список не пустой
add_front(node_val);
else
// не выполняется для пустого списка, т.к. tail->prev =NULL
// и, следовательно, tail->prev->next бессмысленно
{ D_Node *node_to_add = new D_Node(node_val);
node_to_add->next = tail;
node_to_add->prev = tail->prev;
tail->prev->next = node_to_add;
tail->prev = node_to_add;
tail_iterator = iterator(tail);
}
}
bool remove_it(iterator & key_i)
{ for ( D_Node *dn = head; dn != tail; dn = dn->next )
if ( dn == key_i.the_node ) // найден узел для удаления
{ dn->prev->next = dn->next;
dn->next->prev = dn->prev;
delete dn; // удаление узла
key_i.the_node = 0;
return true;
}
return false;
}
// поиск итератора по значению узла
iterator find(T node_val) const
{ for ( D_Node *dn = head; dn != tail; dn = dn->next )
if ( dn->val == node_val ) return iterator(dn);
return tail_iterator;
}
int size( ) const
{ int count = 0;
for ( D_Node *dn = head; dn != tail; dn = dn->next ) ++count;
return count;
}
// Вывод содержимого списка
void print( ) const
{ for ( D_Node *dn = head; dn != tail; dn = dn->next )
dn->print_val( );
cout << endl;
}
};
В файле d_list.cpp содержится main-функция, демонстрирующая некоторые примеры работы со списком.
#include "dlist_2.h"
typedef int tip;
D_List<tip> the_list; // создается пустой список
int main()
{ int ret = 0;
D_List<tip>::iterator list_iter;
// занесение значений 0 1 2 3 4 в список
for (int j = 0; j < 5; ++j)
the_list.add_front(j);
// вывод содержимого списка используя компоненту-функцию
// класса D_List
the_list.print( );
// повторный вывод значения содержимого списка
// используя итератор
for ( list_iter = the_list.front( ) ;
list_iter != the_list.rear( ) ;
++list_iter )
cout << *list_iter << " ";
cout << endl;
// вывод содержимого списка в обратном порядке
for ( list_iter = the_list.rear( ) ; list_iter != the_list.front( ) ; )
{ --list_iter; // декремент итератора
cout << *list_iter << " ";
}
cout << endl;
the_list.remove_it(the_list.find(3));
the_list.print( );
cout<<the_list.size( )<<endl;
return 0;
}
Результат работы программы:
3 2 1 0
3 2 1 0
0 1 2 3 4
2 1 0
4
Итератор реализован как открытый вложенный класс D_List::iterator. Так как класс открытый, может быть в main создан его объект. Класс iterator объявляет дружественным классу D_List, чтобы функция remuv_it класа D_List имела возможность обращаться к private-члену the_node класса iterator.
В дополнение к стандартным указателям на голову и хвост списка (адрес за пределами списка) в программе (в d_list.h) объявлены итераторы head_iterator и tail_iterator, также ссылающиеся на голову и хвост списка.
Использование итератора позволяет скрыть единственный элемент данных класса D_List:
D_Node * the_node.
В классе iterator выполнена перегрузка некоторых операций, позволяющих манипулировать узлом в строго определенных правилах (табл. 5).
Таблица 5
-
Функция
Описание
operator=()
Присваивает the_node значение the_node правой части
operator==()
Возвращает true, если оба итератора ссылаются на один узел
operator!=()
Отрицание операции ==
operator++()
Перемещает итератор на следующий узел
operator--()
Перемещает итератор на предыдущий узел
operator*()
Возвращает значение node_val узла D_node
Для поддержки использования итераторов в качестве аргументов или возвращаемых значений определен конструктор копирования.
В программе функция find(T) возвращает не bool, а iterator, который может быть передан другим функциям, принимающим параметры-итераторы, для доступа к данным или прохода по списку.
Использование итераторов позволяет пользователю манипулировать списком, при этом детали его реализации остаются надежно скрытыми.