3.4.3 Объявление производных классов C++Builder дает возможность объявить производный класс, который наследует свойства, данные, методы и события всех своих предшественников в иерархии классов, а также может объявлять новые характеристики и перегружать некоторые из наследуемых функций. Наследуя указанные характеристики базового класса, можно заставить порожденный класс расширить, сузить, изменить, уничтожить или оставить их без изменений. Наследование позволяет повторно использовать код базового класса в экземплярах производного класса. Концепция повторного использования имеет параллель в живой природе: ДНК можно рассматривать как базовый материал, который каждое порожденное существо повторно использует для воспроизведения своего собственного вида. < Листинг 3.5 иллюстрирует обобщенный синтаксис объявления производного класса. Порядок перечисления секций соответствует расширений привилегий защиты и областей видимости заключенных в них элементов: от наиболее ограниченных к самым доступным. class className : [<спецификатор доступа>] parentClass { <0бъявления дружественных классов> private: <приватные конструкторы> <приватные методы> protected: <защищенные конструкторы> <защищенные методы> public: <общедоступные члены данных> <общедоступные конструкторы> <общедоступный деструктор> <общедоступные методы> _published: <общеизвестные члены данных> <Объявления дружественных функций> Листинг 3.5. Объявление производного класса. Отметим появление новой секции с ключевым словом _published - дополнение, которое C++Builder вводит в стандарт ANSI C++ для объявления общеизвестных элементов компонентных классов. Эта секция отличается от общедоступной только тем, что компилятор генерирует информацию RTTI о свойствах, членах данных и методах объекта и C++Builder организует передачу этой информации Инспектору объектов во время исполнения программы. В главе 6 мы остановимся на этом более подробно. Помимо способности выполнять свою непосредственную задачу объектные методы получают определенные привилегии доступа к значениям свойств и данных других классов. Когда класс порождается от базового, все его имена в производном классе автоматически становятся приватными по умолчанию. Но его легко изменить, указав следующие спецификаторы доступа базового класса: • private. Наследуемые (т.е. защищенные и общедоступные) имена базового класса становятся недоступными в экземплярах производного класса. • public. Общедоступные имена базового класса и его предшественников будут доступными в экземплярах производного класса, а все защищенные останутся защищенными. Можно порождать классы, которые расширяют возможности базового класса: он вполне приемлем для вас, однако содержит функцию, требующую небольшой доработки. Написание заново нужной функции в производном классе является пустой тратой времени. Вместо этого надо повторно использовать код в базовом классе, расширяя его настолько, насколько это необходимо. Просто переопределите в производном классе ту функцию базового класса, которая вас не устраивает. Подобным образом можно порождать классы, которые ограничивают возможности базового класса: он вполне приемлем для вас, но делает что-то лишнее. Рассмотрим применение методик расширения и ограничения характеристик на примере создания разновидностей объекта кнопки - типичных производных классов, получаемых при наследовании базовой компоненты TButtonControl из Библиотеки Визуальных Компонент C++Builder. Кнопки различного вида будут часто появляться в диалоговых окнах графического интерфейса ваших программ.
class SimpleButton: public : TButtonControl public: }; SimpleButton::SimpleButton(int x, int y) :TButtonControl(x, y) { } void SimpleButton::Draw() Листинг 3.6. Ограничение характеристик базового класса. Единственная задача конструктора объекта для SimpleButton - вызвать базовый класс с двумя параметрами. Именно переопределение метода SimpleButton: : Draw () предотвращает вывод обводящей рамки кнопки (как происходит в родительском классе). Естественно, чтобы изменить код метода, надо изучить его по исходному тексту базовой компоненты TButtonControl. Теперь создадим кнопку с пояснительным названием (Рис. 3.3). Для этого нужно построить производный класс TextButton из базового TButtonControl, и перегрузить метод Draw с рас-Рис. 3.3. Кнопка с текстом, ширением его функциональности. Листинг 3.7 показывает, что объект названия title класса Text создается конструктором TextButton, а метод SimpleButton->Draw() отображает его: class Text public: }; class TextButton: public : TButtonControl public: }; TextButton::TextButton(int x, int y, char* caption) TButtonControl(x, y) { title = new Text(x, y, caption); } void TextButton::Draw () { TextButton::Draw() ; title->Draw() ; } Листинг 3.7. Расширение характеристик базового класса. В заключение раздела с изложением методики разработки базовых и производных классов приводится фрагмент C++ программы (Листинг 3.8), в которой объявлена иерархия классов двух простых геометрических объектов: окружности и цилиндра. Программа составлена так, чтобы внутренние значения переменных г-радиус окружности и h-высота цилиндра определяли параметры создаваемых объектов. Базовый класс Circle моделирует окружность, а производный класс Cylinder моделирует цилиндр. const double pi = 4 * atan(1); class Circle protected: public: };
class Cylinder : public Circle protected: public: } void Cylinder::showData() void main() circle.showData () ; } Листинг 3.8. Простая иерархия классов окружности и цилиндра. Объявление класса Circle содержит единственный член данных r, конструктор и ряд методов. При создании объекта конструктор инициализирует член данных r начальным значением радиуса окружности. Отметим новый синтаксис конструктора: при вызове он может обратиться к конструктору базового класса, а также к любому члену данных, указанному после двоеточия. В нашем случае член данных r "создается" обращением к нему с параметром rVal и инициализируется нулевым значением. Метод setRadius устанавливает, a getRadius возвращает значение члена данных r. Метод Area возвращает площадь круга. Метод showData выдает значения радиуса окружности и площади круга. Класс Cylinder, объявленный как производный от Circle, содержит единственный член данных h, конструктор и ряд методов. Этот класс наследует член данных г для хранения радиуса основания цилиндра и методы setRadius и getRadius. При создании объекта конструктор инициализирует члены данных г и h начальными значениями. Отметим новый синтаксис конструктора: в нашем случае член данных h инициализируется значением аргумента hVal, а член данных г - вызовом конструктора базового класса Circle с аргументом rVal. Функция setHeight устанавливает, a getHeight возвращает значение члена данных h. Circle::Area перегружает унаследованную функцию базового класса, чтобы теперь возвращать площадь поверхности цилиндра. Функция showData выдает значения радиуса основания, высоты и площади поверхности цилиндра. Функция main создает окружность circle класса Circle с радиусом 2 и цилиндр cylinder класса Cylinder с высотой 10 и радиусом основания 1, а затем обращается к showData для вывода параметров созданных объектов: Радиус окружности = 2 Площадь круга = 12.566 Радиус основания = 1 Высота цилиндра = 10 Площадь поверхности = 69.115 3.5 Полиморфизм Слово полимрфизм от греческих слов poly (много) и morphos (форма) означает множественность форм. Полиморфизм - это свойство родственных объектов (т.е. объектов, классы которых являются производными от одного родителя) вести себя по-разному в зависимости от ситуации, возникающей в момент выполнения программы. В рамках ООП программист может влиять на поведение объекта только косвенно, изменяя входящие в него методы и придавая потомкам отсутствующие у родителя специфические свойства. Для изменения метода необходимо перегрузить его в потомке, т.е. объявить в потомке одноименный метод и реализовать в нем нужные действия. В результате в объекте-родителе и объекте-потомке будут действовать два одноименных метода, имеющие разную кодовую реализацию и, следовательно, придающие объектам разное поведение. Например, в иерархии родственных классов геометрических фигур (точка, прямая линия, квадрат, прямоугольник, окружность, эллипс и т.д.) каждый класс имеет метод Draw, который отвечает за надлежащий отклик на событие с требованием нарисовать эту фигуру. Благодаря полиморфизму, потомки могут перегружать общие методы родителя с тем, чтобы реагировать специфическим образом на одно и то же событие. 3.5.1 Виртуальные функции В ООП полиморфизм достигается не только описанным выше механизмом наследования и перегрузки методов родителя, но и виртуализацией, позволяющей родительским функциям обращаться к функциям потомков. Полиморфизм реализуется через архитектуру класса, но полиморфными могут быть только функции-члены. В C++ полиморфная функция привязывается к одной из возможных одноименных функций только в момент исполнения, когда ей передается конкретный объект класса. Другими словами, вызов функции в исходном тексте программы лишь обозначается, без точного указания на то, какая именно функция вызывается. Такой процесс известен как позднее связывание. Листинг 3.9 показывает, к чему может привести не полиморфное поведение обычных функций-членов. class Parent public: }; class Child : public Parent public: };
void main() cout << child.F2(3) << endl; } Листинг 3.9. Неопределенное позднее связывание. Класс Parent содержит функции-члены F1 и F2, причем F2 вызывает F1. Класс Child, производный от класса Parent, наследует функцию F2, однако переопределяет функцию F1. Вместо ожидаемого результата 13.5 программа выдаст значение 4.5. Дело в том, что компилятор оттранслирует выражение child.F2(3) в обращение к унаследованной функции Parent::F2, которая в свою очередь вызовет Parent::F1, а не Child::F1, что поддержало бы полиморфное поведение. C++ однозначно определяет позднее связывание в момент выполнения и обеспечивает полиморфное поведение функций посредством их виртуализации. Листинг 3.10 обобщает синтаксис объявления виртуальных функций в базовом и производном классах. class className1 }; class className2 : public className1 }; Листинг 3.10. Объявление виртуальных функции в иерархии классов. Чтобы обеспечить полиморфное поведение функции F1 в объектах классов Parent и Child, необходимо объявить ее виртуальной. Листинг 3.11 содержит модифицированный текст программы. class Parent public: }; class Child : public Parent public: };
void main() cout << child.F2(3) << endl; } Листинг 3.11. Позднее связывание виртуальных функций. Теперь программа выдаст ожидаемый результат 13.5. Компилятор оттранслирует выражение child.F2(3) в обращение к унаследованной функции Parent::F2, которая в свою очередь вызовет переопределенную виртуальную функцию потомка Child::F1. Если функция объявлена в базовом классе как виртуальная, ее можно переопределять только в производных классах и обязательно с тем же списком параметров. Если виртуальная функция производного класса изменила список параметров, то ее версия в базовом классе (и во всех его предшественниках) станет недоступной. Поначалу такая ситуация может показаться тупиковой - и на деле оказывается таковой в языках ООП, которые не поддерживают механизм перегрузки. C++ решает проблему, допуская использовать не виртуальные, а перегруженные функции с тем же именем, но с другим списком параметров. Функция, объявленная виртуальной, считается таковой во всех производных классах - независимо от того, объявлена ли она в производных классах с ключевым словом virtual, или нет. Используйте виртуальные функции для реализации специфического поведения объектов данного класса. Не объявляйте все ваши методы виртуальными - это приведет к дополнительным вычислительным затратам при их вызовах. Всегда объявляйте деструкторы виртуальными. Это обеспечит полиморфное поведение при уничтожении объектов в иерархии классов. 3.5.2 Дружественные функции Дружественные функции, хотя и не принадлежат какому-то классу, однако имеют доступ ко всем приватным и защищенным членам данных внешних классов. Листинг 3.12 обобщает синтаксис объявления дружественных функций с помощью ключевого слова friend перед указанием возвращаемого типа. class className public: }; Листинг 3.12. Объявление дружественных функций. Если обычные функции-члены имеют автоматический доступ ко всем данным своего класса за счет передачи скрытого параметра - указателя this на экземпляр класса, то дружественные функции требуют явной спецификации этого параметра. Действительно, объявленная в классе Х дружественная функция F не принадлежит этому классу, а, значит, не может быть вызвана операторами х.F и xptr->F (где х- экземпляр класса X, a xptr - его указатель). Синтаксически корректными будут обращения F (& х) или F (xpt r). Таким образом, дружественные функции могут решать задачи, реализация которых посредством функций-членов класса оказывается неудобной, затруднительной и даже невозможной.
|