Виртуальные базовые классы




В предыдущих разделах множественное наследование рассматривалось как существенный фактор, позволяющий за счет слияния классов безболезненно интегрировать независимо создававшиеся программы. Это самое основное применение множественного наследования, и, к счастью (но не случайно), это самый простой и надежный способ его применения. Иногда применение множественного наследования предполагает достаточно тесную связь между классами, которые рассматриваются как "братские" базовые классы. Такие классы-братья обычно должны проектироваться совместно. В большинстве случаев для этого не требуется особый стиль программирования, существенно отличающийся от того, который мы только что рассматривали. Просто на производный класс возлагается некоторая дополнительная работа. Обычно она сводится к переопределению одной или нескольких виртуальных функций (см. $$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 общий член }


Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2016-02-16 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: