WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!


Pages:     | 1 |   ...   | 8 | 9 || 11 | 12 |   ...   | 17 |

то принципы преобразования очень просты: неявно может быть выполнено преобразование указателя типа Der* к указателю типа Base*. Обратное преобразование обязательно должно быть явным.

Другими словам, при обращении через указатели объект производного типа может рассматриваться как объект базового тип. Обратное утверждение неверно:

Der derived;

Base *bp = &derived; // Неявное преобразование.

Der *dp1 = bp; // Ошибка.

Der *dp2 = (Der*) bp; // Явное преобразование; теперь верно.

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

Правила здесь таковы.

Если нет точного соответствия списка формальных и фактических параметров, то наивысший приоритет среди выполняемых преобразований имеют преобразования производного типа к базовому. Это относится как к самому типу, так и к указателю на него. Только в том случае, если это невозможно, компилятор пробует выполнить другие преобразования (например, стандартные преобразования указателей).

Пример:

class Base {…};

class Der: public Base {…};

func (Base*);

func (void*);

… Der *dp;

float *fp;

func (dp); // Вызов func (Base*).

func (fp); // Вызов func (void*).

Если в программе используются несколько уровней производных классов, то при выполнении неявных преобразований типа указателя ищется класс “ближайшего” уровня:

class Base {…};

class A: public Base {…};

class B: public A{…};

func (Base*);

func (A*);

… B *db;

func (db); // Вызов функции func (A*).

20. Полиморфизм Одно из самых коротких и выразительных определений полиморфизма таково: полиморфизм – это функциональная возможность, позволяющая старому коду вызвать новый. Это свойство дает возможность расширять и совершенствовать программную систему, не затрагивая существующий код. Осуществляется такой подход с помощью механизма виртуальных функций.

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

Предположим, необходимо написать функцию-член CalculatePay() (Расчет), которая подсчитывает для каждого объекта класса Employee (Служащий) ежемесячные выплаты. Все просто, если зарплата рассчитывается одним способом: можно сразу вставить в вызов функции тип нужного объекта. Проблемы начинаются с появлением других форм оплаты. Допустим, уже есть класс Employee, реализующий расчет зарплаты по фиксированному окладу. А что делать, чтобы рассчитать зарплату контрактников – ведь это уже другой способ расчета! В процедурном подходе пришлось бы переделать функцию, включив в нее новый тип обработки, так как в прежнем коде такой обработки нет. Объектноориентированный подход благодаря полиморфизму позволяет производить различную обработку.

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

Другой пример: базовый класс figure может описывать фигуру на экране без конкретизации её вида, а производные классы triangle (треугольник), ellipse (эллипс) и т.д. однозначно определяют её форму и размер. Если в базовом классе ввести функцию void show () для изображения фигуры на экране, то выполнение этой функции будет возможно только для объектов каждого из производных классов, определяющих конкретные изображения. В каждый из производных классов нужно включить свою функцию void show() для формирования изображения на экране. Доступ к функции show() производного класса возможен с помощью явного указания ее полного имени, например:

triangle::show ();

или с использованием конкретного объекта:

triangle t;

t.show ();

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

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

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

struct base { void fun (int i) { cout <<”base::i = ” << i << ’\n’;} };

struct der: public base { void fun (int i) { cout << ” der::i = ” << i << ‘\n‘;} };

void main () { base B, *bp = &B;

der D, *dp = &D;

base *pbd = &D; // Неявное преобразование от der* к base*.

bp->fun (1);

dp->fun (5);

pbd->fun (8);

} Результат:

base::i = der::i = base::i = Здесь указатель pbd имеет тип base*, но его значение – адрес объекта D класса der. При вызове функции-члена по указателю на объект выбор функции зависит только от типа указателя, но не от его значения, что и иллюстрируется выводом base::i = 8. Настроив указатель базового класса на объект производного класса, не удается с помощью этого указателя вызвать функцию из производного класса. Таким способом не удается достичь позднего или динамического связывания.



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

class base { public:

int i;

virtual void print (){ cout << i << “ внутри base\n“;} };

class D: public base{ public:

void print (){ cout << i << “ внутри D\n“;} };

void main (){ base b;

base *pb = &b;

D f;

f.i = 1 + (b.i = 1);

pb->print ();

pb = &f; // Неявное преобразование D* к Base*.

pb->print ();

} Результат:

1 внутри base 2 внутри D Здесь в каждом случае выполняется различная версия функции print(). Выбор динамически зависит от типа объекта, на который указывает указатель. Служебное слово virtual означает, что функция print() может иметь свои версии для различных порожденных классов.

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

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

Рассмотрим пример.

Вычисление площадей различных фигур.

Различные фигуры будем порождать от базового класса figure.

class figure { protected:

double x, y;

virtual double area (){return 0;} // Площадь по умолчанию.

};

class rectangle: public figure { private:

double height, width;

...

public:

rectangle (double h, double w){height=h; width=w;} double area () {return height * width;}...

};

class circle: public figure { private:

double radius;

...

public:

circle (double r){radius=r;} double area () { return M_PI*radius*radius;}...

};

Код пользователя может выглядеть так:

const N = 30;

figure *p[N];

double tot_area = 0;

... // Здесь устанавливаются указатели p[i], например,... // rectangle r(3, 5); p[0]=&r; circle c(8); p[1]=&c; и т.д.

for (int i = 0; i < N; i ++) tot_area + = p[i]->area ();// Код пользователя.

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

Рассмотрим еще пример для расчета заработной платы с классом Employee.

#include #include #include #include class Employee{ protected:

char * firstName, * lastName; // Имя, фамилия.

int age; // Возраст.

double payRate; // Размер оплаты.

public:

Employee (char* FN, char* LN, int a, double pay){ firstName = new char [strlen (FN) + 1];

strcpy(firstName, FN);

lastName = new char [strlen (LN) + 1];

strcpy(lastName, LN);

age = a;

payRate = pay;

} virtual double CalculatePay (){ return 0;

} void print(){cout<

};

class HourlyEmployee: public Employee { // Почасовая оплата.

int hours; // Проработано часов.

public:

HourlyEmployee (char* FN, char* LN, int a, double pay, int h):

Employee (FN, LN, a, pay){ hours=h; } virtual ~HourlyEmployee (){delete firstName; delete firstName;} virtual double CalculatePay (){ return hours*payRate;

} };

class ContractorEmployee: public Employee { // Работа по контракту. public:

ContractorEmployee (char* FN, char* LN, int a, double pay):

Employee (FN, LN, a, pay){} virtual double CalculatePay (){ return payRate;

} virtual ~ContractorEmployee (){delete firstName; delete firstName;} };

class DaypaymentEmployee: public Employee { // Поденная оплата.

int days; // Отработано дней.

public:

DaypaymentEmployee (char* FN, char* LN, int a, double pay, int d):

Employee (FN, LN, a, pay){days=d;} virtual double CalculatePay (){ return days*payRate/24.0; // Рабочих дней в месяце – 24.

} virtual ~DaypaymentEmployee (){delete firstName; delete firstName;} };

void loademploee (Employee* a[], int &n){ char FN[20], LN[20]; int age, arg; double pay;

char sel; // Селектор, задающий тип оплаты.

ifstream file("emp.txt"); // Создаем входной поток для чтения // file и связываем его с внешним // файлом emp.txt.

n = 0;

while ( file.peek ( ) != EOF ){ // Пока нет конца файла … file >> sel;

file >> FN;





file >> LN;

file >> age;

file >> pay;

file >> arg;

switch (sel){ case 'h': a[n] = new HourlyEmployee (FN, LN, age, pay, arg);

break;

case 'c': a[n] = new ContractorEmployee (FN, LN, age, pay);

break;

case 'd': a[n] = new DaypaymentEmployee (FN, LN, age, pay, arg);

break;

} n++;

} } void main(){ int n;

Employee* a[20];

clrscr();

loademploee (a, n);

double s=0, r;

for(int i=0; iCalculatePay ());

a[i]->print();

cout.width (16); cout << r << "$\n";

} cout<<"This month it is payd: ";

cout.width (16); cout << s << "$\n";

} Пусть во входном файле emp.txt содержится следующая информация:

c Gbanov Ivan 32 4340 c Muhin Sergey 26 1320 h Mazin Petr 27 15.3 d Bobrov Mikhail 40 110 Тогда в результате работы программы на экране появится следующее:

Gbanov Ivan this month has received 4340$ Muhin Sergey this month has received 1320$ Mazin Petr this month has received 489.6$ Bobrov Mikhail this month has received 96.25$ This month it is payd: 6245.85$ 20.3. Абстрактные классы Снова рассмотрим пример с вычислением площадей фигур. В этой программе использована виртуальная функция area(). Эта функция должна была быть первый раз определена в классе figure. Поскольку при нормальной работе не должно существовать объектов типа figure, а только объекты производных от него типов, то версия area () была определена так:

double figure::area {return 0;} Eсли бы тип возвращаемого значения у функции был void (например, при рисовании фигуры void show ( )), можно было бы написать:

void figure::show () {} В обоих случаях эти функции фиктивны. Такого рода виртуальные функции можно было бы использовать для контроля ошибок, связанных с созданием объектов типа figure:

double figure::area () { cout <<”Ошибка: попытка вычислить площадь ”;

cout <<”несуществующего объекта!\n”;

exit (1); return 1;

} В С++ существует более удобный и надежный способ. Версия виртуальной функции, которая, с одной стороны, должна быть определена, а с другой, никогда не должна использоваться, может быть объявлена как чисто виртуальная функция:

class figure {...

virtual double area () = 0;

};

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

21. Переопределение стандартных операций 21.1. Основные определения и свойства В С++ есть возможность распространения действия стандартных операций на операнды абстрактных типов данных.

Для того, чтобы переопределить одну из стандартных операций для работы с операндами абстрактных типов, программист должен написать функцию с именем operator, где – обозначение этой операции (например, + – | += и т.д.).

При этом в языке существует несколько ограничений:

нельзя создавать новые символы операций;

нельзя переопределять операции :: * (– разыменование, не бинарное умножение) :

sizeof..* # ## символ унарной операции не может использоваться для переопределения бинарной операции и наоборот. Например, символ << можно использовать только для бинарной операции, ! – только для унарной, а & – и для унарной, и для бинарной;

переопределение операций не меняет ни их приоритетов, ни порядка их выполнения (слева направо или справа налево);

при перегрузке операции компьютер не делает никаких предположений о ее свойствах. Это означает, что если стандартная операция += может быть выражена через операции + и =, т.е. а + = b эквивалентно а = а + b, то для переопределения операций в общем случае таких соотношений не существует, хотя, конечно, программист может их обеспечить. Кроме того, не делается предположений, например, о коммутативности операции +: компилятор не имеет оснований считать, что а + b, где а и b – абстрактных типов – это то же самое, что и b + a;

никакая операция не может быть переопределена для операндов стандартных типов.

Функция operator () является обычной функцией, которая может содержать от 0 до 2 явных аргументов. Она может быть, а может и не быть функцией-членом класса.

class cl { int i;

public:

int get () {return i;} int operator + (int ); // Бинарный плюс.

};

int operator + (cl&, float); // Бинарный плюс.

В первой форме бинарного плюса не один, а два аргумента. Первый – неявный. Его имеет любая нестатическая функция-член класса;

этот аргумент является указателем на объект, для которого она вызвана.

Реализация обеих функций может выглядеть так:

int cl::operator + (int op2){ return i + op2;} int operator + (cl &op, float op2){ return op.get() + op2;} Что будет, если в глобальной функции ::operator+() второй аргумент будет иметь тип не float, а int В этом случае компилятор выдаст сообщение об ошибке, так как он не сможет сделать выбор между функциями cl::operator + () и ::operator +() – обе подходят в равной степени.

Pages:     | 1 |   ...   | 8 | 9 || 11 | 12 |   ...   | 17 |










© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.