Глава 7 Производные Классы

List Banner Exchange

Не надо размножать объекты без необходимости
- У. Оккам

В этой главе описывается понятие производного класса в С++. Производные классы дают простой, гибкий и эффективный аппарат задания для класса альтернативного интерфейса и определения класса посредством добавления возможностей к уже имеющемус

7.1 Введение

Представим себе процесс написания некоторого универсального средства (например, тип связанный список, таблица имен или планировщик для системы моделирования), которое предназначается для использования многими разными людьми в различных о

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

Написание общецелевых средств - задача непростая, и часто основной акцент в их разработке другой, чем при разработке программ специального назначения. Конечно, нет четкой границы между средствами общего и специального назначения, и к методам и языко

7.2 Производные Классы

Чтобы разделить задачи понимания аппарата языка и методов его применения, знакомство с понятием производных классов делается в три этапа. Вначале с помощью небольших примеров, которые не надо воспринимать как реалистичные, будут описаны с 7.2.1 Построение Производного Класса

Рассмотрим построение программы, которая имеет дело с людьми, служащими в некоторой фирме. Структура данных в этой программе может быть например такой:

  struct employee { // служащий
     char* name;  // имя
     short  age;  // возраст
     short department; // подразделение
     int salary; // жалование
     employee* next; // ...
  };
Список аналогичных служащих будет связываться через поле next. Теперь давайте определим менеджера:
  struct manager { // менеджер
      employee emp;  // запись о менеджере как о служащем
      employee* group; // подчиненные люди
      // ...
  };

Менеджер также является служащим; относящиеся к служащему employee данные хранятся в члене emp объекта manager. Для читающего это человека это, может быть, очевидно, но нет ничего выделяющего член emp для компилятора. Указатель на менеджера

  struct manager : employee {
    employee* group;
    // ...
  };
manager является производным от employee и, обратно, employee есть базовый класс для manager. Класс manager дополнительно к члену group имеет члены класса employee (name, age и т.д.).

Имея определения employee и manager мы можем теперь создать список служащих, некоторые из которых являются менеджерами. Например:

  void f() {
      manager m1,  m2;
      employee e1,  e2;
      employee* elist;
      elist =  &m1; //поместить m1,e1,m2 и e2 в elist
      m1.next = &e1;
      e1.next = &m2;
      m2.next = &e2;
      e2.next = 0;
  }
Поскольку менеджер является служащим, manager* может использоваться как employee*. Однако служащий необязательно является менеджером, поэтому использовать employee* как manager* нельзя.

7.2.2 Функции Члены

Просто структуры данных вроде employee и manager на самом деле не столь интересны и часто не особенно полезны, поэтому рассмотрим, как добавить в них функции. Например:

  class employee { char* name; // ...
  public:
      employee* next; void print(); // ...
  };

  class manager : public employee {
      // ...
  public:
      void print();
      // ...
  };
Надо ответить на некоторые вопросы. Как может функция член производного класса manager использовать члены его базового класса employee? Как члены базового класса employee могут использовать функции члены производного класса manager? Какие ч
  void manager::print() {
      cout << " имя " << name << "\n";
      // ...
  }
Член производного класса может использовать открытое имя из своего базового класса так же, как это могут делать другие члены последнего, то есть без указания объекта. Предполагается, что на объект указывает this, поэтому (корректной) ссылкой на

Это многим покажется удивительным, но представьте себе другой вариант: что функция член могла бы обращаться к закрытым членам своего базового класса. Возможность, позволяющая программисту получать доступ к закрытой части класса просто с помощью вывод

С другой стороны, можно ведь использовать механизм friend, чтобы предоставить такой доступ или отдельным функциям, или всем функциям отдельного класса (как описывается в #5.3). Например:

  class employee { friend void manager::print(); // ...
  };
решило бы проблему с manager::print(), и
  class employee { friend class manager;
      // ...
  };
сделало бы доступным каждый член employee для всех функций класса manager. В частности, это сделает name доступным для manager::print().

Другое, иногда более прозрачное решение для производного класса - использовать только открытые члены его базового класса. Например:

  void manager::print() {
      employee::print();  // печатает информацию о служащем
      // ...              // печатает информацию о менеджере
  }
Заметьте, что надо использовать ::, потому что print() была переопределена в manager. Такое повторное использование имен типично. Неосторожный мог бы написать так:
  void manager::print() {
      print(); // печатает информацию о служащем
      // ... // печатает информацию о менеджере
  }
и обнаружить, что программа после вызова manager::print() неожиданно попадает в последовательность рекурсивных вызовов.

7.2.3 Видимость

Класс employee стал открытым (public) базовым классом класса manager в результате описания:

  class manager : public employee {
      // ...
  };
Это означает, что открытый член класса employee является также и открытым членом класса manager. Например:
  void clear(manager* p) {
      p->next = 0;
  }
будет компилироваться, так как next - открытый член и employee и manager'а. Альтернатива - можно определить закрытый (private) класс, просто опустив в описании класса слово public:
  class manager : employee {
      // ...
  };
Это означает, что открытый член класса employee является закрытым членом класса manager. То есть, функции члены класса manager могут как и раньше использовать открытые члены класса employee, но для пользователей класса manager эти члены недоступны. В

Поскольку, как оказывается, описание открытых базовых классов встречается чаще описания закрытых, жалко, что описание открытого базового класса длиннее описания закрытого. Это, кроме того, служит источником запутывающих ошибок у начинающих.

Когда описывается производная struct, ее базовый класс по умолчанию является public базовым классом. То есть,

  struct D : B { ...
означает
  class D : public B { public: ...
Отсюда следует, что если вы не сочли полезным то сокрытие данных, которое дают class, public и friend, вы можете просто не использовать эти ключевые слова и придерживаться struct. Такие средства языка, как функции члены, конструкторы и перегрузка о

Можно также объявить некоторые, но не все, открытые члены базового класса открытыми членами производного класса. Например:

  class manager : employee {
   // ...
  public:
   // ...
   employee::name;
   employee::department;
  };
Запись
  имя_класса :: имя_члена;
не вводит новый член, а просто делает открытый член базового класса открытым для производного класса. Теперь name и department могут использоваться для manager'а, а salary и age - нет. Естественно, сделать закрытый член базового класса открытым чл

Подытоживая, можно сказать, что вместе с предоставлением средств дополнительно к имеющимся в базовом классе, производный класс можно использовать для того, чтобы сделать средства (имена) недоступными для пользователя. Другими словами, с помощью пр

7.2.4 Указатели

Если производный класс derived имеет открытый базовый класс base, то указатель на derived можно присваивать переменной типа указатель на base не используя явное преобразование типа. Обратное преобразование, указателя на base в указатель на derived, до

  class base { /* ... */ };
  class derived : public base { /* ... */};

  derived m;
  base* pb = &m; // неявное преобразование derived*
  pd = pb;// ошибка: base* не является derived*
  pd = (derived*)pb; //явное преобразование
Иначе говоря, объект производного класса при работе с ним через указатель и можно рассматривать как объект его базового класса. Обратное неверно.

Будь base закрытым базовым классом класса derived, неявное преобразование derived* в base* не делалось бы. Неявное преобразование не может в этом случае быть выполнено, потому что к открытому члену класса base можно обращаться через указатель на bas

  class base { int m1;
  public:
      int m2; // m2 - открытый член base
  };

  class derived : base {
      // m2 - НЕ открытый член derived
  };

  derived d;
  d.m2 = 2; // ошибка: m2 из закрытой части класса base*
  pb = &d;  // ошибка:  (закрытый base)
  pb->m2 = 2;  //  ok
  pb = (base*)&d; // ok: явное преобразование
  pb->m2 = 2; // ok
Помимо всего прочего, этот пример показывает, что используя явное приведение к типу можно сломать правила защиты. Ясно, делать это не рекомендуется, и это приносит программисту заслуженную "награду". К несчастью , недисциплинированное использование явно

7.2.5 Иерархия Типов

Производный класс сам может быть базовым классом. Например:

  class employee { ... };
  class secretary : employee { ... };
  class manager :  employee { ...  };
  class temporary : employee { ... };
  class consultant :  temporary { ... };
  class director : manager { ... };
  class vice_president : manager { ... };
  class president : vice_president { ... };
Такое множество родственных классов принято называть иерархией классов. Поскольку можно выводить класс только из одного базового класса, такая иерархия является деревом и не может быть графом более общей структуры. Например:
  class temporary { ...  };
  class employee { ... };
  class secretary : employee { ... };

 // не С++:
  class temporary_secretary :  temporary : secretary { ... };
  class consultant : temporary : employee { ... };
И этот факт вызывает сожаление, потому что направленный ациклический граф производных классов был бы очень полезен. Такие структуры описать нельзя, но можно смоделировать с помощью членов соответствующих типов. Например:
  class temporary { ...  };
  class employee { ... };
  class secretary : employee { ... };

  // Альтернатива:
  class temporary_secretary :  secretary { temporary temp;  ...  };
  class consultant : employee { temporary temp; ... };
Это выглядит неэлегантно и страдает как раз от тех проблем, для преодоления которых были изобретены производные классы. Например, поскольку consultant не является производным от temporary, consultant'а нельзя помещать с список временных служ

7.2.6 Конструкторы и Деструкторы

Для некоторых производных классов нужны конструкторы. Если у базового класса есть конструктор, он должен вызываться, и если для этого конструктора нужны параметры, их надо предоставить. Например:

  class base {
      // ...
  public:
      base(char* n, short t);
      ~base();
  };

  class derived : public base {
    base m;
  public:
    derived(char* n);
    ~derived();
  };
Параметры конструктора базового класса специфицируются в определении конструктора производного класса. В этом смысле базовый класс работает точно также, как неименованный член производного класса (см. #5.5.4). Например:
  derived::derived(char* n) : (n,10), m("member",123) {
      // ...
  }
Объекты класса конструируются снизу вверх: сначала базовый, потом члены, а потом сам производный класс. Уничтожаются они в обратном порядке: сначала сам производный класс, потом члены а потом базовый.

7.2.7 Поля Типа

Чтобы использовать производные классы не просто как удобную сокращенную запись в описаниях, надо разрешить следующую проблему: Если задан указатель типа base*, какому производному типу в действительности принадлежит указываемый объект? Е

[1] Обеспечить, чтобы всегда указывались только объекты одного типа (#7.3.3),

[2] Поместить в базовый класс поле типа, которое смогут просматривать функции и

[3] Использовать виртуальные функции (#7.2.8). Обыкновенно указатели на базовые классы используются при разработке контейнерных (или вмещающих) классов: множество, вектор, список и т.п. В этом случае решение 1 дает однородные списки, то есть списки объектов одного типа. Решения 2 и 3 можно использовать для построения неоднородных списков, то есть списк

Давайте сначала исследуем простое решение с помощью поля типа, то есть решение 2. Пример со служащими и менеджерами можно было бы переопределить так:

  enum empl_type { M, E };

  struct employee {
    empl_type type;
    employee*  next;
    char*  name;
    short department;
    // ...
  };

  struct manager : employee {
    employee* group;
    short level; // уровень
  };
Имея это, мы можем теперь написать функцию, которая печатает информацию о каждом служащем:
  void print_employee(employee* e) {
  switch (e->type) {
   case E: cout << e->name << "\t" << e->department << "\n";
        // ...
        break;
   case M: cout << e->name << "\t" << e->department << "\n";
        // ...
        manager* p = (manager*)e;
        cout << " уровень " << p->level << "\n";
        // ...
        break;
    }
  }
и воспользоваться ею для того, чтобы напечатать список служащих:
  void f() {
      for (; ll; ll=ll->next) print_employee(ll);
  }
 
Это прекрасно работает,особенно в небольшой программе, написанной одним человеком, но имеет тот коренной недостаток, что неконтролируемым компилятором образом зависит от того, как программист работает с типами. В больших программах это обычно приводи
  void print_employee(employee* e) {
      cout << e->name << "\t" << e->department << "\n";
      // ...
      if (e->type == M) { manager* p = (manager*)e;
       cout << " уровень " << p->level << "\n";
      // ...
   }
  }
Отыскание всех таких операторов if, скрытых внутри большой функции, которая работает с большим числом производных классов, может оказаться сложной задачей, и даже когда все они найдены, бывает нелегко понять, что же в них делается.

7.2.8 Виртуальные Функции

Виртуальные функции преодолевают сложности решения с помощью полей типа, позволяя программисту описывать в базовом классе функции, которые можно переопределять в любом производном классе. Компилятор и загрузчик обеспечивают правильное соответствие м

  struct employee {
    employee* next;
    char* name;
    short department;
    // ...
    virtual void print();
  };
Ключевое слово virtual указывает, что могут быть различные варианты функции print() для разных производных классов, и что поиск среди них подходящей для каждого вызова print() является задачей компилятора. Тип функции описывается в базовом классе и н
  void employee::print() {
      cout << e->name << "\t" << e->department << "\n";
      // ...
  }
Виртуальная функция может, таким образом, использоваться даже в том случае, когда нет производных классов от ее класса, и в производном классе, в котором не нужен специальный вариант виртуальной функции, ее задавать не обязательно. Просто при выводе кла
  struct manager :  employee {
    employee* group;
    short level;
    // ...
    void print();
  };

  void manager::print() {
    employee::print();
    cout  << "\tуровень" << level << "\n";
    //...
  }
Функция print_employee() теперь не нужна, поскольку ее место заняли функции члены print(), и теперь со списком служащих можно работать так:
  void f(employee* ll) {
      for (; ll; ll=ll->next) ll->print();
  }
Каждый служащий будет печататься в соответствии с его типом. Например:
  main() {
   employee e;
   e.name = "Дж.Браун";
   e.department = 1234;
   e.next = 0;

   manager m;
   m.name = "Дж.Смит";
   e.department = 1234;
   m.level = 2;
   m.next = &e;
   f(&m);
  }
выдаст
  Дж.Смит 1234 уровень 2
  Дж.Браун 1234
Заметьте, что это будет работать даже в том случае, если f() была написана и откомпилирована еще до того, как производный класс manager был задуман! Очевидно, при реализации этого в каждом объекте класса employee сохраняется некоторая информация о т

Вызов функции с помощью операции разрешения области видимости ::, как это делается в manager::print(), гарантирует, что механизм виртуальных функций применяться не будет. Иначе manager::print() подвергалось бы бесконечной рекурсии. Применение уточ

7.3 Альтернативные Интерфейсы

После того, как описаны средства языка, которые относятся к производным классам, обсуждение снова может вернуться к стоящим задачам. В классах, которые описываются в этом разделе, основополагающая идея состоит в том, что они однажды написаны, а

7.3.1 Интерфейс

Рассмотрим такое написание класса slist для однократно связанного списка, с помощью которого можно создавать как однородные, так и неоднородные списки объектов тех типов, которые еще должны быть определены. Сначала мы определим тип ent:

  typedef void* ent;
Точная сущность типа ent несущественна, но нужно, чтобы в нем мог храниться указатель. Тогда мы определим тип slink:
  class slink {
    friend class slist;
    friend class slist_iterator;
    slink* next;
    ent e;
    slink(ent a, slink* p) { e=a; next=p;}
  };
В одном звене может храниться один ent, и с помощью него реализуется класс slist:
  class slist {
   friend class slist_iterator;
   slink* last; // last->next - голова списка public:
   int insert(ent a); // добавить в голову списка
   int append(ent a);  // добавить в хвост списка
   ent get(); // вернуться и убрать голову списка
   void clear(); // убрать все звенья
   slist() {last=0;}
   slist(ent  a){
     last=new;
     slink(a,0);
     last->next=last;
   }
   ~slist() { clear(); }
  };
Хотя список очевидным образом реализуется как связанный список, реализацию можно изменить так, чтобы использовался вектор из ent'ов, не повлияв при этом на пользователей. То есть, применение slink'ов никак не видно в описаниях открытых функци

7.3.2 Реализация

Реализующие slist функции в основном просты. Единственная настоящая сложность - что делать в случае ошибки, если, например, пользователь попытается get() что-нибудь из пустого списка. Мы обсудим это в #7.3.4. Здесь приводятся определения членов sl

  int slist::insert(ent a) {
    if (last) last->next = new slink(a,last->next);
     else { last = new slink(a,0); last->next = last;}
      return 0;
  }

  int slist::append(ent a) {
      if (last) last = last->next = new slink(a,last->next);
      else { last = new slink(a,0); last->next = last;}
      return 0;
  }

  ent slist::get() {
      if (last == 0) slist_handler("get fromempty list");
                                 // взять из пустого списка
      slink* f = last->next; ent r f->e; if (f == last)
          last = 0; else
          last->next = f->next; delete f; return f;
  }
Обратите внимание, как вызывается slist_handler (его описание можно найти в #7.3.4). Этот указатель на имя функции используется точно так же, как если бы он был именем функции. Это является краткой формой более явной записи вызова:
  (*slist_handler)("get fromempty list");
И slist::clear(), наконец, удаляет из списка все элементы:
  void slist::clear() {
      slink* l = last; if (l == 0) return;
      do {
       slink* ll = l;
       l = l->next;
       delete ll;
      } while (l!=last);
  }
Класс slist не обеспечивает способа заглянуть в список, но только средства для вставления и удаления элементов. Однако оба класса, и slist, и slink, описывают класс slist_iterator как друга, поэтому мы можем описать подходящий итератор. Вот один, на
  class slist_iterator { slink* ce; slist* cs;
  public:
      slist_iterator(slist& s) { cs = &s; ce = cs->last; }

      ent operator()() {
          // для индикации конца итерации возвращает 0
          // для всех типов не идеален, хорош для указателей
       ent ret = ce ?  (ce=ce->next)->e : 0; if (ce == cs->last)
       ce= 0; return ret;
      }
  };

7.3.3 Как Этим Пользоваться

Фактически класс slist в написанном виде бесполезен. В конечном счете, зачем можно использовать список указателей void*? Штука в том, чтобы вывести класс из slist и получить список тех объектов, которые представляют интерес в конкретной прог

  struct name { char* string; // ...
  };
В список будут помещаться указатели на имена, а не сами объекты имена. Это позволяет использовать небольшое информационное поле e slist'а, и дает возможность имени находиться одновременно более чем в одном списке. Вот определение класса nlist, кот
  #include "slist.h"
  #include "name.h"

  struct nlist : slist
  {
   void insert(name* a) { slist::insert(a); }
   void append(name* a) { slist::append(a);  }
   name* get() {}
   nlist(name* a) : (a) {}
  };
Функции нового класса или наследуются от slist непосредственно, или ничего не делают кроме преобразования типа. Класс nlist - это ничто иное, как альтернативный интерфейс класса slist. Так как на самом деле тип ent есть void*, нет необходимости яв

Списки имен можно использовать в классе, который представляет определение класса:

  struct classdef{
    nlist friends;
    nlist constructors;
    nlist destructors;
    nlist members;
    nlist operators;
    nlist virtuals;
    // ...
    void add_name(name*);
    classdef();
    ~classdef();
  };
и имена могут добавляться к этим спискам приблизительно так:
  void classdef::add_name(name* n) {
   if (n->is_friend())
     { if (find(&friends,n))
        error("friend redeclared"); // friend переописан
        else
       if (find(&members,n))  error("friend  redeclared as member");
                  // friend переописан как member
       else friends.append(n);
   }
    if (n->is_operator()) operators.append(n);
      // ...
  }
где is_operator() и is_friend() являются функциями членами класса name. Функцию find() можно написать так:
  int find(nlist* ll, name* n) {
      slist_iterator ff(*(slist*)ll);
      ent p;
      while ( p=ff() )
      if (p==n) return 1;
      return 0;
  }
Здесь применяется явное преобразование типа, чтобы применить slist_iterator к nlist. Более хорошее решение, сделать итератор для nlist'ов, приведено в #7.3.5. Печатать nlist может, например, такая функция:
  void print_list(nlist* ll, char* list_name) {
   slist_iterator count(*(slist*)ll);
   name* p;
   int n = 0;
   while (count()) n++;
   cout << list_name << "\n" << n << "members\n";
   slist_iterator print(*(slist*)ll);
    while  (p=(name*)print()) cout << p->string << "\n";
  }

7.3.4 Обработка Ошибок

Есть четыре подхода к проблеме, что же делать, когда во время выполнения универсальное средство вроде slist сталкивается с ошибкой (в С++ нет никаких специальных средств языка для обработки ошибок):

[1] Возвращать недопустимое значение и позволить пользователю его проверять

[2] Возвращать дополнительное значение состояния и разрешить пользователю проверять его

[3] Вызывать функцию ошибок, заданную как часть класса slist или

[4] Вызывать функцию ошибок, которую предположительно предоставляет пользователь.

Для небольшой программы, написанной ее единственным пользователем, нет фактически никаких особенных причин предпочесть одно из этих решений другим. Для средства общего назначения ситуация совершенно иная.

Первый подход, возвращать недопустимое значение, неосуществим. Нет совершенно никакого способа узнать, что некоторое конкретное значение будет недопустимым во всех применениях slist.

Второй подход, возвращать значение состояния, можно использовать в некоторых классах (один из вариантов этого плана применяется в стандартных потоках ввода/вывода istream и ostream; как - объясняется в #8.4.2). Здесь, однако, имеется серьезная

Третьему подходу, предоставлять функцию ошибок, недостает гибкости. Тот, кто реализует универсальное средство, не может узнать, как пользователи захотят, чтобы обрабатывались ошибки. Например, пользователь может предпочитать сообщения на д

Четвертый подход, позволить пользователю задавать функцию ошибок, имеет некоторую привлекательность при условии, что разработчик предоставляет класс в виде библиотеки (#4.5), в которой содержатся стандартные функции обработки ошибок.

Решения 3 и 4 можно сделать более гибкими (и по сути эквивалентными), задав указатель на функцию, а не саму функцию. Это позволит разработчику такого средства, как slist, предоставить функцию ошибок, действующую по умолчанию, и при этом программи

  typedef void (*PFC)(char*);  // указатель на тип  функция
  extern PFC slist_handler;
  extern PFC set_slist_handler(PFC);
Функция set_slist_hanlder() позволяет пользователю заменить стандартную функцию. Общепринятая реализация предоставляет действующую по умолчанию функцию обработки ошибок, которая сначала пишет сообщение об ошибке в cerr, после чего завершает программу
  #include "slist.h"
  #include &tl;stream.h>

  void default_error(char* s) {
      cerr << s << "\n"; exit(1);
  }
Она описывает также указатель на функцию ошибок и, для удобства записи, функцию для ее установки:
  PFC slist_handler = default_error;
  PFC set_slist_handler(PFC handler); {
      PFC rr = slist_handler; slist_handler = handler; return rr;
  }
Обратите внимание, как set_slist_hanlder() возвращает предыдущий slist_hanlder(). Это делает удобным установку и переустановку обработчиков ошибок на манер стека. В основном это может быть полезным в больших программах, в которых slist может исп
  { PFC old = set_slist_handler(my_handler);
      // код, в котором в случае ошибок в slist
      // будет использоваться мой обработчик my_handler
      set_slist_handler(old); // восстановление
  }
Чтобы сделать управление более изящным, slist_hanlder мог бы быть сделан членом класса slist, что позволило бы различным спискам иметь одновременно разные обработчики.

7.3.5 Обобщенные Классы

Очевидно, можно было бы определить списки других типов (classdef*, int, char* и т.д.) точно так же, как был определен класс nlist: простым выводом из класса slist. Процесс определения таких новых типов утомителен (и потому чреват оши

Вот пример того, как обобщенный (generic) класс slist, названный gslist, может быть задан как макрос. Сначала для написания такого рода макросов включаются некоторые инструменты из <generic.h>:

  #include "slist.h"

  #ifndef GENERICH
  #include <generic.h>
  #endif
Обратите внимание на использование #ifndef для того, чтобы гарантировать, что <generic.h> в одной компиляции не будет включен дважды. GENERICH определен в <generic.h>.

После этого с помощью name2(), макроса из <generic.h> для конкатенации имен, определяются имена новых обобщенных классов:

  #define gslist(type) name2(type,gslist)
  #define gslist_iterator(type) name2(type,gslist_iterator)
И, наконец, можно написать классы gslist(тип) и gslist_iterator(тип):
  #define gslistdeclare(type) \
  struct gslist(type) : slist
    { \ int insert(type a)
      \ { return slist::insert( ent(a) );  }
      \ int append(type a)
      \ { return slist::append( ent(a) ); }
      \ type get() { return type( slist::get()); }
      \ gslist(type)(){ }
      \ gslist(type)(type a) :(ent(a)) { }
      \ ~gslist(type)() {clear(); }
      \
  };\
    \
  struct gslist_iterator(type) : slist_iterator {
   \ gslist_iterator(type)(gslist(type)& a)
   \ : ( (slist&)s ) {}
   \ type operator()()
   \ {return type( slist_iterator::operator()() ); }
   \ }
\ на конце строк указывает , что следующая строка является частью определяемого макроса. С помощью этого макроса список указателей на имя, аналогичный использованному раньше классу nlist, можно определить так:
  #include "name.h"

  typedef name*  Pname;
  declare(gslist,Pname);  // описывает класс
  gslist(Pname)

  gslist(Pname) nl; // описывает один gslist(Pname)
Макрос declare (описать) определен в <generic.h>. Он конкатинирует свои параметры и вызывает макрос с этим именем, в данном случае gslistdeclare, описанный выше. Параметр имя типа для declare должен быть простым именем. Используемый метод макроопре

Использование вывода класса гарантирует, что все частные случаи обобщенного класса разделяют код. Этот метод можно применять только для создания классов объектов того же размера или меньше, чем базовый класс, который используется в макросе. gsli

7.3.6 Ограниченные Интерфейсы

Класс slist - довольно общего характера. Иногда подобная общность не требуется или даже нежелательна. Ограниченные виды списков, такие как стеки и очереди, даже более обычны, чем сам обобщенный список. Такие структуры данных можно задать, не

  #include "slist.h"

  class iqueue : slist {
    //предполагается sizeof(int)<=sizeof(void*)
  public:
      void put(int a) {
        slist::append((void*)a);}
      int det(){ return int(slist::get());}
      iqueue() {}
  };
При таком выводе осуществляются два логически разделенных действия: понятие списка ограничивается понятием очереди (сводится к нему), и задается тип int, чтобы свести понятие очереди к типу данных очередь целых, iqueue. Эти два действия мо
  #include "slist.h"

  class stack : slist { public:
      slist::insert;
      slist::get;
      stack() {}
      stack(ent a) : (a) {}
  };
который потом используется для создания типа "стек указателей на символы":
  #include "stack.h"

  class cp : stack { public:
   void push(char* a){ slist::insert(a); }
   char* pop() { return (char*)slist::get(); }
   nlist() {}
  };

7.4 Добавление к Классу

В предыдущих примерах производный класс ничего не добавлял к базовому классу. Для производного класса функции определялись только чтобы обеспечить преобразование типа. Каждый производный класс просто задавал альтернативный интерфейс к общему

Для производного класса можно определить данные и функции дополнительно к тем, которые наследуются из его базового класса. Это дает альтернативную стратегию того, как обеспечить средства связанного списка. Заметьте, когда в тот slist, который опред

  struct olink { olink* next;
  };
Класс olist очень напоминает класс slist. Отличие состоит в том, что пользователь класса olist манипулирует объектами класса olink непосредственно:
  class olist { olink* last;
  public:
      void insert(olink* p);
      void append(olink* p);
      olink* get();
      // ...
  };
Мы можем вывести из класса olink класс name:
  class name : public olink {
      // ...
  };
Теперь легко сделать список, который можно использовать без накладных расходов времени на размещение или памяти.

Объекты, помещаемые в olist, теряют свой тип. Это означает, что компилятор знает только то, что они olink'и. Правильный тип можно восстановить с помощью явного преобразования типа объектов, вынутых из olist. Например:

  void f() {
      olist ll;  name nn;  ll.insert(&nn); // тип &nn потерян name*
      pn = (name*)ll.get(); // и восстановлен
  }
Другой способ: тип можно восстановить, выводя еще один класс из olist для обработки преобразования типа:
  class onlist : public olist {
      // ...
      name* get() { return (name*)olist::get(); }
  };
Имя name может одновременно находиться только в одном olist. Для имен это, может быть, и не подходит, но в классах, для которых это подойдет полностью, недостатка нет. Например, класс фигур shape использует для поддержки списка всех фигур именно эт

7.5 Неоднородные Списки

Предыдущие списки были однородными. То есть, в список помещались только объекты одного типа. Это обеспечивалось аппаратом производных классов. Списки не обязательно должны быть однородными. Список, заданный в виде указателей на класс, может содержат

7.6 Законченная Программа

Разберем процесс написания программы для рисования на экране геометрических фигур. Она естественным образом разделяется на три части:

[1] Администратор экрана: подпрограммы низкого уровня и структуры данных, определяющие экран; он ведает только точками и прямыми линиями,

[2] Библиотека фигур: набор определений основных фигур вроде прямоугольника и круга и стандартные программы для работы с ними и

[3] Прикладная программа: множество определений, специализированных для данного приложения, и код, который их использует.

Эти три части скорее всего будут писать разные люди (в разных организациях и в разное время). При этом части будут скорее всего писать именно в указанном порядке с тем осложняющим обстоятельством, что у разработчиков нижнего уровня не будет точ

7.6.1 Администратор Экрана

Вначале было намерение написать администратор экрана на C (а не на С++), чтобы подчеркнуть разделение уровней реализации. Это оказалось слишком утомительным, поэтому пришлось пойти на компромисс: используется стиль C (нет функций членов, виртуаль

Экран представляется как двумерный массив символов, работу с которым осуществляют функции put_point() и put_line(), использующие при обращении с экраном структуру point:

  // файл screen.h

  const XMAX=40, YMAX=24;

  struct point {
   int x,y;  point() {}
   point(int a,  int b){  x=a; y=b; }
  };

  overload put_point;
  extern void put_point(int a,  int b);
  inline void put_point(point p) { put_point(p.x,p.y); }

  overload put_line;
  extern  void put_line(int,  int,  int,  int);
  inline void put_line(point a, point b)
      { put_line(a.x,a.y,b.x,b.y); }

  extern void screen_init();
  extern void screen_refresh();
  extern void screen_clear();

  #include <stream.h>
Перед первым использованием функции put экран надо инициализировать с помощью screen_init(), а изменения в структуре данных экрана отображаются на экране только после вызова screen_refresh(). Как увидит пользователь, это "обновление" ("refresh") о
  #include "screen.h"
  #include <stream.h>

  enum color { black='*', white=' ' };

  char screen[XMAX][YNAX];

  void screen_init() {
   for (int y=0; y<YMAX; y++)
    for (int x=0; x<XMAX; x++)
      screen[x][y] = white;
  }
Точки печатаются, только если они есть на экране:
  inline int on_screen(int a, int b) {
      return 0<=a && a<XMAX && 0<=b && b<YMAX;
  }

  void put_point(int a, int b) {
      if (on_screen(a,b)) screen[a][b] = black;
  }
Для рисования линий используется функция put_line():
  void put_line(int x0, int y0, int x1, int y1)
  /*
      Строит линию от (x0,y0) до (x1,y1).
      Строится линия b(x-x0) + a(y-y0) = 0.
      Минимизирует abs(eps), где eps  =  2*(b(x-x0)+ a(y-y0)).
      См. Newman and Sproull: ``Principles of Interactive
      Computer Graphics'' McGraw-Hill, New York, 1979, pp 33-44.
  */
  { register dx = 1;
    int a = x1 - x0;
    if (a < 0) dx = -1, a = -a;
    register dy = 1;
    int b = y1 - y0;
    if (b < 0) dy = -1, b = -b;
    int two_a = 2*a;
    int two_b = 2*b;
    int xcrit = -b  +  two_a;
    register eps = 0; for (;;) {
      put_point(x0,y0);
      if(x0==x1 && y0==y1) break;
      if(eps<=xcrit) x0 += dx, eps += two_b;
      if(eps>=a || a<=b) y0+=dy, eps -= two_a;
      }
  }
Предоставляются функции для очистки экрана и его обновления:
  void screen_clear() { screen_init(); } // очистка

  void screen_refresh() // обновление
  {
    for (int y=YMAX-1;0<=y; y--) { // сверху вниз
      for (int x=0;x<XMAX; x++) // слева направо
        cout.put(screen[x][y]); cout.put('\n');
      }
  }
Функция ostream::put() применяется для печати символов как символов; ostream::operator<<() печатает символы как малые целые. Пока что вы может представлять себе, что эти определения доступны только в откомпилированном виде, который вы изменить не может

7.6.2 Библиотека Фигур

Нам нужно определить общее понятие фигуры (shape). Это надо сделать таким образом, чтобы оно использовалось (как базовый класс) всеми конкретными фигурами (например, кругами и квадратами), и так, чтобы любой фигурой можно было манипулировать искл

  struct shape { shape() { shape_list.append(this); }

   virtual point north(){return point(0,0);} // север
   virtual point south(){return point(0,0);} // юг
   virtual point east(){return  point(0,0);} // восток
   virtual point neast(){return point(0,0);} // северо-восток
   virtual point seast(){return point(0,0);} // юго-восток
   virtual void draw(){};// нарисовать
   virtual void move(int,int) {}; // переместить
  };
Идея состоит в том, что расположение фигуры задается с помощью move(), и фигура помещается на экран с помощью draw(). Фигуры можно располагать относительно друг друга, используя понятие точки соприкосновения, и эти точки перечисляются после точек ком
  typedef shape* sp; declare(gslist,sp);

  typedef gslist(sp) shape_lst;
  typedef gslist_iterator(sp) sp_iterator;
поэтому shape_list можно описать так:
  shape_lst shape_list;
Линию можно построить либо по двум точкам, либо по точке и целому. В последнем случае создается горизонтальная линия, длину которой определяет целое. Знак целого указывает, каким концом является точка: левым или правым. Вот определение:
 class line : public shape {
  /*
    линия из 'w' в 'e' north() определяется как ``выше  центра  и
    на север как до самой северной точки''
  */
  point w,e; public:
  point north() { return point((w.x+e.x)/2,e.y<w.y?w.y:e.y); }
  point south() { return point((w.x+e.x)/2,e.y<w.y?e.y:w.y); }
  void move(int a, int b){ w.x+=a; w.y+=b; e.x+=a; e.x+= b; }
  void draw() { put_line(w,e); }
  line(point a, point b) { w = a; e = b; }
  line(point a, int l) { w = point(a.x+l-1,a.y); e = a;}
 };
Аналогично определяется прямоугольник rectangle:
  class rectangle : public shape {
  /*
   nw ---- n ---- ne
   |               |
   |               |
   w       c       e
   |               |
   |               |
   sw ---- s ---- se
  */
   point sw,ne; public:
   point north(){return point((sw.x+ne.x)/2,ne.y);}
   point south(){return point((sw.x+ne.x)/2,sw.y);}
   point neast(){return ne;}
   point swest(){return sw;}
   void move (int a,int b)
    { sw.x+=a; sw.y+=b; ne.x+=a; ne.y+=b;}
   void draw();
   rectangle(point, point);
  };
Прямоугольник строится по двум точкам. Код усложняется из-за необходимости выяснять относительное положение этих точек:
 rectangle::rectangle(point a, point b); {
  if (a.x <= b.x) { { sw = a; ne = b;}
    else { sw = point(a.x,b.y); ne = point(b.x,a.y);
    }
  }
  else { if (a.y <= b.y) {
   sw = point(b.x,a.y); ne = point(a.x,b.y);
      }
   else { sw = b; ne = a;
   }
  }
 }
Чтобы построить прямоугольник, строятся четыре его стороны:
 void rectangle::draw(); {
  point nw(sw.x,ne.y);
  point se(ne.x,sw.y);
  put_line(nw,ne);
  put_line(ne,se);
  put_line(se,sw);
  put_line(sw,nw);
 }
Помимо определений фигур в библиотеке фигур содержатся функции для работы с ними. Например:
  void shape_refresh();  // рисует все фигуры
  void stack(shape* p, shape* q); // ставит p на верх q
Чтобы справиться с нашим наивным экраном, нужна обновляющая функция. Она просто рисует все фигуры заново. Обратите внимание, что она совершенно не представляет, какие фигуры рисует:
  void shape_refresh() {
      screen_clear();
      sl_iterator next(shape_list);
      shape* p;
      while ( p=next() ) p->draw(); screen_refresh();
  }
И вот, наконец, настоящая сервисная функция (утилита). Она кладет одну фигуру на верх другой, задавая, что south() одной должен быть сразу над north() другой:
  void stack(shape* q, shape* p) // ставит p на верх q
  {
    point n = p->north();
    point s = q->south();
    q->move(n.x-s.x,n.y-s.y+1);
  }
Теперь представим себе, что эта библиотека считается собственностью некой компании, которая продает программное обеспечение, и что они продают вам только заголовочный файл, содержащий определения фигур, и откомпилированный вариант определений функц

7.6.3 Прикладная Программа

Прикладная программа чрезвычайно проста. Определяется новая фигура myshape (на печати она немного похожа на рожицу), а потом пишется главная программа, которая надевает на нее шляпу. Вначале описание myshape:

  #include "shape.h"

  class myshape :  public rectangle {
    line* l_eye; // левый  глаз
    line* r_eye; // правый глаз
    line* mouth; // рот
  public:
    myshape(point, point);
    void draw();
    void move(int, int);
  };
Глаза и рот - отдельные и независимые объекты, которые создает конструктор myshape:
  myshape::myshape(point a, point b) : (a,b) {
    int ll = neast().x-swest().x+1;
    int hh = neast().y-swest().y+1;
    l_eye = new line(point(swest().x+2,swest().y+hh*3/4),2);
    r_eye = new line(point(swest().x+ll-4,swest().y+hh*3/4),2);
    mouth = new line(point(swest().x+2,swest().y+hh/4),ll-4);
  }
Объекты глаза и рот порознь рисуются заново функцией shape_refresh(), и в принципе могут обрабатываться независимо из объекта myshape, которому они принадлежат. Это один способ определять средства для иерархически построенных объектов вр
  void myshape::draw() {
   rectangle::draw();
   put_point(point((swest().x+neast().x)/2,(swest().y+neast().y)/2));
  }
myshape передвигается посредством перемещения базового прямоугольника rectangle и вторичных объектов l_eye, r_eye и mouth (левого глаза, правого глаза и рта):
  void myshape::move() {
   rectangle::move();
   l_eye->move(a,b);
   r_eye->move(a,b);
   mouth->move(a,b);
  }
Мы можем, наконец, построить несколько фигур и немного их подвигать:
  main() {
   shape* p1 = new rectangle(point(0,0),point(10,10));
   shape* p2 = new line(point(0,15),17);
   shape* p3 = new myshape(point(15,10),point(27,18));
   shape_refresh();
   p3->move(-10,-10);
   stack(p2,p3);
   stack(p1,p2);
   shape_refresh();
   return 0;
  }
Еще раз обратите внимание, как функции вроде shape_refresh() и stack() манипулируют объектами типов, определяемых гораздо позже, чем были написаны (и, может быть, откомпилированы) сами эти функции.
 ***********
 *         *
 *         *
 *         *
 *         *
 *         *
 *         *
 *         *
 *         *
 *         *
 ***********
 *****************
 *************
 *           *
 *   ** **   *
 *           *
 *     *     *
 *           *
 * ********* *
 *           *
 *************

7.7 Свободная Память

Если вы пользовались классом slist, вы могли обнаружить, что ваша программа тратит на заметное время на размещение и освобождение объектов класса slink. Класс slink - это превосходный пример класса, который может значительно выиграть от тог

Если производный класс осуществляет присваивание указателю this, то конструктор его базового класса будет вызываться только после этого присваивания, и значение указателя this в конструкторе базового класса будет тем, которое присвоено конструкто

  #include <stream.h>

  struct base { base(); };
  struct derived : base { derived(); }
  base::base() {
    cout << "\tbase 1: this=" << int(this) << "\n";
    if (this == 0) this = (base*)27;
    cout << "\tbase 2:  this=" << int(this) << "\n";
  }
  derived::derived() {
      cout <<  "\tderived  1:  this=" << int(this) << "\n";
      this = (this == 0) ? (derived*)43 :  this;
      cout <<  "\tderived  2: this=" << int(this) << "\n";
  }

  main() {
   cout << "base b;\n";
   base b;
   cout << "new base  b;\n";
   new base;
   cout << "derived d;\n";
   derived d;
   cout << "new derived d;\n";
   new derived;
   cout << "at the end\n";

  }
порождает вывод
  base b; base 1: this=2147478307 base 2: this=2147478307
  new base; base 1: this=0 base 2: this=27
  derived d;  derived 1:  this=2147478306
  base 1: this=2147478306
  base 2: this=2147478306
  derived 1: this=2147478306
  new derived;
  derived 1: this=0
  base 1: this=43
  base 2: this=43
  derived 1: this=43
  at the end
Если деструктор производного класса осуществляет присваивание указателю this, то будет присвоено то значение, которое встретил деструктор его базового класса. Когда кто-либо делает в конструкторе присваивание указателю this, важно, чтобы присваива
* К сожалению, об этом присваивании легко забыть. Например, в первом издании этой книги (английском - перев.) вторая строка конструктор derived::derived() читалась так:
  if (this == 0) this = (derived*)43;
И следовательно, для d конструктор базового класса base::base() не вызывался. Программа была допустимой и корректно выполнялась, но, очевидно, делала не то, что подразумевал автор. (прим. автора)

7.8 Упражнения

  1. (*1) Определите
      class base { public:
        virtual void iam() { cout << "base\n"; }
      };
    
    Выведите из base два класса и для каждого определите iam() ("я есть"), которая выводит имя класса на печать. Создайте объекты этих классов и вызовите для них iam(). Присвойте адреса объектов производных классов указателям base* и вызовите iam() через эт
  2. (*2) Реализуйте примитивы экрана (#7.6.1) подходящим для вашей системы образом.
  3. (*2) Определите класс triangle (треугольник) и класс circle (круг).
  4. (*2) Определите функцию, которая рисует линию, соединяющую две фигуры, отыскивая две ближайшие "точки соприкосновения" и соединяя их.
  5. (*2) Модифицируйте пример с фигурами так, чтобы line была rectangle и наоборот.
  6. (*2) Придумайте и реализуйте дважды связанный список, который можно использовать без итератора.
  7. (*2) Придумайте и реализуйте дважды связанный список, которым можно пользоваться только посредством итератора. Итератор должен иметь действия для движения вперед и назад, действия для вставления и удаления элементов списка, и способ доступа к теку
  8. (*2) Постройте обобщенный вариант дважды связанного списка.
  9. (*4) Сделайте список, в котором вставляются и удаляются сами объекты (а не просто указатели на объекты). Проделайте это для класса X, для которого определены X::X(X&), X::~X() X::operator=(X&).
  10. 10. (*5) Придумайте и реализуйте библиотеку для написания моделей, управляемых прерываниями. Подсказка: <task.h>. Только это - старая программа, а вы могли бы написать лучше. Должен быть класс task - задача. Объект класса task должен мочь сохр

<--НАЗАД        ДАЛЕЕ-->

Содержание Глава 1 Глава 2 Глава 3 Глава 4 Глава 5 Глава 6 Глава 7 Глава 8

Сделай САМ 

 

Copyright © by Alex Skums




TopList
Сайт создан в системе uCoz