Чем отличается виртуальный вызов от виртуальной цепи
Перейти к содержимому

Чем отличается виртуальный вызов от виртуальной цепи

Чем отличается виртуальный вызов от виртуальной цепи

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

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

#include class Person < public: Person(std::string name): name < >void print() const < std::cout private: std::string name; // имя >; class Employee: public Person < public: Employee(std::string name, std::string company): Person, company  < >void print() const < Person::print(); std::cout private: std::string company; // компания >; int main() < Person tom ; Person* person ; person->print(); // Name: Tom Employee bob ; person = &bob; person->print(); // Name: Bob >

В данном случае класс Employee наследуется от класса Person, но оба этих класса определяют функцию print() , которая выводит данные об объекте. В функции main создаем два объекта и поочередно присваиваем их указателю на тип Person и вызываем через этот указатель функцию print. Однако даже если этому указателю присваивается адрес объекта Employee, то все равно вызывает реализация функции из класса Person:

Employee bob ; person = &bob; person->print(); // Name: Bob

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

Name: Tom Name: Bob

Динамическое связывание и виртуальные функции

Другой тип связывания представляет динамическое связывание (dynamic binding), еще называют поздним связыванием (late binding), которое позволяет на этапе выполнения решать, функцию какого типа вызвать. Для этого в языке С++ применяют виртуальные функции . Для определения виртуальной функции в базовом классе функция определяется с ключевым словом virtual . Причем данное ключевое слово можно применить к функции, если она определена внутри класса. А производный класс может переопределить ее поведение.

Итак, сделаем функцию print в базовом классе Person виртуальной:

#include class Person < public: Person(std::string name): name < >virtual void print() const // виртуальная функция < std::cout private: std::string name; >; class Employee: public Person < public: Employee(std::string name, std::string company): Person, company  < >void print() const < Person::print(); std::cout private: std::string company; >; int main() < Person tom ; Person* person ; person->print(); // Name: Tom Employee bob ; person = &bob; person->print(); // Name: Bob // Works in Microsoft >

Таким образом, базовый класс Person определяет виртуальную функцию print, а производный класс Employee переопределяет ее. В первом же примере, где функция print не была виртуальной, класс Employee не переопределял, а скрывал ее. Теперь при вызове функции print для объекта Employee через указатель Person* будет вызываться реализация функции именно класса Employee. Соответственно тепепрь мы получим другой консольный вывод:

Name: Tom Name: Bob Works in Microsoft

В этом и состоит отличие переопределения виртуальных функций от скрытия.

Класс, который определяет или наследует виртуальную функцию, еще назвается полиморфным (polymorphic class). То есть в данном случае Person и Employee являются полиморфными классами.

Стоит отметить, что вызов виртуальной функции через имя объекта всегда разрешается статически.

Employee bob ; Person p = bob; p.print(); // Name: Bob - статическое связывание

Динамическое связывание возможно только через указатель или ссылку.

Employee bob ; Person &p ; // присвоение ссылке p.print(); // динамическое связывание Person *ptr ; // присвоение адреса указателю ptr->print(); // динамическое связывание

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

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

Ключевое слово override

Чтобы явным образом указать, что мы хотим переопредлить функцию, а не скрыть ее, в производном классе после списка параметров функции указывается слово override

#include class Person < public: Person(std::string name): name < >virtual void print() const // виртуальная функция < std::cout private: std::string name; >; class Employee: public Person < public: Employee(std::string name, std::string company): Person, company  < >void print() const override // явным образом указываем, что функция переопределена < Person::print(); std::cout private: std::string company; >; int main() < Person tom ; Person* person ; person->print(); // Name: Tom Employee bob ; person = &bob; person->print(); // Name: Bob // Works in Microsoft >

То есть здесь выражение

void print() const override

указывает, что мы явным образом хотим переопределить функцию print. Однако может возникнуть вопрос: в предыдущем примере мы не указывали override для вирутальной функции, но переопределение все равно работало, зачем же тогда нужен override ? Дело в том, что override явным образом указывает компилятору, что это переопределяемая функция. И если она не соответствует виртуальной функции в базовом классе по списку параметров, возвращаемому типу, константности, или в базовом классе вообще нет функции с таким именем, то компилятор при компиляции сгенерирует ошибку. И по ошибке мы увидим, что с нашей переопределенной функцией что-то не так. Если же override не указать, то компилятор будет считать, что речь идет о скрытии функции, и никаких ошибок не будет генерировать, компиляция пройдет успешно. Поэтмоу при переопределении виртуальной функции в производном классе лучше указывать слово override

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

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

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

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

vtable, полиформизм и виртуальные функции в C++

  1. Указатель на vtable в объекте используется для поиска адреса vtable для класса.
  2. Затем в таблице идет поиск указателя на вызываемую виртуальную функцию.
  3. Через найденный указатель функции в vtable вызывается сама функция. В итоге вызов виртуальной функции происходит немного медленнее, чем прямой вызов невиртуальной функции, поэтому каждое объявление и вызов виртуальной функции несет некоторые накладные расходы.

Запрет переопределения

С помощью спецификатора final мы можем запретить определение в производных классах функций, которые имеют то же самое имя, возвращаемый тип и список параметров, что и виртуальная функция в базовом классе. Например:

class Person < public: virtual void print() const final < >>; class Employee : public Person < public: void print() const override // Ошибка. < >>;

Также можно переопределить функцию базового класса, но запретить ее переопределение в дальнейших производных классах:

class Person < public: virtual void print() const // переопределение разрешено < >>; class Employee : public Person < public: void print() const override final // в классах, производных от Employee переопределение запрещено < >>;

Чем отличается виртуальный вызов от виртуальной цепи

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

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

А чтобы переопределить метод в классе-наследнике, этот метод определяется с модификатором override . Переопределенный метод в классе-наследнике должен иметь тот же набор параметров, что и виртуальный метод в базовом классе.

Например, рассмотрим следующие классы:

class Person < public string Name < get; set; >public Person(string name) < Name = name; >public virtual void Print() < Console.WriteLine(Name); >> class Employee : Person < public string Company < get; set; >public Employee(string name, string company) : base(name) < Company = company; >>

Здесь класс Person представляет человека. Класс Employee наследуется от Person и представляет сотруднника предприятия. Этот класс кроме унаследованного свойства Name имеет еще одно свойство — Company.

Чтобы сделать метод Print доступным для переопределения, этот метод определен с модификатором virtual . Поэтому мы можем переопределить этот метод, но можем и не переопределять. Допустим, нас устраивает реализация метода из базового класса. В этом случае объекты Employee будут использовать реализацию метода Print из класса Person:

Person bob = new Person("Bob"); bob.Print(); // вызов метода Print из класса Person Employee tom = new Employee("Tom", "Microsoft"); tom.Print(); // вызов метода Print из класса Person
Bob Tom

Но также можем переопределить виртуальный метод. Для этого в классе-наследнике определяется метод с модификатором override , который имеет то же самое имя и набор параметров:

class Employee : Person < public string Company < get; set; >public Employee(string name, string company) : base(name) < Company = company; >public override void Print() < Console.WriteLine($"работает в "); > >

Возьмем те же самые объекты:

Person bob = new Person("Bob"); bob.Print(); // вызов метода Print из класса Person Employee tom = new Employee("Tom", "Microsoft"); tom.Print(); // вызов метода Print из класса Employee
Bob Tom работает в Microsoft

Виртуальные методы базового класса определяют интерфейс всей иерархии, то есть в любом производном классе, который не является прямым наследником от базового класса, можно переопределить виртуальные методы. Например, мы можем определить класс Manager, который будет производным от Employee, и в нем также переопределить метод Print.

При переопределении виртуальных методов следует учитывать ряд ограничений:

  • Виртуальный и переопределенный методы должны иметь один и тот же модификатор доступа. То есть если виртуальный метод определен с помощью модификатора public, то и переопредленный метод также должен иметь модификатор public.
  • Нельзя переопределить или объявить виртуальным статический метод.

Ключевое слово base

Кроме конструкторов, мы можем обратиться с помощью ключевого слова base к другим членам базового класса. В нашем случае вызов base.Print(); будет обращением к методу Print() в классе Person:

class Employee : Person < public string Company < get; set; >public Employee(string name, string company) :base(name) < Company = company; >public override void Print() < base.Print(); Console.WriteLine($"работает в "); > >

Переопределение свойств

Также как и методы, можно переопределять свойства:

class Person < int age = 1; public virtual int Age < get =>age; set < if(value >0 && value < 110) age = value; >> public string Name < get; set; >public Person(string name) < Name = name; >public virtual void Print() => Console.WriteLine(Name); > class Employee : Person < public override int Age < get =>base.Age; set < if (value >17 && value < 110) base.Age = value; >> public string Company < get; set; >public Employee(string name, string company) : base(name) < Company = company; base.Age = 18; // возраст для работников по умолчанию >>

В данном случае в классе Person определено виртуальное свойство Age, которое устанавливает значение, если оно больше 0 и меньше 110. В классе Employee это свойство переопределено — возраст работника должен быть не меньше 18.

Person bob = new Person("Bob"); Console.WriteLine(bob.Age); // 1 Employee tom = new Employee("Tom", "Microsoft"); Console.WriteLine(tom.Age); // 18 tom.Age = 22; Console.WriteLine(tom.Age); // 22 tom.Age = 12; Console.WriteLine(tom.Age); // 22

Запрет переопределения методов

Также можно запретить переопределение методов и свойств. В этом случае их надо объявлять с модификатором sealed :

class Employee : Person < public string Company < get; set; >public Employee(string name, string company) : base(name) < Company = company; >public override sealed void Print() < Console.WriteLine($"работает в "); > >

При создании методов с модификатором sealed надо учитывать, что sealed применяется в паре с override, то есть только в переопределяемых методах.

И в этом случае мы не сможем переопределить метод Print в классе, унаследованном от Employee.

Протокол х.25/3.

Уровень 3 Рекомендации Х.25 МККТТ (Х.25/3) определяет виртуально-датаграмную сеть: «формат пакета и процедуры управления для обмена пакетами, содержащими информацию управления и данные пользователя». Он описывает требования, предъявляемые к двум элементам программной структуры сети: управление передачей и управление сетью. Х.25 однозначно определяет сетевой уровень. В вычислительной сети одновременно создается много логических каналов. Х.25 описывает создание как временных (на один сеанс), так и постоянных логических каналов. Чаще всего постоянные соединения используются для связи хост-машин, а временные – для взаимодействия терминалов с большим числом хост-машин. Временное соединение в протоколе Х.25 называется виртуальным вызовом, а постоянное – виртуальной цепью. Любому виртуальному вызову либо виртуальной цепи присваивается номер группы логических каналов (от 0 до 15) или номер логического канала (от 0 до 255). Виртуальный вызов связан с проведением одного сеанса связи. Поэтому указанные номера приписываются любому сеансу связи и изменяются циклически. Виртуальная цепь существует постоянно. Поэтому номер этой цепи относится ко всем проводимым через нее сеансам связи.

Типы и форматы пакетов протокола х.25

Используемые типы пакетов приведены в таблице 4. Таблица 4

Входящий запрос соединения

Согласие на соединение

Подтверждение разъединения от сети

Подтверждение разъединения от абонента

Данные и прерывания

Данные от абонента

Прерывание от сети

Прерывание от абонента

Подтверждение прерывания от сети

Подтверждение прерывания от абонента

Управление потоком и сброс

Готовность сети к приему

Готовность абонента к приему

Неготовность сети к приему

Неготовность абонента к приему

Подтверждение сброса сетью

Подтверждение сброса абонентом

Подтверждение рестарта сетью

Подтверждение рестарта абонентом

Два из этих пакетов – Данные и Датаграмма – информационные, остальные – обеспечивают управление виртуально – датаграммной сетью и выполняет следующие функции:

— установление виртуального соединения;

— поддержание виртуального соединения;

— управление потоками пакетов;

— выполнение рестарта (повторная организация виртуального соединения);

— разъединение виртуального соединения.

DTE — абонентская машина

DSE — коммуникационная машина

Фаза сброса необходима для продолжения работы как временных, так и постоянных логических каналов после появления ошибок в транспортной сети: нарушение порядка последовательности пакетов, ошибки в процедуре управления, переполнение коммуникационной сети.

Фаза рестарта используется для восстановления после ошибок работы сразу всех логических каналов.

Два формата пакета «Данные».

Рис. 19 Формат пакета Х.25.

Рис. 20 Формат пакета Х.25 с расширенными полями счетчиков.

В левой половине 1 байта записывается идентификатор формата. В этом четырех битовом поле записывается код, указывающий на тип формата. Если пакеты Данные нумеруются циклически от 0 до 7 (модуль 8), то код 0001, если от 0 до 127 (модуль 128), то код 0010. Правая половина байта определяет номер группы логического канала. Во 2 байте – N логического канала, по которому необходимо передавать информацию.

В правом разряде 3 байта записывается 0 (нуль) выделяющий информационные пакеты, а также сигнальные пакеты датаграммного сервиса среди всех 19 пакетов. У остальных (управляющих) 16 пакетов в этом разряде проставлена 1.

N(S) – номер передаваемого пакета, N(R) – номер принимаемого (ожидаемого) пакета, М=1 означает, что пакет промежуточный, М=0 – пакет последний. Структура пакета Датаграмма при нумерации от 0 до 7 показана на рисунке. Если нумерация идет от 0 до 128, то 3 байт заменяется 2 байтами.

Рис. 21. Формат пакета «дейтаграмма» протокола Х.25.

Протокол Х.25 определяет размеры основы пакета в 16, 32, 64, 128, 256, 512 и 1024 байт. Основной размер – 128 байт.

Управляющие пакеты имеют такую же структуру (почти). Управляющие пакеты всегда передаются в виде датаграммы и любой управляющий пакет является командой для программ управления сетью, принимающей абонентской машины. В управляющих пакетах отсутствуют N(R) и N(S), а в этом байте (3) указывается идентификатор типа пакета.

Виртуальное соединение образуется следующим образом. Вызывающий абонент передает в сеть по свободному логическому каналу пакет «Запрос соединения», содержащий адрес вызываемого абонента, вызываемый абонент. Вызываемый абонент может не принять запрос. В этом случае он передает пакет «Запрос разъединения», в котором в начале причины разъединения может быть указано «номер занят». После этого вызываемый абонент не может использовать логический канал для получения пакета «Подтверждение разъединения». Если сеть не может установить соединение с вызываемым абонентом, вызывающему абоненту посылается пакет «Указание разъединения», содержащий причину разъединения: нереализованный вызов, номер занят, неразрешенный вызов, перегрузка сети и т.д. Если вызываемый абонент принимает запрос на соединение, он передает пакет «Согласие на соединение», после чего сеть посылает вызывающему абоненту пакет «Подтверждение соединения». Этим заканчивается фаза установления соединения между абонентами. Начиная устанавливать соединение, вызывающий абонент запускает таймер. Если в течение тайм-аута не поступил пакет «Подтверждение соединения», абонент передает пакет «Запрос разъединения», после чего процедура установления соединения может повторяться. После установления соединения начинается фаза передачи пакетов. Для ликвидации и сброса всех постоянных и временных виртуальных соединений, установленных с абонентом, используется процедура рестарта инициализации абонентом с помощью пакета «Запрос рестарта» и сетью – пакетом «Указание рестарта». При этом ликвидируются соединения, относящиеся ко всем логическим каналам абонента и стираются все пакеты. Передаваемые через эти соединения. Для восстановления потерянных пакетов используются средства более высокого уровня иерархии.

Для передачи срочных данных используются ненумерованные пакеты «Прерывание от сети» и «Прерывание от абонента», несущие в себе 1 байт данных о причине прерывания. Эти пакеты доставляются получателю независимо от состояния передачи нумерованных пакетов – даже тогда, когда пакеты данных не принимаются.

По окончании передачи происходит фаза разъединения с помощью соответствующих пакетов.

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

  1. Что такое логический канал?
  2. Сколько групп логических каналов в протоколе Х.25?
  3. Сколько логических каналов в группе в Х.25?
  4. Чем отличается виртуальный вызов от виртуальной цепи?
  5. Сколько типов информационных пакетов в Х.25?
  6. Каковы размеры основы пакетов в Х.25?
  7. В чем отличие виртуальной сети от дейтаграммной?
  8. Может ли в сети Х.25 быть отказ от кадров?

Полиморфизм. Виртуальные функции. Общие понятия. Спецификаторы virtual и override . Примеры

Виртуальные функции реализуют так называемый полиморфизм. Термин «полиморфизм» происходит от греческих слов poly (много) и morphos (форма). Полиморфизм – это свойство программного кода изменять свое поведение в зависимости от ситуации, возникающей при выполнении программы. В контексте реализации полиморфизм – это технология вызова виртуальных функций, реализуемых в иерархически связанных классах. Иерархия классов формируется на базе механизма наследования.

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

Механизм виртуальных функций реализует основополагающий принцип полиморфизма: «один интерфейс, несколько реализаций» или «один интерфейс, несколько методов».

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

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

Для реализации позднего связывания требуется следующее:

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

Относительно парадигмы классов и объектов для полиморфизма можно выделить следующие характерные особенности:

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

В языке C++ есть возможность реализовывать два вида полиморфизма:

  • статический. Этот вид полиморфизма достигается путём использования перегруженных функций (раннее связывание), шаблонов классов и перегрузки операторов. Более подробно о перегрузке функций можно прочитать здесь , о шаблонах классов здесь и о перегрузке операторов здесь ;
  • динамический. В этом случае используется наследование в сочетании с виртуальными функциями (позднее связывание).

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

Сигнатура виртуальной функции, объявленная в базовом классе, определяет вид интерфейса, реализуемого этой функцией. Интерфейс определяет способ вызова виртуальной функции. Для каждого конкретного класса виртуальная функция имеет свою реализацию, обеспечивающую выполнение действий, свойственных только этому классу. Таким образом, виртуальная функция для конкретного класса является неким уникальным (конкретным) методом (specific method).

В наиболее упрощенном виде объявление виртуальной функции в классе может быть следующим:

class BaseClass < virtual return_type FuncNameVirtual(list_of_parameters) < // . > >;
  • FuncNameVirtual() – имя виртуальной функции;
  • return_type – тип, возвращаемый функцией;
  • list_of_parameters – список параметров, которые получает функция.

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

class DerivedClass < return_type FuncNameVirtual(list_of_parameters) < // Это также виртуальная функция, которая переопределяет функцию базового класса. // . > >

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

  • функция с такой же сигнатурой как в базовом классе, но объявленная как константная ( const );
  • функция, которой передаются аргументы типа, совместимого с аргументами функции базового класса.

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

class DerivedClass < return_type FuncNameVirtual(list_of_parameters) override < // Для цепочки виртуальных функций так нужно делать всегда // . > >

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

Если для функции, объявленной со спецификатором override в производном классе DerivedClass , нет подходящей виртуальной функции в базовом классе BaseClass , компилятор сгенерирует ошибку.

3. Случаи реализации полиморфизма

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

Если в иерархии классов реализованы виртуальные функции, то полиморфизм реализуется в следующих случаях:

  • при объявлении указателя ( * ) на базовый класс и вызове виртуальной функции соответствующего экземпляра класса, являющегося частью иерархии. Как известно, в этом случае указатель на базовый класс может быть установлен в значение экземпляров производных классов. После этого вызывается соответствующая виртуальная функция;
  • при передаче указателя ( * ) на базовый класс в некоторую функцию, вызывающую виртуальную функцию базового класса с помощью оператора -> (доступ по указателю);
  • при передаче ссылки ( & ) на базовый класс в некую функцию, вызывающую виртуальную функцию базового класса с помощью оператора ‘ . (точка, доступ по ссылке).
4. Примеры реализации полиморфизма
4.1. Пример полиморфизма для двух классов. Вызов виртуальной функции по указателю ( -> ). Анализ кода

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

#include iostream> using namespace std; // Базовый класс class Base < public: // virtual - признак виртуальной функции virtual void PrintInfo() < cout "Base." >; // Класс, унаследованный от класса Base, // важно: здесь должен быть модификатор public class Derived : public Base < public: virtual void PrintInfo() override // спецификатор override желательно указывать < cout "Derived." >; void main() < // 1. Создать экземпляры базового и производного класса Base obj1; Derived obj2; // 2. Объявить указатель на базовый класс Base* p; // 3. Использовать правило: указатель на базовый класс может указывать // на любой экземпляр базового и производного от него класса. // Ниже демонстрируется полиморфизм. // 3.1. Установить указатель p на экземпляр базового класса obj1 // и вызвать PrintInfo() p = &obj1; p->PrintInfo(); // Base // 3.2. Установить указатель p на экземпляр производного класса // и вызвать PrintInfo() p = &obj2; p->PrintInfo(); // Derived - это есть полиморфизм (слово virtual) >

Результат выполнения программы

Base. Derived.

Проанализируем вышеприведенный код.

В примере объявляются два класса Base и Derived , образующие иерархию с помощью механизма наследования.

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

. // 3. Объявить указатель на базовый класс Base* p; .

Теперь указателю p можно присваивать адрес любого экземпляра классов Base и Derived . Сначала присваивается адрес экземпляра obj1 типа Base и вызывается метод PrintInfo()

p = &obj1; p->PrintInfo(); // Base

Вывод будет прогнозированным – слово «Base».

Затем указателю p присваивается адрес экземпляра obj2 типа Base и вызывается метод PrintInfo()

p = &obj2; p->PrintInfo();

Вывод будет «Derived». То есть будет вызван метод PrintInfo() производного класса, что нам и нужно. Вызов этого метода обеспечивает ключевое слово virtual в объявлении функции PrintInfo() базового класса Base .

Если в классе Base перед объявление функции PrintInfo() убрать ключевое слово virtual , то в следующем коде

p = &obj2; p->PrintInfo();

будет вызван метод PrintInfo() класса Base , а не класса Derived . Это означает, что полиморфизм не будет поддерживаться, а всегда будет вызываться функция базового класса. В результате программа выведет

Base. Base.

Таким образом, функция PrintInfo() класса Derived для указателя p на базовый класс Base будет недоступна.

Вывод. Полиморфизм реализует правило «один интерфейс, много реализаций». В нашем случае интерфейс один – это объявление и вызов функции PrintInfo()

p->PrintInfo();

Но в зависимости от того, на какой объект указывает указатель p , будет вызван соответствующий метод PrintInfo() – это и есть много реализаций.

4.2. Пример, объясняющий полиморфизм для трех классов. Передача в функцию указателя ( * ) на базовый класс

Приводится пример 3-х классов CalcLength , CalcArea , CalcVolume , в которых в виртуальной функции Calc() возвращается соответственно длина окружности, площадь круга и объем шара.

Для демонстрации создается некоторая функция ShowResult() , в которую передается указатель на базовый класс. Функция вызывает виртуальную функцию Calc() по указателю. В теле функции ShowResult() неизвестно, экземпляр какого класса будет ей передан. Экземпляр будет сформирован во время выполнения.

#include iostream> using namespace std; // Класс, содержащий функцию вычисления длины окружности class CalcLength < public: // Виртуальная функция virtual double Calc(double radius) < return 2 * 3.1415 * radius; > >; // Класс, содержащий функцию вычисления площади окружности class CalcArea : public CalcLength < public: // Виртуальная функция double Calc(double radius) override < return 3.1415 * radius * radius; > >; // Класс, содержащий функцию вычисления объема шара class CalcVolume : public CalcArea < public: // Виртуальная функция double Calc(double radius) override < return 4.0 / 3 * 3.1415 * radius * radius * radius; > >; // Некоторая функция, получающая указатель на базовый класс ClassLength и параметр радиуса, // в данной функции демонстрируется полиморфизм void ShowResult(CalcLength* p, double radius) < // Вызов метода Calc() по указателю p. // Для данной функции неизвестно, метод какого класса будет вызван. // Нужный метод будет сформирован во время выполнения - это есть полиморфизм double res = p->Calc(radius); // cout "Result color: #0000ff;">void main() < // 1. Объявить указатель на базовый класс - это важно CalcLength* p = nullptr; // 2. Создать экземпляры 3-х классов CalcLength obj1; CalcArea obj2; CalcVolume obj3; // 3. Ввести номер функции int num; cout "Enter number of function (1-3): "; cin >> num; if ((num < 1) || (num >3)) return; // 4. Ввести радиус double radius; cout "radius color: #008000;"> // 5. Установить указатель p в зависимости от введеного num if (num == 1) p = &obj1; if (num == 2) p = &obj2; if (num == 3) p = &obj3; // 6. Вызвать метод ShowResult() // Нужный объект подставляется в зависимости от ситуации ShowResult(p, radius); // 7. Вызвать метод ShowResult() непосредственно подставляя экземпляр класса if (num == 1) ShowResult(&obj1, radius); if (num == 2) ShowResult(&obj2, radius); if (num == 3) ShowResult(&obj3, radius); >
4.3. Пример полиморфизма для трех классов. Вызов виртуальной функции в методе. Передача в метод ссылки ( & ) на базовый класс

В примере демонстрируется полиморфизм на основе ссылки на базовый класс, передаваемый в функцию DemoPolymorphism() .

Объявляется 3 класса с именами A , B , C . Класс A является базовым для класса B . Класс B является базовым для класса C . Классы содержат только один метод, выводящий название класса. С помощью механизма полиморфизма в функцию DemoPolymorphism() передается ссылка на один из экземпляров. В соответствии с переданным экземпляром вызывается требуемый метод.

#include iostream> using namespace std; // Класс A - базовый класс в иерархии A class A < public: virtual void Show() < cout "A::Show()" >; // Класс B - унаследованный от класса A class B : public A < public: // виртуальный метод - переопределяет одноименный метод класса A void Show() override < cout "B::Show()" >; // Класс C - унаследован от класса B class C : public B < public: // виртуальный метод - переопределяет одноименный метод класса B void Show() override < cout "C::Show()" >; // Функция, получающая ссылку на базовый класс A, // в функции демонстрируется полиморфизм. void DemoPolymorphism(A& ref) < // Вызов виртуального метода, метод определяется при выполнении. // Здесь неизвестно, метод какого класса A, B или C нужно вызвать. ref.Show(); > void main() < // 1. Объявить экземпляры классов A, B, C A objA; B objB; C objC; // 2. Вызвать функцию DemoPolymorphism(), // в зависимости от того, какой экземпляр передается, // вызывается соответствующий метод класса. DemoPolymorphism(objA); // A::Show() DemoPolymorphism(objB); // B::Show() DemoPolymorphism(objC); // C::Show() >

После запуска на выполнение программа выдаст следующий результат

A::Show() B::Show() C::Show()

Если при объявлении метода Show() в классах A , B , C убрать спецификаторы virtual и override , то полиморфизм поддерживаться не будет. А это значит, что при вызове метода DemoPolymorphism() все передаваемые ссылки на экземпляры objA , objB , objC будут конвертироваться в ссылку на базовый класс A& . Как следствие, 3 раза будет вызван метод Show() класса A , и программа выдаст следующий результат

A::Show() A::Show() A::Show()

Связанные темы

  • Абстрактный класс. Чисто виртуальная функция. Примеры
  • Типы отношений между классами is-a, has-a. Примеры. Агрегация. Композиция

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *