Жанр: Учеба
Язык С++ с комментариями
...ry temp; ... };
class consultant : employee
{ temporary temp; ... };
Это выглядит неэлегантно и страдает как раз от тех проблем, для
преодоления которыз были изобретены производные классы. Например,
поскольку consultant не является производным от temporary,
consultant'а нельзя помещать с список временных служащих (temporary
employee), не написав специальной программы. Однако во многих
полезных программах этот метод успешно используется.
7.2.6 Конструкторы и Деструкторы
Для некоторых производных классов нужны конструкторы. Если у
базового класса есть конструктор, он должен вызываться, и если для
этого конструктора нужны параметры, их надо предоставить. Например:
- стр 206 -
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. Пример со служащими и менеджерами можно было бы
переопределить так:
- стр 207 -
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() часто бывают организованы так, чтобы ползоваться
общность классов, с которыми они работают. Например:
- стр 208 -
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";
// ...
}
Виртуальная функция может, таким образом, использоваться даже в том
случае, когда нет производных классов от ее класса, и в производном
классе, в котором не нужен специальный вариант виртуальной функции,
ее задавать не обязательно. Просто при выводе класса
соответствующая функция задается в том случае, если она нужна.
Например:
- стр 209 -
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 сохраняется некоторая информация о типе.
Занимаемого для этого пространства (в текущей реализации) как раз
хватает для хранения указателя. Это пространство занимается только
в объектах классов с виртуальными функциями, а не во всех объектах
классов и даже не во всех объектах производных классов. Вы платите
эту пошлину только за те классы, для которых описали виртуальные
функции.
- стр 210 -
Вызов функции с помощью операции разрешения области видимости ::,
как это делается в 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:
- стр 211 -
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():
- стр 212 -
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);
}
- стр 213 -
Класс 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 и получить список тех объектов,
которые представляют интерес в конкретной программе. Представим
компилятор языка вроде C++. В нем широко будут использоваться
списки имен; имя - это нечто вроде
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. Так как на
- стр 214 -
самом деле тип 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");
else if (find(&members,n))
error("friend redeclared as member");
else
friends.append(n);
}
if (n-"is_operator()) operators.append(n);
// ...
}
где is_iterator() и 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 может, например,
такая функция:
- стр 215 -
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 сталкивается с ошибкой
(в C++ нет никаких специальных средств языка для обработке ошибок):
[1] Возвращать недопустимое значение и позволить пользователю его
проверять;
[2] Возвращать дополнительное значение состояния и разрешить
пользователю проверять его;
[3] Вызывать функцию ошибок, заданную как часть класса slist; или
[4] Вызывать функцию ошибок, которую предположительно
предоставляет пользователь.
Для небольшой программы, написанной ее единственным
пользователем, нет фактически никаких особенных причин предпочесть
одно из этих решений другим. Для средства общего назначения
ситуация совершенно иная.
Первый подход, возвращать недопустимое значение, неосуществим.
Нет совершенно никакого способа узнать, что некоторое конкретное
значени будет недопустимым во всех применениях slist.
Второй подход, возвращать значение состояния, можно использовать
в некоторых классах (один из вариантов этого плана применяется в
стандартных потоках ввода/вывода istream и ostream; как -
объясняется в #8.4.2). Здесь, однако, имеется серьезная проблема,
вдруг пользователь не позаботится проверить значение состояния,
если средство не слишком часто подводит. Кроме того, средство может
использоваться в сотнях или даже тысячах мест программы. Проверка
значения в каждом месте сильно затруднит чтение программы.
Третьему подходу, предоставлять функцию ошибок, недостает
гибкости. Тот, кто реализует общецелевое средство, не может узнать,
как пользователи захотят, чтобы обрабатывались ошибки. Например,
пользователь может предпочитать сообшения на датском или
венгерском.
Четвертый подход, позволить пользователю задавать функцию ошибок,
имеет некоторую привлекательность при условии, что разработчик
предоставляет класс в виде библиотеки (#4.5), в которой содержатся
стандартные функции обработки ошибок.
Решения 3 и 4 можно сделать более гибкими (и по сути
эквивалентными), задав указатель на функцию, а не саму функцию. Это
позволит разработчику такого средства, как slist, предоставить
функцию ошибок, действующую по умолчанию, и при этом программистам,
которые будут использовать списки, будет легко задать свои
собственные функции ошибок, если нужно, и там, где нужно.
Например:
- стр 216 -
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, что позволило бы различным спискам
иметь одновременно разные обработчики.
- стр 217 -
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, можно определить так:
- стр 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 сможет
манипулировать объектами луюого класса, производн
...Закладка в соц.сетях