
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf700Часть IV ^ Расширенное использование С+Ф
ане уточнение отдельного базового класса. Каждый родительский класс вносит свои элементы в производный класс. Производный класс является объединением базовых возможностей.
Примерами использования множественного наследования являются графиче ские объекты, счета NOW и классы lost ream в стандартной библиотеке C+ + .
Для графического пакета классы Shape и Position использовались как базовые классы для создания класса Object. Объекты класса Object объединили свойства объектов Shape и Position. Это пример неразумного применения множественного наследования. Графические объекты являются фигурами, но трудно утверждать, что они представляют собой положения. Скорее можно говорить о том, что графи ческий объект располагается в каком-то месте.
Для счетов NOW классы представляют собой сберегательные и текущие счета. Это лучший пример использования множественного наследования. Счет NOW действительно объединяет свойства сберегательных и текущих счетов. По нему выплачиваются проценты и разрешается выписывать чеки. Однако если рас спросить служащего банка, можно узнать, что бывают исключительные ситуации, когда счет NOW отличается как от сберегательного, так и от текущего счета. Это означает, что преимущества легкого слияния основных характеристик компенси руются недостатками подавления свойств, которые не соответствуют друг другу.
Для библиотеки C + + класса iost ream имеет смысл использовать множествен ное наследование для слияния характеристик классов входных и выходных пото ков. Полученные в результате классы iost ream поддерживают как операции ввода, так и операции вывода, а в производных классах ничего не требуется подавлять.
Обратите внимание на то, что C + + не устанавливает ограничения на количе ство базовых классов, которые могут участвовать в формировании производного класса. Все приведенные примеры включают только два базовых класса. Сложно придумать примеры множественного наследования с тремя или четырьмя базовы ми классами так, чтобы они имели смысл и не запутывали пользователя. Почему два лучше, чем три или четыре? Кажется, что примеры множественного наследо вания с двумя базовыми классами так же трудно понять.
Именно поэтому рекомендуется использовать множественное наследование осторожно. Имейте в виду, что существуют способы поддержки клиентской про граммы без использования множественного наследования.
Множественное наследование: правила доступа
При множественном наследовании производный класс наследует элементы данных всех базовых классов и все функции-члены этих классов. Область па мяти, которую занимает объект производного класса, представляет собой сумму пространства, занимаемого в памяти объектами базовых классов (возможно, с учетом выравнивания).
Правила доступа для множественного наследования те же, что и для простого наследования. Доступ к методам класса Derived могут осуществлять общедоступ ные и защищенные члены всех других базовых классов без каких-либо ограниче ний. Вы не имеете доступ к закрытым членам базовых классов.
Связи наследования могут быть общедоступными, защищенными или закры тыми. В любом случае все элементы данных и функции-члены базовых классов наследуются производным классом. Однако в зависимости от способа порождения могут изменяться права доступа.
Способы порождения для множественного наследования те же, что и для про стого наследования. При открытом поро>вдении каждый закрытый, защищенный и общедоступный член базового класса имеет те же права доступа в объектах производного класса, что и в базовом объекте. Это наиболее естественный ре жим наследования.
702 |
Часть W ^ 9<лсиь1фенно^ ^к>"сд^: "\-г^\^ ^ле С-^^ |
Преобразования классов
Правила преобразования для множественного наследования и простого насле дования подобны. Если базовый класс наследуется из общедоступного класса, то объекты производного класса могут быть неявно преобразованы в объекты этого базового класса. Для такого преобразования оператор явного приведения не тре буется.
Объект производного класса располагает всеми возможностями, данными и функциями объектов базовых классов. Преобразование из производного объек та в базовый объект не может привести в результате к потере возможностей. Это может произойти в случае, если способ порождения не является общедоступным.
В1 Ы; |
В2 Ь2; Derived d; |
|
В1 = d; |
Ь2 = d; |
//OK; дополнительные возможности отвергаются |
d = Ы; |
d = Ь2; |
/ / ошибка: несогласованное состояние объекта |
Преобразование из базового класса в производный класс не допускается. Ба зовый объект содержит только часть данных и возможностей, которыми распола гает производный объект, а пропущенные возможности не могут быть добавлены. Такое преобразование не безопасно.
Подобные же правила применяются к указателям и ссылкам. Указатель (ссыл ка) базового класса может безопасно указывать на объект производного класса. Объект производного класса может выполнять все то же, что и базовый указа тель. Это безопасно. Однако базовый указатель может вызвать любую часть возможностей производного объекта.
81 *р1; В2 *р2; |
Derived *d; |
|
|
|
Р1 = new Derived; р2 = new Derived; |
/ / |
OK: безопасно |
|
|
d = new B1; d = new B2; |
/ / |
синтаксические |
ошибки |
|
d = p1; d = p2; |
|
/ / |
синтаксические |
ошибки |
d = (Derived*) |
p1; |
/ / |
OK: явное приведение |
Указатель производного класса не должен указывать на базовый объект (третья строка примера). В базовом объекте отсутствуют многие возможности, имеющиеся у производного объекта, которые доступны через указатель произ водного класса. Чтобы избежать ошибок во время выполнения, компилятор объявляет этот код синтаксической ошибкой.
Подобным образом базовый указатель (который, по-видимому, указывает на базовый объект) не может быть скопирован в указатель производного класса (четвертая строка примера). Это не безопасно. Производный указатель может по требовать сервисы, которые базовый объект не в состоянии выполнить, а компи лятор не может за этим проследить. Следовательно, манипулирование указателем также рассматривается как синтаксическая ошибка.
Как поступить, если известно, что базовый указатель ссылается на объект производного класса, а не на базовый объект? Укажите компилятору, что вам известно, что вы делаете с помощью приведения типов.
Эти же правила применяются к передаче параметра. Если функция ожидает указатель (или ссылку) на один из базовых классов, то безопаснее вызвать эту функцию, передавая ей адрес производного объекта.
void fool (В1 *b1) |
/ / |
производные объекты содержат |
||
{ |
b1->fl(); |
} |
/ / |
дополнительные свойства |
|
|
|||
void |
foo2 (В2 *Ь2) |
/ / |
производные объекты содержат |
|
|
|
|
/ / |
дополнительные свойства |
{ b2->f2(); |
} |
|
|
Глава 15 • Виртуальные функции и использование наследования
void foo(Derivecl *с1) |
/ / |
базовые объекты не могут выполнить это |
||
{ d->f3(); } |
|
|
|
|
В1 *Ь1 = new Derived; |
В2 *Ь2 = new Derived; |
|
||
Derived d; |
|
|
|
|
Foo1(&d); |
foo2(&d); |
/ / |
оба - OK: безопасное преобразование |
|
foo(b1); |
foo(b2); |
/ / |
синтаксические |
ошибки: опасное преобразование |
foo((Derived*)b1); foo((Derived*)b2); |
/ / передается на свой риск |
В последнем примере функции fool () и foo2() могут принять объекты Derived как фактические аргументы, поскольку внутри этих функций параметры отвечают только на базовые сообщения (f1() и f2()), а производные объекты — на эти сервисы. Функция foo() не может принимать базовые указатели, потому что внутри нее их параметр должен отвечать на сообщения производного класса f 3(), а базовые объекты этого не могут сделать. С другой стороны, указатели Ь1 и Ь2 ссылаются на объекты класса Derived, которые выполняют такое задание. Чтобы сообщить это компилятору, последняя строка программы, приведенной выше, выполняет явное приведение указателя Base в указатель Derived.
В закрытом или защищенном режиме наследования не допускаются неявные преобразования из объектов производного класса в объекты базового класса. Даже в этом "безопасном" случае требуется явное приведение в клиентской про грамме. Преобразование из любого базового класса в производный класс требует явного приведения для любого вида множественного наследования.
Множественное наследование: конструкторы и деструкторы
Производный класс отвечает за состояние своих компонентов, унаследованных от базовых классов. Как и в простом наследовании, конструкторы базового класса вызываются, когда строится объект производного класса.
Механизм передачи параметров конструкторам базового класса подобен меха низму для простого наследования. Должен использоваться список инициализации элементов. В следующем примере базовый класс В1 содержит один элемент дан ных, базовый класс В2 — другой элемент данных, а производный класс — еще один элемент данных (динамически выделенный массив символов). Класс Derived должен обеспечить для конструктора три параметра, чтобы он мог передать данные своим компонентам В1 и В2 и собственному элементу данных.
class В1 { int m1;
public: B1(int) ;
void fl(); . . . . };
class B2 { double m2;
public:
B2(double);
void f 2 ( ) ; . . . . };
class Derived: public B1, public B2 { char* t;
public:
Derived(const char*, double,int); "DerivedO;
void f3(); ... };
704 |
Часть IV # Раситреииое использование С^--*- |
Если список инициализации элементов не предусматривается, то вызывается конструктор Base по умолчанию. Если базовые классы не имеют в виду конструк торы по умолчанию, то это синтаксическая ошибка.
В списке инициализации элементов конструктор класса Derived вызывает базовые конструкторы, используя имена классов В1 и В2 в последовательности вызовов конструкторов, разделенных запятыми. Имена параметров для базовых конструкторов обычно поступают из списков параметров конструктора Derived.
Derived: :Derived(const |
char *s, doubled, int i ) |
: B1(i),B2(d) |
||
{ i f ( ( t = new char[strlen(s)+l] ) |
== NULL) |
|
||
{ cout « |
"\nOut |
of memory\n"; |
e x i t ( l ) ; |
} |
strcpy(t,s); |
} |
|
|
|
Bee конструкторы базового класса вызываются до вызова конструкторов про изводного класса. Располагаются они в том порядке, в котором базовые классы перечислены в объявлении производного класса.
Подобно простому наследованию, элементы наборов данных производного класса могут инициализироваться либо в теле конструктора производного класса, либо в списке инициализации элементов.
При уничтожении объекта производного класса (динамически или при выходе из области видимости) вначале вызывается деструктор производного класса, а за тем деструкторы базового класса в порядке, обратном вызову конструкторов.
Множественное наследование: неоднозначность
Использование множественного наследования может привести к конфликтам имен. Если производный класс содержит элемент данных или функцию с тем же именем, что и один из базовых классов, то сервис базового класса скрывается именем, определенным в производном классе.
В следующем примере класс Derived содержит элемент данных х с тем же име нем, что и элемент данных в базовом классе В1. Кроме того, класс Derived имеет функцию-член f2() с тем же именем, что и функция-член в базовом классе В2.
class В1 { |
|
|
protected: |
|
|
int х; |
|
/ / скрыто Derived::х |
public: |
|
|
void f1(); |
. . . } ; |
|
class В2 { |
|
|
public: |
. . . } ; |
// скрыто Derived::f2() |
void f2(); |
||
class Derived: public B1, public B2 { |
||
protected: |
|
// скрывает B1::x |
float x; |
|
|
public: |
|
// скрывает B2::f2() |
void f2(); |
|
|
void f3() |
|
|
{ X = 0; |
}. . .. }; |
/ / используется Derived: :x |
В этом примере объект класса Derived содержит два элемента данных х; эле мент данных, наследованный из В1, скрывается в Derived; функция-член f2(), на следованная из В2, скрывается добавленной функцией f2().
Как клиентская программа, так и программа класса Derived может подменять правила области видимости, используя .явный оператор области действия.
void Derived::f3() |
|
{ Bl: :х = 0; } |
/ / игнорируя Derived::х |
706 |
Часть IV * Расширенное использование С^+ |
Если два или более базовых классов содержат элемент данных с одинаковым именем, то объект производного класса включает обе копии. В результате вы столкнетесь с неоднозначностью.
class |
В1 { |
|
|
protected: |
|
|
|
int |
m; |
. |
• |
public: |
|
|
|
B K i n t ) ; |
|
|
|
void |
f ( ) ; ... }; |
|
|
class |
B2 { |
|
|
protected: |
|
|
|
double m; |
|
|
|
public: |
|
|
|
B2(double); |
|
|
|
void |
f ( ) ; ... |
}; |
|
class Derived |
: public B1, public B2 { |
|
char* t; |
|
|
public: |
|
|
Derived (char*,double,int); |
|
|
void f3() { |
cout « "m=" « m « endl; } |
/ / двусмысленное выражение |
Конфликты между именами элементов данных должны разрешаться производ ным классом, чтобы избежать неоднозначности и защитить клиентскую програм му. Используйте оператор области действия.
void Derived::f3() |
|
{ cout « "m=" « B1: :m « endl; } |
/ / двусмысленность отсутствует |
Множественное наследование: ориентированный граф
Это наиболее хитрая форма неоднозначности, возникающая, когда базовый класс наследуется более чем из одного класса. Как правило, C++ против этого, а класс может явно появиться только один раз в списке происхождения для произ водного класса.
class В { public:
int m; . . . .} ;
class Derived: public B, public В |
/ / синтаксическая ошибка |
{. . . . } ;
Вэтом примере класс объявляется синтаксической ошибкой. Однако такой же класс может появляться несколько раз в иерархии наследования. Разные базовые классы могут иметь обш.ие скобки. Подобные скобки появляются несколько раз
впорождениях, и их данные в производных классах будут содержать несколько копий.
class В1 |
: public |
В { |
/ / класс В приводится выше |
protected: |
|
|
|
int mem; |
|
|
|
public: |
|
|
|
void |
f1(); ... |
}; |
|
Глава 15 • Виртуальные функции и использование наследования |
| 7U7 |
||||
class В2 : public |
В { |
/ / |
класс В приводится выше |
||
protected: |
|
|
|
|
|
int mem; |
|
|
|
|
|
public: |
|
|
|
|
|
void f2(); |
... |
}; |
|
|
|
class Derived |
: public B1, public B2 { |
/ / |
унаследовано из В дважды |
||
public: |
|
|
|
|
|
void f3(); |
. . |
. } ; |
|
|
|
В этой структуре класс Derived содержит два элемента наборов данных с име нем mem, унаследованным из разных базовых классов. Имена у них одинаковые, но они указывают на разные положения в памяти. Их роли в программе также от личаются: они происходят из разных классов. Эту проблему не следует переносить на клиента.
Ситуация с элементом данных m намного хуже. Каждый объект класса Derived располагает двумя экземплярами этого элемента данных. Один унаследован через класс В1, а другой через класс В2. Пространство, которое требуется* для несколь ких экземпляров одного и того же базового элемента данных, тратится напрасно. Эти два элемента данных также функционально одинаковы — они происходят из одного и того же класса, но один из них обслуживает части В1 класса Derived,
авторой — части 82 класса Derived.
Вязыке C-f-h предлагается интересное решение этой задачи. Программисту предоставляется возможность явно указать, что использование двух (или более) копий этих же данных и функций нежелательно. Хотелось бы, чтобы это был случай по умолчанию. Это оговаривается путем определения базовых классов виртуальными базовыми классами. Зарезервированное слово virtual модифи цирует объявления производных классов, которые позже используются во мно жественном наследовании,
class |
В { |
|
/ / |
общий базовый класс |
|
|
int m; |
|
|
|
|
|
|
public: |
|
|
|
|
|
|
void |
f ( ) ; . . . } ; |
|
|
|
|
|
class |
81 : v i r t u a l |
public В { |
/ / |
виртуальный |
базовый |
класс |
protected: |
|
|
|
|
|
|
int mem; |
|
|
|
|
|
|
public: |
|
|
|
|
|
|
void f 1 ( ) ; . . . |
}; |
|
|
|
|
|
class 82 : virtual public 8 { |
// виртуальный |
базовый |
класс |
|||
protected: |
|
|
|
|
|
|
int mem; |
|
|
|
|
|
|
public: |
}; |
|
|
|
|
|
void f2(); ... |
|
|
|
|
||
class Derived : public 81, public 82 { |
// работает как волшебник |
|||||
public: |
}; |
|
|
|
|
|
void f3(); ... |
|
|
|
|
Теперь класс Derived содержит только одну копию данных и функций, на следованных из класса В. Обратите внимание, что в классе Derived именно про ектировщик его базовых классов В1 и В2 должен определить эти классы как виртуальные. Это поставит под сомнение принцип, что базовые классы не знают своих производных классов и лишь производные классы уверены в своих базовых классах.
Глава 15 • Виртуальные функции и использование носдвдования |
I 709 Р |
Итоги
В этой главе рассмотрены примеры расширенного использования наследова ния. Все они касались некоторых обидих возможностей базовых и производных классов. В каких-то случаях объекты одного класса могут использоваться вместо объектов другого класса.
Показано, что использование объекта производного класса в случае, когда ожидается объект базового класса, всегда безопасно. Такое преобразование безо пасно, но не очень интересно. Эти объекты должны будут выполнить только то, что может сделать базовый объект, а объект производного класса способен на большее.
Помните, что указатель базового класса можно использовать, когда ожидается указатель производного класса. То есть можно указывать объекты производного класса, используя указатели базового класса.
В языках программирования это всегда было проблемой. Все совокупности объектов, которые поддерживаются современными языками, являются однород ными. Массивы С+ 4- не могут содержать компоненты различных классов. Связанные списки С4-+ не могут использовать узлы разных типов. И только на следование позволяет применять совокупности объектов разных классов. Такие классы не являются совершенно разными. Неоднородные списки не содержат объекты произвольных классов, но могут включать объекты классов, связанные наследованием.
При обработке неоднородной совокупности объектов (связанных наследова нием) объектам в совокупности отправляются четыре типа сообпдений.
•Сообш,ения, на которые может ответить каждый объект
всовокупности объектов. Методы, определенные в базовом классе иерархии наследования, которые не перезаписываются
впроизводных классах.
•Сообнхения, на которые могут ответить только некоторые объекты
всовокупности объектов. Методы, определенные в производных классах
иерархии наследования, за исключением сообш^ений с теми же именами
вбазовом классе.
•Сообш,ения, на которые могут ответить объекты с именами всех типов
всовокупности объектов, но определенные в базовых и производных классах как невиртуальные функции (с тем же самым или
сдругим интерфейсом).
•Сообш.ения, на которые могут ответить все виды объектов в совокупности объектов, определенные как виртуальные функции, используюш,ие один и тот же интерфейс как в базовом, так и в производных классах.
Для доступа к первому типу сообш,ения используйте указатель базового класса. Когда доступ к объекту осуш^ествляется из совокупности объектов, не требуется никаких преобразований.
Отправить второй вид сообш.ений можно с помои;ью указателей производного класса. Когда объект берется из совокупности объектов, базовый указатель должен преобразовываться в указатели того класса, к которому принадлежит объект. Только тогда будут доступны сообщения второго вида. Это преобразова ние не является безопасным, и необходимо хорошо представлять себе, что проис ходит, потому что компилятор не в состоянии защитить вас.
Третий вид сообщений также требует преобразования, если объект должен отвечать на сообщение, определенное в производном классе. Сообщение базового класса скрывается сообщением производного класса.