Е. К. Пугачев Объектно-ориентированное программирование Под общей редакцией Ивановой Г. С. Рекомендовано Министерством общего и профессионального образования Российской Федерации в качестве учебник

Вид материалаУчебник

Содержание


Пример 3.49. Использование виртуальных функций
Абстрактные функции и классы.
Подобный материал:
1   ...   15   16   17   18   19   20   21   22   ...   39

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


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

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

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

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

Пример 3.48. Использование раннего связывания

#include

#include

class A

{ int a;

public:

void show(){cout<<" Содержимое полей объекта"<

print();} //вызов статической полиморфной функции

void print(void) // первый аспект статической полиморфной функции

{ cout<

A(int v):a(v){}

};

class B: public A

{ int b;

public:

void print(void) ) // второй аспект статической полиморфной функции

{ cout<

B(int va,int vb):A(va),b(vb){}

};

class C: public B

{ int c;

public:

void print(void) //аспект 3 статической полиморфной функции

{ cout<

C(int va,int vb,int vc):B(va,vb),c(vc){}

};

void main()

{ clrscr();

A aa(20); B bb(10,100); C cc(50,200,3000);

cout << " Результаты работы: "<

// явный вызов полиморфной функции print()

aa.print(); // выводит: 20

bb.print(); // выводит: 100

cc.print(); // выводит: 3000

getch();

// неявный вызов полиморфной функции print()

aa.show(); // выводит: 20

bb.show(); // выводит: 10

cc.show(); // выводит: 50

getch();

}

Нетрудно заметить, что результаты явного и неявного вызова метода print() различаются. Это объясняется тем, что метод show(), наследуемый в производных классах, вызывает метод print(). При раннем связывании show() жестко соединяется с этим методом на этапе компиляции. Поэтому, независимо от того, объект какого класса вызывает метод show(), из него будет вызван метод print() базового класса, для которого внутренние поля производных классов не доступны. Именно это является отличительной чертой раннего связывания.

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

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

Примечание. Виртуальную функцию в С++ принято называть «полиморфной», что не совсем соответствует общепринятой терминологии, согласно которой она является «динамической полиморфной».

Как уже упоминалось выше, при использовании виртуальных функций нужный аспект полиморфной функции, вызываемой из метода базового класса, определяется на этапе выполнения, когда известно, для какого объекта вызван метод: объекта базового класса или объекта производного. Аналогично, если вызов функции осуществляется через указатель на базовый класс, нужный аспект функции вызывается в зависимости от того, адрес объекта какого класса будет содержать указатель.

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

^ Пример 3.49. Использование виртуальных функций

#include

#include

class A

{ int a;

public:

void show() {cout<<" Содержимое полей объекта"<

virtual void print(void) // описание виртуальности функции

{ cout<

A(int v):a(v){}

};

class B: public A

{ int b;

public:

void print(void) // первый аспект виртуальной функции

{ cout<

B(int va,int vb):A(va),b(vb){}

};

class C: public B

{ int c;

public:

void print(void) // второй аспект виртуальной функции

{ cout<

C(int va,int vb,int vc):B(va,vb),c(vc){}

};

void main()

{ clrscr();

A aa(10),*pa; // указатель на объект базового класса

B bb(10,100);

C cc(10,100,1000);

cout << " Результаты работы: "<

cout << " Явный вызов полиморфной функции print(): "<

aa.print(); // выводит: 10

bb.print(); // выводит: 100

cc.print(); // выводит: 1000

getch();

cout << " Неявный вызов полиморфной функции print(): "<

aa.show(); // выводит: 10

bb.show(); // выводит: 100

cc.show(); // выводит: 1000

getch();

cout<<"Вызов функции print() по указателю"<

pa=&aa; pa->print(); // выводит: 10

pa=&bb; pa->print(); // выводит: 100

pa=&cc; pa->print(); // выводит: 1000

getch();

}

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

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

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

Вызов виртуальной функции обычно реализуется как косвенный вызов по ТВМ (раздел 1.6). Эта таблица создается во время компиляции, а затем используется во время выполнения программы. Именно поэтому для вызова такой функции требуется больше времени.

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

^ Абстрактные функции и классы. Существуют классы, которые выражают некоторую общую концепцию, отражающую основной интерфейс для использования в производных классах. Этот класс применяется при определении данных и методов, которые будут общими для различных производных классов. Он имеет смысл лишь как некоторая база. В этом случае смыслового определения виртуальной функции в базовом классе может не быть, но без ее описания корректная реализация сложной иерархии затруднена. Для решения этой проблемы в языке С++ используются абстрактные виртуальные функции.

Абстрактной называется функция, объявленная в базовом классе как виртуальная, но не имеющая описания. Для описания абстрактной функции используется следующая форма:

virtual <тип><имя_функции>(<список параметров>)=0

Здесь «=0» - признак абстрактной виртуальной функции. При этом производный класс должен определить свою собственную версию функции, так как просто использовать версию, определенную в базовом классе нельзя.

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

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

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

Пример 3.50. Использование абстрактного класса при работе с полиморфными объектами

Пусть необходимо реализовать иерархию классов, представленную на рис. 3.4.



Рис. 3.48. Иерархия классов примера 3.21

#include

#include

#include

#include

class Tpole // абстрактный класс

{ public:

virtual void Print(void)=0; // абстрактная виртуальная функция

Tpole(){}

~Tpole(){}

};

class Tnumpole:public Tpole

{ private: int *pnum;

public:

Tnumpole(int n) {pnum=new int; *pnum=n; }

~Tnumpole(void){delete pnum;}

void Print(void) // аспект виртуальной функции

{ printf("\n число= %d",*pnum);}

};

class Tstrpole:public Tpole

{ private: char *str;

public :

Tstrpole(char *st);

~Tstrpole(void){delete str;}

void Print(void){printf("\n строка= %s",str);} /* аспект виртуальной функции класса Tstrpole*/

};

inline Tstrpole::Tstrpole(char *st)

{ int l; l=strlen(st); str=new char[l+1]; strcpy(str,st); }

void main()

{ int n,i;

char st[80];

Tpole *a[10]; // массив указателей на базовый класс Tpole

for(i=0;i<10;i++)

{ if (i%2)

{ printf("\nвведите целое число: "); scanf("%2d",&n);

a[i]=new Tnumpole(n);} // Указатель на объект класса Tnumpole

else

{printf("\nвведите строку < 80 байт: "); scanf("%s",st);

a[i]=new Tstrpole(st); } // Указатель на объект класса Tstrpole

}

puts("\n РЕЗУЛЬТАТ ");

for(i=0;i<10;i++) { a[i]->Print(); delete a[i]; }

}

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