Начало Решаване на проблеми



страница17/19
Дата20.01.2017
Размер4.54 Mb.
#13105
ТипГлава
1   ...   11   12   13   14   15   16   17   18   19
Глава 8: Обектно-ориентирано програмиране
Обектно-ориентираното програмиране се характеризира с онасле-

дяване и динамично свързване. С++ поддържа онаследяването чрез

извличане на класове - това беше темата на предишната глава.

Динамичното свързване се осъществява чрез виртуалните функции

на клас.

Йерархията на онаследяване дефинира отношения тип-подтип

между типовете класове. Например, Panda е тип Bear, на свой

ред Bear е тип ZooAnimal. По същия начин и сортираният масив,

и масив с проверка на ранга са типове IntArray. Виртуалните

функции дефинират типово зависими операции в йерархията на

онаследяването - например, функцията draw() на ZooAnimal или

subscript оператора на класа масив. Виртуалните функции оси-

гуряват метод за капсулиране на детайли по реализацията на

йерархията на онаследяването от програмите, които ги използват.

В тази глава ще разгледаме подрообно виртуалните функции.

Ще разгледаме също един специален случай на онаследяване на

класове - този на виртуалния (или още споделен) базов клас.

В началото обаче ще преразгледаме презаредимостта на име на

функция с аргумент от тип клас.

8.1 Презаредими функции с аргумент клас


Често една функция се прави презаредима за да се осигури обра-

зец на съществуваща функция за работа с обекти класове. Напри-

мер, ако трябва да се дефинира клас комплексно число, проектантът

трябва след това да дефинира образец на библиотечната функция

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

класа Complex:


extern Complex& sqrt( Complex& );
Първоначалното разглеждане на презаредими функции, което напра-

вихме в параграф 4.3 на страница 155 не включваше съпоставяне

на аргументи от тип клас. Това е предмет на обсъждане на тази

глава.


Сега ще разгледаме точното съпоставяне на аргумент от тип

клас, съпоставянето чрез стандартни преобразования и съпоставя-

нето чрез извикване на дефиниран от потребителя оператор за

преобразуване.


332

--------------

Точно съпоставяне
Обект от клас се съпоставя точно само с формален аргумент от

неговия тип клас. Например,


ff( Bear& );

ff( Panda& );


Panda yingYang;
// exact match : ff( Panda& )

ff( yinYang );


По подобен начин указател към обект от клас се съпоставя точно

само с формален аргумент указател към същия тип клас.

Алгоритъмът за съпоставяне на аргументи не може да прави

разлика между обект и псевдоним от тип клас. Макар че следващите

два образеца декларират две различни функции, реалното обръщение

е двузначно и предизвиква грешка по време на компилация.


// warning : cannot be distinguished

// by the argument matching algorithm

ff( Panda );

ff( Panda& );


// ok : ff( Panda& )

int (*pf)( Panda& ) = ff;


ff( yinYang ); // error : ambiguous

pf( yinYang ); // ok


Стандартни преобразования


Ако класовият аргумент не се съпоставя точно, прави се съпоста-

вяне чрез прилагане на предварително дефинирани стандартни преоб-

разувания.

* Извлечен обект от клас, псевдоним или указател явно се преобра-

зуват в съответния публичен базов тип клас. Например,
ff( ZooAnimal& );

ff( Screen& );


// ff( ZooAnimal& )

ff( yinYang );


* Указател към произволен тип клас явно се преобразува в указател

от тип void*.
333

---------------

ff( Screen& );

ff( void* );


// ff( void* )

ff( yinYang );


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

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

щото обръщение не може да се осъществи съпоставяне:
ff( Bear& );

ff( Panda& );


ZooAnimal za;
ff( za ); // error : no match
Присъствието на два или повече непосредствени базови класове

предизвиква маркирането на обръщението като двузначно. Например,

Panda се извлича едновременно от Bear и Endangered. И двете преобра-

зования на Panda изискват едно и също действие. Тъй като и двете

преобразувания са възможни, обръщението е грешно.
ff( Bear& );

ff( Endangered& );


ff( yinYang ); // error : ambiguous
За да бъде осъществено обръщението, програмистът трябва явно да

укаже в него:


ff( Bear(yinYang));
Извлеченият клас се разглежда като по-близък до неговия непос-

редствен базов клас отколкото до един по-отдалечен базов клас. Следва-

щото обръщение не е двузначно, макар че и в двата образеца се изисква

стандартно преобразуване. Panda се третира по-скоро като вид Bear,

отколкото като вид на ZooAnimal от алгоритъма за съпоставяне на аргу-

менти.


ff( ZooAnimal& );

ff( Bear& );


// ff( Bear& );

ff( yinYang );


Това правило се разширява да включва и void*. Например, дадена

е следната двойка презаредеми функции:


ff( void* );

ff( ZooAnimal* );


Аргументът от тип Panda* се съпоставя с ZooAnimal*.
334

-------------


Дефинирани от потребителя преобразувания


Дефинираното от потребителя преобразуване може да бъде конструктор

с един аргумент или оператор за явно преобразуване. Дефинираните от

потребителя преобразувания се прилагат само ако не може да се

направи точно съпоставяне или не се открие стандартно преобразуване.

За тази част от параграфа нека осигурим ZooAnimal с две потреби-

телски дефинирани преобразувания:


class ZooAnimal {

public:


// conversion : long ==> ZooAnimal

ZooAnimal( long );


// conversion : ZooAnimal ==> char*

operator char*();


// ...
};
Дадена е следната двойка презаредими функции:
ff( ZooAnimal& );

ff( Screen& );


Обръщение с фактически параметър от тип long ще бъде осъществено

чрез образеца на ZooAnimal чрез извличане на потребителски дефи-

нирано преобразувание:
long lval;
// ff( ZooAnimal& )

ff( lval );


Какво ще стане, ако обръщението е с аргумент int? Например,
ff( 1024 ); // ???
Не може да се направи нито точно съпоставяне, нито съпоставяне чрез

стандартни преобразувания. Има винаги едно приложимо потребителски

дефинирано стандартно преобразуване. Проблемът е в това, че конструк-

торът за преобразуване на ZooAnimal очаква стойност от тип long,

а не int.

Алгоритъмът за съпоставяне на аргументи ще приложи стандартно

преобразуване за намирането на приложимо потребителски дефинирано

преобразуване. В случая, 1024 се преобразува да бъде от тип long,

за да се възприеме от конструктора на ZooAnimal. Обръщението се

осъществява чрез образеца на ZooAnimal.

Дефинирано от потребителя преобразуване се прилага само когато

друго преобразуване не е възможно. Ако образецът на ff() беше

деклариран да възприема предварително дефиниран тип, операторът

за преобразуване на ZooAnimal нямаше да бъде викан. Например,


335

---------------

ff( ZooAnimal& );

ff( char );


long lval;
// ff( char );

ff( lval );


В този случай е необходимо явно указване в обръщението за да се

осъществи обръщение към образец на ZooAnimal:


// ff( ZooAnimal& )

ff( ZooAnimal(lval));


В следващия пример се прилага операторът за преобразуване

в char* на ZooAnimal тъй като няма стандартно преобразуване от

обект на базов клас в обект на извлечения клас.
ff( char* );

ff( Bear& );


ZooAnimal za;
// za ==> char*

// ff( char* )

ff( za );
Алгоритъмът за съпоставяне на аргументи ще приложи стан-

дартно преобразуване за постигането на потребителски дефинирано

преобразуване, ако това прави възможно самото съпоставяне. Напри-

мер,
ff( Panda* );

ff( void* );
// za ==> char* ==> void

// ff( void* )

ff( za );
Операторите за преобразуване (но не и конструкторите) се

онаследяват по същия начин като другите членове на класа. И Bear,

и Panda наследяват char* операторът за преобразуване на ZooAnimal.

Например,


ff( char* );

ff( Bear* );


Bear yogi;

Bear *pBear = &yogi;


336

-----------


// ff( char* )

ff( yogi );


// ff( pBear )

ff( pBear );


Ако съпоставянето е възможно чрез прилагането на две или

повече потребителски дефинирани преобразувания, обръщението е

двузначно и предизвиква грешка по време на компилация. Преобразу-

ващите конструктори и операторите за преобразуване имат едно и

също предимство. Ако може да бъде приложен по един образец от

всеки тип, обръщението е двузначно.

Например, нека Endangered дефинира оператор за преобразуване

от тип int:


class Endangered {

public:


// conversion : Endangered ==> int

operator int();


// ...
};

Тогава ако Extinct дефинира преобразуващ конструктор, който възприема

псевдоним на обект от класа Endangered, както следва
class Extinct {

public:


// conversion : Endangered ==> Extinct

Extinct( Endangered& );


// ...
};

Следващото обръщение е двузначно. И операторът за преобразуване

на Endangered и преобразуващият конструктор на Extinct могат да

осъществят съпоставяне.


ff( Extinct& );

ff( int );


Endangered e;
ff( e ); // error : ambiguous
Тук има втори пример на двузначност при извикване на потреби-

телски дефинирано преобразуване. В този случай преобразуващите

конструктори на SmallInt и BitVector са еднакво приложими - обръ-

щението се маркира като грешно.


337

---------------

class SmallInt {

public:


// conversion : int ==> SmallInt

SmallInt( int );


// ...

};

class BitVector {



// conversion : unsigned long ==> BitVector

BitVector( unsigned long );


// ...

};

ff( SmallInt& );



ff( BitVector& );
ff( 1 ); // error : ambiguous

8.2 Виртуални функции


Виртуалната функция е специална член функция викана чрез указател

към публичен базов клас или псевдоним на публичен базов клас; тя

се построява динамично по време на изпълнение. Извикваният образец се

определя чрез типа на класа на актуалния обект, адресиран от ука-

зателя или псевдонима. За потребителя начинът на реализиране на една

виртуална функция е явен.

draw(), например, е виртуална функция с образци в ZooAnimal,

Bear, Panda, Cat и Leopard. Функция, викаща draw(), може да бъде

дефинирана по следния начин:
inline void draw( ZooAnimal& z )

{

z.draw();



}
Ако един аргумент към нечлен образец на draw() адресира обект от

класа Panda, операторът z.draw() ще извика Panda::draw(). Един

следващ аргумент, адресиращ обект от класа Cat ще предизвика

обръщение към Cat::draw(). Компилаторът решава кои членове функции

на класа да извика според на типа на класа на действителния обект.

Преди да разгледаме как се декларира и използва виртуална

функция, нека накратко видим защо искаме да използваме виртуални

функции.
338

-----------

Динамичното построяване е форма на капсулиране


Крайният екран на реализацията ZooAnimal представя множество от

животни, за които посетителят е искал информация. Този екран,

за удоволствие на децата, е направил от дисплейния терминал една

атракция.

За реализирането на множеството се поддържа свързан списък

от указатели към животни, за които посетителят ще бъде уведомя-

ван. Когато се натисне клавиша QUIT, главата на свързания списък

от ZooAnimal се предава на finalCollage(), която показва жи-

вотните в подходящ размер и вид на екрана.

Поддържането на свързания лист е просто, тъй като указател

към ZooAnimal може да адресира всеки публично извлечен клас.

С динамичното построяване не е сложно също да се определи извле-

чен клас, адресиран от указател към ZooAnimal. finalCollage()

може да бъде реализирана по следния начин:


void finalCollage( ZooAnimal *pz ) {

for ( ZooAnimal *p = pz; p ; p = p->next )

p->draw();

}
В език, в който този проблем не е разрешен по време на изпъл-

нение, остава грижа на програмиста да определи извлечен клас, адреси-

ран от указател към ZooAnimal. Обикновено, това довежда до иденти-

фициране на члена на клас isA() и операторите if-else или switch,

които проверяват стойностите на isA(). Без динамично построение

finalCollage може да бъде реализирана по следния начин:
// nonobject-oriented implementation

void finalCollage( ZooAnimal *pz ) {

for ( ZooAnimal *p = pz; p ; p = p->next )

switch ( p->isA() ) {

case BEAR:

((Bear *) p)->draw();

break;

case PANDA:



((Panda *) p)->draw();

break;


// ... every other derived class

} // switch of isA

}
Програмите, написани в този стил, са основани върху детайли в реа-

лизацията на йерархията на извличането. Ако тези детайли се проме-

нят, работата на програмата може да бъъде нарушена и да се наложи

да се разшири кода на програмата.

339

-----------------


След като пандите напуснат зоопарка и си отидат в Китай,

типът клас Panda може да се изтрие. Когато пристигнат коали от

Австралия, трябва да бъде прибавен тип клас Koala. За всяка промяна

в йерархията всеки оператор if-else и switch, който проверява типа

на класа, трябва да бъде променен. Програмният код се променя със

всяка промяна на йерархията.

Всеки оператор switch увеличава забележимо обема на програмния

код. Концептуално прости по своята същност действия се усложняват

поради условните тестове, необходими за проверка на типа клас на

даден обект. Програмите стават трудни за разчитане.

Потребителите на йерархията и нейната реализация, желаещи да

разширят йерархията или да използват приложенията й, трябва да имат

достъп до програмния код. Това прави поддържането на системата доста

по-трудно и за разпространителя, и за потребителя на системата.

Ако проблемът е разрешен по време на изпълнение, това скрива от

потребителя детайлите по реализацията на йерархията на извличането.

Условните проверки на типа клас вече не са необходими. Това опростява

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

телският код, който вече не се променя със всяка промяна на йерар-

хията, е по-лесен за програмиране и поддържане.

На свой ред това опростява разширяването на йерархията.

Прибавянето на ново извличане от ZooAnimal не изисква промяна в

съществуващия код. Главната функция draw() не се интересува от

бъдещите извличания от ZooAnimal. Нейният код остава функционален

независимо от това в каква степен йерархията е била променена,

което означава, че реализацията с изключение на header-файловете

може да бъде разпространена в двоичен вид.

Опростява се също използването на системата. Тъй като и

реализацията на типовете класове, и реализацията на йерархията

на класовете са капсулирани, те могат да бъдат променяни с

минимални добавки в кода на клиента като остава непроменен публич-

ният интерфейс.


Дефиниция на виртуални функции


Една виртуална функция се определя като се прибави в началото на

декларацията на функцията ключовата дума virtual. Само функции,

членове на клас, могат да бъдат декларирани като виртуални. Клю-

човата дума virtual може да се среща само в тялото на клас.

Например,
class Foo {

public:


virtual bar(); // virtual declaration

};
int Foo::bar() { ... }


340

-------------

Следващата опростена декларация на ZooAnimal декларира четири

виртуални функции: debug(), locate(), draw() и isOnDisplay().


#include

class ZooAnimal {

public:

ZooAnimal( char *whatIs = "ZooAnimal")



: isA( whatIs ) {}

void isA() { cout << "\n\t" << isa << "\n"; }

void setOpen( int status ) { isOpen = status; }

virtual isOnDisplay() { return isOpen; }

virtual void debug();

virtual void draw() = 0;

protected:

virtual void locate() = 0;

char *isa;

char isOpen;

};

void ZooAnimal::debug() {



isA();

cout << "\tisOpen:"



<< ((isOnDisplay()) ? "yes" : "no") << "\n";

}

debug(), locate(), draw() и isOnDisplay() са декларирани като



член функции на ZooAnimal защото представляват множество функции,

общи за цялата класова йерархия. Те са декларирани като виртуални,

защото има детайли на реализацията, които зависят от типа на класа

и са първоначално неуточнени. Виртуалната функция на базовия клас

служи като място, където се съхраняват все още неопределените типо-

ве класове.

Виртуална функция, дефинирана в базовия клас на йерархията,

често въобще не се вика, например locate() и draw(). Нито има

някакъв смисъл в абстрактен клас като ZooAnimal. Проектантът на

класовете може да определи, че една виртуална функция не е дефи-

нирана в абстрактен клас като инициализира с 0 нейната деклара-

ция.
virtual void draw() = 0;

virtual void locate() = 0;
draw() и locate() се наричат чисто виртуални функции. Един

клас с една или повече чисто виртуални функции може да бъде из-

ползван само като базов клас за следващи извличания. Не е правилно

създаването на обекти от клас, съдържащ чисто виртуални функции.

Например, следващите две дефиниции на ZooAnimal предизвикват

грешка по време на компилация:


341

---------------

ZooAnimal *pz = new ZooAnimal; // error

ZooAnimal za; // error


Само абстрактен клас, за който не се предвижда да има собствени

образци, може да декларира чисто виртуална функция.

Класът, който първи декларира една функция като виртуална,

трябва също да я декларира като чисто виртуална функция или да

осигури дефиниция.

* Ако е осигурена дефиниция, тя служи като образец по премъл-

чаване ,ако извличаният клас не осигури свой образец на

виртуалната функция.

* Ако е декларирана чисто виртуална функция, извлеченият клас

трябва или да дефинира образец на функцията, или да я декла-

рира отново като чисто виртуална функция. +

Например, Bear трябва или да осигури дефиниции за draw() и locate(),

или да ги декларира отново като чисто виртуални функции.

Какво да правим, обаче, ако възнамеряваме да декларираме

обекти от типа клас Bear, но все още желаем да отсрочим реализа-

цията на draw() докато не бъдат извлечени отделни видове като

Panda или Grizzly ? Не можем да декларираме draw() като чисто

виртуална функция и да продължаваме да дефинираме обекти от кла-

са. Ето три алтернативни решения на въпроса:

1. Дефинираме празен образец на виртуална функция:


class Bear : public ZooAnimal {

public:


void draw() {}

// ...


};

2. Дефинираме образец, обръщението към който предизвиква вътрешна

грешка:
void Bear::draw() {

error( INTERNAL, isa, "draw()" );

}

3. Дефинираме образец, който да следи за неочаквано поведение при



начертаването на родовия образ на Bear. Тоест, системата продъл-

жава работата си, но същевременно се събира информация за изклю-

ченията по време на изпълнение, които трябва да бъдат обработени

по-нататък.

Извлечен клас може да осигурява свой собствен образец на вир-

туална функция или по премълчаване да онаследява образеца на базо-

вия клас. На свой ред, той може да въведе своя собствена виртуална

функция. Например, Bear предефинира debug(), locate() и draw(); той

онаследява образеца на isOnDisplay от ZooAnimal. В допълнение Bear

дефинира две нови виртуални функции hibernates() и feedingHours().

Дефиницията на Bear е опростена с цел да се подчертае значение-

то на виртуалните функции.


class Bear : public ZooAnimal {

public:

Bear( char *whatIs = "Bear" )



: ZooAnimal( whatIs ), feedTime( "2:30" )

{} // intentionally null

void draw(); // replaces ZooAnimal::draw

void locate(); // replaces ZooAnimal::locate

virtual char *feedingHours()

{ return feedime; }

protectted:

void debug(); // replaces ZooAnimal::debug

virtual hibernates() { return 1; }

char *feedTime;

};

Предефинирането на една виртуална функция в извлечен клас

трябва точно да съответства на името, сигнатурата и типа на връ-

щане на образеца на базовия клас. Ключовата дума virtual не е

нужно да бъде специфицирана (макар, че би могла да бъде при жела-

ние на потребителя). Дефиницията е като на обикновена член функция.


void Bear::debug()

{

isA();



cout << "\tfeedTime:"

<< feedingHours() << "\n";

}
void Bear::draw() {/*...code goes here*/}

void Bear::locate() {/*...code goes here*/}

Целият виртуален механизъм всъщност се осъществява от компилатора.

Проектантът на класовете само трябва да зададе ключовата дума

virtual при пъървоначалната дефиниция на всеки образец.

Ако повторната декларация в извлечения клас не се съпоставя

точно, функцията не се третира като виртуална за извлечения клас.

Например, ако Bear декларира debug() по един от следните начини:
343

---------------


// different return type

void *Bear::debug() {...}


// different signature

void Bear::debug( int ) {...}


debug() няма да бъде виртуална за класа Bear. Например,
Bear b;

ZooAnimal &za = b;

za.debug(); // invoke ZooAnimal::debug()

Един извлечен след това клас от Bear, обаче, все още може

да осигурява виртуален образец на debug(), дори Bear да не може.

Например,


class Panda : public Bear {

public:


void debug(); // virtual instance

// ...


};
Образецът на Panda е също виртуален поради точното съпоставяне с

виртуалната декларация на debug():


Panda p;

ZooAnimal &za = p;

za.debug(); // Panda::debug()

Забележете, че нивата на защита за две от виртуалните функции

са различни за образеца на базовия клас и образеца на извлечения

клас. Образецът на locate() в ZooAnimal е protected, докато образе-

цът в Bear е public. Аналогично, образецът на debug() в ZooAnimal

е public, докато образецът в Bear е protected.

Какви са всъщност нивата на защита на locate() и debug()?

Например, целта ни е да напишем такива общи функции като:


void debug( ZooAnimal& z )

{

// compiler resolves intended instance



z.debug();

}
Тъй като Bear::debug() е protected, верни ли са следните обръщения?


344

------------


main()

{

// outputs : Bear



// feedTime : 2.30

Bear ursus;

debug( ursus );

}
Отговорът е не: debug(ursus) не е вярно. Нивото на достъп на виртуал-


Каталог: files -> tu files
tu files -> Увод в компютърната графика
tu files -> Xii. Защита и безопасност на ос
tu files -> Електрически апарати
tu files -> Средства за описание на синтаксиса
tu files -> Stratofortress
tu files -> Писане на скриптове за bash шел : версия 2
tu files -> 6Технологии на компютърната графика 1Модели на изображението
tu files -> Z=f(x), където x- входни данни; z
tu files -> Body name библиотека global Matrix imports (достъп по име) … var m[N, N] := … end decl., proc … resource f final code imports node, Matrix end name var x: node node; if x … Matrix m[3,4] :=: … end


Сподели с приятели:
1   ...   11   12   13   14   15   16   17   18   19




©obuch.info 2024
отнасят до администрацията

    Начална страница