Списки имен можно использовать в классе, который предтавляет определение класса:
   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( amp;friends,n)) error(«friend redeclared»); // friend переописан else if (find( amp;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 «stream.h»
   void default_error(char* s)
   (* cerr «„ s «« «\n“; exit(1); *)
   Она описывает также указатель на функцию ошибок и, для удобства записи, функцию для ее установки:
   PFC slist_handler = default_error;
   PFC set_slist_handler(PFC handler); (* PFC rr = slist_handler; slist_handler = handler; return rr; *)
   Обратите внимание, как set_slist_hanlder() возвращает предыдущий slist_hanlder(). Это делает удобным установку и переустановку обработчиков ошибок на манер стека. В основном это может быть полезным в больших программах, в которых slist может использоваться в нескольких разных ситуациях, в каждой из которых могут, таким образом, задаваться свои собственные подпрограммы обработки ошибок. Например:
   (* PFC old = set_slist_handler(my_handler);
   // код, в котором в случае ошибок в slist // будет использоваться мой обработчик my_handler
   set_slist_handler(old); // восстановление *)
   Чтобы сделать управление более изящным, slist_hanlder мог бы быть сделан членом класса slist, что позволило бы раличным спискам иметь одновременно разные обработчики.

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

   Очевидно, можно было бы определить списки других типов (classdef*, int, char* и т.д.) точно так же, как был опредлен класс nlist: простым выводом из класса slist. Процесс оределения таких новых типов утомителен (и потому чреват ошиками), но с помощью макросов его можно «механизировать». К сожалению, если пользоваться стандартным C препроцессором (#4.7 и #с.11.1), это тоже может оказаться тягостным. Однако полученными в результате макросами пользоваться довольно просто.
   Вот пример того, как обобщенный (generic) класс slist, названный gslist, может быть задан как макрос. Сначала для написания такого рода макросов включаются некоторые инстрменты из «generic.h»:
   #include «slist.h»
   #ifndef GENERICH #include «generic.h» #endif
   Обратите внимание на использование #ifndef для того, чтобы гарантировать, что «generic.h» в одной компиляции не будет включен дважды. GENERICH определен в «generic.h».
   После этого с помощью name2(), макроса из «generic.h» для конкатенации имен, определяются имена новых обобщенных
   классов:
   #define gslist(type) name2(type,gslist) #define gslist_iterator(type) name2(type,gslist_iterator)
   И, наконец, можно написать классы gslist(тип) и gslist_iterator(тип):
   #define gslistdeclare(type) \ struct gslist(type) : slist (* \ int insert(type a) \ (* return slist::insert( ent(a) ); *) \ int append(type a) \ (* return slist::append( ent(a) ); *) \ type get() (* return type( slist::get() ); *) \ gslist(type)() (* *) \ gslist(type)(type a) : (ent(a)) (* *) \ ~gslist(type)() (* clear(); *) \ *); \ \ struct gslist_iterator(type) : slist_iterator (* \ gslist_iterator(type)(gslist(type) amp; a) \ : ( (slist amp;)s ) (**) \ type operator()() \ (* return type( slist_iterator::operator()() ); *)\ *)
   \ на конце строк указывает , что следующая строка явлется частью определяемого макроса.
   С помощью этого макроса список указателей на имя, аналгичный использованному раньше классу nlist, можно определить так:
   #include «name.h»
   typedef name* Pname; declare(gslist,Pname); // описывает класс gslist(Pname)
   gslist(Pname) nl; // описывает один gslist(Pname)
   Макрос declare (описать) определен в «generic.h». Он конкатинирует свои параметры и вызывает макрос с этим именем, в данном случае gslistdeclare, описанный выше. Параметр имя типа для declare должен быть простым именем. Используемый мтод макроопределения не может обрабатывать имена типов вроде 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( amp;nn); // тип amp;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 «stream.h»
   Перед первым использованием функции put экран надо инциализировать с помощью screen_init(), а изменения в структре данных экрана отображаются на экране только после вызова screen_refresh(). Как увидит пользователь, это «обновление» («refresh») осуществляется просто посредством печати новой копии экрана под его предыдущим вариантом. Вот функции и оределения данных для экрана:
   #include «screen.h» #include «stream.h»
   enum color (* black='*', white=' ' *);
   char screen[XMAX][YNAX];
   void screen_init() (* for (int y=0; y«YMAX; y++) for (int x=0; x«XMAX; x++) screen[x][y] = white; *)
   Точки печатаются, только если они есть на экране:
   inline int on_screen(int a, int b) (* return 0«=a amp; amp; a«XMAX amp; amp; 0«=b amp; amp; b«YMAX; *)
   void put_point(int a, int b) (* if (on_screen(a,b)) screen[a][b] = black; *)
   Для рисования линий используется функция put_line():
   void put_line(int x0, int y0, int x1, int y1) /* Строит линию от (x0,y0) до (x1,y1). Строится линия b(x-x0) + a(y-y0) = 0. Минимизирует abs(eps), где eps = 2*(b(x-x0)+ a(y-y0)). См. Newman and Sproull: ``Principles of Interactive Computer Graphics'' McGraw-Hill, New York, 1979, pp 33-44. */ (* register dx = 1; int a = x1 – x0; if (a « 0) dx = -1, a = -a; register dy = 1; int b = y1 – y0;
   if (b « 0) dy = -1, b = -b; int two_a = 2*a; int two_b = 2*b; int xcrit = -b + two_a; register eps = 0; for (;;) (* put_point(x0,y0); if(x0==x1 amp; amp; y0==y1) break; if(eps „= xcrit) x0 += dx, eps += two_b; if(eps“=a !! a«=b) y0 += dy, eps -= two_a; *) *)
   Предоставляются функции для очистки экрана и его обноления:
   void screen_clear() (* screen_init(); *) // очистка
   void screen_refresh() // обновление (* for (int y=YMAX-1; 0«=y; y–) (* // сверху вниз for (int x=0; x«XMAX; x++) // слева направо cout.put(screen[x][y]); cout.put('\n'); *) *)
   Функция ostream::put() применяется для печати символов как символов; ostream::operator««() печатает символы как млые целые. Пока что вы может представлять себе, что эти опрделения доступны только в откомпилированном виде, который вы изменить не можете.

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

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

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

   Прикладная программа чрезвычайно проста. Определяется новая фигура myshape (на печати она немного похожа на рожцу), а потом пишется главная программа, которая надевает на нее шляпу. Вначале описание myshape:
   #include «shape.h»
   class myshape : public rectangle (* line* l_eye; // левый глаз line* r_eye; // правый глаз line* mouth; // рот public: myshape(point, point); void draw(); void move(int, int); *);
   Глаза и рот – отдельные и независимые объекты, которые создает конструктор myshape:
   myshape::myshape(point a, point b) : (a,b) (* int ll = neast().x-swest().x+1; int hh = neast().y-swest().y+1; l_eye = new line( point(swest().x+2,swest().y+hh*3/4),2); r_eye = new line( point(swest().x+ll-4,swest().y+hh*3/4),2); mouth = new line( point(swest().x+2,swest().y+hh/4),ll-4); *)
   Объекты глаза и рот порознь рисуются заново функцией shape_refresh(), и в принципе могут обрабатываться независимо из объекта myshape, которому они принадлежат. Это один способ определять средства для иерархически построенных объектов вроде 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 «stream.h»
   struct base (* base(); *);
   struct derived : base (* derived(); *)
   base::base() (* cout «„ „\tbase 1: this=“ „„ int(this) «« «\n“; if (this == 0) this = (base*)27; cout «« «\tbase 2: this=“ «« int(this) «« «\n“; *)
   derived::derived() (* cout «„ „\tderived 1: this=“ „„ int(this) «« «\n“; this = (this == 0) ? (derived*)43 : this; cout «« «\tderived 2: this=“ «« int(this) «« «\n“; *)
   main() (* cout «„ „base b;\n“; base b; cout „„ „new base b;\n“; new base; cout «« «derived d;\n“; derived d; cout «« «new derived d;\n“; new derived; cout «« «at the end\n“;
   *)
   порождает вывод
   base b; base 1: this=2147478307 base 2: this=2147478307 new base; base 1: this=0 base 2: this=27 derived d; derived 1: this=2147478306 base 1: this=2147478306 base 2: this=2147478306 derived 1: this=2147478306 new derived; derived 1: this=0 base 1: this=43 base 2: this=43 derived 1: this=43 at the end
   Если деструктор производного класса осуществляет присвивание указателю this, то будет присвоено то значение, котрое встретил деструктор его базового класса. Когда кто-либо делает в конструкторе присваивание указателю this, важно, чтобы присваивание указателю 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) Придумайте и реализуйте дважды связанный список, которым можно пользоваться только посредством итератора. Итератор должен иметь действия для движения вперед и нзад, действия для вставления и удаления элементов спика, и способ доступа к текущему элементу.