таких новх типов утомителен (и потому чреват ошибками), но с
помощью макросов его можно "механизировать". К сожалению, если
пользоваться стандартным 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, можно определить так:
- стр 218 -
#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) {}
};
который потом используется для создания типа "стек указателей на
символы":
- стр 219 -
#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:
- стр 220 -
class name : public olink {
// ...
};
Теперь легко сделать список, который можно использовать без
накладных расходов времени на размещение или памяти.
Объекты, помещаемы в olist, теряют свой тип. Это означает, что
компилятор знает только то, что они olink'и. Правильный тип можно
восстановить с помощью явного преобразования типа объектов, вынутых
из olist. Например:
void f()
{
olist ll;
name nn;
ll.insert(&nn); // тип &nn потерян
name* pn = (name*)ll.get(); // и восстановлен
}
Другой способ: тип можно восстановить, выведя еще один класс из
olist для обработки преобразования типа:
class olist : public olist {
// ...
name* get() { return (name*)olist::get(); }
};
Имя name может одновременно находиться только в одном olist. Для
имен это может быть и неподходит, но в классах, для которых это
подойдет полностью, недостатка нет. Например, класс фигур shape
использует для поддержки списка всех фигур именно этот метод.
Обратите внимание, что можно было бы определить slist как
производный от olist, объединяя таким образом оба понятия. Однако
использование бызовых и производных классов на таком
микроскопическом уровне может очень сильно исказить код.
7.5 Неоднородные Списки
Предыдущие списки были однородными. То есть, в список помещались
только объекты одного типа. Это обеспечивалось аппаратом
производных классов. Списки не обязательно должны быть
одднородными. Список, заденный в виде указаетелей на класс, может
содержать объекты любого класса, производного от этого класса. То
есть, список может быть неоднородным. Вероятно, это единственный
наиболее важный и полезный аспект производных классов, и он весьма
существенно используется в стиле программирования, который
демонстрируется приведенным выше примером. Этот стиль
программирования часто называют объектно-основанным или
объектно-ориентированным. Он опирается на то, что действия над
объектами неоднородных списков выполняются одинаковым образом.
Смысл этих действий зависит от фактического типа объектов,
находящихся в списке (что становится известно только на стадии
выполнения), а не просто от типа элементов списка (который
компилятору известен).
- стр 221 -
7.6 Законченна Программа
Разберем процесс написания программы для рисования на экране
геметрических фигур. Она естественным образом разделяется на три
части:
[1] Администратор экрана: подпрограммы низкого уровня и структуры
данных, определяющие экран; он ведает только точками и прямыми
линиями;
[2] Библиотека фигур: набор определений оосновных фигур вроде
прямоугольника и круга и стандартные программы для работы с
ними; и
[3] Прикладная программа: множество определений,
специализированных для данного приложения, и код, в котором
они используются.
Эти три части скорее всего будут писать разные люди (в разных
организациях и в разное время). При этом части будут скорее всего
писать именно в указанном порядке с тем осложняющим
обстоятельством, что у разработчиков нижнего уровня не будет
точного представления, для чего их код в конечном счете будет
использоваться. Это отражено в приводимом примере. Чтобы пример был
короче, графическая библиотека предоставляет только весьма
ограниченный сервис, а сама прикладная программа очень проста.
Чтобы читатель смог испытать программу, даже если у него нет совсем
никаких графических средств, используется чрезвычайно простая
концепция экрана. Не должно составить труда заменить эту экранную
часть программы чем-нибудь подходящим, не изменяя код библиотеки
фигур и прикладной программы.
7.6.1 Администратор Экрана
Вначале было намерени написать администратор экрана на C (а не на
C++), чтобы подчеркнуть разделение уровней реализации. Это
оказалось слишком утомительным, поэтому пришлось пойти на
компромисс: используется стиль C (нет функций членов, виртуальных
функций, определяемых пользователем операций и т.п.), однако
применяются конструкторы, надлежащим образом описываются и
проверяются параметры функций и т.д. Оглядываясь назад, можно
сказать, что администратор экрана очень похож на C программу,
которую потом модифицировали, чтобы воспользоваться средствами C++
не переписывая все полностью.
Экран представляется как двумерный массив символов, работу с
которым осуществляют функции put_point() и put_line(), использующие
при ссылке на экран структуру point:
- стр 222 -
// файл screen.h