Использование виртуальных функций.ppt
- Количество слайдов: 109
Использование виртуальных функций
В С++ полиморфизм поддерживается и во время компиляции и во время выполнение программы. Перегрузка функций и операций – это пример полиморфизма во время компиляции. Поддержка полиморфизма во время выполнения программы достигается использованием указателей на базовые классы и виртуальные функции.
Указатели на производные типы
Указатели на базовый тип и на производный тип зависимы. Пусть имеем базовый тип B_class и производный от B_class тип D_class. В С++ всякий указатель, объявленный как указатель на B_class, может быть также указателем на D_class.
B_class *p; // указатель на объект типа B_class B_obj; // объект типа B_class D_ob; // объект типа D_class После этого можно использовать следующие операции: p=&B_ob; // указатель на объект типа B_class p=&D_ob; // указатель на объект типа D_class
Все элементы класса D_class, наследуемые от класса B_class, могут быть доступны через использование указателя p. Однако на элементы, объявленные в D_class, нельзя ссылаться, используя p. Если требуется иметь доступ к элементам, объявленным в производном классе, используя указатель на базовый класс, надо привести его к указателю на производный тип.
Например, это можно сделать так: ((D_class *)p)->f(); Здесь функция f() – член класса D_class. Внешние круглые скобки необходимы. Хотя указатель на базовый класс может быть использован как указатель на производный класс, обратное, неверно: нельзя использовать указатель на производный класс для присваивания ему адреса объекта базового класса. Кроме того, указатель увеличивается или уменьшается при операциях ++ и – относительно его базового типа.
Пример использования указателей на базовый класс. При этом каждый класс будет содержать функцию void show(void), свою в каждом классе.
#include
void main(void) { Base bobj, *pb; Derive dobj, *pd; Derive 1 d 1 obj, *pd 1; Derive 2 d 2 obj, *pd 2; pb=&bobj; bobj. show(); // вызов функции pb->show(); // show() класса Base pd=&dobj; dobj. show(); // вызов функции pd->show(); // show() класса Derive pd 1=&d 1 obj; d 1 obj. show(); // вызов функции pd 1 ->show(); // show() класса Derive 1 pd 2=&d 2 obj; d 2 obj. show(); // вызов функции pd 2 ->show(); // show() класса Derive 2
pb=&dobj; // указателю на базовый класс присвоен адрес производного класса Derive pb->show(); // вызов show() класса Base!!!!! pb=&d 1 obj; // указателю на базовый класс присвоен адрес производного класса Derive 1 pb->show(); // вызов show() класса Base!!!!! ((Derive 1 *)pb)->show(); // Вызов функции show() класса Derive 1 pd 1=&d 2 obj; // указателю на базовый класс Derive 1 // присвоен адрес производного класса Derive 2 pd 1 ->show(); // вызов функции show() класса Derive 1 pd 1 ->Base: : show(); // вызов функции show() класса Base! pd 1 ->Derive: : show(); // вызов функции show() класса Derive! pd 1 ->show(); // вызов функции show() класса Derive 1 ((Derive 2 *)pb)->show(); // Вызов функции show() класса Derive 2 }
Виртуальные функции
Полиморфизм во время выполнения программы поддерживается использованием производных типов и виртуальных функций. Виртуальные функции – это функции, которые объявляются с использованием ключевого слова virtual в базовом классе и переопределяются (override) в одном или нескольких производных классах. При этом прототипы функций в разных классах одинаковы.
Для виртуальной функции имеет место следующее: при вызове функции, объявленной виртуальной через указатель на базовый тип, во время выполнения программы определяется, какая виртуальная функция будет вызвана, в зависимости от того, на какой объект какого класса будет настроен указатель. Получаем, что когда указателю базового типа присвоены адреса объектов различных производных классов, выполняются различные версии виртуальных функций.
Виртуальная функция объявляется виртуальной в базовом классе с помощью ключевого слова virtual. При переопределении функции в производном классе это слово не указывается.
Пример
class Base { public: virtual void show(void) { cout << “В базовом класее Basen; } }; class Derive: public Base{ void show(void) { cout<<”В производном классе Deriven”; } }; class Derive 1: public Derive{ void show(void) { cout<<”В производном классе Derive 1n”; } }; class Derive 2: public Derive 1{ void show(void) { cout<<”В производном классе Derive 2n”; } };
void main(void) { Base bobj, *pb; Derive dobj, *pd; Derive 1 d 1 obj, *pd 1; Derive 2 d 2 obj, *pd 2; pb=&bobj; pb->show(); // вызов функции show() класса Base pd=&dobj; // вызов функции pd->show(); // show() класса Derive виртуальность функции не исп. pd 1=&d 1 obj; // вызов функции pd 1 ->show(); // show() класса Derive 1 pd 2=&d 2 obj; // вызов функции pd 2 ->show(); // show() класса Derive 2
pb=&dobj; // указателю на базовый класс присвоен // адрес производного класса Derive pb->show(); // вызов show() класса Derive, исп. Механизм вирт. Функций pb=&d 1 obj; // указателю на базовый класс присвоен // адрес производного класса Derive 1 pb->show(); // вызов show() класса Derive 1 pd 1=&d 2 obj; // указателю на базовый класс Derive 1 присвоен // адрес производного класса Derive 2 pd 1 ->show(); // вызов функции show() класса Derive 2 // работает механизм виртуальных функций pd 1 ->Base: : show(); // Явный вызов функции show() класса Base! pd 1 ->Derive: : show(); // Явный вызов функции show() класса Derive! ((Derive 2 *)pb)->show(); // Вызов функции show() класса Derive 2 }
Замечания к использованию виртуальных функций
Виртуальная функция должна быть членом класса. Она не может быть дружественной для класса, в котором определена, но дружественной к другому классу. Функция, которая объявлена виртуальной, остается виртуальной не зависимо от того, сколько производных классов построено. Если в рассмотренном примере класс Derive 1 будет производным классом для Derive, а не для Base, то функция show() в классе Derive 1 остается виртуальной. Если в производном классе функция не замещает виртуальную, так как она не объявлена или имеет другой прототип, то вызывается функция базового класса.
Виртуальные функции удобны для использования, так как в общем случае, базовый класс задает основной интерфейс, который будут иметь производные классы, а производные классы задают свой метод. Для описания полиморфизма часто используется понятие “один интерфейс, много методов”. Так как ООП позволяет создавать сложные программы, то при корректном построении производных классов все объекты, начиная с базового класса, доступны с помощью одного и того же метода, то нужно помнить только интерфейс. Отделение интерфейса от наполнения функций позволяют создавать классы библиотек (class libraries).
Вызов виртуальной функции реализуется как непрямой вызов по таблице виртуальных функций класса. Таблицу создает компилятор, а связывание происходит во время выполнения. Термин позднее связывание (late binding)
Пример использования виртуальных функций
Введем класс figure, который описывает плоскую фигуру, для вычисления площади которой достаточно двух измерений. В этом классе есть виртуальная функция show_area(), выводящая значение площади фигуры. На основе этого класса строятся другие классы triangle, rectangle, circle, для которых определена конкретная формула вычисления площади фигуры.
#include
class triangle: public figure{ public: void show_area() { cout<<”n треугольник с высотой “<< x<<” и основанием “<
class circle: public figure{ public: void show_area() { cout<<”n круг с радиусом “<< x; cout<<” имеет площадь “<< 3. 14159*x*x; <
void main(void) { figure f, *p; // объявление указателя на базовый тип triangle t; // создание объектов производного типа rectangle s; circle c; p=&f; p->set_dim(1, 2); p->show_area(); p=&t; p->set_dim(3, 4); p->show_area(); p=&s; p->set_dim(5, 6); p->show_area(); p=&c; p->set_dim(7); p->show_area(); }
Чистые виртуальные функции и абстрактные типы
Когда виртуальные функции вызываются из производного класса, но не замещаются, то вызывается соответствующая функция базового класса (типа). Но часто во многих функциях нет смыслового определения виртуальной функции в базовом классе. При создании библиотек классов для виртуальных функций еще не известно, будет ли смысловое значение в контексте базового класса. Есть два способа решения этой проблемы. Первый способ – выдача предупреждающего сообщения (не всегда приемлем). Другое решение проблемы – использование в С++ чистых виртуальных функций.
Чистая виртуальная функция – это функция объявленная в базовом классе как виртуальная, но не имеющая описания в базовом классе. Производный тип должен определить свою собственную версию – нельзя просто использовать версию, определенную в базовом классе.
Форма определения чистой виртуальной функции следующая: virtual тип имя-функции(список параметров) = 0; тип – возвращаемый тип функции; =0 - признак чистой виртуальной функции.
Пример, содержащий использование чистой виртуальной функции
#include
class rectangle: public figure{ public: void show_area() { cout<<”n прямоугольник со сторонами “<< x<<” и “<
void main(void) { figure *p; // объявление указателя на базовый тип, создать объект нельзя!! triangle t; // создание объектов производного типа rectangle s; circle c; p=&f; p->set_dim(1, 2); p->show_area(); p=&t; p->set_dim(3, 4); p->show_area(); p=&s; p->set_dim(5, 6); p->show_area(); p=&c; p->set_dim(7); p->show_area(); }
Объявление функции show_area() чистой виртуальной требует от всех производных классов собственных наполнений этой функции. Класс, имеющий по крайней мере одну чистую виртуальную функцию, называется абстрактным классом (abstract class). Абстрактные классы имеют одну важную особенность: может не быть объектов этого класса.
Абстрактный класс должен использоваться только как базовый класс, от которого наследуются другие производные классы. Причина, по которой абстрактные классы не могут использоваться для объявления объекта, состоит в том, что одна или более функций не имеют определения. Однако, даже если базовый класс абстрактный, можно создать указатель на объект базового класса и применить его для использования механизма виртуальных функций.
Производные классы и их конструкторы и деструкторы
Базовый и производные классы могут иметь конструкторы и деструкторы. Рассмотрим, в каком порядке вызываются эти функции. Этот вопрос возникает и при множественном наследовании.
Пример
#include
void main() { Derive 1 d 1; cout<<”n”; Derive 2 d 2; cout<<”n”; } В программе создаются объекты типа Derive 1 и Derive 2, при этом выполняются конструкторы и деструкторы в следующей последовательности.
На экране: Конструктор класса Base Конструктор производного класса Derive 1 Конструктор производного класса Derive 2 Деструктор производного класса Derive 1 Деструктор производного класса Base
Конструкторы и деструкторы при множественном наследовании
В С++ разрешено при создании производного класса пользоваться несколькими базовыми классами. При объявлении производного класса базовые классы перечисляются через запятую. При создании объекта конструкторы выполняются в порядке следования базовых классов слева направо.
Пример
#include
void main() { Base 1 b 1; cout<<”n”; Base 1 b 2; cout<<”n”; Derive d; cout<<”n”; } Деструкторы выполняются в порядке, обратном по отношению к конструкторам.
Виртуальные базовые классы
При множественном наследовании базовый класс не может быть задан в производном классе более одного раза: class Derive: Base, Base {…}; // ошибка В то же время базовый класс может быть передан производному классу более одного раза косвенно: class X: public Base{…}; class Y: public Base{…}; class Derive: public X, public Y{…};
Этот пример соответствует схеме наследования, когда каждый объект класса Derive будет иметь два подобъекта класса Base. Чтобы избежать неоднозначности при обращении к членам базового объекта Base, можно объявить этот базовый класс виртуальным. Для этого используется то же зарезервированное слово virtual, что и при объявлении виртуальных функций. class X: virtual public Base{…}; class Y: virtual public Base{…}; class Derive: public X, public Y{…}; Теперь класс Derive имеет только один подобъект класса Base.
Операции динамического выделения памяти new и delete
В С для динамического выделения и освобождения памяти используются функции malloc() и free(), а для определения размера необходимой памяти – операция sizeof. В C++ используются две операции – new и delete. Их форма использования: pointer_var = new var_type; delete pointer_var; pointer_var – указатель - типа var_type;
Операция new выделяет соответствующее место для переменной в соответствующей области памяти и возвращает адрес выделенного места. Неуспешная попытка – возвращение нулевого указателя NULL. Операция delete освобождает соответствующую память, на которую указывает pointer_var. Удобство операции new состоит в том, что операция сама определяет размер переменной var_type автоматически и возвращает указатель, уже преобразованный к этому типу.
Пример использования этих операций
#include
Пример работы с динамически выделенной памятью под массив
#include
Динамическое выделение памяти целесообразно для выделения памяти под большие объекты, массивы, особенно под массивы неизвестного заранее размера. Часто динамическое выделение памяти используется в конструкторах класса, элементом которого является массив (массивы). При этом для выделения памяти под объект также может использоваться динамическое выделение памяти.
Пример класса queue с динамическим выделением памяти под массив
#include
void queue: : qput(int i) { if(sloc==size) { cout<<”n Очередь полна!n”; return; } q[sloc++]=i; } int queue: : qget(void) { if(sloc==rloc) { cout<<”n Очередь пуста!n”; return 0; } return q[rloc++]; }
void main() { queue a(5), b(100); // объявили два объекта a. qput(10); b. qput(19); a. qput(20); b. qput(1); cout<
pq = new queue (s); // динамическое создание объекта // именно сейчас вызывается конструктор queue if (!pq) { cout<<”n недостаточно памяти n”; return 0; } else cout<<”n Объект класса queue создан n”; for(int i=0; iqput(2*i+1); // заполнение очереди for(i=0; iqget()<<” “; cout<
Виртуальные деструкторы
При программировании на С++ также типичной является ситуация, когда динамически создается объект производного класса, указатель используется базового класса.
Пример
#include
void main(void) { Base *pb=new Derive 2; if(!pb) { cout<<”n Недостаточно памяти!n”; return 1; } cout<
В программе было вызвано три конструктора и всего один деструктор. При удалении объекта через указатель на базовый класс вызывается лишь деструктор базового класса. Если бы в конструкторах выделялась динамическая память при создании объекта, эта память при разрушении объекта не освобождалась бы корректно. Эта проблема решается через использование виртуального деструктора.
Если при объявлении деструктора базового класса он объявляется как виртуальный, то все конструкторы производных классов также являются виртуальными. При разрушении объекта с помощью операции delete через указатель на базовый класс будут корректно вызваны деструкторы всех классов.
Пример
#include
void main(void) { Base *pb=new Derive 2; if(!pb) { cout<<”n Недостаточно памяти!n”; return 1; } cout<
Шаблоны классов и функций
В языке С++ предусмотрена еще одна реализация полиморфизма – шаблоны функций (Function Templates) и шаблоны классов (Class Templates).
Шаблоны функций
При перегрузке функций рассматривали семейство функций sqr_it() с различными типами аргументов. Эти функции возвращали квадрат аргумента и тип возвращаемого значения совпадал с типом аргумента. Для каждого типа описывалось свое тело функции, причем отличались функции только типом аргумента и типом возвращаемого значения. Шаблоны функций позволяют использовать в качестве аргумента тип переменной. template
Любой тип данных, а не только определенный как класс может использоваться применении этих функций. Над типом, для которого будет вызываться функция, определенная шаблоном, должны быть определены операции над переменными типа Т, которые используются в теле функции. Шаблонные функции могут использоваться совместно с функциями, определенными обычным образом, с тем же именем.
Пример
#include
В шаблоне функции может использоваться необязательно один тип как параметр шаблона. Например, функции max (a, b) с различными типами аргументов могут быть определены следующим образом: template
Пример
include
Шаблоны классов
Шаблоны классов позволяют построить отдельные классы аналогично шаблонам функций. Шаблон класса задает параметризованный тип. Имя шаблона класса должно быть уникальным и не может относиться к какому-либо шаблону, классу, функции, объекту и т. д. В качестве параметра при задании шаблона класса может использоваться не только тип, но и другие параметры, например целочисленные значения (но эти параметры должны быть константными выражениями).
Преобразуем класс queue, задав этот класс шаблоном. Параметрами шаблона будет тип массива, входящего в класс queue, и размер size этого массива.
#include
template
void main(void) { queue
// Динамическое создание объекта, именно сейчас вызывается конструктор queue if((!pq) { cout<<”n Недостаточно памяти n”; return 0; } else cout<<”n Объект класса queue создан n”; for (int i=0; iqput((i/2. 0+i); // заполнение очереди for ( i=0; iqget()<<” “; cout<
Статические члены класса
В С++ можно использовать статические члены класса, которые являются общими для всех объектов данного класса. Статические члены класса объявляются с атрибутом памяти static. Изменив значение статического члена класса в одном объекте, получаем изменившееся значение в других объектах данного класса. Объявление статических членов – данных класса внутри объявления класса не является описанием, т. е. Под эти данные не выделяется память. Описание данных должно быть выполнено дополнительно. При использовании статического элемента все объекты ссылаются на одно и то же место в памяти.
В С++ можно использовать статические членыфункции класса. Они не получают указатель this, соответственно эти функции не могут обращаться к нестатическим членам класса. К статическим членам класса статические функции-члены класса могут обращаться посредством операции точка или ->. К статическим данным – членам класса и статическим функциям-членам класса можно обращаться, даже если не создано ни одного объекта данного класса, надо только использовать полное имя члена класса.
Пример использования статических членов класса для подсчета числа существующих и созданных объектов класса
#include
void main(void) { st: : show_count(); // вызов функции до создания объектов st a, b, c, *p; a. show_count(); { st x, y, z; st: : show_count(); // эти два вызова z. show_count(); // дадут одинаковый результат } p=new st; st: : show_count(); delete p; st: : show_count(): }
Локальные классы
Класс объявленный внутри функции называется локальным классом (local class). Функция, в которой объявлен локальный класс, не имеет специального доступа к членам локального класса. Локальный класс не может иметь статических членов – данных. Объект локального класса может быть создан только внутри функции, области действия объявления класса. Все функциичлены локального класса должны быть объявлены внутри объявления класса, т. е. быть подставляемыми функциями
Пример использования локальных классов
#include
Вложенные классы
В С++ допустимо использование вложенных классов (nested). Вложенный класс находится в области действия объемлющего класса, соответственно объекты этого класса могут использоваться как члены этого класса или в функциях – членах класса. Функции – члены класса и статические члены вложенного класса могут быть описаны в глобальной области действия.
Пример использования вложенных классов
class C { class nested_class { int who; public: nested_class(int a); ~nested_class(void); }; public: C(int b); { nested_class obj(b*b); cout<<”Конструктор класса С”<


