Глава 15: Виртуальные функции и полиморфизм



 

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

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

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

 

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

 

Указатель на базовый класс может ссылаться на любой объект, выведенный из этого базового класса.

Фундаментом для динамического полиморфизма служит указатель на базовый класс. Указатели на базовые и производные классы связаны такими отношениями, которые не свойственны указателям других типов. Как было отмечено выше в этой книге, указатель одного типа, как правило, не может указывать на объект другого типа. Однако указатели на базовые классы и объекты производных классов – исключения из этого правила. В C++ указатель на базовый класс также можно использовать для ссылки на объект любого класса, выведенного из базового. Например, предположим, что у нас есть базовый класс B_class и класс D_class , который выведен из класса B_class . В C++ любой указатель, объявленный как указатель на класс B_class , может быть также указателем на класс D_class . Следовательно, после этих объявлений

 

 

обе следующие инструкции абсолютно законны:

 

 

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

В качестве более конкретного примера рассмотрим короткую программу, которая определяет базовый класс B_class и производный класс D_class . В этой программе простая иерархия классов используется для хранения имен авторов и названий их книг.

 

 

При выполнении эта программа отображает следующие результаты.

 

 

В этом примере указатель р определяется как указатель на класс B_class . Но он может также ссылаться на объект производного класса D_class , причем его можно использовать для доступа только к тем элементам производного класса, которые унаследованы от базового. Однако следует помнить, что через "базовый" указатель невозможно получить доступ к тем членам, которые специфичны для производного класса. Вот почему к функции show_title() обращение реализуется с помощью указателя dp , который является указателем на производный класс.

Если вам нужно с помощью указателя на базовый класс получить доступ к элементам, определенным производным классом, необходимо привести этот указатель к типу указателя на производный тип. Например, при выполнении этой строки кода действительно будет вызвана функция show_title() объекта D_ob :

 

 

Внешний набор круглых скобок используется для связи операции приведения типа с указателем р , а не с типом, возвращаемым функцией show_title() . Несмотря на то что в использовании такой операции формально нет ничего некорректного, этого по возможности следует избегать, поскольку подобные приемы попросту вносят в код программы путаницу. (На самом деле большинство С++‑программистов считают такой стиль программирования неудачным.)

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

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

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

 

Ссылки на производные типы

 

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

 

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

 

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

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

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

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

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

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

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

 

 

При выполнении эта программа генерирует такие результаты.

 

 

Теперь рассмотрим код этой программы подробно, чтобы понять, как она работает.

В классе base функция who() объявлена виртуальной. Это означает, что ее можно переопределить в производном классе (в классе, выведенном из base ). И она действительно переопределяется в обоих производных классах first_d и second_d . В функции main() объявляются четыре переменные: base_obj (объект типа base), p (указатель на объект класса base), а также два объекта first_obj и second_obj двух производных классов first_d и second_d соответственно. Затем указателю p присваивается адрес объекта base_obj , и вызывается функция who() . Поскольку функция who() объявлена виртуальной, C++ во время выполнения программы определяет, к какой именно версии функции who() здесь нужно обратиться, причем решение принимается путем анализа типа объекта, адресуемого указателем p . В данном случае р указывает на объект типа base , поэтому сначала выполняется та версия функции who() , которая объявлена в классе base . Затем указателю р присваивается адрес объекта first_obj . Вспомните, что с помощью указателя на базовый класс можно обращаться к объекту любого его производного класса. Поэтому, когда функция who() вызывается во второй раз, C++ снова выясняет тип объекта, адресуемого указателем р , и, исходя из этого типа, определяет, какую версию функции who() нужно вызвать. Поскольку р здесь указывает на объект типа first_d , то выполняется версия функции who() , определенная в классе first_d . Аналогично после присвоения р адреса объекта second_obj вызывается версия функции who() , объявленная в классе second_d .

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

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

 

 

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

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

Поначалу может показаться, что переопределение виртуальной функции в производном классе представляет собой специальную форму перегрузки функций. Но это не так. В действительности мы имеем дело с двумя принципиально разными процессами. Прежде всего, версии перегруженной функции должны отличаться друг от друга типом и/или количеством параметров, в то время как тип и количество параметров у версий переопределенной функции должны в точности совпадать. И в самом деле, прототипы виртуальной функции и ее переопределений должны быть абсолютно одинаковыми. Если прототипы будут различными, то такая функция будет попросту считаться перегруженной, и ее "виртуальная сущность" утратится. Кроме того, виртуальная функция должна быть членом класса, для которого она определяется, а не его "другом" . Но в то же время виртуальная функция может быть "другом" другого класса. И еще: функциям деструкторов разрешается быть виртуальными, а функциям конструкторов – нет.

 


Дата добавления: 2018-09-22; просмотров: 320; Мы поможем в написании вашей работы!

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






Мы поможем в написании ваших работ!