Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология
Образование Политология Производство Психология Стандартизация Технологии


Виртуальные функции. Полиморфизм



 

Рассмотрим еще один пример.

 

//Листинг 29. Проблема статического связывания функций

class Base

{ public:

int func1(int x) {return x*x; }

int func2(int x){return func1(x)/2; }

};

class Child: public Base

{public:

int func1(int x) {return x*x*x; }

};

main()

{ Child c;

cout< < c.func2(5); //на экран выводится 12

Base *ptb=new Child;

cout< < ptb-> func1(2); //на экран выводится 4

cout< < c.func1(2); // на экран выводится 8

}

В классе Base определены две функции func1 и func2, причем вторая функция вызывает первую. В классе Child переопределена функция func1, а функция func2 просто наследуется. При вызове функций результаты их работы оказываются для многих неожиданными. Так, вызов c.func2(5) дает результат 12 вместо ожидаемых 62, а вызов ptb-> func1(2) дает результат 4, а не 8. Дело в том, что в обоих случаях будет вызвана функция func1 базового класса, а не переопределенная в производном классе. Такое поведение объектов связано со статическим (ранним) связыванием функций при трансляции программы. Когда транслятор в процессе обработки программы встречает вызов какой-либо функции, то на место вызова он подставляет в текст оттранслированной программы адрес вызываемой функции. Таким образом, компилируя тело компонентной функции func2 класса Base, транслятор на место вызова функции func1 подставит адрес компонентной функции func1 из класса Base, так как только эта функция с подобным именем ему известна (содержимое класса Child транслируется позже). В итоге функция Base:: func2 всегда будет вызывать функцию Base:: func1, как бы ни был оформлен вызов самого метода func2.

Аналогично, компилируя тело функции main и встретив вызов ptb-

> func1(2), транслятор должен подставить на место вызова адрес функции, которой будет передано управление в данной точке программы. К этому моменту транслятору известны две функции с именем func1: Base:: func1 и Child:: func1. Так как вызов метода осуществляется для указателя на объект Base, транслятор подставит на место вызова адрес функции именно этого класса (определить, что в указатель ptb записан адрес объекта класса Child, и поэтому вызвать метод Child:: func1, транслятор не может).

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

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

virtual тип имя_функции (список_формальных параметров)

{тело функции }

 

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

 

//Листинг 30. Использование виртуальных функций

class Base

{ …

virtual int func1(int x) {return x*x; }

};

class Child: public Base

{…

virtual int func1(int x) {return x*x*x; }

};

main()

{ Child c;

cout< < c.func2(5); //на экран выводится 62

Base *ptb=new Child;

cout< < ptb-> func1(2); //на экран выводится 8

}

 

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

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

pObj-> func2()

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

(*(pObj-> vptr[1])) (pObj)

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

Использование позднего связывания не отрицает возможности вызова из производного класса экземпляра виртуальной функции базового. Просто для подобного использования необходимо указывать при вызове полное квалифицированное имя функции. Пример для программы из листинга 30:

main()

{

Base * ptb=new Child;

cout< < ptb-> Base:: func1(2); //на экран выводится 4

}

 

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

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

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

 

//Листинг 31. Пример использования виртуальных функций

#include < conio.h>

#include < iostream.h>

#include < dos.h>

#include < graphics.h>

#include < stdlib.h>

class Point

{ public: int x, y; int color;

Point(int xb=0, int yb=0)

{x=xb; y=yb; color=15; }

};

class Shape

{ protected: Point base; public:

Shape(int xp, int yp)

{base.x=xp; base.y=yp;

}

virtual void show()

{putpixel(base.x, base.y, base.color); }

void move(int xn, int yn)

{setcolor(0);

show(); base.x+=xn; base.y+=yn; setcolor(base.color); show();

}

};

class Circle: public Shape

{ int radius;

public:

Circle(int xc, int yc, int r): Shape(xc, yc)

{radius=r; }

void show() {circle(base.x, base.y, radius); }

};

class Rectangle: public Shape

{ int dx, dy;

public:

Rectangle(int xc, int yc, int a, int b): Shape(xc, yc)

{dx=a; dy=b; }

void show() {rectangle(base.x, base.y, base.x+dx, base.y+dy); }

};

main()

{ int gd=DETECT, gm; initgraph(& gd, & gm, " c: \\borlandc\\bgi" ); if (graphresult()==grOk)

{

Shape *mas[10];

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

{mas[i++]=new Circle(random(600), random(400), random(100)+10);

mas[i]=new Rectangle(random(600), random(400), random(100)+10, random(50)+10);

} for(i=0; i< 10; i++) mas[i]-> show(); getch();

for(int j=0; j< 3; j++)

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

mas[i]-> move(random(100)-50, random(100)-50);

getch();

}

closegraph();

}

else { cout< < " error"; getch(); }

}

 

В программе из листинга 31 определены классы Circle и Rectangle, описывающие графические фигуры, окружность и прямоугольник соответственно. Совпадающие свойства этих геометрических фигур вынесены в базовый класс Shape. Этими свойствами являются – базовая точка фигуры base, определяющая местоположение фигуры на экране, цвет фигуры color, метод move, перемещающий фигуру по экрану. Необходимо обратить внимание на реализацию метода move. Для того чтобы переместить фигуру, ее сначала отображаем цветом фона, изменяем координаты базовой точки фигуры, а затем отображаем цветом фигуры. Такая процедура перемещения подходит для работы с любой фигурой и окружностью, и прямоугольником, поэтому метод move наследуется классами Circle и Rectangle. Для отображения фигуры метод move использует метод show. В классе Shape метод move просто отображает на экране базовую точку фигуры (вообще то, метод move в классе Shape не должен ничего отображать, поскольку этот класс с точки зрения предметной области является абстракцией, служащей основой для созда- ния производных классов, но об абстрактных классах речь пойдет в следующей главе). В классах Circle и Rectangle метод show отображает соответствующую геометрическую фигуру. Для правильной работы программы метод show должен быть объявлен виртуальным, поскольку вызов метода move для объектов классов Circle и Rectangle требует полиморфного поведения метода show. Необходимо также обратить внимание на использование определенных классов в функции main. Для того чтобы в одном массиве можно было хранить объекты разных (но родственных, то есть имеющих общего предка) классов, объявлен массив из эле- ментов типа указатель на базовый класс (Shape в нашем случае):

Shape *mas[10];

 

Элементы этого массива могут хранить адреса объектов как класса Circle,

так и Rectangle. Однако, когда далее в тексте программы для объектов, хранящихся в массиве mas, вызывается метод show:

 

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

mas[i]-> show();

 

только полиморфное поведение этого метода обеспечивает правильное отображение геометрических фигур. Если бы метод show не был бы объявлен виртуальным, то для всех объектов был бы вызван метод из класса Shape.

 

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

 

При проектировании иерархических систем классов программисты обычно стремятся выделить некоторые обобщающие свойства некоторой группы описываемых сущностей в базовый класс (в предыдущих главах такими классами были Subject, Figure, Shape). Зачастую созданные по такому принципу классы носят абстрактный смысл, не описывая какой-то реально существующий объект, а лишь являясь некоторой основой, на которой строятся действительно необходимые классы. Это приводит к некоторому несоответствию спроектированной системы классов предметной области: оказывается, что программист может определить в программе объекты, реально к предметной области не имеющие никакого отношения. Для того, чтобы предотвратить возможность использования объектов таких классов в программе, их объявляют абстрактными. Абстрактным называется класс, в котором определена хотя бы одна чистая виртуальная функция. Чистая виртуальная функция определяется следующим образом:

virtual тип имя_функции( список_формальных_параметров )=0;

Чистая виртуальная функция не имеет реализации, ее нельзя вы- звать в программе, она служит лишь как основа для дальнейшего поли- морфного переопределения в производном классе. Соответственно, аб- страктный класс не может иметь объектов, так как в нем не определены операции над объектами (или, по крайней мере, хотя бы одна операция, реализуемая чистой виртуальной функцией).

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

 

//Листинг 32. Использование абстрактных классов

#include < stdio.h>

#include < iostream.h>

#include < string.h>

#include < conio.h>

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

{ protected:

char **str; //адрес буфера для временного хранения информации из файла

char Name[30]; //имя файла

int n; //количество строк в буфере

virtual int ReadFile()=0; //чистая виртуальная функция чтения информации из файла в буфер

public: File(char*, int);

~File();

void display(); //метод, отображающий содержимое буфера на экране

};

File:: File(char * FileName, int k) //конструктор

{ strcpy(Name, FileName);

n=k;

str=new char*[n]; //выделяем память под буфер

for(int i=0; i< n; i++)

str[i]=new char[80];

}

File:: ~File() //деструктор

{for(int i=0; i< n; i++)

delete []str[i]; //освобождаем память

delete [] str;

}

void File:: display()

{ clrscr();

int k=ReadFile(); //считываем информацию из файла в буфер

for(int i=0; i< k; i++)

cout< < str[i]; // построчно выводим содержимое буфера на экран

getch();

}

struct info //тип информации, хранящейся в файле

{char name[20]; char numb[10]; float value;

};

class InfoFile: public File //класс «файл с базой данных»

{ int ReadFile(); //переопределяем функцию чтения информации из файла

public:

InfoFile(char* st, int k): File(st, k) {} //конструктор вызывает конструктор базового класса

void WriteFile(int ); //метод записи информации в файл

};

int InfoFile:: ReadFile()

{ FILE *fp; int i=0; info x;

if((fp=fopen(Name, " r" ))! =NULL) //открываем файл

{ while(! feof(fp)) //пока не конец файла

{ fread(& x, sizeof(info), 1, fp); //считываем очередную запись из файла

sprintf(str[i], ”Запись N %d %s %s %f\n", i+1, x.name, x.numb, x.value);

//заносим очередную запись в буфер файла

i++;

}

fclose(fp);

return i-1; //функция возвращает количество считанных из файла записей

}

return 0; }

void InfoFile:: WriteFile(int k)

{ info x; FILE *fp;

if((fp=fopen(Name, " w+" ))! =NULL)

{ for(int i=0; i< k; i++)

{cout< < ”Введите " < < i+1< < " -ю запись";

cin> > x.name> > x.numb> > x.value; //вводим с клавиатуры очередную запись

fwrite(& x, sizeof(info), 1, fp); //записываем ее в файл

}

fclose(fp);

}}

class HelpFile: public File //класс «файл с помощью»

{ int ReadFile(); //переопределяем функцию чтения информации из файла

public:

HelpFile(): File(" help.dat", 25){} //имя файла и его размер фиксированы

};

int HelpFile:: ReadFile()

{ int i=0; FILE *fp;

if((fp=fopen(Name, " r" ))! =NULL)

{ while(! feof(fp))

{fgets(str[i], 80, fp); //считываем содержимое файла в буфер

i++;

} fclose(fp); return i-1;

}

return 0; }

 

main()

{ HelpFile hp; //объект класса «файл с помощью»

hp.display(); //выводим содержимое файла на экран

InfoFile If(" info", 40); //создаем объект класса «файл с информацией» If.WriteFile(3); //записываем в файл 3 записи

If.display(); //выводим содержимое файла на экран

}

 

Программа, приведенная в листинге 32, работает с файлами двух типов –информационным файлом (класс InfoFile), предназначенным для хранения простейшей базы данных – нескольких структур типа info, а также файлом помощи (класс HelpFile), который хранит текстовую информацию, предположительно – справку о самой программе. Общаясущность двух этих типов файлов, выражающаяся в имени файла Name, буфере str для временного хранения информации из файла, размере используемого файлом буфера n, а также метода display вывода считанной из файла информации на экран, выделена в родительский класс (класс File).

Метод display класса File считывает информацию из файла методом ReadFile в буфер и построчно выводит ее на экран. Однако, метод ReadFile в самом классе File не может быть полноценно определен, поскольку этот класс в терминах предметной области является абстрактной основой для двух других классов (InfoFile и HelpFile) и для него не известен, например, тип хранящейся в файле информации. Метод ReadFile определен в классах InfoFile и HelpFile , причем в первом данный метод считывает из файла информацию в виде экземпляров структуры info, преобразует ее в текстовую форму и записывает в буфер, а во втором - информация непосредственно считывается в виде текстовых строк, которые записываются в буфер str. Таким образом, метод ReadFile не надо определять в классе File по логике представления предметной области, однако обязательно необходимо определить по правилам синтаксиса языка С++ (так как метод display использует этот метод). При этом необходимо обеспечить полиморфное поведение метода ReadFile для того, чтобы при вызове метода display объектом класса InfoFile в теле метода display была вызвана реализация метода ReadFile для соответствующего класса (аналогичная ситуация рассматривалась в листинге 31). Поэтому метод ReadFile объявлен в классе File как чистая виртуальная функция:

virtual int ReadFile()=0;

Таким образом, класс File является абстрактным классом, для которого запрещено создание объектов, то есть ошибкой будет такое объявление:

File MyFile(“file.txt”, 50);

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

 


 


Поделиться:



Популярное:

  1. Бесконечно малые и бесконечно большие функции.
  2. БИЛЕТ 30. ЯЗЫК ХУДОЖЕСТВЕННОЙ ЛИТЕРАТУРЫ. ОСНОВНЫЕ ФУНКЦИИ. СОДЕРЖАТЕЛЬНОСТЬ ЯЗЫКОВОГО УРОВНЯ ХУДОЖЕСТВЕННОЙ ФОРМЫ. ЯЗЫК ХУДОЖЕСТВЕННОЙ ЛИТЕРАТУРЫ И РЕЧЬ ХУДОЖЕСТВЕННАЯ.
  3. Вещи в мире произведения, их изображения и функции.
  4. Виртуальные функции и полиморфические кластеры
  5. Виртуальные, динамические методы.
  6. Возможные (виртуальные) перемещения системы
  7. Возрастание и убывание функции. Экстремумы функции
  8. Вопрос 2. Ощущения и восприятие: сущность и функции. Значение ощущений в познавательной деятельности человека. Теории восприятия.
  9. Вопрос 33. Язык художественной литературы. Основные функции. Содержательность языкового уровня художественной формы. Речь художественная.
  10. Глава 34.Виртуальные корпорации
  11. Гликозаминогликаны и протеогликаны. Строение и функции. Роль гиалуроновой кислоты в организации межклеточного матрикса.


Последнее изменение этой страницы: 2016-07-14; Просмотров: 702; Нарушение авторского права страницы


lektsia.com 2007 - 2024 год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! (0.068 с.)
Главная | Случайная страница | Обратная связь