<< Предыдущая страница

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. Кнопки различного вида будут часто появляться в диалоговых окнах графического интерфейса ваших программ.

Рис. 3.1 показывает, что базовый класс TButtonControl способен с помощью родительского метода Draw отображать кнопку в виде двух вложенных прямоугольников: внешней рамки и внутренней закрашенной области.

Чтобы создать простую кнопку без рамки (Рис. 3.2), нужно построить производный класс SimpleButton, использовав в качестве родительского TButtonControl, и перегрузить метод Draw с ограничением его функциональности (Листинг 3.6)

class SimpleButton: public : TButtonControl
{

public:
   SimpleButton(int x, int y) ;
   void Draw() ;
   ~SimpleButton() { }

};

SimpleButton::SimpleButton(int x, int y) :TButtonControl(x, y) { }

void SimpleButton::Draw()
{
    outline->Draw();
}

Листинг 3.6. Ограничение характеристик базового класса.

Единственная задача конструктора объекта для SimpleButton - вызвать базовый класс с двумя параметрами. Именно переопределение метода SimpleButton: : Draw () предотвращает вывод обводящей рамки кнопки (как происходит в родительском классе). Естественно, чтобы изменить код метода, надо изучить его по исходному тексту базовой компоненты TButtonControl.

Теперь создадим кнопку с пояснительным названием (Рис. 3.3). Для этого нужно построить производный класс TextButton из базового TButtonControl, и перегрузить метод Draw с рас-Рис. 3.3. Кнопка с текстом, ширением его функциональности.

Листинг 3.7 показывает, что объект названия title класса Text создается конструктором TextButton, а метод

SimpleButton->Draw() отображает его:

class Text
{

public:
   
Text(int x, int y, char* string) { }
    void
Draw() { }

};

class TextButton: public : TButtonControl
{
    Text* title;

public:
   
TextButton(int x, int y, char* title);
    void Draw();
   ~TextButton() { }

};

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:
   
double r ;

public:
    Circle (double rVal =0) : r(rVal) {}
    void setRadius(double rVal) { r = rVal; }
    double getRadius() { return r; }
     double
Area() { return pi*r*r; }
    void showData() ;

};

 

class Cylinder : public Circle
{

protected:
   
double h; 

public:
    Cylinder(double hVal = 0, double rVal = 0) : getHeight(hVal), Circle(rVal) { }
    void setHeight(double hVal) { h = hVal; }
    double getHeight() { return h; }
    double Area() { return 2*Circle::Area()+2*pi*r*h; }
    void showData() ;
    void Circle::showData() {
       cout << "Радиус окружности = " << getRadius() << endl
       << "Площадь круга = " << Area() << endl << endl;

}

void Cylinder::showData()
{
    cout << "Радиус основания = " << getRadius() << endl
    << "Высота цилиндра = " << getHeight() << endl
    << "Площадь поверхности = " << Area () << endl;
}

void main()
{
    Circle circle(2) ;
    Cylinder cylinder(10, 1);

   circle.showData () ;
   cylinder.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:
   
double F1(double x) { return x*x; }
   double F2(double x) { return F1(x)/2; } 

};

class Child : public Parent
{

public:
   
double F1(double x) { return x*x*x; }

};

 

void main()
{
    Child child;

    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
{
// Другие функции-члены
virtual returnType functionName(<список параметров>) ;

};

class className2 : public className1
{
// Другие функции-члены
virtual returnType functionName(<cпиcoк параметров>) ;

};

Листинг 3.10. Объявление виртуальных функции в иерархии классов.

Чтобы обеспечить полиморфное поведение функции F1 в объектах классов Parent и Child, необходимо объявить ее виртуальной. Листинг 3.11 содержит модифицированный текст программы.

class Parent
{

public:
virtual double F1(double x) { return x*x; }
double F2(double x) { return F1(x)/2; }

};

class Child : public Parent
{

public:
virtual double F1(double x) { return x*x*x; }

};

 

void main()
{
    Child child;

    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:
    className(); // Конструктор по умолчанию
// Другие конструкторы

friend
returnType friendFunction(<список параметров>) ;

};

Листинг 3.12. Объявление дружественных функций.

Если обычные функции-члены имеют автоматический доступ ко всем данным своего класса за счет передачи скрытого параметра - указателя this на экземпляр класса, то дружественные функции требуют явной спецификации этого параметра. Действительно, объявленная в классе Х дружественная функция F не принадлежит этому классу, а, значит, не может быть вызвана операторами х.F и xptr->F (где х- экземпляр класса X, a xptr - его указатель). Синтаксически корректными будут обращения F (& х) или F (xpt r).

Таким образом, дружественные функции могут решать задачи, реализация которых посредством функций-членов класса оказывается неудобной, затруднительной и даже невозможной.

<< Предыдущая страница