Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
sp_lb3.doc
Скачиваний:
2
Добавлен:
14.11.2019
Размер:
99.33 Кб
Скачать

Лабораторная работа № 3. РАБОТА С виртуальными ФУНКЦИЯМИ В СРЕДЕ ПРОГРАММИРОВАНИЯ Microsoft VISUAL C++

1. Цель работы

Использование виртуальных функций в языке C++.

2. Указания по выполнению работы

Лабораторная работа выполняется с использованием шаблона “Win32 Console Project” MS Visual Studio.NET.

3. Теоретические сведения

3.1Указатели на базовые классы

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

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

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

3.2Виртуальные функции

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

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

CFrame Frm;

CWindow Wind;

После таких объявлений версию функции Draw(), определенную в классе CFrame, будет вызывать инструкция:

Frm.Draw();

а версию функции Draw(), определенную в рамках CWindow, будет вызывать инструкция:

Wind.Draw();

В обоих случаях компилятор без проблем определяет, какая версия функции вызывается, так как вызов функции содержит ссылку на объект класса. Однако в языке C++ принято использовать указатель на базовый класс, содер­жащий либо адрес объекта базового, либо адрес объекта производного класса.

Например, рассмотрим следующий указатель на класс CFrame:

CFrame *PFrm;

В C++ разрешается присваивать этому указателю адрес как объекта класса CFrame, так и объекта класса, прямо или косвенно производного от CFrame, без приведения типов.

Например, в пока­занном ниже фрагменте программы допустимы оба присваивания в последних двух инструкциях:

CFrame *PFrm; // объявляется указатель на класс CFrame

CFrame Frm; // создается объект класса CFrame

CWindow Wind; // создается объект класса CWindow

PFrm = &Frm; // допустимо - присваивается указателю адрес

// объекта CFrame

PFrm = &Wind; // также допустимо - присваивается указателю

// адрес объекта CWindow

Если для вызова функции Draw() используется указатель PFrm, может возникнуть проблема. Компилятор не может определить заранее, на какой тип объекта указывает PFrm, пока программа не начнет выполняться. Поэтому компилятор всегда генерирует вызов версии Draw(), определенной в классе CFrame, так как указатель PFrm объявлен как указатель на CFrame.

Допустим, указатель PFrm содержит адрес объекта Frm, являющегося экзем­пляром класса CFrame:

CFrame *PFrm;

CFrame Frm;

// …

PFrm = &Frm;

В данном случае применение указателя PFrm при вызове функции-члена Draw() будет инициировать вызов версии Draw(), определенной в классе CFrame:

PFrm->Draw(); // генерируется вызов функции Draw() класса CFrame

Предположим теперь, что указатель PFrm содержит адрес объекта класса CWindow:

CFrame *PFrm;

CWindow Wind;

// ...

PFrm = &Wind;

При использовании этого указателя для вызова функции-члена Draw() программа будет по-прежнему вызывать версию Draw(), определенную в классе CFrame:

PFrm->Draw(); // генерируется вызов всё той же функции Draw()

// класса CFrame

В результате получается вызов неправильной версии функции Draw(), создающей прозрачный, а не закрашенный прямоугольник. Решением этой проблемы может быть превращение Draw() в виртуальную функцию. Объявление Draw() как виртуальной функции гарантирует, что при запуске программы будет вызвана корректная версия функции, даже если вызов будет осуществляться через указатель базового класса. Чтобы задать функцию Draw() как виртуальную, нужно включить специфи­катор virtual в объявление этой функции в базовом классе CFrame:

class CFrame

{

// другие объявления

public:

virtual void Draw ();

// другие объявления

};

Спецификатор virtual нельзя включать в определение функции Draw(), находящееся вне объявления класса. Спецификатор virtual можно включить и в объявление функции Draw() в производном классе CWindow, хотя в этом и нет необходимости:

class CWindow : public CFrame

{

// другие объявления

public:

virtual void Draw ();

// другие объявления

};

Если функция Draw() объявлена как виртуальная и программа вызывает ее через указатель PFrm, как показано ниже, компилятор не будет генерировать вызов версии Draw(), объявленной в классе CFrame:

CFrame *PFrm;

// …

PFrm->Draw();

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

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

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

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]