Лекция №

Вид материалаЛекция
2.3Полиморфизм. Лекция №10
Перегрузка операций
Преобразования типов, определяемые классом
Перегрузка функций.
Чистый полиморфизм
Виртуальные элементы-функции
Пример 1: программа с виртуальной функцией vyvod()
Пример 2: программа с не виртуальной функцией vyvod()
Абстрактные классы. Лекция №11.
Параметрический полиморфизм
Шаблоны функций
Шаблоны классов
Подобный материал:
1   2   3   4   5   6   7   8   9   10   11

2.3Полиморфизм. Лекция №10


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

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


В 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;
  • обычно использует в качестве параметров ссылки на объекты, а в качестве возвращаемого значения - значение объекта;
  • критерия выбора между элементом класса и внешней функцией не существует, т.к. это дело вкуса; но можно предложить некоторые рекомендации: если функция-операция должна изменять значения элементов класса, то лучше ее определить как элемент класса (например, операции ++,--,+=,*= и т.д.); для бинарных операций лучше ее определить как внешнюю функцию, т.к. это естественней.

Примеры перегрузки (+) для сложения комплексных чисел.

//Пример 1: функция-операция + , как внешняя функция

//файл заголовков complex1.hpp

#include

#include

class complex

{

public:

float d,m;

complex() {d=m=0;}

complex(float x,float y) {d=x;m=y;}

friend complex operator +(complex& x1,complex& x2);

};

//файл кодов complex1.cpp

#include "complex1.hpp"

complex operator +(complex& x1,complex& x2)

{

complex y;

y.d=x1.d+x2.d;

y.m=x1.m+x2.m;

return(y);

}

main()

{

clrscr();

complex obj1(0,1),obj2(1,0),obj3;

cout<

obj3=obj1+obj2; //интерпрет.,как obj3=operator+(obj1,obj2);

cout<

cout<

cout<

getch();

}


Пример 2: функция-операция +, как элемент класса

//файл заголовков complex2.hpp, заменим прототип друж. функ.

#include

#include

class complex

{//... см. файл complex1.hpp

complex operator +(complex& x); };

//файл кодов complex2.cpp

#include "complex2.hpp"

complex complex::operator +(complex& x)

{

complex y;

y.d=d+x.d;

y.m=m+x.m;

return(y); //main без изменений (см. complex1.cpp), результаты те же

}//теперь obj3=obj1+obj2; интерпретир. как obj3=obj1.operator +(obj2);

Пример полезных перегруженных операций извлечения (>>) и вставки (<<) для ввода/вывода объектов класса, и перегруженной операции отрицания (!) для проверки наличия данных объекта:

//Пример: перегрузка операций (>>), (<<), (!)

//файл кодов peregruz.cpp

#include

#include

class akt

{

public:

char tabn[10];

float objem;

akt() { tabn[0]=’\0’; }

int operator!(); //Элемент-функция класса

friend istream& operator>>(istream& is, akt& z); //внешняя друж. функция

friend ostream& operator<<(ostream& os, akt& z);//внешн. друж. функция

};

int akt::operator!() //перегрузка (!) для проверки наличия данных объекта

{ return(tabn[0]==’\0’; }

istream& operator>>(istream& is, akt& z) //перегрузка (>>) для объектов

{ //класса akt

cout<<”Таб. номер :”;

is>>z.tabn;

cout<<”Объем работ:”;

is>>z.objem;

return(is);

}

ostream& operator<<(ostream& os, akt& z)

{

os<<”Таб. номер:”<

os<<”Объем :”<

return(os);

}

main()

{ clrscr();

akt obj;

if (!obj) cout<<”Объект пустой, введите данные:\n”;

cin>>obj;

if (!!obj) cout<<”Объект имеет данные:\n”;

cout<

getch();

}

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


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

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

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


Пример 3:

//файл заголовков complex3.hpp

#include

#include

class complex

{

public:

float d,m;

complex () {d=m=0;}

complex(float x,float y) {d=x;m=y;}

complex operator +(complex& x);

complex(float x) {d=x;m=0;} //конструктор преобразования

};

//файл кодов complex3.cpp

#include "complex3.hpp"

complex complex::operator +(complex& x)

{

complex y;

y.d=d+x.d;

y.m=m+x.m;

return(y);

}

main()

{

clrscr();

complex obj1(0,1);

float a=4.5;

obj1=obj1+a; //a преобразуется из float в complex

cout<

getch();

}

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

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

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

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

Пример 4:

//файл заголовков complex4.hpp, заменим complex(float x) {d=x;

// m=0;} на operator float() {return(d);}

#include

#include

class complex

{ //... см. complex3.hpp

operator float() {return(d);}

};

//файл кодов complex4.cpp

#include "complex4.hpp"

complex complex::operator +(complex& x)

{

complex y;

y.d=d+x.d;

y.m=m+x.m;

return(y);

}

main()

{

clrscr();

complex obj1(0,1);

float a=4.5;

float b=obj1+a; //obj1 преобразуется из типа complex в тип float

cout<

getch();

}

Для преобразования объектов одного и того же класса используются конструкторы копирования, задаваемые явно либо по умолчанию. Пусть выполняется операция присваивания: 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++ имеются средства, позволяющие переложить связывание вызова таких функций с определениями функций с программиста на саму программу во время ее выполнения. Это механизм виртуальных функций или позднее связывание.

Чистый полиморфизм


"Комбинация виртуальных функций и общего полиморфизма представляет собой форму чистого полиморфизма и является наиболее общим и гибким способом формирования программного обеспечения"[4].

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


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

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

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

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

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

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

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

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




Рис. 2.5. Пример иерархии классов

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

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

Таблица 2.3. Использование указателей и ссылок.

Объекты

Указатели cl1* p[3];

или ссылки

cl1 obj1;

cl2 obj2;

cl3 obj3;

p[0]=&obj1

p[1]=&obj2;

p[2]=&obj3;

cl1& r1=obj1;

cl1& r2=obj2;

cl1& r3=obj3;


Пример 1: программа с виртуальной функцией vyvod():

//файл заголовков virt1.hpp

#include

#include

class cl1

{ public: virtual void vyvod(); };

class cl2:public cl1

{ public: void vyvod(); };

class cl3:public cl2

{ public: void vyvod(); };

//файл кодов virt1.cpp

#include "virt1.hpp"

void cl1::vyvod() { cout<<"Работники предприятия\n"; }

void cl2::vyvod() { cout<<"Работники цеха\n"; }

void cl3::vyvod() { cout<<"Рабочие участка\n"; }

main()

{

clrscr();

cl1 obj1; cl2 obj2; cl3 obj3;

cl1 *p[3]; //cl1& r1=obj1; ссылки

p[0]=&obj1; //cl1& r2=obj2;

p[1]=&obj2; //cl1& r3=0bj3; //Результат решения:

p[2]=&obj3; //r1.vyvod(); //Работники предприятия

for (int i=0;i<3;i++)//r2.vyvod(); //Работники цеха

p[i]->vyvod(); //r3.vyvod(); //Рабочие участка

getch(); }

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

Составим пример без виртуальных функций. Если просто убрать слово virtual, то три раза будет вызываться функция vyvod() класса cl1. Раннее связывание не дало результата. После инициализации (p[1]=& obj2; p[2]=&obj3;) произойдет преобразование cl2 и cl3 в cl1.

Для решения этой проблемы добавим в базовый класс cl1 элемент-данное id, значения которого идентифицируют классы. Эти значения присваиваются конструкторами классов.

Элемент-функция vyvod() класса cl1 просматривает значения id, распознает тип объекта и с помощью оператора switch вызывает функции vyvod() соответствующих классов.

Пример 2: программа с не виртуальной функцией vyvod():

//файл заголовков virt2.hpp

#include

#include

enum ident {id_cl1, id_cl2, id_cl3};

class cl1

{ public: ident id;

void vyvod();

cl1() {id=id_cl1;}

};

class cl2: public cl1

{ public: void vyvod();

cl2() {id=id_cl2;}

};

class cl3: public cl2

{ public: void vyvod();

cl3() {id=id_cl3;}

};

//файл кодов virt2.cpp

#include "virt2.hpp"

void cl1::vyvod()

{ switch(id)

{ case id_cl1: cout<<"Работники предприятия\n"; break;

case id_cl2: ((cl2 *)this)->vyvod(); break;

case id_cl3: ((cl3 *)this)->vyvod(); break;

}

}

void cl2::vyvod() { cout<<"Работники цеха\n"; }

void cl3::vyvod() { cout<<"Рабочие участка\n"; }

//функция main() без изменений (см. пример 1)

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

Таблица 2.4. Использование таблиц виртуальных функций.

Объекты

Таблицы VT

Функции

obj1:

VP=(a1, hvyvod)---

//VT_cl1:

-->a1)...(a1_vyvod)--

//функция класса cl1

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

obj2:

VP=(a2, hvyvod)---

//VT_cl2:

-->a2)...(a2_vyvod)--

// функция класса cl2

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

obj3:

VP=(a3, hvyvod)---

//VT_cl3:

-->a3)...(a3_vyvod)--

// функция класса cl3

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


p[0]->vyvod(); //p[0]=&obj1;

p[1]->vyvod(); //p[1]=&obj2;

p[2]->vyvod(); //p[2]=&obj3;

Алгоритм вызова функции содержит следующие этапы:
  • по адресу объекта в указателе (например, в p[2]) находится объект (obj3) и определяется указатель VP на таблицу виртуальных функций VT (VP=(a3,hvyvod)), содержащий адрес таблицы (a3) и смещение в таблице VT адреса функции (hvyvod);
  • по адресу таблицы (a3) и смещению (hvyvod) в таблице VT находится адрес виртуальной функции (a3_vyvod);
  • по адресу функции находится функция vyvod() класса cl3.

Абстрактные классы. Лекция №11.


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

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

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

{ public: { public: { public:

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

Абстрактный класс характеризуется следующими свойствами:
  • имеет чистые виртуальные функции со спецификатором =0;
  • если в производном классе чистая виртуальная функция не определена, то такой класс тоже считается абстрактным;
  • не имеет объектов и используется только как базовый класс;

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


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

Шаблоны функций


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

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

template //спецификация шаблона

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

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

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

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

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

template

void vyvod(tip x) { tip y=x; cout<
  • параметр шаблона - это любой идентификатор с ключевым словом class, обозначающий параметризованный тип; слово class означает обобщенное понятие любого типа, включая и тип class;
  • в списке параметров функции можно иметь также переменные со встроенными типами и типами, определяемыми пользователем; например: void vvod(tip x,int n);
  • при вызове функции компилятор автоматически создает экземпляр функции, заменяя параметры шаблона на заданные в операторе вызова типы; например: int a; vyvod(a);
  • могут быть перегруженными, как и обычные функции.

Шаблоны классов


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

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

template //спецификация шаблона

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

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

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

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

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

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

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

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

Шаблон класса характеризуется следующими свойствами:
  • состоит из спецификации шаблона и определения класса; спецификация имеет список параметров шаблона, окаймленный скобками <>; определение класса использует параметры шаблона;
  • параметр шаблона - это любой идентификатор с ключевым словом class, обозначающий параметризованный тип; слово class означает обобщенное понятие любого типа, включая и тип class;
  • в списке параметров шаблона можно иметь также переменные со встроенными типами и типами, определяемыми пользователем, так называемые нетипированные (нетиповые) параметры, значения которых должны быть константами; например: ;
  • при вызове класса компилятор автоматически создает экземпляр класса, заменяя параметры шаблона на заданные в операторе вызова типы.