Б. Страуструп - Язык программирования С++ (1119446), страница 48
Текст из файла (страница 48)
Однако, в производном классеследующего уровня появляется ловушка: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() предназначались только для использования в производных классах, посколькупредоставляли неполный набор возможностей, а поэтому не были достаточны удобны и надежны дляобщего применения. Они были как бы строительным материалом для более развитых функций. Сдругой стороны, функции draw() предназначались для общего применения. Это различие можновыразить, разбив интерфейсы классов window на две части - защищенный интерфейс и общийинтерфейс:171Бьерн Страуструп.Язык программирования С++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;prot = 2;publ = 3;////////ошибка: priv частный членнормально: prot защищенный член, аmderived() член производного класса Yнормально: publ общий член}В глобальной функции доступны только общие члены:void f(Y* p){p->priv = 1;p->prot = 2;p->publ = 3;////////}172ошибка: priv частный членошибка: prot защищенный член, а f()не друг или член классов X и Yнормально: publ общий членБьерн Страуструп.Язык программирования С++6.6.2 Доступ к базовым классамПодобно члену базовый класс можно описать как частный, защищенный или общий: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 - общий базовый класс Y1py1->a = 7;// нормальноpx = py2;// ошибка: X - защищенный базовый класс Y2py2->a = 7;// ошибкаpx = py3;// ошибка: X - частный базовый класс Y3py3->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 - общий базовый класс Y1py1->a = 7;// нормальноpx = py2;// нормально: X - защищенный базовый класс Y2,// а Z2 - производный класс Y2py2->a = 7;// нормальноpx = py3;// ошибка: X - частный базовый класс Y3py3->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 - общий базовый класс Y1py1->a = 7;// нормальноpx = py2;// ошибка: X - защищенный базовый класс Y2py2->a = 7;// ошибкаpx = py3;// нормально: X - частный базовый класс Y3,// а Y3::f член Y3py3->a = 7;// нормально173Бьерн Страуструп.Язык программирования С++}6.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();освободить эту память для повторного использованияНазначение до сей поры загадочного параметра типа size_t становится очевидным.