Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

книги / Объектно-ориентированное программирование

..pdf
Скачиваний:
10
Добавлен:
12.11.2023
Размер:
16.61 Mб
Скачать

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

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

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

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

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

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

#;include <iostream.h> #include <conio.h> class A

{ int a; public:

voidshowO {соШ«"Содержимое полей объекта"« en d l;p rin t();} virtual voidprintfvoid) II описание виртуальности функции

{ cou t«a«en dl;}

A(intv):a(v){} }; class B: public A

{ int b; public:

voidprintfvoid) И первый аспект виртуальной функции

{ cou t«b«en dl;}

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

};

class C: public В { int c;

public:

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

{ cou t«c«en dl;}

131

3. Средства ООП в Borland C+ + 3.1

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

};

void mainO { clrscrO;

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

ВЬЬ(10,100); С сс(10,100,1000);

cout«

"

Результаты работы: "« en d l;

c o u t«

"Явный вызов полиморфной функцииprint(): "« en d l;

aa.print();

// выводит: 10

bb.printO;

//выводит: 100

cc.printO;

//выводит: 1000

getchO;

 

 

c o u t«

" Неявный вызов полиморфной функцииprintf): "« en d l;

aa.showQ;

//выводит: 10

bb.showQ;

//выводит: 100

cc.showQ;

//выводит: 1000

getchO;

соШ «"Вызов функцииprintf) по указателю"« en d l; ра=&аа; pa->print(); //выводит: 10

pa=&bb; pa->print(); //выводит: 100 ра=&сс; pa->print(); //выводит: 1000 getchO;

}

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

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

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

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

132

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

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

полиморфным.

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

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

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

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

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

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

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

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

Поля:

int * pnum Методы: print

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

133

3. Средства ООП в Borland C+ + 3.1

Mnclude <string.h> #include <stdio.h> #include <stdlib.h> itinclude <iostream.h>

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

{public:

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

TpoleOO ~TpoleOO

};

class Tnumpole.public Tpole {private: int *pnum;

public:

Tnumpole(int n) {pnum-new int; *pnum=n;} ~Tnumpole(void){delete pnum;}

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

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

};

class Tstrpole.public Tpole { private: char *str; public:

Tstrpole(char *st); ~Tstrpole(void){delete str;}

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

};

inline Tstrpole::Tstrpole(char *st)

{in ti; l=strlen(st); str=new char[l+lj; strcpy(str,st); } void mainO

{ int n,i; char st[80J;

Tpole *a[10J; // массив указателей на базовый класс Tpole for(i=0;i<10;i++)

{ if (i%2)

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

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

else

{printf("\neeedume строку <80 байт: "); scanf("%s",st); a[ij=new Tstrpole(st);} 11Указатель на объект класса Tstrpole

}

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

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

)

134

3.5. Дружественные функции и классы

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

3.5. Дружественные функции и классы

Как было отмечено ранее (раздел 3.1), механизм управления доступом позволяет вы делить внутренние (private), защ ищ енные (protected) и общедоступные (public) компоненты классов. Причем внутренние компоненты локализованы в классе и не доступны извне, а защищенные доступны только компонентным функциям класса и его наследникам. Такое ограничение на доступ к внутренним и защищенным компонентам класса может оказаться неоправданно строгим. Оно может сущ ественно сужать возможности наследования других классов от данного и сокращать количество вариантов его использования.

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

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

Пример 3.22. Внешняя дружественная функция.

#,include <iostream.h> Mnclude <conio.h> class FIXED {private: int a;

public: FIXED(int v):a(v){} friend void show(FIXED);

};

void showfFIXED Par)

{ cout«P ar.a«endl; II функция имеет доступ к внутреннему полю а

Раг.а+=10;

cout«P ar.a«endl; }

135

3. Средства ООП в Borland C++ 3.1

void main() { clrscrQ;

FIXED aa(25);

cout« "результаты работы: "« en d l; show(aa); // выводит: 25 35

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

Пример 3.23. Дружественная функция - компонент другого класса.

#include <iostream.h> #include <conio.h>

class FLOAT; / / объявление класса без его определения class FIXED

{private: int a; public:

FIXED(int v):a(v){} double Add(FLOAT);

voidprint(){cout « a « endl;}

};

class FLOAT {private: double b; public:

FLOAT(double val){b=val;} voidprint() {cout « b « endl;}

friend double FIXED::Add(FLOAT); I* компонентная функция класса FIXED объявляется дружественной классу FLOAT */

};

double FIXED::Add(FLOATPar)

{ return a+Par.b;} I* функция получает доступ к внутреннему полю класса FLOAT */

void mainQ { clrscrO;

FIXED aa(25); FLOATbb(45);

cout« "результаты работы: "« en d l; cout«aa.A dd(bb)«endl\ // выводит: 70

aa.print();

II выводит: 25

bb.printO;

И выводит: 45

getch();

 

}

 

136

3.5. Дружественные функции и классы

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

Так как дружественная функция не является компонентом класса, на нее не распространяется действие спецификаторов доступа (public, protected, pri­ vate). М есто размещ ения прототипа дружественной функции внутри определения класса безразлично. Права доступа дружественной функции не изменяются и не зависят от спецификаторов доступа.

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

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

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

friend class <имя класса >.

Пример 3.24. Объявление дружественного класса.

^include <iostream.h> Mnclude <conio.h>

class SHOW; II объявление класса без его определения class PAIR

{private: char *Head, *Tail; public:

PAIR(char *one,char *two):Head(one),Tail(two){}

friend class SHOW; II объявление дружественного класса

};

class SHOW /*всем функциям класса SHOW доступны внутренние поля класса PAIR*/

{private: PAIR Twins; public:

SHOW(char *one,char 'ktwo):Twins(one,two)Q void HeadO{cout «Twins.PAIR::Head « endl;} void Tatt(){cout «Twins.PAIR::Tail« endl;}

};

void mainO { clrscrf);

SHOW aa("HELLO", "FRIEND");

137

 

3. Средства ООП в Borland C++ 3.1

cout« "результаты работы: "« en d l;

aa.Head();

// выводит: HELLO

аа. Tail();

// выводит: FRIEND

getchQ;}

 

3.6. П ереоп ределен и е о п ер ац и й

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

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

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

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

Общая форма определения функции-оператора приведена в табл. 3.2.

Таблица 3.2. Формы описания функции-оператора

Простая функция

Компонентная функция

Одноместная

Одноместная

<тип pe3ym>TaTa>operator @

<тип результата>орега1ог @ ()

(аргумент)

Двуместная

Двуместная

<тип результата>орега!ог @

<тип результата>орега!ог @ (арг2)

(<аргуменг1>, <аргумент2>)

 

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

При переопределении операций следует помнить, что

-нельзя переопределять операции * , s i z e o f , c l a s s : : ;

-операции =,[],() можно переопределять только в составе класса;

-переопределенная операция = не наследуется в производных классах;

-нельзя изменять приоритет и ассоциативность операции (порядок выполнения операций одного приоритета).

138

3.6. Переопределение операций

Функция-оператор допускает две формы вызова, которые представлены в табл. 3.3.

Т а б л и ц а 3.3. Формы вызова функции-оператора

Стандартная форма

Операторная форма

Для простой функции

Для простой функции

operator @ (<аргуменг>)

@<аргуменг>

operator @

<аргумент1>@<аргумент2>

(<аргуменг1>,<аргуменг2>)

 

Для компонентной функции

Для компонентной функции

<apryMem».operator@0

@<аргуменг>

<аргуменг1>.operator@

<аргуменг1>@<аргумент2>

(<аргуменг2>)

 

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

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

Пример 3.25. Описания функции-оператора вне класса.

#include <string.h> ^include <iostream.h> #include <conio.h>

class sstrll описание класса строка sstr

 

{private: // скрытые поля класса

 

char *str;

 

int

len;

 

public:

II общедоступные поля класса

 

voidprint(void)

 

{ c o u t« "Содержимое скрытых полей: " « endl;

 

cout<<" строка: "<<str<<endl;

 

c o u t« " длина строки: " «

len « en d l;}

sstr(){};

 

sstrfchar *vs)

 

{ len=strlen(vs); str=new char[len+l];

strcpy(str,vs); }

~sstr(){delete str;}

 

friend

sstr & operator +(sstr &,sstr &); /*переопределение операции «+» -

 

функция описывается вне класса */

};

sstr & operator +(sstr &А, sstr & В) И описание функции-оператора

139

3. Средства ООП в Borland C++ 3.1

{ int l =A.len +B.len; char *s;

s=new char[l+l]; strcpyfs, A.str); strcat(s,B.str);

sstr *ps=new sstr(s);

delete []s;

return *ps; }

 

void mainQ

 

sstr аа("пример использования "); sstr ЬЬ("переопределения операции");

sstr cc=aa+bb+" добавка"; Иоператорная форма вызова cc.printQ;

sstr dd;

dd=operator +(aa,bb); Истандартная форма вызова

dd.print();

getchO;}

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

Пример 3.26. Пример описания компонентной функции-оператора.

^include <string.h> #include <stdio.h> #,include <iostream.h> ^include <conio.h>

II описание класса строка sstr class sstr

{private: II скрытые поля класса char *str;

int len; public:

voidprint(void)

{ c o u t« " содержимое скрытых полей: " « endl; c o u t« " строка: "<<str«endl;

cout< <" длина строки: "<< len«endl;} sstrOO

sstr(char *vs) { len=strlen(vs); str=new charflen+1]; strcpy(str,vs);} ~sstr(){delete str;}

sstr & operator +(sstr &); I* прототип компонентной функции, реализующей бинарную операцию «+»*/

sstr & operator -0; /* прототип компонентной функции, реализующей унарную операцию «-» */

};

140