straustrup2 (852740), страница 46
Текст из файла (страница 46)
В большинстве случаев для этого не требуется особый стильпрограммирования, существенно отличающийся от того, который мы только что рассматривали. Простона производный класс возлагается некоторая дополнительная работа. Обычно она сводится кпереопределению одной или нескольких виртуальных функций (см. $$13.2 и $$8.7).
В некоторыхслучаях классы-братья должны иметь общую информацию. Поскольку С++ - язык со строгим контролемтипов, общность информации возможна только при явном указании того, что является общим в этихклассах. Способом такого указания может служить виртуальный базовый класс.Виртуальный базовый класс можно использовать для представления "головного" класса, который можетконкретизироваться разными способами:class window {// головная информацияvirtual void draw();};Для простоты рассмотрим только один вид общей информации из класса window - функцию draw().Можно определять разные более развитые классы, представляющие окна (window).
В каждомопределяется своя (более развитая) функция рисования (draw):class window_w_border : public virtual window {// класс "окно с рамкой"// определения, связанные с рамкойvoid draw();};167Бьерн Страуструп.Язык программирования С++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() будет вызываться дважды! Для большинства графическихпрограмм это не просто излишний вызов, а порча картинки на экране. Обычно вторая выдача на экранзатирает первую.Чтобы избежать ловушки, надо действовать не так поспешно.
Мы отделим действия, выполняемые168Бьерн Страуструп.Язык программирования С++базовым классом, от действий, выполняемых из базового класса. Для этого в каждом классе введемфункцию _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 {169Бьерн Страуструп.Язык программирования С++// головная информация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_inputvoid draw();};Транслятор обнаруживает подобную ошибку, а устранить неоднозначность можно обычным способом:ввести в классы window_w_input и window_w_menu функцию, переопределяющую "функциюнарушителя", и каким-то образом устранить неоднозначность:class window_w_input_and_menu170Бьерн Страуструп.Язык программирования С++: 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.6.6 Контроль доступаЧлен класса может быть частным (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.f(2);x.a++;}// ошибка: X::A частный член// ошибка: X::f частный член// ошибка: X::a частный член6.6.1 Защищенные членыДадим пример защищенных членов, вернувшись к классу window из предыдущего раздела. Здесьфункции _draw() предназначались только для использования в производных классах, посколькупредоставляли неполный набор возможностей, а поэтому не были достаточны удобны и надежны дляобщего применения.