ЭЛЕКТРОННАЯ БИБЛИОТЕКА КОАПП
Сборники Художественной, Технической, Справочной, Английской, Нормативной, Исторической, и др. литературы.



 

Часть 9

                                Глава 7 Производные Классы
           
                                 Не надо размножать объекты без необходимости
                                                                   - У. Оккам
           
             В этой главе описывается понятие производного класса в С++.  Про-
           изводные классы дают простой,  гибкий и эффективный аппарат задания
           для   класса   альтернативного   интерфейса  и  определения  класса
           посредством добавления возможностей к уже имеющемуся классу без пе-
           репрограммирования   или   перекомпиляции.  С  помощью  производных
           классов можно также обеспечить общий интерфейс для нескольких  раз-
           личных  классов так,  чтобы другие части программы могли работать с
           объектами этих классов одинаковым образом. При этом обычно в каждый
           объект помещается информация о типе,  чтобы эти объекты могли обра-
           батываться соответствующим образом в ситуациях, когда их тип нельзя
           узнать во время компиляции. Для элегантной и надежной обработки та-
           ких динамических зависимостей  типов  имеется  понятие  виртуальной
           функции. По своей сути производные классы существуют для того, что-
           бы облегчить программисту формулировку общности.
                                        7.1 Введение
           
             Представим себе   процесс   написания  некоторого  универсального
           средства (например,  тип связанный список,  таблица имен или плани-
           ровщик  для  системы  моделирования),  которое  предназначается для
           использования многими разными людьми в  различных  обстоятельствах.
           Очевидно,  что в кандидатах на роль таких средств недостатка нет, и
           выгоды от их стандартизации огромны.  Кажется,  любой опытный прог-
           раммист написал (и отладил) дюжину вариантов типов множества,  таб-
           лицы имен,  сортирующей функции и т.п.,  но оказывается, что каждый
           программист и каждая программа используют свою версию этих понятий,
           из-за чего программы слишком трудно  читать,  тяжело  отлаживать  и
           сложно модифицировать. Более того, в большой программе вполне может
           быть несколько копий идентичных (почти) частей кода  для  работы  с
           такими фундаментальными понятиями.
           
             Причина этого хаоса частично состоит в том, что представить такие
           общие понятия в языке программирования сложно с концептуальной точ-
           ки зрения,  а частично в том,  что средства, обладающие достаточной
           общностью,  налагают дополнительные расходы по памяти и/или по вре-
           мени,  что делает их неудобными для самых простых и наиболее напря-
           женно используемых средств (связанные списки,  вектора и т.п.), где
           они  были  бы наиболее полезны.  Понятие производного класса в С++,
           описываемое в #7.2,  не обеспечивают общего решения всех этих проб-
           лем,  но  оно  дает  способ справляться с довольно небольшим числом
           важных случаев. Будет, например, показано, как определить эффектив-
           ный  класс  обобщенного связанного списка таким образом,  чтобы все
           его версии разделяли код.
           
             Написание общецелевых средств - задача непростая,  и часто основ-
           ной акцент в их разработке другой, чем при разработке программ спе-
           циального назначения.  Конечно, нет четкой границы между средствами
           общего и специального назначения, и к методам и языковым средствам,
           которые описываются в этой главе,  можно относиться  так,  что  они
           становятся  все более полезны с ростом объема и сложности создавае-
           мых программ.
           
                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 для компилятора.  Указатель на менеджера (manager*) не яв-
           ляется указателем на служащего (employee*),  поэтому просто исполь-
           зовать один там,  где требуется другой, нельзя. В частности, нельзя
           поместить менеджера в список служащих,  не написав для этого специ-
           альный код.  Можно либо применить к 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?  Какие члены базового
           класса employee может использовать функция не член на объекте  типа
           manager?  Каким образом программист может повлиять на ответы на эти
           вопросы, чтобы удовлетворить требованиям приложения?
           
             Рассмотрим:
           
             void manager::print() {
                 cout << " имя " << name << "\n";
                 // ...
             }
           
             Член производного класса может использовать открытое имя из свое-
           го базового класса так  же,  как  это  могут  делать  другие  члены
           последнего,  то есть без указания объекта.  Предполагается,  что на
           объект указывает this, поэтому (корректной) ссылкой на имя name яв-
           ляется this->name. Однако функция manager::print компилироваться не
           будет,  член производного класса не имеет  никакого  особого  права
           доступа к закрытым членам его базового класса, поэтому для нее name
           недоступно.
           
             Это многим покажется удивительным, но представьте себе другой ва-
           риант: что функция член могла бы обращаться к закрытым членам свое-
           го базового класса.  Возможность, позволяющая программисту получать
           доступ к закрытой части класса просто с помощью вывода из него дру-
           гого класса,  лишила бы понятие закрытого члена всякого смысла. Бо-
           лее того,  нельзя было бы узнать все использования закрытого имени,
           посмотрев на функции,  описанные как члены и друзья  этого  класса.
           Пришлось бы проверять каждый исходный файл во всей программе на на-
           личие в нем производных классов,  потом исследовать каждую  функцию
           этих классов, потом искать все классы, производные от этих классов,
           и т.д. Это по меньшей мере утомительно и скорее всего нереально.
             С другой стороны,  можно ведь использовать механизм 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 эти члены недоступны. В частности, при
           таком  описании  класса  manager функция clear() компилироваться не
           будет.  Друзья производного класса имеют к членам  базового  класса
           такой же доступ, как и функции члены.
           
             Поскольку, как  оказывается,  описание  открытых  базовых классов
           встречается чаще описания закрытых,  жалко,  что описание открытого
           базового класса длиннее описания закрытого. Это, кроме того, служит
           источником запутывающих ошибок у начинающих.
             Когда описывается производная 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 можно обращаться через указатель на base, но нельзя че-
           рез указатель на derived:
           
             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
           
             Помимо всего прочего, этот пример показывает, что используя явное
           приведение к типу можно сломать правила защиты. Ясно, делать это не
           рекомендуется, и это приносит программисту заслуженную "награду". К
           несчастью ,  недисциплинированное использование явного преобразова-
           ния может создать адские условия для невинных жертв,  эксплуатирую-
           щих программу,  в которой это делается.  Но, к счастью, нет способа
           воспользоваться приведением для получения доступа к закрытому имени
           m1.  Закрытый член класса может  использоваться  только  членами  и
           друзьями этого класса.
                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'а нельзя помещать с список временных служащих (temporary
           employee),  не написав специальный код.  Однако во многих  полезных
           программах этот метод успешно используется.
                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 можно использовать
           для построения неоднородных списков, то есть списков объектов (ука-
           зателей  на  объекты)  нескольких различных типов.  Решение 3 - это
           специальный вариант решения 2 с гарантией типа.
           
             Давайте сначала исследуем простое решение с помощью поля типа, то
           есть решение 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);
             }
             Это прекрасно работает,особенно в небольшой программе, написанной
           одним человеком,  но имеет тот коренной недостаток, что неконтроли-
           руемым компилятором образом зависит от того,  как программист рабо-
           тает  с типами.  В больших программах это обычно приводит к ошибкам
           двух видов.  Первый - это невыполнение проверки поля типа, второй -
           когда  не все случаи case помещаются в переключатель switch,  как в
           предыдущем примере. Оба избежать достаточно легко , когда программу
           сначала пишут на бумаге,  но при модификации нетривиальной програм-
           мы, особенно написанной другим человеком, очень трудно избежать как
           того,  так  и другого.  Часто от этих сложностей становится труднее
           уберечься из-за того,  что функции вроде print() часто бывают орга-
           низованы так,  чтобы пользоваться общностью классов, с которыми они
           работают. Например:
           
             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()
           подвергалось бы бесконечной рекурсии.  Применение уточненного имени
           имеет еще один  эффект,  который  может  оказаться  полезным:  если
           описанная как virtual функция описана еще и как inline (в чем ниче-
           го необычного нет), то там, где в вызове применяется ::, может при-
           меняться  inline-подстановка.  Это  дает  программисту  эффективный
           способ справляться с теми важными специальными случаями, когда одна
           виртуальная функция вызывает другую для того же объекта.  Поскольку
           тип объекта был определен при вызове  первой  виртуальной  функции,
           обычно  его  не надо снова динамически определять другом вызове для
           того же объекта.
                7.3 Альтернативные Интерфейсы
           
             После того,  как описаны средства языка, которые относятся к про-
           изводным классам,  обсуждение снова может вернуться к стоящим зада-
           чам.  В классах, которые описываются в этом разделе, основополагаю-
           щая идея состоит в том,  что  они  однажды  написаны,  а  потом  их
           используют программисты,  которые не могут изменить их определение.
           Физически классы состоят из одного или более  заголовочных  файлов,
           определяющих интерфейс, и одного или более файлов, определяющих ре-
           ализацию.  Заголовочные файлы будут помещены куда-то  туда,  откуда
           пользователь  может  взять  их  копии с помощью директивы #include.
           Файлы,  определяющие реализацию,  обычно компилируют и  помещают  в
           библиотеку.
           
                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'ов никак не видно в описаниях открытых  функций  slist'ов,  а
           видно только в закрытой части и определениях функций.
           
                7.3.2 Реализация
           
             Реализующие slist функции в основном просты. Единственная настоя-
           щая сложность - что делать в случае ошибки, если, например, пользо-
           ватель  попытается  get() что-нибудь из пустого списка.  Мы обсудим
           это в #7.3.4.  Здесь приводятся определения членов slist.  Обратите
           внимание,  как  хранение  указателя  на последний элемент кругового
           списка дает возможность просто реализовать оба действия append()  и
           insert():
           
             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 как друга, поэтому
           мы можем описать подходящий итератор.  Вот один,  написанный в духе
           #6.8:
           
             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 и получить список тех  объектов,
           которые  представляют  интерес  в конкретной программе.  Представим
           компилятор языка вроде  С++.  В  нем  широко  будут  использоваться
           списки имен; имя name - это нечто вроде
           
             struct name { char* string; // ...
             };
           
           В список будут помещаться указатели на имена,  а  не  сами  объекты
           имена.  Это  позволяет использовать небольшое информационное поле e
           slist'а, и дает возможность имени находиться одновременно более чем
           в одном списке.  Вот определение класса nlist, который очень просто
           выводится из класса slist:
           
             #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*,  нет необходимости явно преобразовы-
           вать указатели name*,  которые используются в качестве  фактических
           параметров (#2.3.4).
           
             Списки имен можно использовать в классе, который представляет оп-
           ределение класса:
           
             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,  после чего завершает программу  с  помощью
           exit():
           
             #include "slist.h"
             #include 
           
             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.  Процесс определения
           таких новых типов утомителен (и потому чреват ошибками),  но с  по-
           мощью макросов его можно "механизировать".  К сожалению, если поль-
           зоваться стандартным C препроцессором (#4.7 и  #с.11.1),  это  тоже
           может оказаться тягостным. Однако полученными в результате макроса-
           ми пользоваться довольно просто.
           
             Вот пример того,  как обобщенный (generic) класс slist, названный
           gslist,  может быть задан как макрос.  Сначала для написания такого
           рода макросов включаются некоторые инструменты из :
           
             #include "slist.h"
           
             #ifndef GENERICH
             #include 
             #endif
           
             Обратите внимание на использование #ifndef для того, чтобы гаран-
           тировать, что  в одной компиляции не будет включен дваж-
           ды. GENERICH определен в .
           
             После этого с помощью name2(),  макроса из  для конка-
           тенации имен, определяются имена новых обобщенных классов:
           
             #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 (описать) определен в . Он конкатиниру-
           ет свои параметры и вызывает макрос с этим именем,  в данном случае
           gslistdeclare, описанный выше. Параметр имя типа для declare должен
           быть  простым именем.  Используемый метод макроопределения не может
           обрабатывать имена типов вроде name*, поэтому применяется typedef.
           
             Использование вывода класса гарантирует,  что все частные  случаи
           обобщенного класса разделяют код. Этот метод можно применять только
           для создания классов объектов того же размера или меньше, чем базо-
           вый  класс,  который  используется в макросе.  gslist применяется в
           #7.6.2.
                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, который определялся выше,
           помещается элемент,  то создается slink,  содержащий два указателя.
           На их создание тратится время, а ведь без одного из указателей мож-
           но обойтись,  при условии,  что нужно только чтобы объект мог нахо-
           диться в одном списке.  Так что указатель next на  следующий  можно
           поместить в сам объект, вместо того, чтобы помещать его в отдельный
           объект slink.  Идея состоит в том,  чтобы  создать  класс  olink  с
           единственным полем next,  и класс olist, который может обрабатывать
           указатели на такие звенья olink.  Тогда olist сможет манипулировать
           объектами любого класса,  производного от olink. Буква "o" в назва-
           ниях стоит для того,  чтобы напоминать вам,  что объект может нахо-
           диться одновременно только в одном списке olist:
           
             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
           использует для поддержки списка всех фигур именно этот метод. Обра-
           тите внимание,  что можно было бы определить slist как  производный
           от olist, объединяя таким образом оба понятия. Однако использование
           базовых и производных классов на таком микроскопическом уровне  мо-
           жет очень сильно исказить код.
           
                7.5 Неоднородные Списки
           
             Предыдущие списки были однородными.  То есть, в список помещались
           только объекты одного типа.  Это обеспечивалось аппаратом производ-
           ных классов. Списки не обязательно должны быть однородными. Список,
           заданный в виде указателей на класс, может содержать объекты любого
           класса,  производного от этого класса.  То есть,  список может быть
           неоднородным. Вероятно, это единственный наиболее важный и полезный
           аспект производных классов,  и он весьма существенно используется в
           стиле программирования,  который демонстрируется  приведенным  выше
           примером. Этот стиль программирования часто называют объектно-осно-
           ванным  или  объектно-ориентированным.  Он  опирается  на  то,  что
           действия  над объектами неоднородных списков выполняются одинаковым
           образом. Смысл этих действий зависит от фактического типа объектов,
           находящихся  в списке (что становится известно только на стадии вы-
           полнения), а не просто от типа элементов списка (который компилято-
           ру известен).
                7.6 Законченная Программа
           
             Разберем процесс написания программы для рисования на экране гео-
           метрических  фигур.  Она  естественным  образом  разделяется на три
           части:
           
             [1] Администратор экрана: подпрограммы низкого уровня и структуры
                данных, определяющие экран; он ведает только точками и прямыми
                линиями,
           
             [2] Библиотека фигур: набор определений основных фигур вроде пря-
                моугольника  и круга и стандартные программы для работы с ними
                и
           
             [3] Прикладная программа: множество определений, специализирован-
                ных для данного приложения, и код, который их использует.
           
             Эти три части скорее всего будут писать разные люди (в разных ор-
           ганизациях и в разное время).  При этом части  будут  скорее  всего
           писать  именно  в указанном порядке с тем осложняющим обстоятельст-
           вом,  что у разработчиков нижнего уровня не будет точного представ-
           ления,  для чего их код в конечном счете будет использоваться.  Это
           отражено в приводимом примере. Чтобы пример был короче, графическая
           библиотека предоставляет только весьма ограниченный сервис,  а сама
           прикладная программа очень проста.  Чтобы  читатель  смог  испытать
           программу, даже если у него нет совсем никаких графических средств,
           используется чрезвычайно простая концепция экрана. Не должно соста-
           вить  труда заменить эту экранную часть программы чем-нибудь подхо-
           дящим, не изменяя код библиотеки фигур и прикладной программы.
                7.6.1 Администратор Экрана
           
             Вначале было  намерение  написать администратор экрана на C (а не
           на С++),  чтобы подчеркнуть разделение уровней реализации. Это ока-
           залось слишком утомительным,  поэтому пришлось пойти на компромисс:
           используется стиль 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 
             Перед первым  использованием функции put экран надо инициализиро-
           вать с помощью screen_init(), а изменения в структуре данных экрана
           отображаются  на  экране только после вызова screen_refresh().  Как
           увидит пользователь,  это "обновление"  ("refresh")  осуществляется
           просто посредством печати новой копии экрана под его предыдущим ва-
           риантом. Вот функции и определения данных для экрана:
           
             #include "screen.h"
             #include 
           
             enum color { black='*', white=' ' };
           
             char screen[XMAX][YNAX];
           
             void screen_init() {
                 for (int y=0; y=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;
                     xdraw(); 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,  которому они принадлежат. Это один способ опреде-
           лять средства для иерархически построенных объектов вроде  myshape.
           Другой способ демонстрируется на примере носа. Никакой нос не опре-
           деляется, его просто добавляет к картинке функция draw():
           
             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  -  это  превосходный пример
           класса, который может значительно выиграть от того, что программист
           возьмет  под контроль управление свободной памятью.  Для этого вида
           объектов идеально подходит оптимизирующий метод,  который описан  в
           #5.5.6.  Поскольку каждый slink создается с помощью new и уничтожа-
           ется с помощью delete членами класса slist, другой способ выделения
           памяти не представляет никаких проблем.
           
             Если производный  класс осуществляет присваивание указателю this,
           то конструктор его базового класса будет  вызываться  только  после
           этого присваивания,  и значение указателя this в конструкторе базо-
           вого класса будет тем, которое присвоено конструктором производного
           класса.  Если  базовый  класс присваивает указателю this,  то будет
           присвоено то значение,  которое использует конструктор производного
           класса. Например:
           
             #include 
           
             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, важно, чтобы присваивание указателю
           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. (*5) Придумайте и реализуйте библиотеку для  написания  моде-
                лей, управляемых прерываниями. Подсказка: . Только это
                - старая программа,  а вы могли бы написать лучше. Должен быть
                класс task - задача.  Объект класса task должен мочь сохранять
                свое состояние и восстанавливаться в это состояние (вы  можете
                определить  task::save()  и  task::restore()),  чтобы  он  мог
                действовать как сопрограмма. Отдельные задачи можно определять
                как  объекты классов,  производных от класса task.  Программа,
                которую должна исполнять задача,  может задаваться как  вирту-
                альная функция. Должна быть возможность передавать новой зада-
                че ее параметры как параметры ее конструктора(ов).  Там должен
                быть планировщик,  реализующий концепцию виртуального времени.
                Обеспечьте функцию задержки  task::delay(),  которая  "тратит"
                виртуальное  время.  Будет ли планировщик отдельным или частью
                класса task - это один из основных вопросов,  которые надо ре-
                шить при проектировании.  Задача должна передавать данные. Для
                этого разработайте класс queue (очередь).  Придумайте  способ,
                чтобы  задача  ожидала ввода из нескольких очередей.  Ошибки в
                ходе выполнения обрабатывайте однородным образом.  Как  бы  вы
                отлаживали программы, написанные с помощью такой библиотеки?


Яндекс цитирования