Б. Страуструп - Язык программирования С++. Специальное издание, 3-изд. Бином. 2004 (1160791), страница 187
Текст из файла (страница 187)
Вот версия, которая гарантирует, чтой() вернет 1: с1авв Р1: риЫ1с В ( 1пг Я ( ге1игп а-г1; ) ); а вот версия, которая заставит д () вывести "Здравствуй, мир(" н возвратить 0: с1аввР2: риЫ(сВ ( шгТ() ( соиг' "Здравствуй, мир0,п", ге1игп а; ) Этот пример просто иллюстрирует важнейшее свойство виртуальных функций. Почему это должно выглядеть глупо? Почему бы какому-нибудь программисту когда-нибудь не написать что-либо подобное? Ответ заключается в том, что виртуальная функция входит в интерфейс базового класса, и этот класс предположительно может использоваться без каких-либо знаний о его производных классах. Следовательно, должна бьп ь предоставлена возможность описать ожидаемое поведение обьекта базового класса таким образом, чтобы можно было составить программу, ничего не зная о производном классе.
Каждый класс, заменяющий виртуальную функцшо, должен реализовать вариант этого поведения. Например, виртуальная функция го(а(е () (повернуть) из класса 5?гире (фигура) поворачивает фигуру. Функции го(а(е() в таких производных классах как С(гс1е (круг) и Тпапу(е (треугольник) должны поворачивать соответствующую фигуру; в противном случае, фундаментальное предположение о классе 5(заре нарупзается. Такого предположения не делается относительно класса В или его прса вводных классов Р1 и Р2; таким образом, пример бессмыслен. Даже имена В, Р1, Р2, Т я д выбраны так, что понять их предназначение невозможно. Спецификация ожидаемого поведения виртуальных функций — это главный аспект проектировании классов.
Выбор хороших имен для классов и функций очень важен — и не всегда легок. Зависимость от неизвестного (возможно, еще не написанного) производного класса — это хорошо или плохо? Естественно, все зависит от намерений программиста. Если цель в том, чтобы изолировать класс от всех внешних влияний, дабы гарантировать его специфическое поведение, то защищенных членов и виртуальных функций лучше избегать.
Если, однако, наши намерения состоят в том, чтобы обеспечить каркас, к которому следующий программист (или тот же самый программист через несколько недель) сможет добавлять код, то виртуальные функции часто являются наиболее элегантным средством для достижения этого; и защи где иные функции-члены доказали свою пригодность для использования в таких целях. Этот прием приме- 814 няется в библиотеке потокового ввода/вывода (5 21.6) и проиллюстрирован окончательной версией иерархии 7оа! Ьох (8 12.4.2). Если виртуальная функция предназначается только для косвенного использования производным классом, ее можно оставить закрытой. Например, рассмотрим простой шаблон буфера: //вызывает ооег)7оиз(Т) (переполнение сверху), если буфер полон //вызывает ипаег/!от() (переполнение снизу), если буфер пуст Функции ри1 () и де1 () вызывают соответственно виртуальные функции ооег))ощ () и ипс/ег/7ото().
Теперь пользователь, чтобы удовлетворить потребность в разнообразных буферах, может реализовать несколько буферных типов, заместив ооегЯоа () и ипс)е!у7 оси (): // уикли ческий буфер гетр!иге<с!аяя Т с!аяя Сггси!аг ЬиЯег:риЫ!сви//ет-Тл( тгорегЯозо (7); // если переполнение, наплел~ звполнтпь сначала сп1 ил беГ7 озо (7); )' // расширяющийся буфер 1етр!а1е<с!аяя Т с!аяя Вхрапс(!пу Ьи//ег; риЫ1сВиХ~ег<7ь ( !п1 ооегЯот (7); //если переполнение, увелинюе рознер буфера !п1 ипа1ефот (); '1'оль ко если бы производному классу было нужно обращаться к ооефото () и ипс(ег)(7оиз () напрямую, эти функции нужно было бы сделать защищенными, а не закрытыми.
24.3.3. Отношения включения При применении включения (содержания в себе) для представления объекта класса Хестьдве основные альтернативы: (1] Объявить член типаХ. (2) Объявить член типаХ' или Хй. Если значение указателя никогда не изменяется, эти варианты равносильны (не учи- тывая эффективности и способа написания конструкторов и деструкторов): с!аяя Х( риЫсс Х(!п)); //.. ' 1етр!а!в<с!иея Т> с!аяя Ви//ег( риЫ)с оо!а ри1 (7); Туе1 (); // р Пои 1е; Ыг1и а ! т1 о о ее/7 озо (7); о!г1иа! гп1 ипс(ег/(озо (); Глава 24.
Проектирование и программирование 24.3. Классы с1аеа С( Ха; Х'р; Х8 г; риЫ!с: С(т! 1, 1п!/, 1п1)е), и (!), р (пеюХ(1)), г(*пеи Х(й)) () -С () ( Ые1е!е р; Йе(е1е 2 г, ) В таких случаях предпочтительнее членство собственно объекта, как в случае Сса, поскольку оно эффективнее по быстродействию, памяти и нажатиям клавиш. Оно также меныпе подвержено ошибкам, поскольку связь между содержап!имея и содержащим объектами описывается правилами конструирования и уничтожения Я 10.4.1, 2 12.2.2, 2 14А.1).
Олнако см. также 2 24.4.2 и 2 25.7. Решение с указателем можно применять тогда, когда за время жизни «содержап1его» объекта нужно изменить указатель на «содержащийся» объект. Е! апример: с1аее С2 ( Х'р; риЫи. С2 (!и! !): р (пею Х (!)) ( ) -С2 () ( Ие1е!ер;) Л" сйапие (Х* о) ( Х" ! = р; Р= 9' ге!игп 1; Другое соображение в пользу применения члена-указателя может состоять в том, что- бы позволить задавать «содержащийся» член в качестве аргумента: с1аее СЯ ( Х'р, риЫга Сг (Х*д).р(ч)() //-. ); Имея объекты, включающие указатели на другие объекты, мы создаем то, что часто называют иерархияии объектов.
Они являются альтернативой и дополнением к иерархии классов. Как показано в примере с транспортными средствами экстренных служб Я 24.3.2), при проектировании часто нелегко выбрать между представлением свойства класса в виде базового класса, или в виде члена. Необходимость в замещении является признаком того, что лучше выбрать первое. И наоборот, если вам нужно иметь возможность представлять свойство множеством типов — это признак того, что лучше выбрать последнее. Например: с1аееХХ риЫ!сХ(/" ...
'/); с1аеа ХХХ: риЫ!с Х ( /' ... '/ ), оо!с(,/() 616 // СЗ 'содержит в себе" Х // СЗ "содержит в себе' ХХ // СЗ 'содержит в себе' ХХХ Такое нельзя смоделировать, сделав СЗ производным от Хили включив в СЗ член типа Х, поскольку нужно использовать точный тип члена.
Это важно для классов с виртуальными функциями, таких как класс фигур Я 2.6.2) или класс абстрактных множеств Я 25.3). Ссылки могут использоваться для упрощения классов, основанных на членстве указателей, когда за время сугцествования содержащего объекта ссылаются только на один объект. Например: Члены-указатели и члены-ссылки также нужны, когда объект является совместно используемым: Х" р = пеги ХХ; С4 о Ь1'1 ('р), С4 оЬ/2 (*р); //о(/1 и оЬ!2 теперь совл~есьпно используют повий ХХ Естественно, обращение с совместно используемыми объектами требует чрезвычай- ной осторожности — особенно в параллельных системах. 24.3.4.
Включение и наследование С учетом важности отношений наследования не покажется удивительным, что ими часто злоупотребляют, н нх нередко неправильно понимают. Когда производный класс Р порожден открытым образом от базового класса В, часто говорят, что Р есть ((з а) В: с1аее В ( /' ... */ ), с1иееР риЫ1сВ(/*... '/); //Р— это раэновидноапьв Иначе это выражается словамн, что наследование является отношением 1е-а, или— несколько более точно — что Р есть ризновидность В. С другой стороны, о классе Р, имеющем член другого класса В, часто говорят, что он имеет (Ьаз а) В или содержит В.
Например: //О содержит В с1аее Р ( риЬВс. ВЬ; О Иначе зто выражается словами, что членство является отношением Ьае-а. СЗ* р! = пет СЗ (песо Х); СЗ*р2 = пет СЗ (песо ХХ); СЗ" рЗ = песо СЗ (пеьв ХХХ), //.. с1аев С4 ( ХЗг; риЬ11с: С4 (ХЗ д): г (у) () //- Глава 24. Проектирование и программирование 817 24.3. Классы Для данных классов В и .0 как нам выбрать между наследованием и членством? Рассмотрим классы А(гр1апе (самолет) и Епячпе (двигатель). Новичкам часто приходит в голову сделать А/гр(апе производным от Елее.
Это — плохая идея, поскольку самолет не является двигателем, он имеет двигатель. Один из способов увидеть это — задуматься, может ли самолет иметь несколько двигателей? Поскольку это предо~валяемся возможным Глаже если в нашей программе все самолеты одномоторные), нам следует воспользоваться членством, а не наследованием. Во многих случаях, когда есть сомнения, полезен вопрос «может ли у него их быть несколько?» Как всегда, именно неосязаемая природа программ приводит к тому, что приходится анализировать такие «очевидные» вопросы. Если бы все классы было так легко наглядно представить, как самолет и двигатель, было бы легко избежать тривиальных ошибок, когда А1гр1апе делают производным от Елр(пе. Однако такие ошибки случаются весьма часто — в частности, их допускают люди, считающие наследование просто механизмом комбинирования конструкций уровня языка программирования.
Несмотря на удобство и краткость записи, которые обеспечивает наследование, его следует использовать почти исключительно для выражения отношений, четко определенных в проекте. Рассмотрим пример: е1аззВ( рибйа Ыг1иа1 воин); ооЫ и (); //01 содержит В с(аззР1 ( риЬИе: ВЬ; ооЫЛ; ); //не заиеилает Ь/() ооЫ Ь1 (Р1*ре() ( // ошибка: нет преобразования 01* в В* В*рЬ =рд; рЬ=ард- Ь; рь- и(); рд-'2(); рд- Ь.и(); рь- /(); рл1-'У0; // вызы виет В ли // ошибка: Р1 не ниеет члена я() // вызывает В: / (не замен(енную Р1: Я // вызывает Р1 и/ //02являетея разновидноетьюВ //валави(ает Вл/О е(азз Р2.