Механизм переопределения

Применение виртуальных функций для модификации базовых классов

Применение виртуальных функций для управления объектами классов

Полиморфизм

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

PRect->Draw( );

для рисования прямоугольников, блоков и блоков с закругленными углами. Конкретно выполняемое действие определяется классом объекта, на который в данный момент адресуется указателем PRect.

Виртуальные функции позволяют создавать простые универсальные подпрограммы, автоматически управляющие множеством объектов различных типов. Предположим, имеется программа, позволяющая создавать прямоугольники, блоки или блоки с закругленными углами. Каждый раз, когда пользователь создает одну из этих фигур, программа вызывает новый оператор для динамического создания объекта соответствующего класса (CRectangle, CBlock или CRoundBlock), управляющего новой фигурой. Так как CBlock и CRoundBlock являются производными от CRectangle, то указатели на все объекты удобно хранить в виде одномерного массива указателей на CRectangle, как показано в следующем фрагменте программы.

const int MAXFIGS = 100; CRectangle *PFigure [MAXFIGS]; int Count = 0;// ...// пользователь создает блок:PFigure [Count++] = new CBlock (10, 15, 25, 30, 5);// ...// пользователь создает прямоугольник:PFigure [Count++] = new CRectangle(5, 6, 19, 23);// ...// пользователь создает блок с закругленными углами;PFigure [Count++] = new CRoundBlock(27, 33, 43, 56, 10, 5);

Предположим, что имеется подпрограмма перерисовки всех объектов на экране. Если функция Draw не определена как виртуальная, то для каждого элемента массива необходимо каким-либо образом определить тип фигуры, а затем вызвать соответствующую версию функции Draw. Например, в класс CRectangle можно добавить переменную с именем Туре, в которой будет храниться код, идентифицирующий класс объекта.

//НЕ РЕКОМЕНДУЕТСЯ:class CRectangle { // другие определения ... public: int: Type; // наследуется всеми производными классами; // хранит код, идентифицирующий класс объекта: // RECT, BLOCK, либо ROUNDBLOCK }

В данном примере предполагается, что три символьные константы RECT, BLOCK и ROUNDBLOCK были определены в программе предварительно.

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

// дурной тон; НЕ РЕКОМЕНДУЕТСЯ: for (int i = 0; i < Count; ++i) switch (PFigure [i]->Type) { case RECT: PFigure[i]->Draw(); break; case BLOCK: ((CBlock *)PFigure[i])->Draw(); break; case ROUNDBLOCK: ((CRoundBlock *)PFigure[i])->Draw(); break; }

Этот пример не только неуклюж, но и требует добавления новой ветки case в оператор switch при изменении программы для поддержки нового типа фигур (например, при внесении в иерархию класса для создания новой фигуры).

Но, если сделать функцию Draw виртуальной, добавив спецификатор virtual в ее объявление внутри класса CRectangle, программа автоматически вызовет правильную версию функции для текущего типа объекта. Перерисовка фигуры может быть выполнена с помощью следующего фрагмента программы.

for (int i = 0; i < Count; ++i) PFigure [i]->Draw{);

Этот код проще и компактнее. К тому же он не требует изменения при добавлении в иерархию нового класса, поддерживающего фигуры другого типа.

Примечание

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

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

class CMessageBox{ protected: char *Message; virtual void DrawBackground (int L, int T, int R, int B); // закрашивает фон окна сообщения в белый цвет public: CMessageBox() { Message = new char (‘\0'}; } ~CMessageBox() { delete [] Message; } void Display() { DrawBackground (0, 0, 35, 25); // код для вывода строки сообщения ... } void Set (char *Msg); };

Открытая функция-член Set позволяет передать строку сообщения, a Display выводит это сообщение на экран. Отметим: функция Display очищает фон, вызывая другую функцию-член DrawBackground и передавая ей размеры окна сообщения. Эта функция закрашивает фон, используя непрозрачный белый цвет. Функция DrawBackground предназначена для использования внутри класса. Она не предназначена для вызова извне класса и, следовательно, объявлена как защищенный член класса.

Функция DrawBackground также объявлена виртуальной. Соответственно, при порождении от класса CMessageBox нового класса, содержащего собственную версию DrawBackGround, новая функция перекроет старую, даже если будет вызываться из функции-члена класса CMessageBox. Например, от класса CMessageBox можно породить следующий класс.

class CMyMessageBox : public CMessageBox{ protected: virtual void DrawBackground (int L, int T, int R, int B) { // закрашивает фон окна сообщения СИНИМ цветом ... } };

Обратите внимание: новая версия функции DrawBackground создает синий фон, а не белый. Следовательно, в приведенном ниже фрагменте будет создано окно сообщения с синим фоном.

CMyMessageBox MyMessageBox;MyMessageBox.Set ("hello"); MyMessageBox.Display ();

Делая функцию DrawBackground виртуальной, мы получаем возможность настраивать класс CMessageBox (а именно, цвет или узор, которым заполняется фон окна сообщения) без модификации исходного текста данного класса (при этом даже не нужно просматривать исходный программный код). Многие классы, определенные в библиотеке MFC, предусматривают использование виртуальных функций, которые можно переопределять в производных классах, что позволяет легко модифицировать MFC-классы.

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

Вспомним: ссылка на член класса из функции-члена неявно связана с применением указателя this. Таким образом, функцию Display класса CMessageBox можно записать следующим способом.

class CMessageBox { // другие определения ... public: void Display() { this->DrawBackground (0, О, 35, 25); // ... } // другие определения ...};

Если бы функция DrawBackground не являлась виртуальной, то ее вызов из функции Display инициализировал бы вызов версии DrawBackground класса CMessageBox (так как в пределах этого класса указатель this является указателем на объект класса CMessageBox). Однако если функция DrawBackground является виртуальной, ее вызов инициализирует вызов версии DrawBackground класса текущего объекта. Таким образом, если функция Display вызвана для объекта класса CMyMessageBox, то будет вызвана и DrawBackground, определенная внутри данного класса.

CMyMessageBox MyMessagBox;// ...MyMessageBox.Display ();