Опубликован: 10.10.2006 | Доступ: свободный | Студентов: 6105 / 466 | Оценка: 4.26 / 3.88 | Длительность: 31:30:00
Лекция 8:

Шаблоны типа

8.3.3 Реализация списка

Реализация функций slist_base очевидна. Единственная трудность связана с обработкой ошибок. Например, что делать если пользователь с помощью функции get() пытается взять элемент из пустого списка. Подобные ситуации разбираются в функции обработки ошибок slist_handler(). Более развитый метод, рассчитанный на особые ситуации, будет обсуждаться в "лекции 9" .

Приведем полное описание класса slist_base:

class slist_base {
    slink* last;  // last->next является началом списка
 public:
    void insert(slink* a);  // добавить в начало списка
    void append(slink* a);  // добавить в конец списка
    slink* get();           // удалить и возвратить
                                // начало списка
    void clear() { last = 0; }

    slist_base() { last = 0; }
    slist_base(slink* a) { last = a->next = a; }

    friend class slist_base_iter;
  };

Чтобы упростить реализацию обеих функций insert и append, хранится указатель на последний элемент замкнутого списка:

void slist_base::insert(slink* a) // добавить в начало списка
{
  if (last)
     a->next = last->next;
  else
     last = a;
  last->next = a;
}

Заметьте, что last->next - первый элемент списка.

void slist_base::append(slink* a) // добавить в конец списка
{
  if (last) {
     a->next = last->next;
     last = last->next = a;
  }
  else
     last = a->next = a;
}

                          slink* slist_base::get() // удалить и возвратить начало списка
                          {
 if (last == 0)
    slist_handler("нельзя взять из пустого списка");
 slink* f = last->next;
 if (f== last)
    last = 0;
 else
    last->next = f->next;
 return f;
                          }

Возможно более гибкое решение, когда slist_handler - указатель на функцию, а не сама функция. Тогда вызов

slist_handler("нельзя взять из пустого списка");

будет задаваться так

(*slist_handler)(" нельзя взять из пустого списка");

Как мы уже делали для функции new_handler ( \S 3.2.6), полезно завести функцию, которая поможет пользователю создавать свои обработчики ошибок:

typedef void (*PFV)(const char*);

PFV set_slist_handler(PFV a)
{
  PFV old = slist_handler;
  slist_handler = a;
  return old;
}

PFV slist_handler = &default_slist_handler;

Особые ситуации, которые обсуждаются в "лекции 9" , не только дают альтернативный способ обработки ошибок, но и способ реализации slist_handler.

8.3.4 Итерация

В классе slist_base нет функций для просмотра списка, можно только вставлять и удалять элементы. Однако, в нем описывается как друг класс slist_base_iter, поэтому можно определить подходящий для списка итератор. Вот один из возможных, заданный в том стиле, какой был показан в \S 7.8:

class slist_base_iter {
   slink* ce;      // текущий элемент
   slist_base* cs; // текущий список
 public:
   inline slist_base_iter(slist_base& s);
   inline slink* operator()()
 };

 slist_base_iter::slist_base_iter(slist_base& s)
 {
   cs = &s;
   ce = cs->last;
 }

 slink* slist_base_iter::operator()()
   // возвращает 0, когда итерация кончается
 {
   slink* ret = ce ? (ce=ce->next) : 0;
   if (ce == cs->last) ce = 0;
   return ret;
 }

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

template<class T> class Islist_iter;

  template<class T> class Islist {
    friend class Islist_iter<T>;
    // ...
  };

  template<class T> class Slist_iter;

  template<class T> class Slist {
    friend class Slist_iter<T>;
    // ...
  };

Обратите внимание, что имена итераторов появляются без определения их шаблонного класса. Это способ определения в условиях взаимной зависимости шаблонов типа.

Теперь можно определить сами итераторы:

template<class T>
 class Islist_iter : private slist_base_iter {
 public:
   Islist_iter(Islist<T>& s) : slist_base_iter(s) { }

   T* operator()()
      { return (T*) slist_base_iter::operator()(); }
 };

 template<class T>
 class Slist_iter : private slist_base_iter {
 public:
   Slist_iter(Slist<T>& s) : slist_base_iter(s) { }
   inline T* operator()();
 };

 T* Slist_iter::operator()()
 {
  return ((Tlink<T>*) slist_base_iter::operator()())->info;
 }

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

void f(name* p)
 {
   Islist<name> lst1;
   Slist<name> lst2;

   lst1.insert(p);
   lst2.insert(p);
   // ...

   Islist_iter<name> iter1(lst1);
   const name* p;
   while (p=iter1()) {
      list_iter<name> iter2(lst1);
      const name* q;
      while (q=iter2()) {
         if (p == q) cout << "найден" << *p << '\n';
      }
   }
 }

Есть несколько способов задать итератор для контейнерного класса. Разработчик программы или библиотеки должен выбрать один из них и придерживаться его. Приведенный способ может показаться слишком хитрым. В более простом варианте можно было просто переименовать operator()() как next(). В обоих вариантах предполагается взаимосвязь между контейнерным классом и итератором для него, так что можно при выполнении итератора обработать случаи, когда элементы добавляются или удаляются из контейнера. Этот и некоторые другие способы задания итераторов были бы невозможны, если бы итератор зависел от функции пользователя, в которой есть указатели на элементы из контейнера. Как правило, контейнер или его итераторы реализуют понятие "установить итерацию на начало" и понятие "текущего элемента".

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

class slist_base {
  // ...
  slink* last;  // last->next голова списка
  slink* current;  // текущий элемент
public:
  // ...
  slink* head() { return last?last->next:0; }
  slink* current() { return current; }
  void set_current(slink* p) { current = p; }
  slink* first() { set_current(head()); return current; }
  slink* next();
  slink* prev();
};

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

void f(Islist<name>& ilst)
// медленный поиск имен-дубликатов
{
  list_iter<name> slow(ilst);  // используется итератор
  name* p;
  while (p = slow()) {
   ilst.set_current(p); // рассчитываем на текущий элемент
   name* q;
   while (q = ilst.next())
      if (strcmp(p->string,q->string) == 0)
         cout << "дубликат" << p << '\n';
   }
}

Еще один вид итераторов показан в \S 8.8.

Дарья Федотова
Дарья Федотова
Сергей Березовский
Сергей Березовский

В рамках проф. переподготовки по программе "Программирование"

Есть курсы, которые я уже прошел. Но войдя в курс я вижу, что они не зачтены (Язык Ассемблера и архитектура ЭВМ, Программирование на С++ для профессионалов). Это как?

Роман Островский
Роман Островский
Украина
Оксана Пагина
Оксана Пагина
Россия, Москва