Страница:
point west() const;
point neast() const;
point seast() const;
point nwest() const;
point swest() const;
void move(int a, int b)
{ w.x +=a; w.y +=b; e.x +=a; e.y +=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; }
};
Аналогично определяется прямоугольник:
class rectangle : public shape {
/* nw ------ n ----- ne
| |
| |
w c e
| |
| |
sw ------ s ----- se
*/
point sw, ne;
public:
point north() const { return point((sw.x+ne.x)/2,ne.y); }
point south() const { return point((sw.x+ne.x)/2,sw.y); }
point east() const;
point west() const;
point neast() const { return ne; }
point seast() const;
point nwest() const;
point swest() const { 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) {
if (a.y <= b.y) {
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, const shape* q); // поместить p над q
Функция обновления фигур нужна, чтобы работать с нашим примитивным
представлением экрана; она просто заново рисует все фигуры. Отметим,
что эта функция не имеет понятия, какие фигуры она рисует:
void shape_refresh()
{
screen_clear();
for (shape* p = shape::list; p; p=p->next) p->draw();
screen_refresh();
}
Наконец, есть одна действительно сервисная функция, которая рисует
одну фигуру над другой. Для этого она определяет юг (south()) одной
фигуры как раз над севером (north()) другой:
void stack(shape* p, const shape* q) // поместить p над q
{
point n = q->north();
point s = p->south();
p->move(n.x-s.x,n.y-s.y+1);
}
Представим теперь, что эта библиотека является собственностью
некоторой фирмы, продающей программы, и, что она продает только
заголовочный файл с определениями фигур и оттранслированные
определения функций. Все равно вы сможете определить новые фигуры,
воспользовавшись для этого купленными вами функциями.
Прикладная программа предельно проста. Определяется новая фигура
myshape (если ее нарисовать, то она напоминает лицо), а затем
приводится функция main(), в которой она рисуется со шляпой. Вначале
дадим описание фигуры 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) : rectangle(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()
по отдельности. В принципе с ними можно работать независимо от
объекта my_shape, к которому они принадлежат. Это один из способов
задания черт лица для строящегося иерархически объекта myshape.
Как это можно сделать иначе, видно из задания носа. Никакой тип "нос"
не определяется, он просто дорисовывается в функции draw():
void myshape::draw()
{
rectangle::draw();
int a = (swest().x+neast().x)/2;
int b = (swest().y+neast().y)/2;
put_point(point(a,b));
}
Движение фигуры myshape сводится к движению объекта базового класса
rectangle и к движению вторичных объектов (l_eye, r_eye и mouth):
void myshape::move(int a, int b)
{
rectangle::move(a,b);
l_eye->move(a,b);
r_eye->move(a,b);
mouth->move(a,b);
}
Наконец, определим несколько фигур и будем их двигать:
int main()
{
screen_init();
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();
screen_destroy();
return 0;
}
Вновь обратим внимание на то, что функции, подобные shape_refresh()
и stack(), работают с объектами, типы которых были определены
заведомо после определения этих функций (и, вероятно, после
их трансляции).
Вот получившееся лицо со шляпой:
***********
* *
* *
* *
* *
* *
* *
* *
***********
*****************
***********
* *
* ** ** *
* *
* * *
* *
* ******* *
* *
***********
Для упрощения примера копирование и удаление фигур не обсуждалось.
В $$1.5.3 и $$6.2.3 уже говорилось, что у класса может быть несколько
прямых базовых классов. Это значит, что в описании класса после :
может быть указано более одного класса. Рассмотрим задачу моделирования,
в которой параллельные действия представлены стандартной библиотекой
классов task, а сбор и выдачу информации обеспечивает библиотечный
класс displayed. Тогда класс моделируемых объектов (назовем его
satellite) можно определить так:
class satellite : public task, public displayed {
// ...
};
Такое определение обычно называется множественным наследованием.
Обратно, существование только одного прямого базового класса называется
единственным наследованием.
Ко всем определенным в классе satellite операциям добавляется
объединение операций классов task и displayed:
void f(satellite& s)
{
s.draw(); // displayed::draw()
s.delay(10); // task::delay()
s.xmit(); // satellite::xmit()
}
С другой стороны, объект типа satellite можно передавать функциям с
параметром типа task или displayed:
void highlight(displayed*);
void suspend(task*);
void g(satellite* p)
{
highlight(p); // highlight((displayed*)p)
suspend(p); // suspend((task*)p);
}
Очевидно, реализация этой возможности требует некоторого (простого)
трюка от транслятора: нужно функциям с параметрами task и
displayed передать разные части объекта типа satellite.
Для виртуальных функций, естественно, вызов и так выполнится
правильно:
class task {
// ...
virtual pending() = 0;
};
class displayed {
// ...
virtual void draw() = 0;
};
class satellite : public task, public displayed {
// ...
void pending();
void draw();
};
Здесь функции satellite::draw() и satellite::pending() для объекта
типа satellite будут вызываться так же, как если бы он был объектом типа
displayed или task, соответственно.
Отметим, что ориентация только на единственное наследование
ограничивает возможности реализации классов displayed, task и
satellite. В таком случае класс satellite мог бы быть task или
displayed, но не то и другое вместе (если, конечно, task не является
производным от displayed или наоборот). В любом случае теряется
гибкость.
Возможность иметь более одного базового класса влечет за собой
возможность неоднократного вхождения класса как базового. Допустим,
классы task и displayed являются производными класса link, тогда
в satellite он будет входить дважды:
class task : public link {
// link используется для связывания всех
// задач в список (список диспетчера)
// ...
};
class displayed : public link {
// link используется для связывания всех
// изображаемых объектов (список изображений)
// ...
};
Но проблем не возникает. Два различных объекта link используются
для различных списков, и эти списки не конфликтуют друг с другом.
Конечно, без риска неоднозначности нельзя обращаться к членам класса
link, но как это сделать корректно, показано в следующем разделе.
Графически объект satellite можно представить так:
Но можно привести примеры, когда общий базовый класс не должен
представляться двумя различными объектами (см. $$6.5.3).
Естественно, у двух базовых классов могут быть функции-члены
с одинаковыми именами:
class task {
// ...
virtual debug_info* get_debug();
};
class displayed {
// ...
virtual debug_info* get_debug();
};
При использовании класса satellite подобная неоднозначность функций
должна быть разрешена:
void f(satellite* sp)
{
debug_info* dip = sp->get_debug(); //ошибка: неоднозначность
dip = sp->task::get_debug(); // нормально
dip = sp->displayed::get_debug(); // нормально
}
Однако, явное разрешение неоднозначности хлопотно, поэтому
для ее устранения лучше всего определить новую функцию в
производном классе:
class satellite : public task, public derived {
// ...
debug_info* get_debug()
{
debug_info* dip1 = task:get_debug();
debug_info* dip2 = displayed::get_debug();
return dip1->merge(dip2);
}
};
Тем самым локализуется информация из базовых для satellite классов.
Поскольку satellite::get_debug() является переопределением функций
get_debug() из обоих базовых классов, гарантируется, что именно она
будет вызываться при всяком обращении к get_debug() для объекта
типа satellite.
Транслятор выявляет коллизии имен, возникающие при определении
одного и того же имени в более, чем одном базовом классе. Поэтому
программисту не надо указывать какое именно имя используется, кроме
случая, когда его использование действительно неоднозначно. Как правило
использование базовых классов не приводит к коллизии имен. В большинстве
случаев, даже если имена совпадают, коллизия не возникает, поскольку
имена не используются непосредственно для объектов производного класса.
Аналогичная проблема, когда в двух классах есть функции с одним
именем, но разным назначением, обсуждается в $$13.8 на примере
функции draw() для классов Window и Cowboy.
Если неоднозначности не возникает, излишне указывать имя
базового класса при явном обращении к его члену. В частности, если
множественное наследование не используется, вполне достаточно
использовать обозначение типа "где-то в базовом классе". Это
позволяет программисту не запоминать имя прямого базового класса и
спасает его от ошибок (впрочем, редких), возникающих при перестройке
иерархии классов. Например, в функции из $$6.2.5
void manager::print()
{
employee::print();
// ...
}
предполагается, что employee - прямой базовый класс для manager.
Результат этой функции не изменится, если employee окажется косвенным
базовым классом для manager, а в прямом базовом классе функции
print() нет. Однако, кто-то мог бы следующим образом перестроить
классы:
class employee {
// ...
virtual void print();
};
class foreman : public employee {
// ...
void print();
};
class manager : public foreman {
// ...
void print();
};
Теперь функция foreman::print() не будет вызываться, хотя почти
наверняка предполагался вызов именно этой функции. С помощью
небольшой хитрости можно преодолеть эту трудность:
class foreman : public employee {
typedef employee inherited;
// ...
void print();
};
class manager : public foreman {
typedef foreman inherited;
// ...
void print();
};
void manager::print()
{
inherited::print();
// ...
}
Правила областей видимости, в частности те, которые относятся к
вложенным типам, гарантируют, что возникшие несколько типов
inherited не будут конфликтовать друг с другом. В общем-то дело
вкуса, считать решение с типом inherited наглядным или нет.
В предыдущих разделах множественное наследование рассматривалось
как существенный фактор, позволяющий за счет слияния классов
безболезненно интегрировать независимо создававшиеся программы.
Это самое основное применение множественного наследования, и,
к счастью (но не случайно), это самый простой и надежный способ
его применения.
Иногда применение множественного наследования предполагает
достаточно тесную связь между классами, которые рассматриваются
как "братские" базовые классы. Такие классы-братья обычно должны
проектироваться совместно. В большинстве случаев для этого не
требуется особый стиль программирования, существенно отличающийся
от того, который мы только что рассматривали. Просто на производный
класс возлагается некоторая дополнительная работа. Обычно она
сводится к переопределению одной или нескольких виртуальных
функций (см. $$13.2 и $$8.7). В некоторых случаях классы-братья
должны иметь общую информацию. Поскольку С++ - язык со строгим контролем
типов, общность информации возможна только при явном указании того,
что является общим в этих классах. Способом такого указания может
служить виртуальный базовый класс.
Виртуальный базовый класс можно использовать для представления
"головного" класса, который может конкретизироваться разными
способами:
class window {
// головная информация
virtual void draw();
};
Для простоты рассмотрим только один вид общей информации из класса
window - функцию draw(). Можно определять разные более развитые
классы, представляющие окна (window). В каждом определяется своя
(более развитая) функция рисования (draw):
class window_w_border : public virtual window {
// класс "окно с рамкой"
// определения, связанные с рамкой
void draw();
};
class window_w_menu : public virtual window {
// класс "окно с меню"
// определения, связанные с меню
void draw();
};
Теперь хотелось бы определить окно с рамкой и меню:
class window_w_border_and_menu
: public virtual window,
public window_w_border,
public window_w_menu {
// класс "окно с рамкой и меню"
void draw();
};
Каждый производный класс добавляет новые свойства окна. Чтобы
воспользоваться комбинацией всех этих свойств, мы должны
гарантировать, что один и тот же объект класса window используется
для представления вхождений базового класса window в эти
производные классы. Именно это обеспечивает описание window во
всех производных классах как виртуального базового класса.
Можно следующим образом изобразить состав объекта класса
window_w_border_and_menu:
Чтобы увидеть разницу между обычным и виртуальным наследованием,
сравните этот рисунок с рисунком из $$6.5, показывающим состав объекта
класса satellite. В графе наследования каждый базовый класс с данным
именем, который был указан как виртуальный, будет представлен
единственным объектом этого класса. Напротив, каждый базовый
класс, который при описании наследования не был указан как
виртуальный, будет представлен своим собственным объектом.
Теперь надо написать все эти функции draw(). Это не слишком
трудно, но для неосторожного программиста здесь есть ловушка.
Сначала пойдем самым простым путем, который как раз к ней и ведет:
void window_w_border::draw()
{
window::draw();
// рисуем рамку
}
void window_w_menu::draw()
{
window::draw();
// рисуем меню
}
Пока все хорошо. Все это очевидно, и мы следуем образцу определения
таких функций при условии единственного наследования ($$6.2.1), который
работал прекрасно. Однако, в производном классе следующего уровня
появляется ловушка:
void window_w_border_and_menu::draw() // ловушка!
{
window_w_border::draw();
window_w_menu::draw();
// теперь операции, относящиеся только
// к окну с рамкой и меню
}
На первый взгляд все вполне нормально. Как обычно, сначала выполняются
все операции, необходимые для базовых классов, а затем те, которые
относятся собственно к производным классам. Но в результате
функция window::draw() будет вызываться дважды! Для большинства
графических программ это не просто излишний вызов, а порча
картинки на экране. Обычно вторая выдача на экран затирает первую.
Чтобы избежать ловушки, надо действовать не так поспешно. Мы
отделим действия, выполняемые базовым классом, от действий,
выполняемых из базового класса. Для этого в каждом классе введем
функцию _draw(), которая выполняет нужные только для него
действия, а функция draw() будет выполнять те же действия плюс
действия, нужные для каждого базового класса. Для класса window
изменения сводятся к введению излишней функции:
class window {
// головная информация
void _draw();
void draw();
};
Для производных классов эффект тот же:
class window_w_border : public virtual window {
// класс "окно с рамкой"
// определения, связанные с рамкой
void _draw();
void draw();
};
void window_w_border::draw()
{
window::_draw();
_draw(); // рисует рамку
};
Только для производного класса следующего уровня проявляется
отличие функции, которое и позволяет обойти ловушку с повторным
вызовом window::draw(), поскольку теперь вызывается window::_draw()
и только один раз:
class window_w_border_and_menu
: public virtual window,
public window_w_border,
public window_w_menu {
void _draw();
void draw();
};
void window_w_border_and_menu::draw()
{
window::_draw();
window_w_border::_draw();
window_w_menu::_draw();
_draw(); // теперь операции, относящиеся только
// к окну с рамкой и меню
}
Не обязательно иметь обе функции window::draw() и window::_draw(),
но наличие их позволяет избежать различных простых описок.
В этом примере класс window служит хранилищем общей для
window_w_border и window_w_menu информации и определяет интерфейс
для общения этих двух классов. Если используется единственное
наследование, то общность информации в дереве классов достигается
тем, что эта информация передвигается к корню дерева до тех
пор, пока она не станет доступна всем заинтересованным в ней
узловым классам. В результате легко возникает неприятный эффект:
корень дерева или близкие к нему классы используются как пространство
глобальных имен для всех классов дерева, а иерархия классов вырождается
в множество несвязанных объектов.
Существенно, чтобы в каждом из классов-братьев переопределялись
функции, определенные в общем виртуальном базовом классе. Таким
образом каждый из братьев может получить свой вариант операций,
отличный от других. Пусть в классе window есть общая функция
ввода get_input():
class window {
// головная информация
virtual void draw();
virtual void get_input();
};
В одном из производных классов можно использовать эту функцию,
не задумываясь о том, где она определена:
class window_w_banner : public virtual window {
// класс "окно с заголовком"
void draw();
void update_banner_text();
};
void window_w_banner::update_banner_text()
{
// ...
get_input();
// изменить текст заголовка
}
В другом производном классе функцию get_input() можно определять,
не задумываясь о том, кто ее будет использовать:
class window_w_menu : public virtual window {
// класс "окно с меню"
// определения, связанные с меню
void draw();
void get_input(); // переопределяет window::get_input()
};
Все эти определения собираются вместе в производном классе следующего
уровня:
class window_w_banner_and_menu
: public virtual window,
public window_w_banner,
public window_w_menu
{
void draw();
};
Контроль неоднозначности позволяет убедиться, что в классах-братьях
определены разные функции:
class window_w_input : public virtual window {
// ...
void draw();
void get_input(); // переопределяет window::get_input
};
class window_w_input_and_menu
: public virtual window,
public window_w_input,
public window_w_menu
{ // ошибка: оба класса window_w_input и
// window_w_menu переопределяют функцию
// window::get_input
void draw();
};
Транслятор обнаруживает подобную ошибку, а устранить неоднозначность
можно обычным способом: ввести в классы window_w_input и
window_w_menu функцию, переопределяющую "функцию-нарушителя", и
каким-то образом устранить неоднозначность:
class window_w_input_and_menu
: public virtual window,
public window_w_input,
public window_w_menu
{
void draw();
void get_input();
};
В этом классе window_w_input_and_menu::get_input() будет
переопределять все функции get_input(). Подробно механизм разрешения
неоднозначности описан в $$R.10.1.1.
Член класса может быть частным (private), защищенным (protected)
или общим (public):
Частный член класса X могут использовать только функции-члены и
друзья класса X.
Защищенный член класса X могут использовать только функции-члены
и друзья класса X, а так же функции-члены и друзья всех
производных от X классов (см. $$5.4.1).
Общий член можно использовать в любой функции.
Эти правила соответствуют делению обращающихся к классу функций на три
вида: функции, реализующие класс (его друзья и члены), функции,
реализующие производный класс (друзья и члены производного класса) и
все остальные функции.
Контроль доступа применяется единообразно ко всем именам. На
контроль доступа не влияет, какую именно сущность обозначает имя.
Это означает, что частными могут быть функции-члены, константы и т.д.
наравне с частными членами, представляющими данные:
class X {
private:
enum { A, B };
void f(int);
int a;
};
void X::f(int i)
{
if (i<A) f(i+B);
a++;
}
void g(X& x)
{
int i = X::A; // ошибка: X::A частный член
x.f(2); // ошибка: X::f частный член
x.a++; // ошибка: X::a частный член
}
Дадим пример защищенных членов, вернувшись к классу window из
предыдущего раздела. Здесь функции _draw() предназначались только для
использования в производных классах, поскольку предоставляли неполный
набор возможностей, а поэтому не были достаточны удобны и
надежны для общего применения. Они были как бы строительным
материалом для более развитых функций. С другой стороны, функции draw()
предназначались для общего применения. Это различие можно выразить,
разбив интерфейсы классов window на две части - защищенный интерфейс
и общий интерфейс:
class window {
public:
virtual void draw();
// ...
protected:
void _draw();
// другие функции, служащие строительным материалом
private:
// представление класса
};
Такое разбиение можно проводить и в производных классах, таких, как
window_w_border или window_w_menu.
Префикс _ используется в именах защищенных функций, являющихся
частью реализации класса, по общему правилу: имена, начинающиеся с _ ,
не должны присутствовать в частях программы, открытых для общего
использования. Имен, начинающихся с двойного символа подчеркивания,
лучше вообще избегать (даже для членов).
Вот менее практичный, но более подробный пример:
class X {
// по умолчанию частная часть класса
int priv;
protected:
int prot;
public:
int publ;
void m();
};
Для члена X::m доступ к членам класса неограничен:
void X::m()
{
priv = 1; // нормально
prot = 2; // нормально
publ = 3; // нормально
}
Член производного класса имеет доступ только к общим и защищенным
членам:
class Y : public X {
void mderived();
};
Y::mderived()
{
priv = 1; // ошибка: priv частный член
prot = 2; // нормально: prot защищенный член, а
// mderived() член производного класса Y
publ = 3; // нормально: publ общий член
}
В глобальной функции доступны только общие члены:
void f(Y* p)
{
p->priv = 1; // ошибка: priv частный член
p->prot = 2; // ошибка: prot защищенный член, а f()
// не друг или член классов X и Y
p->publ = 3; // нормально: publ общий член
}
Подобно члену базовый класс можно описать как частный, защищенный
или общий:
class X {
public:
int a;
// ...
};
class Y1 : public X { };
class Y2 : protected X { };
class Y3 : private X { };
Поскольку X - общий базовый класс для Y1, в любой функции, если есть
необходимость, можно (неявно) преобразовать Y1* в X*, и притом
в ней будут доступны общие члены класса X:
void f(Y1* py1, Y2* py2, Y3* py3)
{
X* px = py1; // нормально: X - общий базовый класс Y1
py1->a = 7; // нормально
px = py2; // ошибка: X - защищенный базовый класс Y2
py2->a = 7; // ошибка
px = py3; // ошибка: X - частный базовый класс Y3
py3->a = 7; // ошибка
}
Теперь пусть описаны
class Y2 : protected X { };
class Z2 : public Y2 { void f(); };
Поскольку X - защищенный базовый класс Y2, только друзья и члены Y2,
а также друзья и члены любых производных от Y2 классов (в частности
Z2) могут при необходимости преобразовывать (неявно) Y2* в X*.
Кроме того они могут обращаться к общим и защищенным членам класса X:
void Z2::f(Y1* py1, Y2* py2, Y3* py3)
{
X* px = py1; // нормально: X - общий базовый класс Y1
py1->a = 7; // нормально
px = py2; // нормально: X - защищенный базовый класс Y2,
// а Z2 - производный класс Y2
py2->a = 7; // нормально
px = py3; // ошибка: X - частный базовый класс Y3
py3->a = 7; // ошибка
}
Наконец, рассмотрим:
class Y3 : private X { void f(); };
Поскольку X - частный базовый класс Y3, только друзья и члены Y3
могут при необходимости преобразовывать (неявно) Y3* в X*.
Кроме того они могут обращаться к общим и защищенным членам
класса X:
void Y3::f(Y1* py1, Y2* py2, Y3* py3)
{
X* px = py1; // нормально: X - общий базовый класс Y1
py1->a = 7; // нормально
px = py2; // ошибка: X - защищенный базовый класс Y2
py2->a = 7; // ошибка
px = py3; // нормально: X - частный базовый класс Y3,
// а Y3::f член Y3
py3->a = 7; // нормально
}
Если определить функции operator new() и operator delete(),
управление памятью для класса можно взять в свои руки. Это также можно,
(а часто и более полезно), сделать для класса, служащего базовым
для многих производных классов. Допустим, нам потребовались свои
функции размещения и освобождения памяти для класса employee ($$6.2.5)
и всех его производных классов:
class employee {
// ...
public:
void* operator new(size_t);
void operator delete(void*, size_t);
};
void* employee::operator new(size_t s)
{
// отвести память в `s' байтов
// и возвратить указатель на нее
}
void employee::operator delete(void* p, size_t s)
{
// `p' должно указывать на память в `s' байтов,
// отведенную функцией employee::operator new();
// освободить эту память для повторного использования
point neast() const;
point seast() const;
point nwest() const;
point swest() const;
void move(int a, int b)
{ w.x +=a; w.y +=b; e.x +=a; e.y +=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; }
};
Аналогично определяется прямоугольник:
class rectangle : public shape {
/* nw ------ n ----- ne
| |
| |
w c e
| |
| |
sw ------ s ----- se
*/
point sw, ne;
public:
point north() const { return point((sw.x+ne.x)/2,ne.y); }
point south() const { return point((sw.x+ne.x)/2,sw.y); }
point east() const;
point west() const;
point neast() const { return ne; }
point seast() const;
point nwest() const;
point swest() const { 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) {
if (a.y <= b.y) {
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, const shape* q); // поместить p над q
Функция обновления фигур нужна, чтобы работать с нашим примитивным
представлением экрана; она просто заново рисует все фигуры. Отметим,
что эта функция не имеет понятия, какие фигуры она рисует:
void shape_refresh()
{
screen_clear();
for (shape* p = shape::list; p; p=p->next) p->draw();
screen_refresh();
}
Наконец, есть одна действительно сервисная функция, которая рисует
одну фигуру над другой. Для этого она определяет юг (south()) одной
фигуры как раз над севером (north()) другой:
void stack(shape* p, const shape* q) // поместить p над q
{
point n = q->north();
point s = p->south();
p->move(n.x-s.x,n.y-s.y+1);
}
Представим теперь, что эта библиотека является собственностью
некоторой фирмы, продающей программы, и, что она продает только
заголовочный файл с определениями фигур и оттранслированные
определения функций. Все равно вы сможете определить новые фигуры,
воспользовавшись для этого купленными вами функциями.
Прикладная программа предельно проста. Определяется новая фигура
myshape (если ее нарисовать, то она напоминает лицо), а затем
приводится функция main(), в которой она рисуется со шляпой. Вначале
дадим описание фигуры 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) : rectangle(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()
по отдельности. В принципе с ними можно работать независимо от
объекта my_shape, к которому они принадлежат. Это один из способов
задания черт лица для строящегося иерархически объекта myshape.
Как это можно сделать иначе, видно из задания носа. Никакой тип "нос"
не определяется, он просто дорисовывается в функции draw():
void myshape::draw()
{
rectangle::draw();
int a = (swest().x+neast().x)/2;
int b = (swest().y+neast().y)/2;
put_point(point(a,b));
}
Движение фигуры myshape сводится к движению объекта базового класса
rectangle и к движению вторичных объектов (l_eye, r_eye и mouth):
void myshape::move(int a, int b)
{
rectangle::move(a,b);
l_eye->move(a,b);
r_eye->move(a,b);
mouth->move(a,b);
}
Наконец, определим несколько фигур и будем их двигать:
int main()
{
screen_init();
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();
screen_destroy();
return 0;
}
Вновь обратим внимание на то, что функции, подобные shape_refresh()
и stack(), работают с объектами, типы которых были определены
заведомо после определения этих функций (и, вероятно, после
их трансляции).
Вот получившееся лицо со шляпой:
***********
* *
* *
* *
* *
* *
* *
* *
***********
*****************
***********
* *
* ** ** *
* *
* * *
* *
* ******* *
* *
***********
Для упрощения примера копирование и удаление фигур не обсуждалось.
В $$1.5.3 и $$6.2.3 уже говорилось, что у класса может быть несколько
прямых базовых классов. Это значит, что в описании класса после :
может быть указано более одного класса. Рассмотрим задачу моделирования,
в которой параллельные действия представлены стандартной библиотекой
классов task, а сбор и выдачу информации обеспечивает библиотечный
класс displayed. Тогда класс моделируемых объектов (назовем его
satellite) можно определить так:
class satellite : public task, public displayed {
// ...
};
Такое определение обычно называется множественным наследованием.
Обратно, существование только одного прямого базового класса называется
единственным наследованием.
Ко всем определенным в классе satellite операциям добавляется
объединение операций классов task и displayed:
void f(satellite& s)
{
s.draw(); // displayed::draw()
s.delay(10); // task::delay()
s.xmit(); // satellite::xmit()
}
С другой стороны, объект типа satellite можно передавать функциям с
параметром типа task или displayed:
void highlight(displayed*);
void suspend(task*);
void g(satellite* p)
{
highlight(p); // highlight((displayed*)p)
suspend(p); // suspend((task*)p);
}
Очевидно, реализация этой возможности требует некоторого (простого)
трюка от транслятора: нужно функциям с параметрами task и
displayed передать разные части объекта типа satellite.
Для виртуальных функций, естественно, вызов и так выполнится
правильно:
class task {
// ...
virtual pending() = 0;
};
class displayed {
// ...
virtual void draw() = 0;
};
class satellite : public task, public displayed {
// ...
void pending();
void draw();
};
Здесь функции satellite::draw() и satellite::pending() для объекта
типа satellite будут вызываться так же, как если бы он был объектом типа
displayed или task, соответственно.
Отметим, что ориентация только на единственное наследование
ограничивает возможности реализации классов displayed, task и
satellite. В таком случае класс satellite мог бы быть task или
displayed, но не то и другое вместе (если, конечно, task не является
производным от displayed или наоборот). В любом случае теряется
гибкость.
Возможность иметь более одного базового класса влечет за собой
возможность неоднократного вхождения класса как базового. Допустим,
классы task и displayed являются производными класса link, тогда
в satellite он будет входить дважды:
class task : public link {
// link используется для связывания всех
// задач в список (список диспетчера)
// ...
};
class displayed : public link {
// link используется для связывания всех
// изображаемых объектов (список изображений)
// ...
};
Но проблем не возникает. Два различных объекта link используются
для различных списков, и эти списки не конфликтуют друг с другом.
Конечно, без риска неоднозначности нельзя обращаться к членам класса
link, но как это сделать корректно, показано в следующем разделе.
Графически объект satellite можно представить так:
Но можно привести примеры, когда общий базовый класс не должен
представляться двумя различными объектами (см. $$6.5.3).
Естественно, у двух базовых классов могут быть функции-члены
с одинаковыми именами:
class task {
// ...
virtual debug_info* get_debug();
};
class displayed {
// ...
virtual debug_info* get_debug();
};
При использовании класса satellite подобная неоднозначность функций
должна быть разрешена:
void f(satellite* sp)
{
debug_info* dip = sp->get_debug(); //ошибка: неоднозначность
dip = sp->task::get_debug(); // нормально
dip = sp->displayed::get_debug(); // нормально
}
Однако, явное разрешение неоднозначности хлопотно, поэтому
для ее устранения лучше всего определить новую функцию в
производном классе:
class satellite : public task, public derived {
// ...
debug_info* get_debug()
{
debug_info* dip1 = task:get_debug();
debug_info* dip2 = displayed::get_debug();
return dip1->merge(dip2);
}
};
Тем самым локализуется информация из базовых для satellite классов.
Поскольку satellite::get_debug() является переопределением функций
get_debug() из обоих базовых классов, гарантируется, что именно она
будет вызываться при всяком обращении к get_debug() для объекта
типа satellite.
Транслятор выявляет коллизии имен, возникающие при определении
одного и того же имени в более, чем одном базовом классе. Поэтому
программисту не надо указывать какое именно имя используется, кроме
случая, когда его использование действительно неоднозначно. Как правило
использование базовых классов не приводит к коллизии имен. В большинстве
случаев, даже если имена совпадают, коллизия не возникает, поскольку
имена не используются непосредственно для объектов производного класса.
Аналогичная проблема, когда в двух классах есть функции с одним
именем, но разным назначением, обсуждается в $$13.8 на примере
функции draw() для классов Window и Cowboy.
Если неоднозначности не возникает, излишне указывать имя
базового класса при явном обращении к его члену. В частности, если
множественное наследование не используется, вполне достаточно
использовать обозначение типа "где-то в базовом классе". Это
позволяет программисту не запоминать имя прямого базового класса и
спасает его от ошибок (впрочем, редких), возникающих при перестройке
иерархии классов. Например, в функции из $$6.2.5
void manager::print()
{
employee::print();
// ...
}
предполагается, что employee - прямой базовый класс для manager.
Результат этой функции не изменится, если employee окажется косвенным
базовым классом для manager, а в прямом базовом классе функции
print() нет. Однако, кто-то мог бы следующим образом перестроить
классы:
class employee {
// ...
virtual void print();
};
class foreman : public employee {
// ...
void print();
};
class manager : public foreman {
// ...
void print();
};
Теперь функция foreman::print() не будет вызываться, хотя почти
наверняка предполагался вызов именно этой функции. С помощью
небольшой хитрости можно преодолеть эту трудность:
class foreman : public employee {
typedef employee inherited;
// ...
void print();
};
class manager : public foreman {
typedef foreman inherited;
// ...
void print();
};
void manager::print()
{
inherited::print();
// ...
}
Правила областей видимости, в частности те, которые относятся к
вложенным типам, гарантируют, что возникшие несколько типов
inherited не будут конфликтовать друг с другом. В общем-то дело
вкуса, считать решение с типом inherited наглядным или нет.
В предыдущих разделах множественное наследование рассматривалось
как существенный фактор, позволяющий за счет слияния классов
безболезненно интегрировать независимо создававшиеся программы.
Это самое основное применение множественного наследования, и,
к счастью (но не случайно), это самый простой и надежный способ
его применения.
Иногда применение множественного наследования предполагает
достаточно тесную связь между классами, которые рассматриваются
как "братские" базовые классы. Такие классы-братья обычно должны
проектироваться совместно. В большинстве случаев для этого не
требуется особый стиль программирования, существенно отличающийся
от того, который мы только что рассматривали. Просто на производный
класс возлагается некоторая дополнительная работа. Обычно она
сводится к переопределению одной или нескольких виртуальных
функций (см. $$13.2 и $$8.7). В некоторых случаях классы-братья
должны иметь общую информацию. Поскольку С++ - язык со строгим контролем
типов, общность информации возможна только при явном указании того,
что является общим в этих классах. Способом такого указания может
служить виртуальный базовый класс.
Виртуальный базовый класс можно использовать для представления
"головного" класса, который может конкретизироваться разными
способами:
class window {
// головная информация
virtual void draw();
};
Для простоты рассмотрим только один вид общей информации из класса
window - функцию draw(). Можно определять разные более развитые
классы, представляющие окна (window). В каждом определяется своя
(более развитая) функция рисования (draw):
class window_w_border : public virtual window {
// класс "окно с рамкой"
// определения, связанные с рамкой
void draw();
};
class window_w_menu : public virtual window {
// класс "окно с меню"
// определения, связанные с меню
void draw();
};
Теперь хотелось бы определить окно с рамкой и меню:
class window_w_border_and_menu
: public virtual window,
public window_w_border,
public window_w_menu {
// класс "окно с рамкой и меню"
void draw();
};
Каждый производный класс добавляет новые свойства окна. Чтобы
воспользоваться комбинацией всех этих свойств, мы должны
гарантировать, что один и тот же объект класса window используется
для представления вхождений базового класса window в эти
производные классы. Именно это обеспечивает описание window во
всех производных классах как виртуального базового класса.
Можно следующим образом изобразить состав объекта класса
window_w_border_and_menu:
Чтобы увидеть разницу между обычным и виртуальным наследованием,
сравните этот рисунок с рисунком из $$6.5, показывающим состав объекта
класса satellite. В графе наследования каждый базовый класс с данным
именем, который был указан как виртуальный, будет представлен
единственным объектом этого класса. Напротив, каждый базовый
класс, который при описании наследования не был указан как
виртуальный, будет представлен своим собственным объектом.
Теперь надо написать все эти функции draw(). Это не слишком
трудно, но для неосторожного программиста здесь есть ловушка.
Сначала пойдем самым простым путем, который как раз к ней и ведет:
void window_w_border::draw()
{
window::draw();
// рисуем рамку
}
void window_w_menu::draw()
{
window::draw();
// рисуем меню
}
Пока все хорошо. Все это очевидно, и мы следуем образцу определения
таких функций при условии единственного наследования ($$6.2.1), который
работал прекрасно. Однако, в производном классе следующего уровня
появляется ловушка:
void window_w_border_and_menu::draw() // ловушка!
{
window_w_border::draw();
window_w_menu::draw();
// теперь операции, относящиеся только
// к окну с рамкой и меню
}
На первый взгляд все вполне нормально. Как обычно, сначала выполняются
все операции, необходимые для базовых классов, а затем те, которые
относятся собственно к производным классам. Но в результате
функция window::draw() будет вызываться дважды! Для большинства
графических программ это не просто излишний вызов, а порча
картинки на экране. Обычно вторая выдача на экран затирает первую.
Чтобы избежать ловушки, надо действовать не так поспешно. Мы
отделим действия, выполняемые базовым классом, от действий,
выполняемых из базового класса. Для этого в каждом классе введем
функцию _draw(), которая выполняет нужные только для него
действия, а функция draw() будет выполнять те же действия плюс
действия, нужные для каждого базового класса. Для класса window
изменения сводятся к введению излишней функции:
class window {
// головная информация
void _draw();
void draw();
};
Для производных классов эффект тот же:
class window_w_border : public virtual window {
// класс "окно с рамкой"
// определения, связанные с рамкой
void _draw();
void draw();
};
void window_w_border::draw()
{
window::_draw();
_draw(); // рисует рамку
};
Только для производного класса следующего уровня проявляется
отличие функции, которое и позволяет обойти ловушку с повторным
вызовом window::draw(), поскольку теперь вызывается window::_draw()
и только один раз:
class window_w_border_and_menu
: public virtual window,
public window_w_border,
public window_w_menu {
void _draw();
void draw();
};
void window_w_border_and_menu::draw()
{
window::_draw();
window_w_border::_draw();
window_w_menu::_draw();
_draw(); // теперь операции, относящиеся только
// к окну с рамкой и меню
}
Не обязательно иметь обе функции window::draw() и window::_draw(),
но наличие их позволяет избежать различных простых описок.
В этом примере класс window служит хранилищем общей для
window_w_border и window_w_menu информации и определяет интерфейс
для общения этих двух классов. Если используется единственное
наследование, то общность информации в дереве классов достигается
тем, что эта информация передвигается к корню дерева до тех
пор, пока она не станет доступна всем заинтересованным в ней
узловым классам. В результате легко возникает неприятный эффект:
корень дерева или близкие к нему классы используются как пространство
глобальных имен для всех классов дерева, а иерархия классов вырождается
в множество несвязанных объектов.
Существенно, чтобы в каждом из классов-братьев переопределялись
функции, определенные в общем виртуальном базовом классе. Таким
образом каждый из братьев может получить свой вариант операций,
отличный от других. Пусть в классе window есть общая функция
ввода get_input():
class window {
// головная информация
virtual void draw();
virtual void get_input();
};
В одном из производных классов можно использовать эту функцию,
не задумываясь о том, где она определена:
class window_w_banner : public virtual window {
// класс "окно с заголовком"
void draw();
void update_banner_text();
};
void window_w_banner::update_banner_text()
{
// ...
get_input();
// изменить текст заголовка
}
В другом производном классе функцию get_input() можно определять,
не задумываясь о том, кто ее будет использовать:
class window_w_menu : public virtual window {
// класс "окно с меню"
// определения, связанные с меню
void draw();
void get_input(); // переопределяет window::get_input()
};
Все эти определения собираются вместе в производном классе следующего
уровня:
class window_w_banner_and_menu
: public virtual window,
public window_w_banner,
public window_w_menu
{
void draw();
};
Контроль неоднозначности позволяет убедиться, что в классах-братьях
определены разные функции:
class window_w_input : public virtual window {
// ...
void draw();
void get_input(); // переопределяет window::get_input
};
class window_w_input_and_menu
: public virtual window,
public window_w_input,
public window_w_menu
{ // ошибка: оба класса window_w_input и
// window_w_menu переопределяют функцию
// window::get_input
void draw();
};
Транслятор обнаруживает подобную ошибку, а устранить неоднозначность
можно обычным способом: ввести в классы window_w_input и
window_w_menu функцию, переопределяющую "функцию-нарушителя", и
каким-то образом устранить неоднозначность:
class window_w_input_and_menu
: public virtual window,
public window_w_input,
public window_w_menu
{
void draw();
void get_input();
};
В этом классе window_w_input_and_menu::get_input() будет
переопределять все функции get_input(). Подробно механизм разрешения
неоднозначности описан в $$R.10.1.1.
Член класса может быть частным (private), защищенным (protected)
или общим (public):
Частный член класса X могут использовать только функции-члены и
друзья класса X.
Защищенный член класса X могут использовать только функции-члены
и друзья класса X, а так же функции-члены и друзья всех
производных от X классов (см. $$5.4.1).
Общий член можно использовать в любой функции.
Эти правила соответствуют делению обращающихся к классу функций на три
вида: функции, реализующие класс (его друзья и члены), функции,
реализующие производный класс (друзья и члены производного класса) и
все остальные функции.
Контроль доступа применяется единообразно ко всем именам. На
контроль доступа не влияет, какую именно сущность обозначает имя.
Это означает, что частными могут быть функции-члены, константы и т.д.
наравне с частными членами, представляющими данные:
class X {
private:
enum { A, B };
void f(int);
int a;
};
void X::f(int i)
{
if (i<A) f(i+B);
a++;
}
void g(X& x)
{
int i = X::A; // ошибка: X::A частный член
x.f(2); // ошибка: X::f частный член
x.a++; // ошибка: X::a частный член
}
Дадим пример защищенных членов, вернувшись к классу window из
предыдущего раздела. Здесь функции _draw() предназначались только для
использования в производных классах, поскольку предоставляли неполный
набор возможностей, а поэтому не были достаточны удобны и
надежны для общего применения. Они были как бы строительным
материалом для более развитых функций. С другой стороны, функции draw()
предназначались для общего применения. Это различие можно выразить,
разбив интерфейсы классов window на две части - защищенный интерфейс
и общий интерфейс:
class window {
public:
virtual void draw();
// ...
protected:
void _draw();
// другие функции, служащие строительным материалом
private:
// представление класса
};
Такое разбиение можно проводить и в производных классах, таких, как
window_w_border или window_w_menu.
Префикс _ используется в именах защищенных функций, являющихся
частью реализации класса, по общему правилу: имена, начинающиеся с _ ,
не должны присутствовать в частях программы, открытых для общего
использования. Имен, начинающихся с двойного символа подчеркивания,
лучше вообще избегать (даже для членов).
Вот менее практичный, но более подробный пример:
class X {
// по умолчанию частная часть класса
int priv;
protected:
int prot;
public:
int publ;
void m();
};
Для члена X::m доступ к членам класса неограничен:
void X::m()
{
priv = 1; // нормально
prot = 2; // нормально
publ = 3; // нормально
}
Член производного класса имеет доступ только к общим и защищенным
членам:
class Y : public X {
void mderived();
};
Y::mderived()
{
priv = 1; // ошибка: priv частный член
prot = 2; // нормально: prot защищенный член, а
// mderived() член производного класса Y
publ = 3; // нормально: publ общий член
}
В глобальной функции доступны только общие члены:
void f(Y* p)
{
p->priv = 1; // ошибка: priv частный член
p->prot = 2; // ошибка: prot защищенный член, а f()
// не друг или член классов X и Y
p->publ = 3; // нормально: publ общий член
}
Подобно члену базовый класс можно описать как частный, защищенный
или общий:
class X {
public:
int a;
// ...
};
class Y1 : public X { };
class Y2 : protected X { };
class Y3 : private X { };
Поскольку X - общий базовый класс для Y1, в любой функции, если есть
необходимость, можно (неявно) преобразовать Y1* в X*, и притом
в ней будут доступны общие члены класса X:
void f(Y1* py1, Y2* py2, Y3* py3)
{
X* px = py1; // нормально: X - общий базовый класс Y1
py1->a = 7; // нормально
px = py2; // ошибка: X - защищенный базовый класс Y2
py2->a = 7; // ошибка
px = py3; // ошибка: X - частный базовый класс Y3
py3->a = 7; // ошибка
}
Теперь пусть описаны
class Y2 : protected X { };
class Z2 : public Y2 { void f(); };
Поскольку X - защищенный базовый класс Y2, только друзья и члены Y2,
а также друзья и члены любых производных от Y2 классов (в частности
Z2) могут при необходимости преобразовывать (неявно) Y2* в X*.
Кроме того они могут обращаться к общим и защищенным членам класса X:
void Z2::f(Y1* py1, Y2* py2, Y3* py3)
{
X* px = py1; // нормально: X - общий базовый класс Y1
py1->a = 7; // нормально
px = py2; // нормально: X - защищенный базовый класс Y2,
// а Z2 - производный класс Y2
py2->a = 7; // нормально
px = py3; // ошибка: X - частный базовый класс Y3
py3->a = 7; // ошибка
}
Наконец, рассмотрим:
class Y3 : private X { void f(); };
Поскольку X - частный базовый класс Y3, только друзья и члены Y3
могут при необходимости преобразовывать (неявно) Y3* в X*.
Кроме того они могут обращаться к общим и защищенным членам
класса X:
void Y3::f(Y1* py1, Y2* py2, Y3* py3)
{
X* px = py1; // нормально: X - общий базовый класс Y1
py1->a = 7; // нормально
px = py2; // ошибка: X - защищенный базовый класс Y2
py2->a = 7; // ошибка
px = py3; // нормально: X - частный базовый класс Y3,
// а Y3::f член Y3
py3->a = 7; // нормально
}
Если определить функции operator new() и operator delete(),
управление памятью для класса можно взять в свои руки. Это также можно,
(а часто и более полезно), сделать для класса, служащего базовым
для многих производных классов. Допустим, нам потребовались свои
функции размещения и освобождения памяти для класса employee ($$6.2.5)
и всех его производных классов:
class employee {
// ...
public:
void* operator new(size_t);
void operator delete(void*, size_t);
};
void* employee::operator new(size_t s)
{
// отвести память в `s' байтов
// и возвратить указатель на нее
}
void employee::operator delete(void* p, size_t s)
{
// `p' должно указывать на память в `s' байтов,
// отведенную функцией employee::operator new();
// освободить эту память для повторного использования