Бьерн Страуструп. Язык программирования С++. Специальное издание (2011) (1004033), страница 100
Текст из файла (страница 100)
И это достигается независимо от конкретного типа окна, для которого в(гав() вызывается: ыоЫ Изидою вдй Ьоп1ег::й'ав () ( И'ЬЫов::йан (); они дгав(); // отобразить рамку юЫ И(идою в!УЬ тени::йав() ( И'тит!ов:: Й ав ( ); ови Йав(); // отобразить меню 484 Глава ) 5. Иерархии классов уо!д С!оса:: йот ( ) ( аидом:: Й он' ( ); яг!пдот я!(Ь Ьогйег:: ои и г!гам ( ); и!пдот п!(Ь тени::оип дгазг(); опп бган (); У отобразить циферблат и стрелки ) Приведение типа от виртуального базового класса к производному обсуждается в 515.4.2. 15.2.5.
Применение множественного наследования Простейшим и наиболее очевидным применением множественного наследования является «склейка» двух несвязанных классов в качестве получения строительного блока для третьего класса. Класс Ваге1Вге, который мы в 815.2 строили на основе классов таза и 1»1зр1ауел(, иллюстрирует этот случай. Такой вариант применения множественного наследования прост, эффективен и важен, но не очень интересен.
В своей основе он позволяет программисту сэкономить на написании большого числа функций, переадресуюших вызовы друг другу. Эта техника в целом не сильно влияет на дизайн программной системы, но может вступать в конфликт с необходимостью сокрытия деталей реализации. По счастью, чтобы технике программирования быть полезной, ей не обязательно быть слишком умной. Применение множественного наследования для реализации абстрактных классов более фундаментально в том смысле, что влияет на проектный дизайн программы.
Класс ВВ 1ва! зВбег (812.4.3) служит соответствующим примером: с!аез ВВ 1»а1 з1Ыег: риЬВс 1»а1 з!Ыег, У интерфейс ргогесзед ВВзд !ег У реализация (г реализация функций !уа! зЫег и ВВз!й!ег средствами ВВз1й!ег В этом примере два базовых класса играют различные с логической точки зрения роли. Один базовый класс формирует открытый абстрактный интерфейс, а другой является защищенным (рго(есге()) конкретным классом, предоставляющим «детали реализации», Эти роли отражаются и в стилистике классов, и в их режимах наследования (рцЫгс или рго(ес(ед).
Множественное наследование весьма существенно в данном примере, поскольку производный класс вынужден замещать виртуальные функции как «интерфейсного класса», так и «класса реализации». Множественное наследование позволяет дочерним классам одного уровня иерархии (з)Ы)пя с!взлез) совместно использовать информацию без необходимости введения зависимости от единственного общего базового класса всей программы. Это как раз тот случай, который называют ромбовидным наследованием (д!атопг1-зйарес1 1пЬеп1апсе) (см.
класс Вал(!о в 815.2.4 и класс С7оса в 815.2.4.1). Если при этом базовый класс нельзя реплицировать, его нужно использовать в иерархии как виртуальный базовый класс. Я считаю ромбовидное наследование приемлемым в тех случаях, когда либо используется виртуальный базовый класс, либо когда непосредственно наследуемые 485 ! 5.2. Множественное наследование от него классы абстрактные. Рассмотрим снова классы семейства 1та1 оох из 8!2.4, где в конце концов я сделал классы этого семейства абстрактными, дабы отразить их чисто интерфейсную роль. Это позволило мне отнести все детали реализации в конкретные классы.
Кроме того, совместное использование деталей реализации распространялось лишь на классическую иерархию классов оконной системы, в рамках которой реализация и выполнялась. Было бы неплохо, если бы класс, реализующий более специализированный интерфейс Рорир Ьа! я1Ыег, разделял большую часть реализации с классом, реализующим более общий интерфейс Ьа! я1Ыег, В конце концов, эти реализационные классы могли бы совместно использовать практически все, за исключением деталей взаимодействия с пользователем программы.
В этом случае, однако, следует избегать реплицирования подобъектов Ьа1 я!Ыег в объектах специализированных элементов управления (ползунков). Для этого .Ьа1 я!Ыег следует использовать как виртуальный базовый класс: с(азя ВВ Ьа! з!Ыет: риьдс т(т(иа( 1та1 з(Ыег, рго(ес(е((ВВ(Иет ! /* ... */ )( с1аяз Рорир (та( я!Ыег ." РиЫЫ Ыг(иа!1та( зИет ! /* ...
*/ ); с1аяз ВВ рорир !та! я1Ыет: риЫ(с Ыт(иа(Рорир Ьа( зИет, рго(ес(е((ВВ Ьа1 ЫЫег ! /* ... */ ! ( или графически: ВВ(1Ыег .Л' Рорир (та! яИет ВВ Ьа( ЫЫет .т ВВ рорир Ьа! я!Ыет Можно легко себе представить дальнейшие интерфейсы, наследующие от Рорир Ьа1 я1Ыег, и дальнейшие реализационные классы, наследующие от этих интерфейсов и ВВ рорир Ьа1 з1Ыег. Если довести эту идею до логического конца, то все интерфейсы в системе должны наследоваться от абстрактных классов в виртуальном режиме. Такой подход выглядит наиболее логичным, общим и гибким.
Но я не следую этому подходу отчасти по исторической причине, а отчасти из-за того, что наиболее распространенные практические приемы реализации концепции виртуальных базовых классов требуют избыточных затрат памяти н времени выполнения, а это делает нх тотальное применение менее привлекательным. Если такие избыточные накладные расходы неприемлемы, то тогда следует обратить внимание на то, что подобъекты класса Ьа! ЫЫег хранят лишь указатель на виртуальную таблицу и, как отмечалось в З!5.2.4, такие абстрактные классы, не имеющие полей данных, могут реплицироваться без отрицательных последствий.
Итак, мы отказываемся от виртуального наследования в пользу обычного наследования: с!аяя ВВ Ьа! ЫЫег: риЫ(с 1та! я!Ыет, рто(ес(е(! ВВЫЫет ! /* ... */ ! ( с1аяя Рорир Ьа! я1Ыет: риЫЬ 1та! (Иег ! /* ... */ ); с1аяя ВВ рорир Ьа! УИег: риЫЫ Рорир Ьа! ЫЫег, рго(ес(е(( ВВ ! а! з(Ыег ! /* ... */ ! ( 486 Глава 15. Иерархии классов или графически: ВВз1Ыег .
-'г 1га1 ь!Ыег 1га1 з1Ыег Рорир Ыа) з(Ыег ВВ Ыа1 з!Ыег . -'г ВВ рорир Ыа) з1Ыег Это можно рассматривать как жизнеспособную оптимизацию более понятной альтернативы, представленной ранее. Некоторой потенциальной проблемой при этом является невозможность неявного приведения от ВВ рорир Ыа! ЫЫег к 1га! з!Ыег. 15.2.5.1.
Замещение функций виртуальных базовых классов В производных классах можно замешать виртуальные функции прямых или косвенных виртуальных базовых классов. В частности, два различных класса могут заместить разные виртуальные функции виртуального базового класса. Таким образом, несколько производных классов могут предоставить реализации интерфейса из виртуального базового класса. Пусть, например, класс Игт!!он формулирует интерфейс в виде виртуальных функций зе! со!оз () и рготр!() . В этом случае класс $Кп!!ои и!УЬ Ьогйег может заместить зе! со!ог () для управления цветовой схемой, а класс $Плг!он иИЬ тели — заместить рготр!() для организации интерактивного взаимодействия с пользователем: //задать цвет фона с)ат и'таил нз(Ь Ьоп3ег: риЫ)с гаеиа1 и'!паол ( //... гоЫ зе! со!ог (Со1ог); )' //управление цветом фона смт Из!лдоя л1!Ь тели: риЬВс Ыгта( и'ЬЫоп ( // ...
гоЫ рготр! ( ); )' с!азз Му тп4ом~: рий!с И'!прозе в1!Ь тели, риЬВс Иг!паол ануа Ьогг!ег ( //... )' А что если разные производные классы заместят одну и ту же функцию? Это допускается только в случаях, когда один из таких классов наследует от всех осталь- с(ат Иг)лооп ( // ... Ылиа)зе! со(ог(Со)ог) = О; г!гела! гоЫ рготрг () = О! )' В управление взаимодействием с пользователем 15.3. Контроль доступа 487 ных классов этой группы.
То есть одна функция должна замещать все остальные. Например, класс Му эг!пэ!оэг мог бы заместить ргопэрг() лля улучшения возможностей, предоставляемых классом Игл!(оиэ ингЬ тели: сйтМу эггпэ(оп: риЫ(с Игэпэ(оэг эг(гЬ тени, риЫэс Иггпэгоэг э«!!в Ьогэгег У... гоЫрготрг() ! дне астап«лен балов. классу вопросы взаимодействие с полтователем ); или графически: и!пйоэ«(лег со(ог (), рготр! () ) Игпэ(оэг т(УЬ Ьогэгег (ве! со(ог () ) И'гпаоэг э«!1Ь тени (рготр! () ) Если в двух классах замещается виртуальная функция базового класса, но при этом ни один из классов не замещает соответствующую функцию из другого, то такая классовая иерархия ошибочна: невозможно создать корректную виртуальную таблицу, ибо вызов виртуальной функции через объект «наиболее производного класса» неоднозначен. Например, если бы класс 2!аа!о из 815.2.4 не замеШал функцию жгу!в(), то объявления иНге() в Месеугег и Тгапвт(ггег приводили бы к ошибке в определении класса 2!аа!о.
Такой конфликт разрешается так, как показано в этом примере — путем замещения функции в «наиболее производном классе», 15.3. Контроль доступа Член класса может быть закрытым (рнгаге), защищенным (ргогесге!!) или открытым (риЫс): ° Если он закрытый, то его имя могут использовать лишь функции-члены или друзья того же самого класса. ° Если он защищенный, то его имя могут использовать функции-члены и друзья того же самого класса, а также функции-члены и друзья непосредственных производных классов (см. 811.5). ° Если он открытый, то его имя может использовать любая функция.
Данная классификация отражает точку зрения, что в отношении доступа к классу существует три вида функций: реализующие класс (друзья и члены), реализующие производный класс (друзья и члены) и остальные функции. Это можно изобразить графически следующим образом: 488 Глава 15. Иерархии классов остальные функции производного класса ны и друзья класса Контроль доступа применяется ко всем именам одинаково. То, к чему относится имя, не влияет на контроль доступа к этому имени. Это означает, что закрытыми могут быть функции-члены, типы, константы и т.д., а также поля данных.
Например, классы эффективных неинтрузивных (поп-(п(гцз(че — ненавязчивых) списков (516.2.1) часто нуждаются в специальных структурах данных для учета своих элементов. Такую информацию лучше делать закрытой: гетр(аге<с!ат Т> с!азе ЕЬ! ( рига!е: и!гисгЕ(пй ( Тга1) Етй* лехг; ); зйис! Сйипй ( епит ( сйипй з1ге = 15 ); Ыпй г(сйипй з!ге) г Сйипй* пехг; Сйилй* а1!оса!ее; Е(ай* /гее/ Е(лй* де! угее(); Е(пй* йеаА риЫ(с: с!азе Юпггег)гоп ( ) // класс исключения гоЫ 1ллем(Т) Тле!(); //.. )' !етр!а!е<с!азз Т> гоЫ Егл!<Т>:: (пзегг( Т га1) ( Етй* 1пй = де! Тгее(); 1пй->га1 = га1; 1лй->пах! = йеаа; йеас( = 1пй; 489 15.3. Контроль доступа !етр)а!е<с1авв Т> Е!м<Т>::1Лпк* ЕЫ<Т>::ле! аггее() (У(рее == О) гУ выбелить новый блок памяти и включить связи в список(гее Е(п!г* р = угее; аггее =)гее->пах!г ге!ига рг ) гетр)а!е<с!азз Т> ТЕЬз<Т>::яе!() ( )Т(!зеай == О) !)згозг сезйегфозг() г Е!п!г* р= !звал! !зев = р->пехн р->пех! =угее; !"гее = р; ге!иго р->га1; ) В определениях функций-членов область видимости шаблона Е1зг<з> вводится с помощью префикса Хм!<л>::, но поскольку возврат функции дег аггее () указывается раньше, то его нужно обозначать с полной квалификацией, то есть Емг<Т>:: Е1п», а не просто Еуп1с.