- •Раздел 4. Разработка по Тема 4.1. Проектирование интерфейса с пользователем
- •4.1.1. Типы пользовательских интерфейсов.
- •4.1.2. Пользовательская и программная модели интерфейса.
- •4.1.3. Разработка диалогов.
- •4.1.4. Основные компоненты графических пользовательских интерфейсов.
- •Тема 4.2. Реализация графических пользовательских интерфейсов.
- •4.2.1. Диалоги, управляемые пользователем.
- •4.2.2. Диалоги, управляемые системой.
- •4.2.3. Использование метафор.
- •4.2.4. Технология Drag and Drop.
- •4.2.5. Интеллектуальные элементы.
- •4.3.1. Базовые типы данных.
- •Константы
- •Область действия имен
- •4.3.2. Указатели и адресная арифметика.
- •4.3.3. Составные типы данных. Структуры
- •Битовые поля
- •Определение типов
- •Перечислимые типы
- •4.3.4. Выражения и операции.
- •4.3.5. Управляющие конструкции. Условные операторы
- •Операторы циклов
- •4.4.1. Статические одномерные массивы.
- •4.4.2. Статические многомерные массивы.
- •4.4.3. Динамические массивы.
- •4.4.4. Массивы указателей.
- •4.5.1. Стеки.
- •4.5.2. Очереди.
- •4.5.3. Списки.
- •4.5.4. Бинарные деревья.
- •4.6.1. Объявление классов и экземпляров классов.
- •4.6.2. Инкапсуляция данных и методов.
- •4.6.3. Конструкторы классов.
- •Конструктор по умолчанию
- •Конструктор копирования
- •4.6.4. Деструкторы классов.
- •4.7.1. Разделы в описании класса.
- •4.7.2. Friend-конструкции.
- •4.7.3. Статические члены классов.
- •4.7.4. Использование описателя const в классах.
- •4.8.1. Вложенность классов.
- •4.8.2. Наследование данных и методов.
- •4.8.3. Типы наследования.
- •4.9.1. Полиморфизм раннего связывания.
- •4.9.2. Полиморфизм позднего связывания и виртуальные функции.
- •4.9.3. Абстрактные методы и классы.
- •4.10.1. Функции консольного ввода-вывода.
- •4.10.2. Функции файлового ввода-вывода.
- •4.10.3. Использование библиотеки классов потокового ввода-вывода.
- •4.11.1. Перегрузка операций.
- •4.11.2. Шаблоны функций.
- •4.11.3. Шаблоны классов.
- •4.11.4. Обработка исключений.
- •Тема 4.12. Com-технология.
- •4.12.1. Основные понятия.
- •4.12.2. Типы интерфейсов.
- •Свойства интерфейсов
- •Типы интерфейсов
- •4.12.3. Типы com-объектов.
- •4.12.4. Фабрика классов.
- •Тема 4.13. Построение com-сервера.
- •4.13.1. Язык idl.
- •Содержимое файла idl
- •4.13.2. Определение пользовательского интерфейса.
- •4.13.3. Реализация пользовательского интерфейса.
- •4.13.4. Создание тестового клиента.
- •Тема 4.14. Обзор платформы ms .Net.
- •4.14.1. Общая идея архитектуры .Net.
- •4.14.2. Достоинства и недостатки .Net.
- •4.14.3. Схема трансляции программ в .Net.
- •4.14.4. Язык msil.
- •4.14.5. Объектно-ориентированная модель .Net.
4.9.1. Полиморфизм раннего связывания.
Полиморфизм наряду с инкапсуляцией, переопределением методов и операций, скрытием и наследованием данных является еще одним фундаментальным понятием в ООП. Термин «полиморфизм» происходит от латинских корней, которые можно перевести как многообразие форм или множественность форм. Его реализация в C++ позволяет пользователю иерархической структуры классов посылать сообщения общего характера объектам разных классов, не заботясь о деталях интерпретации конкретного действия внутри класса. Одно и то же по смыслу действие может иметь множество конкретных воплощений, форм реализации. Полиморфизм сильно облегчает задачу пользования библиотекой классов. Пользователь должен понимать лишь общий смысл или эффект воздействия метода на любой объект в иерархии классов, а не особенности его реализации для конкретного класса.
Главным моментом в понимании сути полиморфизма является момент времени, в который система определяет детали реализации метода для объекта. Если система решает, как выполнить действие во время компиляции, то этот процесс называется ранним связыванием (early binding), если решение принимается динамически во время исполнения программы, то это называется поздним связыванием (late binding). Под связыванием понимают связывание сути послания (имени метода) с конкретными кодами его реализации для данного объекта.
Раннее связывание дает преимущество в скорости, так как компилятор имеет возможность оптимизировать код до его выполнения. Позднее связывание дает преимущество в гибкости и высоте абстракции задачи. Если метод или операция переопределены в двух или более классах, то компилятор может связать послание объекту с конкретными кодами заранее, так как он знает класс объекта, которому послано данное сообщение. Рассмотренное нами ранее переопределение методов является по сути простейшей формой полиморфизма. Ниже рассматривается полиморфизм позднего связывания, в котором конкретные коды метода выбираются на этапе исполнения программы.
4.9.2. Полиморфизм позднего связывания и виртуальные функции.
Виртуальные функции являются средством, с помощью которого полиморфизм позднего связывания осуществляется на практике. Если в базовом классе Parent определить какую-нибудь функцию (метод) как virtual, то она может быть переопределена (overriden) в одном (или более) из производных классов. Преимущества такого определения функций в родственных классах будут ясны после того, как мы вспомним, что дает переопределение обычной, не виртуальной функции в производном классе Derived. Пусть для определенности в обоих классах была объявлена public-функция void print(). Рассмотрим теперь фрагмент функции main, использующий объекты классов Parent и Derived:
Parent *pp,pobj;
Derived *pd,dobj;
pobj.print (); // Будут вызваны разные printQ
dobj.print(); // Полиморфизм раннего связывания
Компилятор заранее решает, каким классам должны принадлежать функции print в последних двух операторах. Очевидно, что для объекта pobj класса Parent будет вызван метод своего же класса, то есть Parent::print(). Аналогично для производного класса справедлив вызов Derived::print().
В языке C++ указатель на базовый класс может содержать адрес объекта производного класса. При этом нет необходимости использовать операцию явного приведения типов вида: рр=(Parent*)&dobj;. Итак, указатель рр содержит адрес объекта dobj производного класса. Туда же показывает и Derived *pd. Какие методы будут вызваны в каждом из последующих операторов программы?
pp->print(): pd->print():
Второй оператор не вызывает сомнений. Так как мы используем указатель на класс Derived, который содержит в данный момент адрес объекта dobj того же класса, то будет вызван метод Derived::print(). Действия для первого оператора не так очевидны. Полиморфизм раннего связывания диктует следующее правило. Так как рр объявлен указателем на класс Parent, то оператор pp->print(); вызовет Parent::print(). Тот факт, что рр в данный момент времени содержит адрес объекта производного класса, не влияет на решение, принятое на этапе компиляции.
Теперь рассмотрим этот же пример, но с одним небольшим изменением. Добавим спецификатор virtual в объявление метода print в базовом классе. В этих условиях изменится функционирование только одного оператора pp->print. В игру вступает полиморфизм позднего связывания, и выбор метода print производится на этапе исполнения программы в зависимости от того, на объект какого класса ссылается в данный момент времени указатель Parent *pp. Если он ссылается на объект производного класса (как в нашем случае), то будет вызван Derived::print(), если же указатель ссылается на объект базового класса, то будет вызван Parent::print(). Такая гибкость функционирования виртуальных функций является мощным средством, позволяющим выбрать нужный метод при получении указателем (на текущий объект одного из производных классов иерархии) сообщения общего характера вида print(), input() и т. д.
Цена этой гибкости — необходимость для каждого объекта производных классов хранить таблицу ссылок на конкретные воплощения виртуальных функций, чтобы на этапе выполнения программы выбрать нужную. Если эта таблица пуста, то есть виртуальная функция не переопределялась в данном производном классе, то происходит вызов одной из функций в таблице виртуальных функций класса, стоящего на одну ступень выше, и т. д. Если метод класса не является виртуальным, то таблица не создается. Только один метод из одноименных методов иерархии может быть применен к объекту данного класса (свой или ближайший унаследованный из классов-предков).
ЗАМЕЧАНИЕ
Отметим, что симметричное присвоение pd=&pobj; не проходит, так как нарушена иерархия. Указатели на низшие классы не могут ссылаться на объекты высших классов. Возможно, однако, явное приведение типов, которое делает присвоение законным:
pd=(Derived*)&pobj; //Явное приведение типа
После такого присвоения вызов pd->print() будет функционировать в зависимости от наличия описателя virtual в объявлении функции print в базовом классе. Если print виртуальная, то будет вызвана функция Parent::print(), если нет, то Derived::print().
Все методы классов могут быть виртуальными, за исключением конструктора. Деструктор класса, однако, может быть объявлен виртуальным. Это позволяет вызвать требуемый деструктор с помощью указателя на базовый класс. Если указатель в данный момент ссылается на объект одного из производных классов, то будет вызван деструктор соответствующего класса. Деструктор класса, производного от класса с virtual -деструктором, сам является virtual -деструктором (так же, как и любая другая виртуальная функция). Например:
class Box
{
public: // Виртуальный деструктор
virtual ~Box() { puts(“Box”); }
}
class Menu : public Box
{
public: // ~Menu() тоже виртуальный
~Menu() { puts(“Menu”); }
}
class PullDownMenu : public Menu
{ publiс: // Этот деструктор тоже виртуальный
~PullDownMenu() { puts(“Pull”); }
}
Теперь объявим и заполним массив указателей на базовый класс:
Box *window[3];
window[0] = new Box;
window[l] = new Menu;
window[2] = new PullDownMenu;
Вызовы деструкторов порождают следующие цепочки действий:
delete window[0]; // Будет вызван ~Вох()
delete window[l]; // Будет вызван ~Мепи(), потом ~Вох()
delete window[2]; // Будет вызван ~PullDownMenu(),
// потом ~Мепи(), и, наконец, ~Вох()
Если убрать спецификатор virtual при объявлении деструктора ~Вох(), то во всех трех случаях будет вызван деструктор базового класса, который, возможно, некорректно сработает в последних двух случаях при уничтожении объектов классов Menu и PullDownMenu.
При определении в производных классах виртуальных функций количество и типы параметров у всех функций в разных классах иерархии должны быть одинаковы. Только в этом случае они считаются виртуальными.
Необходимо различать три варианта переопределения функций, для которых в английском языке используются различные специальные термины. Говорят, что переопределенная в производном классе виртуальная функция (overrides) преобладает (над) или заменяет одноименную функцию базового класса. Если в производном классе переопределена обычная функция, то говорят, что она скрывает (hides) функцию из базового класса с таким же именем. Если в классе определены две функции с одинаковым именем, но разным набором параметров, то говорят, что они совмещены или что вторая является перегруженной (overloaded) версией первой функции.
В случае скрытия (hiding) количество и типы аргументов не влияют на сам факт скрытия. Совмещение (overloading) функций с одним именем, но различием в аргументах возможно только в рамках одного класса и одинаковом типе возвращаемого значения. Если объявленная виртуальной в базовом классе функция имеет в производном классе другой тип или набор параметров, то она считается спрятанной (hiden) и виртуальный механизм игнорируется. Если в производном классе она имеет другой тип возвращаемого значения, то это рассматривается как ошибка.