Наследование и полиморфизм являются наиболее важными особенностями объектно-ориентированного программирования и позволяют эффективно преодолевать сложности программного обеспечения. Полиморфизм позволяет создавать программы, обрабатыващие широкий круг как существующих, так и еще даже не описанных родственных классов.
Наследование
Наследование – это способ повторного использования программного обеспечения, при котором новые классы создаются из уже существующих классов путем использования их атрибутов и функций. При создании нового класса можно указать, что он является наследником данных-элементов и функций-элементов ранее определенного базового класса. Новый класс называют производным классом. Он также может быть базовым для будущих производных классов. При простом наследовании класс порождается одним базовым классом.
Базовый класс может наследоваться как public (открытый), protected (защищенный) или private (закрытый). Защищенное и закрытое наследования встречаются редко. В случае public открытые элементы базового класса становятся открытыми элементами производного класса, а защищенные элементы базового класса становятся защищенными элементами производного класса. Закрытые элементы базового класса никогда не бывают доступны для производного класса. Элементы базового класса, которые не должны быть доступны производному классу через наследование, объявляются в базовом классе закрытыми.
Базовый класс может быть прямым или косвенным базовым классом производного класса. Прямой базовый класс явно перечисляется в заголовке при объявлении производного класса, а косвенный – наследуется через два или более уровней иерархии.
Производный класс обычно добавляет свои собственные данные-элементы и функции-элементы, так что производный класс в общем случае больше своего базового класса. Производный класс более специфичен по своему назначению, более узок, чем его базовый класс, и представляет меньшую группу объектов. При простом наследовании предполагается, что производный класс будет выполнять примерно те же функции, что и базовый класс.
Каждый объект производного класса при открытом наследовании является также объектом соответствующего базового класса, но не наоборот.
Одной из проблем наследования является то, что производный класс может наследовать открытые функции-элементы, когда в этом нет необходимости или когда это недопустимо. В таких случаях эти элементы соответствующим образом переопределяют в производном классе. Если имена функций-элементов в классах совпадают, то при переопределении в производном классе функции-элемента базового класса принято вызвать версию базового класса и после этого выполнять некоторые дополнительные операции. При этом ссылка на функцию-элемент базового класса должна использовать операцию разрешения области действия. Следует отметить, что при переопределении нет необходимости получать такую же сигнатуру, как у функции-элемента базового класса.
Наследование формирует древовидные иерархические структуры. Класс может существовать сам по себе, но когда класс входит в иерархию, он становится либо базовым классом, который снабжает атрибутами и функциями другие классы, либо производным классом, который наследует эти атрибуты и функции.
Виртуальные функции и полиморфизм
Виртуальные функции и полиморфное программирование позволяют автоматически выполнять логику, аналогичную логике оператора switch.
Виртуальная функция объявляется с помощью ключевого слова virtual, предшествующего прототипу функции в базовом классе, например, virtual float getX()const;. Функция может быть чистой виртуальной, тогда virtual float getX()const=0;. Несмотря на то, что некоторые функции могут быть неявно виртуальными, поскольку они были объявлены такими на более высоком уровне иерархии, рекомендуется явно объявлять функции виртуальными на каждом уровне иерархии, чтобы обеспечить ясность программы.
Производные классы могут иметь собственные реализации виртуальных функций базового класса, но если они их не имеют, то используются реализации, описанные в базовом классе.
Если виртуальная функция вызывается обращением к объекту по имени и при этом используется операция доступа точка, то эта ссылка обрабатывается во время компиляции (это называется статическим связыванием) и в качестве вызываемой определяется функция класса данного объекта (или наследуемая этим классом).
Использование виртуальных функций делает целесообразным определять в иерархии абстрактный базовый класс, который содержит хотя бы одну чистую вуртуальную функцию. В отличие от конкретных классов, абстрактные классы не могут порождать объекты. Однако указатель на абстрактный базовый класс может быть объявлен. Если класс является производным от класса с чистой виртуальной функцией и если эта чистая виртуальная функция не определена в производном классе, то функция остается чистой виртуальной и в производном классе. Тогда производный класс также является абстрактным классом.
Переопределенная в производном классе виртуальная функция должна иметь тот же самый тип возвращаемого значения и ту же сигнатуру, что и виртуальная функция базового класса.
С помощью виртуальных функций реализуется полиморфизм – это возможность для объектов разных классов, связанных с помощью наследования, реагировать различным образом при обращении к одной и той же функции-элементу.
Если запрос виртуальной функции для объекта иерархии осуществляется с помощью указателя базового класса (или ссылки, но в случае абстрактного базового класса - только с помощью указателя), то будет выбрана правильная переопределенная функция в соответствующем производном классе, связанном с данным объектом. Следовательно, благодаря использованию виртуальных функций и полиморфизму, один и тот же вызов функции-элемента может привести к различным действиям в зависимости от типа объекта, принимающего этот вызов. Очевидно, указатели на абстрактные базовые классы наиболее удобны для полиморфного оперирования объектами производных конкретных классов.
Когда вызов виртуальной функции осуществляется по указателю, вызываемая виртуальная функция будет установлена для вызванного объекта во время выполнения программы, т.е. при помощи динамического (позднего) связывания.
Динамическое связывание требует, чтобы во время выполнения программы вызов виртуальной функции-элемента был бы направлен варианту виртуальной функции соответствующего класса. Для этого служит таблица виртуальных методов, которая реализуется в виде массива, содержащего указатели на функции. У каждого класса, который содержит виртуальные функции, имеется таблица виртуальных методов. Для каждой виртуальной функции в классе таблица имеет элемент, содержащий указатель на вариант виртуальной функции, используемый в объектах данного класса. Виртуальная функция, используемая в некотором классе, может быть определена в этом классе или прямо или косвенно наследоваться из базового класса, стоящего выше в иерархии. В производных классах может быть переопределена или не переопределена виртуальная функция-элемент базового класса. Поэтому производный класс может использовать вариант виртуальной функции-элемента базового класса и это будет отражено а таблице виртуальных методов.
Каждый объект класса, содержащего виртуальные функции, имеет указатель на таблицу виртуальных методов этого класса, недоступный для программиста. Во время выполнения программы вызовы виртуальных функций осуществляются через разыменование соответствующего указателя в таблице виртуальных методов.
В класс с виртуальными функциями рекомендуется включать виртуальный деструктор. Это приведет к тому, что все деструкторы производных классов станут виртуальными, даже если они имеют имена, отличные от имени деструктора базового класса. В этом случае, если объект в иерархии уничтожен явным использованием операции delete, примененной к указателю базового класса на объект производного класса, то будет вызван деструктор соответствующего класса.
Конструкторы не могут быть виртуальными.
В данной лабораторной работе изучается пример иерархии – точка, круг, цилиндр, с прямым открытым наследованием. Для демонстрации возможностей полиморфизма в лабораторной работе рассмотрено два варианта иерархии. Виртуальные функции находятся во втором варианте. В первом варианте для отображения объектов использовано приведение типов указателей базовых классов к указателям производных классов, во втором – полиморфизм, реализованный при помощи виртуальных функций-элементов.
Проектирование приложения.
Выбор, размещение и задание свойств компонентов.
Коды классов, функций и обработчиков событий
Сохраните модуль главной формы под именем LR_9, а проект – под именем PR_LR_9.
Для размещения классов в проекте использованы модули, не связанные с формой. Для первого варианта иерархии использованы следующие модули.
В нижеприведенных заголовочном файле модуля формы LR_9.h и форме с результатами выполнения задания (рис.9.1) можно получить информацию для размещения компонентов на форме.
Перенесем на форму компоненты и зададим значения их свойствам. Со страницы Стандарт – три панели общего назначения GroupBox1,2,3 ( в свойство Caption впишите соответственно точка, круг, цилиндр), пять меток Label1,…,5 (Caption – координаты, координаты центра, координаты центра основания, без виртуальных функций, с виртуальными функциями), два компонента Memo1,2, пять кнопок Button1,…,5 (Caption – вычислить, очистить, вычислить, очистить, выход), причем под меткой Label4 (без виртуальных функций) расположено окно Memo1 и кнопки Button1,2 (соответственно вычислить, очистить). Со страницы Дополнительно – девять окон редактирования с присоединенными к ним метками LabeledEdit1,…,9, причем первые два окна расположены на панели GroupBox1 (точка), следующие три - на панели GroupBox2 (круг), и последние четыре - на панели GroupBox3 (цилиндр).
В представленном ниже файле реализации модуля формы LR_9.cpp программа драйвер иерархии по варианту 1 расположена в обработчике щелчка на кпопке Button1 (вычислить), а по варианту 2 – Button3 (вычислить).
1.Выполните тестирование согласно рис.9.1 и получите результаты, представленные на рис.9.2. Объясните полученные результаты.
2.Постройте иерархию с виртуальными функциями по заданию, полученному у преподавателя.
Рис.9.2 – результаты выполнения задания
Контрольные вопросы
1.Приведите примеры прямого, косвенного, множественного, открытого, защищенного и закрытого наследования. Как в этих случаях осуществляется доступ к открытым, защищенным и закрытым элементам базового класса?
2.Как можно отобразить объекты классов иерархии?
3.При каком условии и почему каждый объект производного класса является также объектом соответствующего базового класса?
4.Какой класс иерархии имеет наибольшее количество объектов?
5.Можно ли ссылаться на элементы производного класса по указателю производного класса, полученному преобразованием типа указателя базового класса, который указывает на объект базового класса?
6.Можно ли ссылаться на объект базового класса с помощью указателя производного класса? Что изменится после приведения указателя производного класса к типу указателя базового класса?
7.Почему можно присваивать указатель производного класса (адрес объекта производного класса) указателю базового класса, но не наоборот?
8.Когда указатель на объект производного класса неявно преобразуется в указатель на объект базового класса? Приведите пример.
9.Как указатель базового класса преобразовать в указатель производного класса? На что должен указывать при этом указатель базового класса?
10.Как отображается объект производного класса по указателю базового класса?
11.Как переопределяют функцию-элемент базового класса в производном классе? Приведите пример.
12.Как создается объект производного класса иерархии? Как он разрушается?
13.Влияет ли создание производного класса на исходный код базового класса?
14.Как можно изменять базовый класс, чтобы не вносить изменений в производный класс?
15.Что является признаком абстрактного базового класса?
16.Как создать неявно виртуальную функцию?
17.Почему предпочтительнее создавать иерархию классов, порожденную абстрактным базовым классом?
18.Какое требование нужно соблюдать при переопределении виртуальных функций?
19.Что такое статическое связывание? Приведите пример.
20.Объясните суть полиморфизма. Когда и как используется полиморфизм? Как реализовать полиморфизм?
21.Объясните суть динамического или позднего связывания.
22.Как реализовано динамическое связывание?
23.Как виртуальные функции и полиморфное программирование могут исключить потребность в применении логики оператора switch?
24.Почему деструкторы необходимо объявлять виртуальными?
25.Почему конструкторы не могут быть виртуальными?
Задания
1.Форма : прямоугольник : параллелепипед (периметр, площадь, площадь поверхности, объем).
3.Форма : круг : прямой круговой конус : усеченный прямой круговой конус (площадь, площадь поверхности, объем).
4.Двумерная форма : квадрат : ромб (периметр, площадь).
5.Строка : строка-идентификатор. Если в составе инициализирующей строки будут встречены недопустимые символы, строка-идентификатор создается пустой. (Использовать динамическую память, реализовать операции: =, +, >).
6.Строка : битовая строка. Если в составе инициализирующей строки будут встречены любые символы, отличные от символов ‘0 ’ или ‘1 ’, битовая строка принимает нулевое значение. (Использовать динамическую память, реализовать операции: =, +, ==).
7.Строка : десятичная строка. Строки производного класса могут содержать только символы ‘–‘ и ‘+’, задающие знак числа, и цифры. Символы ‘-‘ или ‘+’ могут находиться только в первой позиции числа, причем символ ‘+’ может отсутствовать, в этом случае число считается положительным. Если в составе инициализирующей строки будут встречены любые символы, отличные от допустимых, десятичная строка принимает нулевое значение. (Использовать динамическую память, реализовать операции: =, +, *, >).
8.Строка : комплексное число. Строки производного класса состоят из двух полей, разделенных символом ‘i’. Первое поле задает значение реальной части числа, а второе – мнимой. Каждое из полей может содержать только символы десятичных цифр и символы ‘–‘ и ‘+’, задающие знак числа. Символы ‘–‘ или ‘+’ могут находиться только в первой позиции числа, причем символ ‘+’ может отсутствовать, в этом случае число считается положительным. Если в составе инициализирующей строки будут встречены любые символы, отличные от допустимых, комплексное число принимает нулевое значение. Примеры строк: 33i12, -7i100, +5i-21. (Использовать динамическую память, реализовать операции: =, +, *, ==).
9.Переменная целого типа : множество целых (операции =, +, >).