Бьерн Страуструп. Язык программирования С++. Специальное издание (2011) (1004033), страница 188
Текст из файла (страница 188)
с1ам (ЭЗ: риЫ(с В ( 1л<1 () (сонг« "НеИо, вогЫ! '~л" г ге(игл а; ) Приведенный пример просто иллюстрирует формальный синтаксис виртуальных функций. Что в этом глупого? Кто-нибудь вполне может написать что-нибудь в этом роде. На самом деле, проблема здесь в том, что виртуальные функции являются частью открытого интерфейса базового класса, который предположительно может использоваться без каких-либо знаний о производных классах. Нужно описать предполагаемое поведение объектов базового класса, чтобы можно было разрабатывать программы, ничего не зная о производных классах.
Каждый производный класс, замешаюший виртуальную функцию, должен реализовать'вариант этого поведения. Например, виртуальная функция гоготе() класса опаре врашает объект этого класса Глава 24. Проектирование и программирование 866 гетр!а!е<с)азз Т> сдззз ВиЯег риЬНс: воЫриг(Т) ! Тке! (); уу ... У вызывает обж ег!)оы(7) при переполнении буфера УУ вызывает ипг!ег/)озг(), если буфер пуст рг1га!е: Ымиа1 (п! огег)!оы (Т) ! г!миа! !зм ипггегЯоы ( ); уу ... Функции риг() и Вег() вызывают виртуальные функции огегЯон () и аяв(егЯон (), соответственно.
Пользователь может теперь реализовать разные типы буферов, заместив овегЯои() и ипбегЯои(): 1етр1а!е<с!азз Т> с1авв С(гси1аг ЬиЯег: риЫ!с ВиЯег<Тг зп! огегЯоы (Т); Ьи ипв(егЯон () У... УУ при заполнении возвращаемся на начало буфера гетр!а!е<с)азз Т> с(азз Ехрапсдпя ЬиЯег: риЫ)с ВиЯег<Тл ии озегЯоы (Т); ии ипв(егЯоы О ! УУ при заполнении увеличиваем размер буфера (фигуру).
Функции гоготе() из производных классов Сзгс1е и Тг1апВ1е должны врашать круги (сггс!ез) и треугольники ((папа)ез), соответственно; в противном случае нарушается фундаментальное предположение (представление) о классе Ядре. Такого важного предположения о поведении класса В и производных классов Ш н !з2 сделано не было; в результате, пример бессмыслен. Даже имена В, Ш, !г2, у и В выбраны так, что понять их предназначение невозможно.
Спецификация поведения виртуальных функций — важнейший аспект классового дизайна. Выбор хороших имен для классов и их функций также очень важен (но не всегда легок). Зависимость от неизвестного (например, еше не написанного) производного класса — это хорошо или плохо? Туг все зависит от намерений программиста. Если нужно изолировать класс от всех внешних влияний так, чтобы можно было гарантировать его специфическое поведение — то виртуальных функций и зашишенных членов лучше избегать. Если же целью является построение исходного кодового каркаса, к которому позже другим (или тем же самым) программистом будет добавляться новый код, то виртуальные функции являются элегантным механизмом реализации этой идеи; зашишенные члены поддерживают этот механизм.
Эта технология положена в основу библиотеки потокового ввода/вывода (В21.6); она также иллюстрируется окончательной версией иерархии Тга! Ьох 512.4.2). Если виртуальная функция предназначена лишь для косвенного использования производным классом, ее можно объявить закрытой. Рассмотрим, например, простой шаблон буфера; 867 24.3. Классы Если бы производному классу нужно было обращаться к оееюЯои () и юйегг1ои() напрямую, их следовало сделать защищенными, а не закрытыми.
24.3.3. Агрегация (отношение включения) Агрегация предполагает использование некоторым классом полей данных, связанных двумя способами с иным классовым типом Х: 1. Член класса имеет тип Х 2. Член класса имеет тип Х" или Ха. Если значение указателя никогда не изменяется, эти варианты равносильны (за исключением эффективности и написания конструкторов и деструкторов): с1ак«Х ( риЫ1с: Х(Ы1); 11 ...
сгиб С ( Ха; х*р; Хь ю риввс: С(1пгю', (п(1, 1пгй): а(1), р(пе»»Х(1) ), с(*пегих(й) ) () -С() (Иегесер; Ие(еге аю ) ) В таких случаях включение самого объекта, то есть С:: а в данном примере, эффективнее по быстродействию и памяти (и даже по нажатиям клавиш). Это и надежнее, ибо связь между внешним (содержащим) и внутренним (содержащимся) объектами определяется правилами конструирования и уничтожения ($10.4.1, В12.2.2, 814.4.1).
См, также 824.4.2 и 825.7. Указатель следует применять тогда, когда за время жизни «содержащего» объекта требуется изменить значение указателя на «содержащийся» объект. Например: Ы Сг ( хр; риЫ(с: Сг(гп(1): р(пепХйй) () -СЗ() (»(е(егер; ) Х* саепяе(Х* Ч) ( Х* 1 = р1 р=«г сегисп Г; Глава 24. Проектирование и программирование 868 Другое соображение в пользу применения указателя состоит в том, чтобы разрешить задавать «содержащийся» объект в качестве аргумента: сгаее СЗ ( Х.р; риЫ)с: СЗ(Х* о): р(В) () /У ...
Создавая объекты, содержащие указатели на другие объекты, мы получаем то, что часто называют иерархией обьектое (оЬ/ес( ЫегагсЫез). Это альтернатива иерархиям классов. Как показано в примере с транспортными средствами экстренных служб в $24.3.2, в процессе проектирования нелегко выбрать способ отражения свойства класса — в виде базового класса, или в виде содержащегося в классе члена.
Первое предпочтительнее в случае, когда требуется замещение. Второе же предпочтительнее тогда, когда свойство может соответствовать разным типам. Например: с1аея ХХ: риЫ(с Х ( / *... * / ); с1аяя ХХХ: риЬИс Х ( / *... * / ); гойЦ'( ) ( СЗ* р1 = пеп СЗ(пет Х); //СЗ "содержит" Х СЗ* р1 = пем СЗ(пе»»ХХ); /УСЗ "содержит" ХХ СЗ* рЗ = пет СЗ(петХХХ); //СЗ "содержит" ХХХ // ... Это нельзя смоделировать наследованием СЗ от Х или включением в СЗ члена типа Х, ибо точный тип члена становится известен позже. Это также важно для классов с виртуальными функциями, например, для класса фигур (в2.6.2) или абстрактного класса множеств (З25.3).
Вместо классов, содержащих указатели, можно для упрощения применить класс, содержащий ссылку, если только требуется работать с единственным включенным объектом. Например: с1аее С4 ( Ха ю риЫ(с: С4(Ха 4): г(4) () //... )' Члены-указатели и члены-ссылки также нужны для совместного использования объекта: Х*р=пеп ХХ; С4 оЬ/1 (»р); С4оЬ12("р) ' //о121 и оЦ2 роздеяяют новый ХХ 869 24.3. Классы Естественно, что управление совместно используемым объектом требует повышенного внимания — особенно в параллельных системах.
24.3.4. Агрегация и наследование С учетом важности отношений наследования не покажется удивительным, что ими часто злоупотребляют. Когда класс Р открыто наследует классу В, говорят, что Р есть В (Р й а В): с1азз В (/* ...*/); с1озг Вч риЫ!г В (/*... */); У!) - это разновидность В Говорят даже, что наследование есть отношение /з-а" (й-а гг!а1ьопзЬр) или несколько точнее, что Р есть разновидность В (Э !з а й!пИ о!'В) . Если же класс Р содержит член типа В, говорят, что Р агрегирует (содержит — санга/пз) В. Например: У!) содержит !огрегирует) В с)озг 1) ( риЫ)с: ВЬ; //...
Про этот тип взаимосвязи говорят, что это отношение типа "иметь" !паз-а ге!а11опзЫр), Для заданных классов В и Р, как выбрать между наследованием и агрегацией? Рассмотрим для примера классы Айр!апе (самолет) и Епд!пе (двигатель). Новичкам часто приходит в голову сделать А1гр!апе производным от Епй!пе. Это плохая идея, поскольку самолет не является двигателем, он содержит (агрегирует, имеет) двигатель. Подумайте, может ли самолет иметь несколько двигателей? Поскольку это возможно, нам следует применить агрегирование, а не наследование. Во многих других случаях стоит задать вопрос — может ли быть несколько экземпляров одной сущности у другой сущности? Неосязаемая природа программ заставляет нас задавать такие вопросы.
Если бы все классы были столь наглядными, как классы Айр!апе и Епя(пе, было бы легко избежать таких тривиапьных ошибок, когда Айр!апе делают производным от Епй!пе. Однако в реальной практике подобные ошибки случаются весьма часто, особенно у людей, считающих наследование просто механизмом комбинирования свойств на уровне конструкций языка программирования. Несмотря на определенные преимушества наследования в виде краткости н удобства записи, его следует применять исключительно для выражения идеи общности/подтипизации, отраженной в проектных спецификациях.
Рассмотрим пример: с1азз В ( риЫ!зп Ытгиа1 ю!д.Г() ' ю!дя() г )' /У1)! содержит В с(азз Э1 8гО риЫгс г ВЬ; )г()'() ' В не заггеи(ение Ь~() У еггог. непг преобразования от Р1* к В* У вызываепгсн Вке() Веггот у Р1 нет члена-функции я() В вызывается В:Я у) вызывается Р!: 2() Заметьте, что не существует неявных преобразований от типа класса, содержащего член некоторого другого класса, к этому другому классу, и что первый из перечисленных классов не замещает виртуальных функций второго. Это резко контрастирует со случаем открытого наследования: с)азв Р2: риЫгс В ( риЫ(с г го)г1)'( ) г )г Узоиегцает В:Я УУ о(гг неявное приведение от Р2* к В* У вызывается Вкд() УУ вызывается Вкд() УУ виртуальный вызогк вызывается Р2:Я) В вызывается Р2::1() Удобство и большая краткость записи в случае класса Р2 настолько превосходит таковое в случае класса Р1, что этим легко злоупотребить.
Стоит помнить, что за удобство записи приходится платить большей степенью зависимости между В и Р2 (см. 524.3.2.) ). В частности, стоит помнить о неявном преобразовании из Р2 в В. Если такое преобразование не является приемлемой частью семантики ваших классов, открытого наследования стоит избегать. Когда же класс отражает некоторую сущность, а наследование от него используется в проекте для отражения зависимостей типа 'Ъ-а", то указанное неявное преобразование часто оказывается именно тем, что нужно. Наконец, бывают случаи, когда наследование вполне подходит, но неявного преобразования нужно избежать. Рассмотрим класс Свею, который помимо прочего обеспечивает динамическое (во время выполнения программы) управление доступом для другого класса Рге1И.