Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Delete pcircle; // удаление окружности

Поиск

Класс окружностей CCircle наследует от фигур CCenteredShape данные для отображения координат центра, а также функцию для печати этих координат в консольном окне. Окружность добавляет компонент для хранения радиуса, а также функцию для вычисления площади. Особое внимание следует обратить на тело конструктора CCircle. В нем в списке инициализаторов (между символом двоеточие и открывающей фигурной скобкой) записан вызов конструктора базового класса CCenteredShape. Он инициализирует компоненты __x и __y значениями x и y соответственно. Когда управление возвращается в конструктор класса CCircle, он инициализирует компонент __radius значением r.

Рассмотрим еще один пример. Определим класс «дата» CDate и класс «сотрудник» CEmployee. Отношение между этими классами можно представить закрытым наследованием, так как дата (например, приема на работу) – один из атрибутов сотрудника.

struct СDate { // дата

public:

// типы

typedef unsigned int day_t; // день

typedef enum month_t { // месяц

jan = 1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec

} month_tag;

typedef unsigned int year_t; // год

protected:

// конструктор

СDate(day_t d, month_t m, year_t y): __day(d), __month(m), __year(y) {}

// виртуальный деструктор[9]

virtual ~CDate() {}

// печать даты

void print(void) const {

cout << endl << __day << ":" << __month << ":" << __year;

}

private:

day_t __day;

month_t __month;

year_t __year;

}; /* СDate */

class СEmployee: private СDate { // сотрудник

public:

// типы

typedef unsigned long salary_t; // оклад

typedef enum gender_t { male, female } gender_tag; // пол

// конструктор

СEmployee(const string & name, gender_t gender, const string &

position, const string & department, salary_t salary,

const string & address, day_t day, month_t month, year_t year)

// вызов конструктора базового класса

: СDate(day, month, year),

// инициализация компонент-объектов

__name(name), __position(position),

__department(department), __address(address)

{ // инициализация компонент встроенных типов

__gender = gender;

__salary = salary;

}

// осведомительные функции

const string & TellName() const;

const string & TellDepartment() const;

const string & TellPosition() const;

gender_t TellGender() const;

salary_t TellSalary() const;

const string & TellAddress() const;

Void PrintHireDate() const; // печать даты найма

// функции, имитирующие поведение объекта «сотрудник»

string Work(const string & task) const; // «работа»

String DoMission(// «командировка»

const string & destination, const string & task) const;

salary_t GetSalary(void) const; // «получение зарплаты»

...

Private: // атрибуты сотрудника

string __name;

gender_t __gender;

string __position;

string __department;

salary_t __salary;

string __address;

}; /* СEmployee */

...

// имитация поведения виртуального сотрудника

CEmployee employee(

“Ivan Petrov”, CEmployee::male, “salesman”,

“accounting”, 2300, “Moscow...”);

Employee.PrintHireDate(); // дата найма

string result = employee.Work(“negotiations”); // работа

if (result == “success”)

Employee.GetSalary(); // получение зарплаты

...


Виртуальные и невиртуальные функции. Полиморфизм

При наследовании производный класс заимствует от базового класса все функции, в том числе унаследованные. Если они сохраняются без изменений, то поведение объектов производного класса идентично поведению объектов базового класса. Если производный класс добавляет новые функции, то у его объектов появляются новые аспекты поведения, т.е. новая функциональность. На практике поведение объектов производных классов редко целиком копирует поведение объектов базовых классов: часть функций может, конечно, сохраниться без изменений, однако производный класс, почти наверное, «захочет» иным способом реализовать некоторые из них. Например, класс «закрашенная окружность», производный от класса «окружность», вряд ли без изменения оставит унаследованную функцию рисования, поскольку у закрашенных окружностей изменяется алгоритм рисования. Подобные изменения (переопределения) функций называют полиморфным поведением, или полиморфизмом. При полиморфизме объекты классов, связанных отношением наследования, по-разному реализуют определенные аспекты поведения, что придает гибкость в построении приложений.

Чтобы ввести полиморфное поведение в класс, необходимо объявить в нем хотя бы одну виртуальную функцию. Виртуальной может быть только нестатическая компонентная функция. Заголовок такой функции характеризуется наличием служебного слова virtual перед типом возвращаемого значения. Класс с виртуальными функциями принято называть полиморфным. Виртуализация функции имеет глубокий смысл и внутреннюю логику. Если функция объявлена виртуальной, то производные классы могут по своему определить (реализовать) эту функцию. В случае если производному классу не нужно переопределять данную функцию, он просто наследует ее от базового класса. Независимо от того, переопределяется ли функция или нет, ее заголовок (интерфейс) сохраняется без изменений.

В примере, приведенном ниже, упрощенно определяется класс «животное» (CAnimal), инкапсулирующий свойства и поведение абстрактного животного. От него формируются два производных класса: «рыбы» (CFish) и «птицы» (CBird). В классе CAnimal имеется функция Move, отображающая «движение» животного. Поскольку не все животные движутся одинаковым способом (какие-то из них плавают, какие-то бегают, какие-то летают, а какие-то ползают), функция Move сделана виртуальной, что дает возможность производным классам (даже тем, которых пока нет в текущей иерархии классов) переопределить алгоритм «движения» животного.

class CAnimal { // Абстрактное животное

public:

Virtual void Move() const // виртуальная функция

{

cout << "Животное неизвестного вида движется\n";

}

virtual ~CAnimal() {} // виртуальный деструктор

protected:

Int nlegs; // число конечностей

// иные атрибуты

};

class CFish: public CAnimal { // Рыба

public:

CFish(int n) { nlegs = n; }

void Move() const { cout << "Рыба плывет\n"; }

};

class CBird: public CAnimal { // Птица

public:

CBird(int n){ nlegs = n; }

void Move() const { cout << "Птица летит\n"; }

};

Имея приведенные определения, можно создать объекты классов CAnimal, CFish, CBird в стеке или динамической памяти:

CAnimal A; // какое-то животное

CFish Perch(3); // окунь

CBird Sparrow(2); // воробей

Вызывая функцию Move для разных объектов, получим проявление полиморфного поведения:

Perch.Move(); // напечатает «Рыба плывет»

Sparrow.Move(); // напечатает «Птица летит»

A.Move(); // напечатает «Животное неизвестного вида движется»

Подобное проявление называют статическим полиморфизмом. Ключевая его особенность в том, что вызываемая функция явно выбирается по типу объекта, который ее «вызывает». Этот тип принято называть статическим (полученным при объявлении).

Рассмотрим теперь следующее определение функции (эта функция просто вызывает для животного a его функцию «движения»):

void SimulateMoving(const CAnimal & a) {

a.Move();

}

и ее вызовы в виде:

SimulateMoving(A);

SimulateMoving(Perch);

SimulateMoving(Sparrow);

Первый вызов, очевидно, даст результат «Животное неизвестного вида движется». Однако результаты второго и третьего вызовов будут отличаться. Второй даст «Рыба плывет», а третий – «Птица летит». Такое поведение принято называть виртуальным полиморфизмом. Виртуальный полиморфизм предполагает выбор вызываемой функции не по статическому типу объекта, а по так называемому динамическому типу. В функции SimulateMoving параметр a имеет статический тип CAnimal, однако динамический тип a будет зависеть от того, объект какого класса является аргументом. Если вместо a подставить объект класса CFish, динамический тип a будет уточнен до const CFish &. Если вместо a подставить объект класса CBird, динамический тип a будет уточнен до const CBird &. Виртуальный полиморфизм становится возможным именно благодаря такому преобразованию типов.

Еще один пример проявления виртуального полиморфизма приведен ниже (вместо ссылок теперь везде фигурируют указатели).

Пример

...

CAnimal * pAnimal = &A;

pAnimal –> Move(); // напечатает «Животное неизвестного вида движется»

pAnimal = &Perch;

pAnimal –> Move(); // напечатает «Рыба плывет»

pAnimal = &Sparrow;

pAnimal –> Move(); // напечатает «Птица летит»

Здесь статический тип объекта *pAnimal есть CAnimal, но динамический тип изменяется от присваивания к присваиванию. Соответственно, вызывается та функция, которая отвечает текущему динамическому типу указателя.

После рассмотрения примеров можно дать более строгое определение. Динамический тип указателя или ссылки – это статический тип того объекта, на который ссылается указатель или ссылка.

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

Основа в работе с виртуальными функциями – таблица виртуальных функций (называемае сокращенно vtable). Такая таблица есть у каждого полиморфного класса. В ней записаны указатели на все виртуальные функции класса, включая и унаследованные им виртуальные функции. Каждая функция в таблице имеет индекс, по которому ее можно легко идентифицировать. Объект всякого полиморфного класса имеет доступ к таблице vtable. Доступ организуется за счет добавления к объекту дополнительного указателя на таблицу виртуальных функций класса (его называют vptr). Добавление осуществляется компилятором автоматически, причем расположение указателя vptr относительно данных объекта зависит от реализации и стандартом языка С++ не оговаривается.

В терминах С++ вызов виртуальной функции через vtable можно записать следующим оператором:

(*pObject –> vptr[i])(pObject);

где pObject – указатель (this) на объект, вызывающий функцию, а i – индекс виртуальной функции в таблице vtable. При вызове сначала происходит обращение к указателю vptr, по которому определяется начало таблицы vtable. Далее, с использованием индекса i, выбирается указатель на нужную виртуальную функцию. После этого указатель разыменовывается и выполняется вызов по нему функции. Отметим, что в данном примере вызывается функция без аргументов. Если аргументы имеются, то они следуют за указателем pObject в списке аргументов.

При работе с виртуальными функциями и полиморфными классами нужно помнить о некоторых правилах и ограничениях.

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

Во-вторых, одноименные функции базового и производного классов, отличающиеся списками параметров, не будут виртуальными друг к другу даже при наличии служебного слова virtual (функция производного класса будет просто скрывать унаследованную функцию базового класса в сфере действия производного класса). Следует отметить, что скрытая в производном классе функция базового класса может быть переопределена в следующем производном классе.

В-третьих, виртуальные функции должны иметь идентичные или ковариантные (соизмеримые) типы возвращаемых значений. Эти типы (в случае неидентичности) будут соизмеримы при выполнении следующих трех условий: а) они являются указателями или ссылками на классы; б) классы по п.«а» совпадают или класс в возвращаемом значении переопределяемой функции (overriden) является однозначным базовым классом для класса переопределяющей функции (overrider), причем первый доступен в производном классе; в) типы имеют одинаковый уровень квалификации модификаторами const, volatile, const volatile или же в переопределяющей функции уровень квалификации ниже. Если одноименные функции базового и производного классов имеют несоизмеримые типы возвращаемых значений (например, void и char), возникает ошибка компиляции.

В-четвертых, если деструктор первичного базового класса виртуален, то деструкторы всех производных от него классов также будут виртуальными, несмотря на то, что деструкторы не наследуются. Виртуализация деструкторов позволяет корректно удалять объекты производных классов; без нее будет всегда удаляться только часть объекта, соответствующая последнему производному классу[10].

Ниже рассмотрены примеры, поясняющие описанные правила работы с виртуальными функциями.

Пример

struct CBase {

virtual void member_function();

virtual const CBase * member_function_2(int);

virtual ~CBase() {} // все деструкторы иерархии будут виртуальными

};

struct CDerived: public CBase {

void member_function(int); // скрывает виртуальную функцию из CBase

CBase * member_function_2(int); // переопределяет функцию из CBase

// тип значения менее квалифицирован

// виртуальный деструктор создается автоматически

};

struct CDerived_2: public CDerived {

void member_function(); // переопределяет функцию из CBase

CDerived_2 * member_function_2(int); // переопределяет функцию CBase

// типы значений соизмеримы

// виртуальный деструктор создается автоматически

};

// неправильная иерархия классов

class BaseClass {

public:

// невиртуальный деструктор

~BaseClass() { /* код деструктора */ };

...

};

class DerivedClass: public BaseClass {

...

};

...

BaseClass * pBaseClass = new DerivedClass;

...

delete pBaseClass;

// неопределенное поведение из-за невиртуального деструктора

Кроме отмеченных выше общих требований к виртуальным функциям, существует ряд специфических правил, следование которым способно улучшить качество программ. Одно из них относится к совместному использованию виртуального полиморфизма и массивов. Сразу же следует отметить, что такое использование очень рискованно. Рассмотрим для примера функцию, которая просто печатает элементы массива объектов в некоторый поток:

void PrintObjects(ostream & OutStream, const SomeBaseClass array[], int nItems)

{

for (int i=0; i<nItems; ++i) OutStream << array[i];

}

Если в функцию передается массив из nItems константных объектов класса SomeBaseClass, то она корректно выводит их значения в поток[11]:

SomeBaseClass arrayBase[10];

...



Поделиться:


Последнее изменение этой страницы: 2016-08-14; просмотров: 123; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 18.117.94.180 (0.007 с.)