Полиморфизм. Виртуальные функции — КиберПедия 

Общие условия выбора системы дренажа: Система дренажа выбирается в зависимости от характера защищаемого...

Таксономические единицы (категории) растений: Каждая система классификации состоит из определённых соподчиненных друг другу...

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

2018-01-03 284
Полиморфизм. Виртуальные функции 0.00 из 5.00 0 оценок
Заказать работу

Концепция полиморфизма является очень важной в ООП. Этот термин, как показано выше, используется для описания процесса, при котором различ­ные реализации функции могут быть доступны с использованием одного имени. В С++ полиморфизм поддерживается и во время компиляции, и во время испол­нения программы. Рассмотренные выше перегрузки функций и операций — это примеры полиморфизма во время компиляции.

Предположим, что нам требуется создать иерархию классов для различ­ных геометрических фигур (точка, линия, окружность, прямоугольник и др.) и разработать соответствующие методы для отображения фигур на экране компьютера (Show). При этом разные типы фигур будут отличаться тем, каким методом они это делают. Те же вопросы возникают для стирания, перемещения и других манипуляций с фигурами на экране. Рассмотренные ранее методы классов позволяют решить эту задачу определением функции Show для каждо­го класса. Но такой подход имеет существенный недостаток. При введении но­вого класса потребуется вносить изменения и выполнять повторную компиля­цию вследствие появления нового метода с именем Show.

Заложенные в С++ механизмы определения того, какой метод следует в данный момент использовать, позволяют решить эту задачу одним из трех способов.

1. Имеются отличия в типах параметров в объявлении функции, напри­мер, при перегрузке функций – Show(int, char) и Show(int, *char) не одно и то же.

2. Задана операция доступа к области действия, поэтому Circle::Show отличается от Point::Show и::Show.

3. Объект класса идентифицирует метод: Acircle.Show вызывает метод Circle::Show, а Apoint.Show – Point::Show. Аналогично и в случае указа­телей на объекты: pointptr->Show инициирует Point::Show.

 

Любой из указанных способов позволяет определить нужную функцию на этапе компиляции и поэтому носит название раннего (или статического) свя­зывания.

Стандартная графическая библиотека предоставляет в распоряжение пользователя объявления классов в соответствующих исходных h-файлах и ме­тоды в объектных obj-файлах или библиотечных lib-файлах. Накладываемые при этом ограничения раннего связывания не позволяют пользователю легко до­бавлять новые классы. С++ предоставляет гибкий механизм для решения этих проблем с помощью специальных методов, называемых виртуальными функция­ми.

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

Для простоты можно сказать, что виртуальная функция — это функция, вызов которой зависит от типа объекта. На практике это означает, что решение о том, какую функцию Show вызывать, откладывается до момента выявления типа объекта на стадии исполнения. В языке С можно передать объект данных функции, при этом необходимо было задать его тип, когда писалась программа. В ООП можно писать виртуальные функции так, чтобы объект сам определял, какую функцию необходимо вызвать во время исполнения программы. Это до­стигается использованием указателей на базовые типы и виртуальных функций.

 

Указатели на производные типы.

Для лучшего понимания виртуальных функций важно понимать принцип взаимодействия классов в С++, связанных отношением наследования — указа­тели на базовый тип и на производный тип зависимы: указатель на базовый класс может ссылаться на объект этого класса или любой объект производного класса, но указатель производного класса не может ссылаться на объекты базового класса:

Объект базового класса

Указатель на базовый класс Объект производного Указатель на

класса производный

Объект производного класс

класса

Пусть есть цепь наследования классов: А <- B <- C. В программе объекты классов могут объявляться так:

A obA; B obB; C obC;

A* p; // объявлен указатель на базовый класс типаA *

p = &obB; // указателю р присвоен адрес объекта obB

Объект obB можно представлять как особый вид объекта класса А (так зо­лотая рыбка — особая разновидность рыб, но это рыба). Но объект типа А не является особым видом объектов классов В и С. Например, вы обладаете мно­гими чертами ваших родителей, наследуемых от них и их родителей (цвет во­лос, глаз). Ваши родители и их родители – часть вас, но вы не являетесь их ча­стью.

Все элементы класса С, наследуемые от классов А и В, могут быть до­ступны через использование указателя р. Однако на элементы, объявленные (собственные) в классе С нельзя ссылаться, используя р. Если требуется иметь доступ к элементам, объявленным в производном классе, используя указатель на базовый класс, его надо привести к указателю на производный класс так:

((С*)р) -> f(), где функция f() является элементом класса С, причем внешние скобки необходимы. И, наконец, указатель р изменяется при операци­ях ++ и -- относительно базового класса.

Пример 31.

Рассмотрим программу использования указателей на базовый класс из производных классов по цепи наследования: Base <- Derive <- Derive1.

#include<iostream.h>

#include<conio.h>

#include<stdio.h>

class Base // базовый класс Base

{ public:

void show(void)

{ cout<<"In Base class\n"; }

};

class Derive: public Base // производный класс от Base

{ public:

void show(void)

{ cout<<"In Derive class\n"; }

};

class Derive1: public Derive // производный класс от Base, Derive

{ public:

void show(void)

{ cout<<"In Derive1 class\n"; }

};

 

void main() // главная функция

{ clrscr();

Base bobj, *pb; // объект и указатель базового класса

Derive dobj, *pd; // объекты и указатели производных классов

Derive1 d1obj, *pd1;

 

pb=&bobj; // указатель на объект класса Base

bobj.show(); // вызов объектом функции show() класса Base

pb->show(); // вызов указателем функции show() класса Base

 

pd=&dobj; // указатель на объект класса Derive

dobj.show(); // вызов объектом функции show() класса Derive

pd->show(); // вызов указателем функции show() класса Derive

 

pd1=&d1obj; // указатель на объект класса Derive1

d1obj.show(); // вызов объектом функции show() класса Derive1

pd1->show(); // вызов указателем функции show() класса Derive1

 

pb=&dobj; // базовый указатель на объект класса Derive

pb->show(); // вызов указателем функции show() класса Base!

 

pb=&d1obj; // базовый указатель на объект класса Derive1

pb->show(); // вызов указателем функции show() класса Base!

 

((Derive1 *)pb)->show(); // вызов show() класса Derive1 указателем Base,

// приведенным к типу Derive1

getch();

}

Результаты программы:

In Base class

In Base class

In Derive class

In Derive class

In Derive1 class

In Derive1 class

In Base class!

In Base class!

In Derive1 class

 

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

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

Виртуальная функция должна быть элементом класса (она не может иметь спецификатор friend). Однако виртуальная функция может быть "другом" другого класса. Допустимо, чтобы деструктор имел спецификатор virtual, од­нако для конструктора это запрещено. Вследствие запретов и различий между перегрузкой обычных функций и переопределением виртуальных функций для них часто используется термин " замещение " (overriding).

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

 

Пример 32.

Модифицируем программу примера 31 так, что в классе Base объявим функцию show() как виртуальную:

class Base // базовый класс Base

{ public:

virtual void show(void) // виртуальная функция класса

{ cout<<"In Base class\n";}

};

В главной функции исключим операторы вызова объектом функции show() (например, bobj.show(); и др.) и оставим вызовы функции show() толь­ко с помощью указателей (pb->show(); и др.).

 

Результаты программы примера 32:

In Base class

In Derive class

In Derive1 class

In Derive class

In Derive1 class

 

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

Базовый класс задает основной интерфейс виртуальных функций, кото­рый будут иметь производные классы. Но производные классы задают свой ме­тод согласно принципу полиморфизма: " один интерфейс – разные методы". Отделение интерфейса и реализации функций позволяет создавать библиотеки классов (class libraries).

Возникает вопрос: как осуществляется вызов виртуальной функции для активного объекта? Реализации компиляторов пользуются технологией преоб­разования имени виртуальной функции в индекс в таблице, содержащей указа­тели на функции, называемой " таблицей виртуальных функций" (virtual function table — vtbl). Каждый класс с виртуальными функциями имеет свою vtbl, иден­тифицирующую его виртуальные функции. Это можно изобразить схематиче­ски:

Объект класса Base: vtbl:

  *pf1 f1()
  *pf2 f2()

 

Вызов виртуальных функций реализуется как непрямой вызов по vtbl. Эта таблица создается во время компиляции, а связывание происходит во время вы­полнения программы (отсюда термин позднее связывание). Функции в vtbl поз­воляют корректно использовать объект даже в тех случаях, когда ни размер объекта, ни расположение его данных не известны в месте вызова.

 

Пример 33.

Рассмотрим программу использования виртуальных функций при расши­ряющемся наследовании. Базовый класс Figure описывает плоскую фигуру для вычисления площади, которой достаточно двух измерений. Виртуальная функ­ция show_area() печатает значение площади фигуры. На основе этого класса создаются производные классы – Triangle (треугольник), Rectangle (прямо­угольник), Circle (круг с одним измерением), в которых определены конкретные формулы (методы) вычисления площадей. Классы связаны расширяющимся на­следованием по схеме: Figure <- (Triangle, Rectangle, Circle).

 

#include<iostream.h>

#include<conio.h>

class Figure // базовый класс

{ protected: // защищенные элементы, доступные в производных классах

double x, y;

public:

void set_dim (double i, double j=0) // 2-й параметр по умолчанию

{ x = i, y = j; } // задание измерений фигур

virtual void show_area() // виртуальная функция Figure

{ cout<<"Площадь не определена для Figure\n";}

};

class Triangle: public Figure // производный класс

{ public:

void show_area() // виртуальная функция Triangle

{ cout<<"Треугольник с высотой "<< x <<" и основанием "<< y;

cout<<" имеет площадь = "<< x*0.5*y <<"\n";

}

};

class Rectangle: public Figure // производный класс

{ public:

void show_area() // виртуальная функция Rectangle

{ cout<<"Прямогольник со сторонами "<< x <<" и "<< y;

cout<<" имеет площадь = "<< x*y <<"\n";

}

};

class Circle: public Figure // производный класс

{ public:

void show_area() // виртуальная функция Circle

{ cout<<"Круг с радиусом "<< x;

cout<<" имеет площадь = "<< 3.14*x*x <<"\n";

}

};

void main ()

{ clrscr();

Figure f, *p; // объявление объекта и указателя класса Figure

Triangle t; // объявление объекта класса Triangle

Rectangle r; // объявление объекта класса Rectangle

Circle c; // объявление объекта класса Circle

p = &f; // указатель базового типа Figure

p -> set_dim (1,2); // задание размерностей объекта Figure

p -> show_area(); // вывод сообщения о площади объекта типа Figure

p = &t; // базовый указатель на объект типа Triangle

p -> set_dim (3,4); // задание размерностей объекта типа Triangle

p -> show_area(); // вывод сообщения о площади объекта типа Triangle

p = &r; // базовый указатель на объект типа Rectangle

p -> set_dim (5,6); // задание размерностей объекта типа Rectangle

p -> show_area(); // вывод сообщения о площади объекта типа Rectangle

p = &c; // базовый указатель на объект типа Circle

p -> set_dim (2); // задание размерностей объекта типа Circle

p -> show_area(); // вывод сообщения о площади объекта типа Circle

getch(); // задержка экрана результатов

}

Результаты программы:

Треугольник с высотой 3 и основанием 4 имеет площадь = 6

Прямогольник со сторонами 5 и 6 имеет площадь = 30

Круг с радиусом 2 имеет площадь = 12.56

 

Пример 34.

Рассмотрим программу использования виртуальных функций в классах с конструкторами. Вводится базовый класс Value, который задает некоторое зна­чение. В этом классе описана виртуальная функция getvalue(), печатающая по­лученное значение. На основе этого класса строится производный класс Mult, вычисляющий произведение двух чисел. Он тоже имеет виртуальную функцию. Оба класса используют конструкторы.

 

#include<iostream.h>

#include<conio.h>

class Value // базовый класс

{ protected:

int value;

public:

Value (int n) { value = n; } // конструктор с параметром

virtual int getvalue () { return value; } // виртуальная функция

};

class Mult: public Value // производный класс

{ protected:

int mult;

public:

Mult (int n, int m): Value (n) // конструктор производного класса

{ mult = m;}

int getvalue () { return value * mult;}

};

void main ()

{ clrscr ();

cout<<"Работа программы";

Value *basep; // указатель базового типа

basep = new Value (10); // дин-ое создание объекта класса Value

cout<<"Для базового класса Value число = "<<basep->getvalue()<<endl;

delete basep; // освобождение динамической памяти объекта Value

basep = new Mult (10, 2); // базовый указатель на объект типа Mult

cout<<"Для производ. класса Mult число = "<<basep->getvalue()<<endl;

delete basep; //освобождение динамической памяти объекта типа Mult

}

Результаты программы:

Работа программы

Для базового класса Value число = 10

Для производ. класса Mult число = 20

 

Обычные или виртуальные функции.

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

Предположим, что объявляется класс Base с методом Action. Должен ли он быть виртуальным? Общее правило: если существует вероятность, что в производном от Base классе будет использована функция, перекрывающая Action, которой нужен доступ к Base, то Action должна быть виртуальной. Но она должна быть обычной функцией, если очевидно, что в производных типах Action будет выполнять те же действия (даже если это вызывает инициацию других виртуальных функций) или же производные типы не будут пользовать­ся Action.


Поделиться с друзьями:

Общие условия выбора системы дренажа: Система дренажа выбирается в зависимости от характера защищаемого...

Историки об Елизавете Петровне: Елизавета попала между двумя встречными культурными течениями, воспитывалась среди новых европейских веяний и преданий...

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

Археология об основании Рима: Новые раскопки проясняют и такой острый дискуссионный вопрос, как дата самого возникновения Рима...



© cyberpedia.su 2017-2024 - Не является автором материалов. Исключительное право сохранено за автором текста.
Если вы не хотите, чтобы данный материал был у нас на сайте, перейдите по ссылке: Нарушение авторских прав. Мы поможем в написании вашей работы!

0.107 с.