Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
КЛ_ТехнолПрогр2010_090103.doc
Скачиваний:
22
Добавлен:
27.04.2019
Размер:
1.35 Mб
Скачать

3.4. Полиморфизм

Перегрузка операций

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

Средствами перегрузки операций являются специальные функции-операции с ключевым словом operator. Синтаксис функции:

tip operator @(t1 per1[,...])//@ - перегружаемая операция

{...} //tip - тип возвращаемого значения

//t1 per1[,...]- параметры функции

Функция-операция характеризуется следующими свойствами:

- перегружает все операции, кроме (.),(.*),(::),(?:); операция присваивания (=) уже предопределена для любого типа;

- наследуется, кроме функции-операции operator=();

- должна быть либо элементом-функцией класса, либо внешней функцией, но дружественной данному классу;

если функция-операция является внешней и дружественной данному классу, то для бинарных операций она должна иметь два параметра, для унарных - один параметр; выражение obj1@obj2 интерпретируется как operator @(obj1,obj2), выражение @obj интерпретируется как operator @(obj);

если функция-операция является элементом-функцией данного класса, то тогда ей передается неявный указатель this на текущий объект класса, т.е. она уже имеет один неявный параметр, и именно первый; поэтому, для бинарных операций она должна иметь один параметр, для унарных - вообще без параметров; выражение obj1@obj2 интерпретируется как obj1.operator@(obj2), т.е. результат заносится в obj1, а выражение @obj интерпретируется как obj.operator@(), т.е. результат заносится в obj;

- обычно использует в качестве параметров ссылки на объекты, а в качестве возвращаемого значения - значение объекта.

Преобразования типов, определяемые классом

Преобразование типов данных является одной их форм полиморфизма. При объектно-ориентированном программировании необходимы средства преобразований, определяемые классом (элементы-функции класса). Функциями преобразований являются: конструкторы преобразования и операции преобразования.

Конструктор преобразования - это конструктор класса с одним аргументом, служащий для преобразования из типа аргумента к типу класса.

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

Операция преобразования - это элемент-функция класса, осуществляющая явное преобразование типа класса в другой тип.

Операция преобразования имеет следующие свойства:

- синтаксис операции преобразования напоминает синтаксис перегруженной операции: operator tip() {...} ,где tip - идентификатор нового типа, в который происходит преобразование;

- не имеет аргументов и типа возвращаемого значения;

- должна быть нестатической элементом-функцией класса;

- наследуется и может быть как virtual.

Операция удобна для преобразования из типа класса в базовый тип. Пусть необходимо складывать комплексные числа с вещественными числами с получением вещественных чисел. Заменим в предыдущем примере в классе complex конструктор преобразования на операцию преобразования с типом float.

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

Для преобразования объекта одного класса в объект другого класса (без наследования) используются рассмотренные выше функции преобразования: конструктор преобразования и операция преобразования. Пусть требуется преобразовать объект obj1 класса cl1 в объект obj2 класса cl2, т.е. ,например, obj2=obj1;. Тогда синтаксис функций преобразования будет иметь вид:

- конструктор класса cl2: cl2::cl2(const cl1&){...}

- операция класса cl1: cl1::operator cl2(){...}

Необходимо иметь одновременно только одну из этих функций преобразования.

Рассмотрим влияние наследования на преобразование объектов базовых и производных классов. При преобразовании объектов производного класса функции преобразования не требуется. Например, при выполнении операции присваивания происходит неявное поэлементное копирование собственной части и наследуемой (базовой) части одного объекта в другой. Пусть obj1 - объект базового класса cl1, obj2 - объект производного класса cl2. Преобразование объекта производного класса в объект базового класса допустимо без функции преобразования: obj1=obj2; ,т.е. часть объекта obj2, унаследованная от класса cl1, присваивается объекту obj1.

Преобразование объекта базового класса в объект производного класса не допустимо без функции преобразования: obj2=obj1; , это не допустимо, так как нет функции преобразования. Требуется явное преобразование из типа cl1 в тип cl2, поэтому, надо создать либо конструктор преобразования в классе cl2 (cl2::cl2 (const cl1&) {...}), либо операцию преобразования в классе cl1 (cl1::operator cl2(){...}). Теперь можно преобразование: obj2=obj1; , преобразование из типа cl1 в тип cl2 допустимо.

Перегрузка функций

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

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

Перегруженные функции имеют следующие свойства:

- должны отличаться сигнатурой, т.е. числом, порядком следования и типами параметров; тип возвращаемого значения и имена параметров не учитываются; пример перегруженной функции vvod:

void vvod(int m, int n) {...}

void vvod(float m, float n) {...}

void vvod(float m) {...}

void vvod(char ch) {...}

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

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

Алгоритм выбора функции состоит из следующих этапов:

- проверка на точное соответствие;

- проверка на стандартные преобразования типов;

- проверка на преобразования, определяемые классом (см. выше - в примерах на преобразование из float в complex и из complex в float эти типы становятся совместимыми).

Примеры:

int x, y; vvod(x, y); //вызов vvod(int, int), точное соответствие

int x; vvod(x); //вызов vvod(float), стандартное преобразование

vvod((char)1); //вызов vvod(char), преобразование пользователя

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

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

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

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

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

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

- определяется в базовом классе иерархии наследования со словом virtual и переопределяется в производных классах;

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

- не может быть объявлена как static, так как наследуется;

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

- вызывается динамически во время выполнения программы, что приводит к увеличению времени выполнения.

Пусть имеется иерархия классов, показанная на рис. 3.5, в каждом из которых содержится элемент-функция void vyvod().

cl1 //работники предприятия

|

cl2 //работники цеха

|

cl3 //рабочий участка

Рис. 3.5. Иерархия классов

Требуется составить программу последовательного вызова функций vyvod() классов cl1, cl2, cl3.

Приведем следующие рассуждения. Так как каждый работник цеха и участка являются работниками предприятия, то указатели (или ссылки) на cl2 и cl3 можно присвоить указателям ( или ссылкам ) на cl1, т.е. объекты производных классов можно рассматривать, как объекты базового класса:

- объявление объектов: cl1 obj1; cl2 obj2; cl3 obj3;

- объявление указателей (или ссылок) на класс cl1: cl1* p[3]; или

cl1& r1; cl1& r2; cl1& r3;

- присваивание указателям (или ссылкам) на класс cl1 указатели (или ссылки) на классы cl2 и cl3: p[0]=&obj1; p[1]=&obj2; p[2]=&obj3; или r1=obj1; r2=obj2; r3=obj3;

Итак, вызываются последовательно экземпляры виртуальной функции vyvod() классов cl1, cl2, cl3, что и было необходимо.

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

Таблица 3.3.

Объекты классов

Таблицы VT

Функции классов

obj1:

VP=(a1, hvyvod)---à

//VT_cl1:

-->a1)...(a1_vyvod)--à

-->a1_vyvod)void vyvod() {...}

obj2:

VP=(a2, hvyvod)---à

//VT_cl2:

-->a2)...(a2_vyvod)--à

-->a2_vyvod)void vyvod() {...}

obj3:

VP=(a3, hvyvod)---à

//VT_cl3:

-->a3)...(a3_vyvod)--à

-->a3_vyvod)void vyvod() {...}

Рассмотрим алгоритм вызова функции vyvod() класса cl3:

- по адресу объекта в указателе (p[2]=&obj3;) находится объект (obj3) и определяется указатель VP на таблицу виртуальных функций VT (VP=(a3,hvyvod)), содержащий адрес таблицы (a3) и смещение в таблице VT адреса функции (hvyvod);

- по адресу таблицы (a3) и смещению (hvyvod) в таблице VT находится адрес виртуальной функции (a3_vyvod);

- по адресу функции (a3_vyvod) находится функция vyvod() класса cl3.

Абстрактные классы

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

Абстрактным классом называется базовый класс в иерархии наследования, содержащий чистые виртуальные функции, определенные в производных классах. Чистая виртуальная функция - это виртуальная функция, объявленная в абстрактном классе со спецификатором (=0) и определенная в производном классе. Пример:

class cl1 class cl2:public cl1 class cl3:public cl2

{ public: { public: { public:

virtual void vyvod()=0; void vyvod() {...}; void vyvod() {...};

}; } }

Абстрактный класс имеет следующие свойства:

- чистые виртуальные функции со спецификатором =0;

- если в производном классе чистая виртуальная функция не определена, то такой класс тоже считается абстрактным;

- не имеет объектов и используется как базовый класс;

Параметрический полиморфизм

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

Шаблон функции - это обобщенное определение функции, использующее типы в качестве параметров функции.

- Синтаксис шаблона функции:

template <class t1[,...]> //спецификация шаблона

t_vozvr imf(t1 p1[,...]) //определение функции

{ //t1 - идентификатор, обозначающий тип

... //imf - имя функции

} //p1 - параметр функции типа t1

Шаблон функции характеризуется следующими свойствами:

- состоит из спецификации шаблона и определения функции; спецификация имеет список параметров шаблона, окаймленный скоб-ками <>; определение функции использует параметры шаблона; например:

template <class tip>

void vyvod(tip x) { tip y=x; cout<<y;}

- параметр шаблона - это любой идентификатор с ключевым словом class, обозначающий параметризованный тип; слово class означает обобщенное понятие любого типа, включая и тип class;

- в списке параметров функции можно иметь также переменные со встроенными типами и типами, определяемыми пользователем; например: void vvod(tip x,int n);

- при вызове функции компилятор автоматически создает экземпляр функции, заменяя параметры шаблона на заданные в операторе вызова типы; например: int a; vyvod(a);

- могут быть перегруженными, как и обычные функции.

Шаблон класса - это обобщенное определение класса, использующее тип в качестве параметра класса.

- Синтаксис шаблона класса:

template <class t1[,...]>//спецификация шаблона

class imcl //определение класса

{ //t1 - идентификатор, обозначающий тип

... //class t1 - параметр шаблона

} //imcl - имя класса

- Синтаксис оператора вызова:

imcl<tip1> obj; //объявление объекта класса с типом tip1

imcl<int> obj1; //создается класс для типа int

imcl<t> obj2; //создается класс для типа t

Шаблон класса характеризуется следующими свойствами:

- состоит из спецификации шаблона и определения класса; спецификация имеет список параметров шаблона, окаймленный скобками <>;

- параметр шаблона - это любой идентификатор с ключевым словом class, обозначающий параметризованный тип; слово class означает обобщенное понятие любого типа, включая и тип class;

-в списке параметров шаблона можно иметь также переменные со встроенными типами и типами, определяемыми пользователем, так называемые нетиповые параметры, значения которых должны быть константами; например: <class t,int n> ;

- при вызове класса компилятор автоматически создает экземпляр класса, заменяя параметры шаблона на заданные в операторе вызова типы.

Контрольные вопросы

  1. Что такое объектно-ориентированное программирование?

  2. Что такое классы и объекты?

  3. Какие специальные элементы-функции имеются в классе?

  4. Какие бывают спецификаторы доступа к элементам класса?

  5. Чем отличаются простое и множественное наследование?

  6. Как управляют доступом к наследуемым элементам?

  7. Зачем нужны виртуальные базовые классы?

  8. Что означает перегрузка операций и перегрузка функций?

  9. Какие преимущества дают виртуальные функции?

  10. Для чего используется параметрический полиморфизм?