Concepte avansate ale OOP
4.1. Mostenirea; ierarhii de clase
4.1.1. Consideratii generale
Unul dintre modurile cele mai naturale de organizare a lumii il constituie clasificarea. Ratiunea umana este astfel organizata incit toata experienta capatata se stratifica in organizari de obiecte inconjuratoare si fiecare nou obiect detectat se incearca a fi inclus intr-o ierarhie cunoscuta.
Si limbajele OOP permit astfel de relatii "de clasificare" intre obiecte: un obiect poate fi un "descendent" (mostenitor, rafinament) al unui alt obiect "parinte". Aceasta relatie intre doua obiecte se numeste "mostenire".
Un descendent mosteneste de la parinte toate atributele acestuia (structurile de date) si toata comportarea (metodele). Din "afara" acestea sint ca si cum ar fi definite chiar de catre acesta.
Exemplu
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
┌──────────▀ miscare ▀
╔═══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;╗ │ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
║ ║ │ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
║ ANIMAL ╟────┘ ┌─────▄ blana ▄
║ ║ │ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
╚═══ 555e49f 9552;══ 555e49f 9574;═══ 555e49f 9552;══ 555e49f 9565; │ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
║ ├─────▄ singe_cald ▄
╔═══ 555e49f 9552;══ 555e49f 9577;═══ 555e49f 9552;══ 555e49f 9559; │ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
║ ║ │ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
║ MAMIFER ╟─────────┴─────▄ etc... ▄
║ ║ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
╚═══ 555e49f 9552;══ 555e49f 9574;═══ 555e49f 9552;══ 555e49f 9565;
║
╔═══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9577;═══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9559;
╔═══ 555e49f 9577;═══ 555e49f 9552;╗ ╔═══ 555e49f 9552;═╩═══ 555e49f 9552;═╗ ** ** ***
║ iepure ║ ║ vulpe ╟──────* cauta iepuri *
╚═══ 555e49f 9572;═══ 555e49f 9552;╝ ╚═══ 555e49f 9552;═╤═══ 555e49f 9552;═╝ ** ** ***
└──────────┐ └────┐
┌──────┴──────┐ ┌─────┴──────┐ ** **
│ iepure 1 │ │ vulpe 1 │ ┌──*cauta iepuri*
│ │ │ ├──┤ ** **
│ "Bugs Bunny"│ │ "FRED" │ │
└──────┬──────┘ └────────────┘ │ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄
│ ├──▄ miscare = ▄
│ │ ▄ "furis" ▄
└──── etc... │ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄
│ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄
│ ▄ blana = ▄
├──▄ "rosu" ▄
│ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄
│ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄
│ ▄singe_cald= ▄
└──▄ "true" ▄
▄▄▄▄▄▄▄▄▄▄▄▄▄▄
Se observa ca atributul "miscarea" este al obiectului "animal", dar prin mostenire si al obiectelor "mamifer", "iepure" "vulpe". In acelasi mod, cele trei atribute ale obiectului "mamifer" (blana, singe_cald, localizare) sint mostenite si de obiectul "iepure" si "vulpe".
Sint patru obiecte: "animal", "mamifer", "iepure", "vulpe". Fiecare din acestea este o CLASA (un model, sablon). Ele descriu caracteristicile generale ale orcarui "animal", "mamifer", etc. Daca se doreste crearea de obiecte specifice, ce se refera la anumite obiecte particulare, atunci, prin atribuirea de valori caracteristicilor comune se creaza instante ale claselor respective (Fred, Bugs, etc).
Deci mostenirea este proprietatea obiectelor de a putea fi construite pe baza unor alte obiecte deja constituite.
Mostenirea este o unealta pentru a organiza, construi si folosi clase deja create. Fara mostenire clasele ar fi unitati independente, fiecare dezvoltindu-se de la zero.
Mostenirea face posibila definirea software-ului in aceeasi maniera in care este prezentat un concept nou cuiva care nu-l cunoaste: "O zebra este un cal cu dungi".
Mostenirea leaga conceptele intr-un tot unitar, astfel incit modificarea conceptului de nivel inalt implica modificarea in mod automat a intregii ierarhii ce priveste acel concept; pentru exemplul precedent: "Daca un cal maninca iarba atunci si zebra maninca iarba".
Odata cu proprietatea de mostenire apare si posibilitatea crearii ierarhiilor de obiecte. De regula, un obiect de "nivel" mai ridicat in ierarhie este mai abstract, in timp ce obiectele de nivel coborit sint mai specifice. In definirea ierarhiilor de obiecte este foarte important sa se "incapsuleze" toate atributele (date+cod) ce sint comune mai multor obiecte diferite intr-unul singur si toate celelalte sa-l mosteneasca.
Motivul pentru care se concep aceste obiecte (fara a fi neparat nevoie de ele) este acela al modificarilor si/sau extensiilor ulterioare definirii. Modificarea a ceea ce este comun nu se face la fiecare obiect, ci numai la unul singur. Daca mai multe obiecte au ceva in comun nu se adauga la fiecare ci numai la unul singur.
Un astfel de obiect ce incapsuleaza atribute comune, conceput numai pentru a le oferi descendentilor se numeste obiect abstract sau container.
Mostenirea ofera o simplificare enorma si pentru ca reduce numarul lucrurilor ce trebuie specificate si retinute. Aceasta se face datorita interdependentei.
Obiectele ce sint dezvoltate independent foarte frecvent sint inconsistnte, lasind multe amanunte pentru a fi retinute.
Mostenirea reduce aceste legaturi intre obiecte reducind implicit suprafata de interfata (se creste astfel consistenta), dar totodata reducind codul ce trebuie scris.
In cele mai multe limbaje OOP, mostenirea ce este permisa este "liniara" (un fiu are numai un tata): Pascal. Aceasta nu este intotdeauna suficienta: "Un tractor-jucarie ce este ? Tractor sau jucarie ?". Astfel incit au aparut imbunatatiri si extinderi ale acestui concept, permitindu-se mostenirea multipla.
In C++ mostenirea se obtine prin derivarea unei/unor clase din alta clasa. O clasa folosita pentru a deriva alta clasa se numeste in C++ clasa de baza (mai exista si denumirile: clasa parinte sau superclasa). O clasa creata din alta clasa se numeste clasa derivata (sau subclasa).
Cele mai importante utilizari ale mostenirii sint:
* Crearea codului in mod incremental (in OOP a "obiectelor": clase de obiecte si ierarhii), plecind de la alte obiecte, folosind (partajind) atit structuri de date cit si cod;
* Crearea unor algoritmi generali ce pot fi scrisi pentru un tip de date(obiecte) dar pot fi refolositi pentru tipuri NOI, chiar si DUPA compilare!
4.1.2. Reguli generale pentru derivarea claselor
Sintaxa pentru a exprima in C++ ca o clasa (clasa derivata) mosteneste o alta clasa (clasa de baza) este urmatoarea:
<spec_clasa> <nume_clasa> : [modif_acces] nume_clasa_baza_1 [,...]
semnifica aparitia optionala a elementului cuprins intre paranteze
semnifica posibila repetitie a elementului prec.
spec_clasa = poate fi "class" sau "struct" (nu se pot deriva clase noi din "union")
modif_acces = poate avea "valorile" (cuvintele cheie) "public" sau "protected"; daca nu apare, valoarea sa implicita se determina in functie de "valoarea" lui <spec_clasa>
cuprind lista membrilor "suplimentari"
Exemplu
Se definesc doua clase pentru reprezentarea grafica a elementelor geometrice "punct" si "cerc".
class point ;
class circle ;
Definindu-le in aceasta maniera se elimina legatura "intrinseca" ce exista intre cele doua tipuri de figuri geometrice (un cerc este un punct mai "mare") si se rescrie pentru "circle" TOT ceea ce era DEJA scris pentru "point". Avind mostenirea ca tehnica de proiectare a software-ului, cele doua clase se pot defini:
class point
class circle : public point
Astfel clasa "circle" nu defineste acum decit membri (variabile sau functii!) care sint "suplimentari" fata de "point", primind in urma mostenirii TOT ceea ce are si "point" (clasa de baza).
Elementele componente ale declaratiei clasei "circle" pot fi identificate in figura urmatoare:
┌───────────────────────────── numele clasei derivate
┌─ class circle : public point ───── numele clasei de baza
│ │
└──────│──── operator de combinare al claselor
;
└───── <spec_clasa> (cuvint cheie)
Grafic cele doua clase pot fi reprezentate astfel:
class circle
╔═══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;╗
║ class point ║
║ │ float x, y; │ ║
║ │ set_loc(); │ ║
║ float radius; ║
║ set_size(); ║
╚═══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;╝
De remarcat ca o clasa derivata poate modifica membrii mosteniti de la alta clasa: poate adauga membri (date si functii) si poate suprapune functiile clasei de baza. Dar NU poate "elimina" nimic din clasa de baza.
Din acest exemplu se poate deduce una din facilitatile cele mai puternice ale OOP: in locul reproiectarii (in mare parte prin copiere) a unui program, la fiecare modificare (adaugare) se foloseste ceea ce este deja scris si se adapteaza noilor cerinte.
Aceasta trasatura realmente noua a proiectarii software se numeste EXTENSIBILITATE si se deosebeste de "refolosire".
La prima vedere clasele pot sa ofere
"un pic mai mult" decit structurile tip "record" din Pascal
(sau
Diferenta fundamentala o constituie mostenirea: se poate refolosi codul existent dar, mai important, se poate si extinde in pasi incrementali.
4.1.3. Accesarea si referirea membrilor clasei de baza
Regula fundamentala a posibilitatilor de acces la membrii clasei de baza se poate exprima astfel:
"intr-o clasa derivata accesul la membrii clasei de baza poate fi facut MAI restrictiv, dar niciodata nu poate fi facut mai putin restrictiv !"
O clasa derivata mosteneste toti membrii clasei de baza dar poate FOLOSI numai membrii "protected" si "public" ! Membrii privati ai clasei de baza nu pot fi disponibili direct; ei nu pot fi mosteniti mai departe la urmatorul nivel din ierarhie, in timp ce membrii "public" si "protected" se pot mosteni (utiliza).
Trebuie avut in vedere insa si "modificatorul de acces" declarat cu clasa de baza:
private = "ingheata" clasa de baza la nivelul clasei derivate; in continuare in ierarhie nu ar mai fi accesibili nici unul din membrii clasei de baza;
public = permite accesarea in "josul ierarhiei" a membrilor "private" si "public";
Exemplu
class Base
void Print (void)
class Derived : public Base
class Derived2 : private Base ;
void main(void)
Se observa ca pentru accesarea unui membru mostenit nu este nevoie de calificator si numele clasei de baza.
Membrii "protected" ai unei clase de baza sint tratati ca publici in cadrul claselor derivate si privati in afara acestora.
Exemplu
class Base
void Print(void)
class Derived1 : public Base
void main(void)
Accesul la membrii unei clase de baza dintr-o clasa mostenita si din afara acesteia este sintetizat de tabelul urmator:
╔═══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;═╤═══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9572;═══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9572;═══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;═╗
║ Accesul in │Modificator │Acces mostenit IN │ Accesare IN ║
║clasa de baza │ de acces │ CLASA derivata │ AFARA CLASEI ║
│ │ │ derivate ║
╠═══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;═╪═══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9578;═══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9578;═══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;═╣
║ public │ public │ public │ ACCESIBIL ║
║ protected │ public │ protected │ INACCESIBIL ║
║ private │ public │ inaccesibil │ INACCESIBIL ║
║ public │ private │ private │ INACCESIBIL ║
║ protected │ private │ private │ INACCESIBIL ║
║ private │ private │ inaccesibil │ INACCESIBIL ║
╚═══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;═╧═══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9575;═══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9575;═══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;══ 555e49f 9552;═╝
4.1.4. Ascunderea datelor in ierarhiile de clase
Prin derivarea unei clase folosind modificatorul de acces "private" in clasa derivata se "opreste" accesul la toti membrii clasei de baza ascunzindu-i pentru restul ierarhiei.
Prin derivarea unei clase folosind modificatorul de acces "public" toti membrii publici si protected ai clasei de baza devin accesibili in ierarhie (la urmatoarele nivele): nu mai sint ascunsi.
Daca cele 2 "extreme" provoaca, pe de o parte o limitare a extensibilitatii, iar pe de alta parte violarea conceptelor de incapsulare, se poate alege o modalitate intermediara.
Se foloseste modificatorul de acces "private" (sau prin folosirea cuvintului cheie "class" pentru stabilirea modificatorului de acces se poate omite), iar in sectiunea "public" a clasei derivate se introduc doar acei membri din clasa de baza ce se doresc a fi vizibili in ierarhie, pe urmatoarele nivele, precedati de numele clasei si operatorul "::" de apartenenta la o clasa.
Exemplu
class Derived : private Base ;
class square ;
class box3d : square ;
Daca diferenta dintre membrii "protected" si "public" nu exista in clasele derivate (si se poate pune problema utilitatii acestor doua nivele de ascundere a informatiei), in afara clasei diferenta este fundamentala. Pentru respectarea conceptelor cheie OOP de ascundere a datelor si incapsulare, ORICE membru accesibil DOAR prin mostenire (si doar PENTRU clase derivate) trebuie ascuns pentru exterior, prin declararea lui ca facind parte din sectiunea "protected" !
Acelasi mecanism de acces se foloseste si in cazul in care o clasa derivata redefineste un membru al clasei de baza; acest procedeu se numeste "overriding" (suprascriere).
Pentru o clasa derivata referirea unui nume "suprascris" (cu mai multe definitii - in clasa de baza si in cea derivata) se "traduce" implicit prin referirea membrului din clasa derivata.
Pentru a accesa membrul din clasa de baza, numele acestuia trebuie prefixat de numele clasei de baza si operatorul "::".
Exemplu
class Base
class Derived : public Base //adica Derived::i
void PrintTotal(void)
4.1.5. Constructori si destructori in ierarhii de clase
Spre deosebire de ceilalti membri ai unei clase de baza, constructorii si destructorii nu sint mosteniti!
In momentul crearii unui obiect dintr-o clasa derivata, deci in momentul in care se apeleaza constructorul clasei derivate, acesta va apela automat, mai intii un constructor al clasei de baza pentru a asigura ca membri-date mosteniti au fost creati si initializati. Daca insasi clasa de baza este derivata procesul continua "recursiv" spre nivelele superioare ale ierarhiei.
Daca o anumita clasa nu are un constructor definit de programator, se va genera (automat de catre compilator) unul implicit de forma X::X(). Constructorii clasei derivate pot apela si explicit constructorii clasei de baza, folosind regulile sintactice de la obiectele imbricate (cu liste de initializare).
Exemplu
class telefon
class telefon_int : public telefon ;
telefon_int::telefon_int(int q, int n, int i, int c)
: telefon(p, n, i)
Observatii
1. Daca toti constructorii din clasa de baza (si exista cel putin unu) necesita parametri, atunci clasa derivata trebuie sa aiba in mod obligatoriu un constructor (cu parametri) si acesta trebuie sa apeleze explicit unul din constructorii clasei de baza (cu argumentele corespunzatoare).
2. Daca in clasa de baza exista un constructor fara argumente atunci acesta nu trebuie apelat explicit; o va face compilatorul. Aceasta are ca si consecinta posibilitatea ca o clasa derivata sa NU aiba constructori.
Destructorii neavind argumente sint apelati implicit de compilator. Se apeleaza mai intii destructorul clasei derivate. Daca acesta se refera la un obiect ce contine alte obiecte se apeleaza destructorii acestora. Se apeleaza apoi destructorul clasei de baza si daca e cazul si eventualii destructori ai obiectelor "componente".
4.2. Polimorfismul
4.2.1. Regulile de compatibilitate intre clase
Se cunosc regulile de compatibilitate intre tipurile predefinite (din C sau din orice limbaj procedural). Pentru limbajele OOP, si in particular pentru C++, apare in plus urmatoarea
REGULA
O clasa derivata este tratata ca un subtip al clasei sale de baza (din care provine prin derivare).
Rezulta deci ca un obiect al clasei derivate este compatibil la atribuire cu un obiect al clasei de baza. Aceeasi regula este valabila si pentru pointeri si referinte.
Observatii
1. Intr-o astfel de atribuire sint copiati in obiectul destinatie numai membrii "comuni" (definiti in clasa de baza).
2. Atribuirile pot functiona numai intr-o directie. Nu se poate atribui un obiect al unei clase de baza unui obiect al unei clase derivate.
Exemplu
class Location ;
class Point : public Location ;
class Circle : public Point ;
Location ALocation, *PLocation;
Point Apoint, *PPoint;
Circle ACircle, *PCircle;
// Asignari valide:
ALocation=APoint; PLocation=PPoint;
ALocation=ACircle; PLocation=PCircle;
APoint=ACircle; PPoint=PCircle;
Relatia este valabila NUMAI in aceasta directie ! De ce ? Pentru ca un descendent are TOATE partile componente ale unui parinte si, de exemplu, intr-o asignare nu lasa nimic neinitializat ("neacoperit"). Daca s-ar putea folosi si in sens invers ar ramine "gauri" (slot-uri) neinitializate ce sint deosebit de periculoase (date neinitializate).
Datorita acestei reguli de compatibilitate obiectele create dintr-o clasa derivata pot fi folosite in oricare loc in care pot fi folosite obiecte din clasa de baza.
Cea mai importanta utilizare este in cazul parametrilor functiilor.
Exemplu
struct circle
struct cylinder : circle
Fie o functie ce calculeaza lungimea unui cerc:
double circumf (circle* c)
si o functie ce calculeaza volumul unui cilindru:
double volum (cylinder* c)
Si pentru aceste functii se incearca:
circle ring(55);
double v=volum( (cylinder*) &ring); //EROARE
chiar si incercarea de a forta cu "cast" transmiterea de parametri este o eroare (daca s-ar permite cine ar fi ring->h ??)
In schimb:
cylinder piston(3.5,4.0);
double a=circumf(&piston); //PERFECT OK
deoarece se pot folosi toti membri obiectului derivat (ii are!)
Aceleasi observatii sint valabile si in cazul parametrilor referinta:
double circumfr (circle& c)
double volumr (cylinder& c)
circle ringr(3.6);
cylinder pistonr(4.5,5.0);
double c=circumfr(pistonr); //OK
double v=volumr(ringr); //EROARE !!
Folosind aceasta regula rezulta deci ca se poate scrie o functie care sa aiba ca argument un obiect "parinte-al-tuturor", apelul putindu-se face cu ORICE descendent!
Se creaza astfel o functie care sa fie "universal" valabila pentru toti descendentii unei clase (care de obicei poate fi un "container").
Observatie
Pentru folosirea deplina a acestei facilitati trebuie insa cunoscut si aplicat inca un mecanism - functiile virtuale.
4.2.2. Functii si obiecte polimorfice
Se poate crea o comportare generala (sub forma unei metode) care sa fie atasata unui obiect dintr-o ierarhie. Fiecare dintre descendentii obiectului respectiv va mosteni atit structurile de date ale obiectului parinte cit si metodele acestuia, putind astfel folosi metoda respectiva. In plus, daca se doreste, aceasta metoda poate fi modificata (ca si continut) pastrindu-si numele. Astfel, un nume ajunge sa fie "partajat" intr-o ierarhie de obiecte, manifestindu-se specific, insa, pe diferitele "trepte" ale ierarhiei. Acest proces se numeste polimorfism.
Observatie
poli=mai multe ;morfos= fete
Aceasta comportare este de altfel cunoscuta si folosita in limbajele procedurale, obisnuite, fiind insa foarte limitata. De exemplu operatorii aritmetici (+, -, *) se pot aplica diferitelor tipuri de date numerice (intregi, real, subdomeniu), manifestindu-se diferit in functie de operanzi, chiar daca el are acelasi nume. De remarcat ca un operator obisnuit, infixat (+) poate fi interpretat si ca +(a,b). Aceasta posibilitate este consistenta cu modul natural de gindire al omului, dar la limbajele procedurale este foarte limitata si strict impusa de proiectant. Alt exemplu il constituie functiile "print", "write", etc.
In limbajele OOP proprietatea se extinde la oricare obiect (definit de utilizator).
Exemplu
Se dezvolta o aplicatie grafica, utilizindu-se diferite tipuri de imagini. Presupunem ca incepem ierarhia cu un obiect "poligon" caruia ii descriem datele (nr. de laturi, pozitia pe ecran, etc.) si metodele (cum se leaga punctele, calculul ariei, etc.)
// Ierarhia:
class Poligon ;
class Patrat : Poligon ;
class Dreptunghi : Patrat ;
// Implementarea:
void Poligon::Draw (void)
void Patrat::Draw (void)
void Dreptunghi::Draw (void)
Avind descris un poligon se descrie un obiect "dreptunghi" si un altul "patrat". In limbajele conventionale se relua totul de la capat. Intr-un sistem OOP, avind avantajul mostenirii se creaza "dreptunghiul" ca fiind descendent al "poligonului", iar "patratul" ca descendent al "dreptunghiului".
In acest fel "patratul" mosteneste structura de date a "dreptunghiului", iar acesta pe cea a "poligonului".
Poligonul are o metoda ce-i permite trasarea pe ecran (se interpoleaza, de exemplu, sau orice alta metoda).
Aceasta metoda este mostenita de descendenti dar poate fi modificata pentru a materializa caracteristicele fiecaruia, modificindu-se metoda mostenita (eventual prin rescriere).
DAR: numele si (eventual) functia ramin ACELEASI, schimbindu-se numai modalitatea specifica de actiune (in functie de obiectul caruia i se aplica), rezultind astfel un "nume" cu mai multe fete: POLIMORFISM.
4.3. Metode statice si metode virtuale (dinamice)
4.3.1. Consideratii generale
Ne bazam observatiile pentru inceput pe urmatorul exemplu (clasele sint descrise atit cit este necesar scopului urmarit):
// Ierarhia de clase:
class Location
int GetX()
int GetY()
class Point : public Location
void Show();
void Hide();
void MoveTo(int NewX, int NewY);
class Circle : public Point ;
// functiile membru pentru clasa Point
Point::Point(int InitX, int InitY)
: Location (InitX, InitY) // apel constructor clasa de baza
void Point::Show()
void Point::Hide()
void Point::MoveTo(int NewX, int NewY)
// functiile membru pentru clasa Circle
Circle::Circle(int InitX, int InitY, int InitRadius)
: Point (InitX, InitY)
void Circle::Show()
void Circle::Hide()
void Circle::MoveTo(int NewX, int NewY)
Sa observam cele 2 metode "MoveTo" ce produc "mutarea" pe ecran a unui obiect: "Point.MoveTo" si "Circle.MoveTo". Cele doua functii par identice. De ce este nevoie sa copiem inca o data metoda "MoveTo" pentru "Circle" care este obiect derivat din "Point" ? Deoarece aceste metode, declarate in acest mod se numesc STATICE si se manifesta in modul urmator:
Daca nu s-ar fi rescris (chiar si prin aceasta "simpla" copiere) atunci pentru obiectul "circle" ar fi existat o metoda "MoveTo" prin mostenire de la "Point". Dar un apel al acestei metode chiar pentru un obiect "Circle" ar fi mutat... un PUNCT! si nu un CERC.
Explicatia consta in faptul ca aceste referinte (la diverse metode: proceduri, functii) sint statice, adica sint rezolvate in faza de compilare, de catre compilator astfel:
- se intinlesc metodele pentru "Point", sint compilate si primesc adrese de implantare in segmentul de cod.
- cind se compileaza metoda "Point.MoveTo" se rezolva referintele din aceasta metoda folosind adresele metodelor intilnite pina atunci (ale obiectului "Point").
- daca obiectul "Circle" mosteneste metoda "MoveTo" fara a o redefini, atunci are dreptul de a o folosi, dar folosind pentru referire adresele atribuite de compilator in segmentul de cod, acestea fiind cele ce se refera la "Point".
- chiar daca obiectul "Circle" are metodele corespunzatoare "Show" si "Hide" datorita rezolvarii statice (la momentul compilarii) a referintelor acestea nu vor fi folosite.
"Logica" unui compilator este urmatoarea:
- cind se intilneste numele unei metode intr-o referire se cauta o metoda cu acel nume in obiectul respectiv; daca se gaseste se rezolva referinta.
- daca nu se gaseste, atunci compilatorul "urca" pe arborele de obiecte la "parintele" obiectului ce refera acea metoda; daca se gaseste metoda se rezolva referinta, altfel continua urcarea.
Cind o referinta este gasita la un "stramos" este folosita exact cum a fost gasita (definita sau compilata).
Astfel de metode sint STATICE pentru acelasi motiv pentru care si variabilele sint statice: sint alocate de compilator si toate referintele catre el sint rezolvate in timpul compilarii.
Acest dezavantaj se poate indeparta numai daca referintele ar fi rezolvate in timpul rularii si nu la compilare, devenind astfel referinte dinamice.
Aceasta posibilitate este oferita de un mecanism special: "metodele virtuale".
Exemplu
// Ierarhia de clase:
class Location
int GetX()
int GetY()
class Point : public Location
virtual void Show();// ATENTIE !!
virtual void Hide(); // virtual !!
void MoveTo(int NewX, int NewY);
class Circle : public Point ;
// functiile membru pentru clasa Point
Point::Point(int InitX, int InitY)
: Location (InitX, InitY) // apel constructor clasa de baza
void Point::Show()
void Point::Hide()
void Point::MoveTo(int NewX, int NewY)
//functiile membru pentru clasa Circle FARA MoveTo (mostenita)
Circle::Circle(int InitX, int InitY, int InitRadius)
: Point (InitX, InitY)
void Circle::Show()
void Circle::Hide()
Metodele virtuale generalizeaza o unealta (si o caracteristica) deosebit de puternica a sistemului OOP: POLIMORFISMUL, adica se da unei actiuni (metoda) un nume ce este partajat in ierarhie, dar fiecare obiect din ierarhie implementeaza actiunea intr-un mod caracteristic (se generalizeaza obiectul polimorfic).
Exemplu
Fiecare figura "se arata" pe sine insasi, dar ceea ce le deosebeste este felul in care se arata: fiecare are metoda "Show" dar aceasta se implementeaza in moduri diferite. Un nume "Show" este folosit pentru a arata mai multe fete ale aceleiasi operatii.
Cum se mai poate insa rezolva problema "Circle.MoveTo" ?
Se observa ca cele doua metode (Circle.MoveTo si Point.MoveTo) sint identice. S-ar putea amina rezolvarea referintelor in cadrul acestei metode (respectiv la "Show" si "Hide") astfel incit sa nu se faca la compilare, ci mai tirziu. Astfel, MoveTo nu se mai rescrie, rescriindu-se doar "Show" si "Hide". In timpul rularii, referintele la adevaratele "Show" si "Hide" pot fi rezolvate corect, cunoscindu-se ambele "variante", pentru fiecare obiect.
Rezolvarea neambigua a referintelor in timpul compilarii se numeste "legare timpurie" (early binding), iar rezolvarea referintelor in timpul rularii ("run-time") se numeste legare intirziata (late binding).
"Late binding" este caracteristica metodelor virtuale.
Un alt exemplu ce conduce la necesitatea folosirii metodelor virtuale este legat de utilizarea pointerilor (alocarii dinamice de memorie). Nici in acest caz compilatorul nu poate cunoaste (avind in vedere regula de compatibilitate) tipul exact al obiectului catre care indica un pointer.
Exemplu
class sclav
class proletar : public sclav
Aceste doua clase (una fiind derivata din alta) au aceeasi metoda "salut" (metoda polimorfica). Daca se declara doua obiecte distincte:
sclav asterix;
proletar nicolae;
Atunci apelul ACELEIASI metode (acelasi mesaj) pentru fiecare dintre cele doua obiecte va produce rezultate corecte (compilatorul poate face legatura dintre obiect si metoda):
asterix.salut(); // produce:"Da stapine"
nicolae.salut(); // produce:"Hai la lupta cea mare"
Aceleasi rezultate se obtin si daca se declara 2 pointeri DIFERITI pentru fiecare din cele doua clase:
sclav* sclavptr;
proletar* proletarptr;
sclavptr = new sclav;
proletarptr = new proletar;
sclavptr->salut(); //rezultate O.K.
proletarptr->salut();
Dar avind in vedere regula de compatibilitate intre clase putem avea si situatia:
sclav asterix;
proletar nicolae;
sclav* sptr = &nicolae;
sptr->salut(); //!!!
In acest caz compilatorul NU poate face CORECT legatura intre obiect si metoda. Daca cele 2 metode sint statice (suprapuse, totusi statice) atunci rezultatul secventei de program de mai sus este INCORECT! Static se leaga obiectul de baza al pointerului (aici "sclav") cu metoda apelata ("salut") si rezultatul NU va fi: "Hai la lupta cea mare!", ci: "Da, stapine".
Pentru a se apela CORECT "versiunea" de metoda "salut", aceasta "legare" obiect-metoda trebuie realizata la rulare. Aceasta se obtine declarind metoda "sclav" ca fiind virtuala:
class sclav
class proletar
In acest fel pentru declaratiile anterioare:
proletar nicolae;
sclav* sptr = &nicolae;
sptr->salut(); //O.K. acum
stabilirea exacta a metodei apelate se face la rulare in functie de OBIECTUL efectiv catre care indica pointerul!
Aceasta tehnica este deosebit de utila, de exemplu in utilizarea unui numar oarecare de obiecte - de exemplu intr-un tablou.
Restrictia fundamentala este aceea ca elemtele unui tablou nu pot fi de diverse tipuri (clase). Astfel incit, folosind regula de compatibilitate si mecanismul metodelor virtuale se declara un tablou de elemente de baza si in el se introduc orice fel de obiecte derivate. Metodele virtuale sint apelate corect deoarece sint legate la rulare !
Exemplu
sclav* clan[2]; // tablou de pointeri
clan[0] = new scav; // se introduce un sclav
clan[1] = new proletar; //se introduce un proletar
Acum se poate prelucra UNIFORM tablou:
clan[0]->salut();
clan[1]->salut(); //rezultate OK
Grafic aceasta situatie se poate prezenta astfel:
┌───┐
┌────────┐ │ *─┼─────> scav::salut()
┌───────────>│ sclav ├────────> ├───┤
│ ┌────────┐ │ *─┼─────> proletar::salut()
│ ┌──────>│proletar├────────> ├───┤
│ │ Tabela de functii
┌──┼──┬─┼─┬────── VIRTUALE
│ * │ * │ ... (nu statice)
Fig. 18
Tablouri "eterogene"
Observatii
1. Apelul unei functii nonvirtuale (statice) cu un pointer la un obiect va conduce la identificarea functiei de catre tipul obiectului catre care e definit pointerul si nu de catre obiectul efectiv catre care indica acesta.
2. Cuvintul cheie "virtual" este necesar doar in clasa de baza. Clasele derivate ce "suprapun" functia respectiva nu trebuie neaparat sa-l foloseasca. Dar acest lucru este recomandabil !!!
3. In clasele derivate functiile virtuale sint IDENTICE din punct de vedere al argumentelor (numar si tip) si tipului returnat cu functia virtuala din clasa de baza !!
4. Un constructor NU poate fi niciodata virtual! Chiar si pentru motivul ca o clasa derivata pentru a crea o instanta trebuie sa creeze mai intii membrii mosteniti (deci clasa de baza!) si apoi pe cei proprii, deci constructorul clasei de baza trebuie sa fie apelat NEAPARAT.
5. Un constructor NU poate apela metode virtuale!
4.3.2. Implementarea metodelor virtuale; Virtual Method Table
4.3.2.1. Implementarea metodelor statice
Pentru a intelege mecanismul metodelor virtuale si modul in care sint implementate se prezinta pe scurt mecanismul de declarare si apel a unei variabile si functii statice, "uzuale" (membre sau nemembre) folosite de compilator.
Compilatorul creaza pentru orice program cel putin doua sectiuni:
- o sectiune de date: in care se "implementeaza" variabilele utilizate prin rezervarea de spatiu si atribuirea unei adrese (de memorie) pentru fiecare variabila (obiect).
- o sectiune de cod: in care se "translateaza" in cod masina instructiunile din program; in aceasta sectiune se "implementeaza" fiecare functie a unui program (inclusiv main) primind la rindul lor cite o adresa.
Deci, la intilnirea unei declaratii de variabila, compilatorul rezerva spatiu in sectiunea de date incepind cu o anumita adresa (disponibila) si asociaza NUMELE variabilei cu acea adresa. Orice referire ulterioara la variabila respectiva se face prin intermediul acelei adrese.
La intilnirea unei declaratii de functie, compilatorul asociaza cu numele functiei o adresa (din segmentul de cod) disponibila si translateaza instructiunile "sursa" ale functiei in cod masina.
La intilnirea unei referiri de variabila se foloseste adresa asociata numelui variabilei respective (din segmentul de date), in instructiuni de tipul "mov adr,...".
La intilnirea unui apel de functie se va face de fapt un apel la adresa la care a fost "implantata" functia respectiva (si care este "pusa" in corespondenta cu numele functiei).
Deci un obiect, chiar daca va "contine" functii membru, acestea nu sint "implantate IN OBIECT"; apelul lor este determinat de compilator si se inlocuieste cu apelul la adresa asociata functiei obiectului respectiv. Un obiect ce contine numai functii statice "uzuale" este o structura de date "pura" ce este alocata la o anumita adresa, adresa pusa in corespondenta cu NUMELE obiectului.
Exemplu
class ex ;
void ex::f2(int i) int ex::f1(void)
}
ex e1,e2;
e1.f1();
e2.f2(10);
Aceste declaratii si instructiuni au ca efect "aparitia" in segmentul de date si cod a urmatoarelor "zone" (create de catre COMPILATOR):
SEGMENT DE DATE │ SEGMENT DE COD
│
adresa: │ adresa:
│
dadr1─> ┌─────────┐<── e1 │ cadr1──>┌────────────┐<─ ex::f1()
│ a │ │ │ - instr. │
├─────────┤ │ │ - pt. │
│ b │ │ │ - f1 │
└─────────┘ │ └────────────┘
│
dadr2-> ┌─────────┐<── e2 │ cadr2──>┌────────────┐<─ ex::f2()
│ a │ │ │ - instr. │
├─────────┤ │ │ - pt. │
│ b │ │ │ - f2 │
└─────────┘ │ └────────────┘
Iar apelurile la functiile (aici membru) f1 si f2 ale clasei ex pentru obiectele e1 si e2 sint echivalente cu:
e1.f1() <=> call cadr1(dadr1)
e2.f2(10) <=> call cadr2(dadr2,10)
in care "cadr1", "cadr2" sint adresele din segmentul de cod pentru functiile "f1" si respectiv "f2", iar ca prim argument se transmite in fiecare caz adresa din segmentul de date la care a fost implantat obiectul caruia i se transmite mesajul (pointerul "this"). In cadrul obiectului cimpurile acestuia sint calculate ca deplasamente fata de inceputul obiectului (fata de adresa de inceput !).
Pentru apelul e1.f1() in "interiorul" functiei se apeleaza zonele de date dupa "modelul":
dadr11 = adresa e1.a = dadr1
dadr12 = adresa e1.b = dadr1+sizeof(e1.a)
iar instructiunea:
b=a; <=> *dadr12=*dadr11
Deci pentru obiectele ce contin si/sau apeleaza numai functii membru sau nemembru statice (uzuale) TOATE adresele sint cunoscute de compilator si toate referintele (apelurile) sint satisfacute de acesta.
4.3.2.2. Implementarea metodelor virtuale
In cazul in care intr-o clasa apare cel putin o functie virtuala, compilatorul isi schimba "strategia", generind pe linga "zonele" (din segmentul de date si de cod) traditionale si o structura specifica, sub forma unei tabele.
Aceasta tabela este creata de COMPILATOR in segmentul de date si este de fapt un tablou de pointeri (adrese) catre metodele virtuale ale clasei respective.
Pointerii sint determinati la compilare. Mai intii se implementeaza in segmentul de cod TOATE functiile unei clase (virtuale si statice) si apoi se creaza tabela.
In tabela se trec adresele tuturor metodelor virtuale definite de clasa respectiva si a tuturor metodelor virtuale mostenite de clasa (daca exista).
Daca o functie virtuala este redefinita de o clasa in tabela clasei nu se trece decit adresa functiei "NOI" (in locul celei mostenite de la clasa parinte).
Daca se mosteneste o astfel de functie virtuala suprapusa se introduce adresa functiei "celei mai recente" (cea mai apropiata in cadrul ierarhiei de clasa respectiva, pentru care se creaza tabela).
Aceasta tabela se numeste Tabela Functiilor Virtuale (TFV) (Tabela Metodelor Virtuale: TMV sau VFT, VMT).
TFV se creaza intr-un SINGUR exemplar pentru fiecare CLASA! Obiectele instanta NU contin o copie a acestei tabele, ci contin un cimp suplimentar ("invizibil" pentru programator, neadaugat de acesta). Acest cimp suplimentar este ADRESA TFV pentru clasa respectiva.
Exemplu
class baza ;
class deriv : public baza ;
void main (void)
Se obtine figura urmatoare:
TFV pt. baza
dard1──>┌─────────────┐ cadr1 ──>┌───────┐<── baza ::fbstat
┌──────>│fb1() │ │ ... │
│ 0 │ cadr2 *──┼─────────┐ │ ... │
├─────────────┤ │ └───────┘
│ 1 │f2() │ │
│ │ cadr3 *──┼─────┐ │ cadr2 ──>┌───────┐<── baza::fb1
└─────────────┘ │ └─────────>│ ... │
│ │ ┌─────────>│ ... │
│ TFV pt. deriv │ │ └───────┘
│ dard2──>┌─────────────┐ │ │
┌─┼──────>│fb1() │ │ │
│ │ 0 │ cadr2 *──┼───┼───┘ cadr3 ──>┌───────┐<── baza::f2
│ │ ├─────────────┤ └─────────────>│ ... │
│ │ 1 │f2() │ │ ... │
│ │ │ cadr6 *──┼───┐ └───────┘
│ │ 2 │fd1() │ │ cadr4 ──>┌───────┐<── deriv::fdstat
│ │ │ cadr5 *──┼───┼──┐ │ ... │
│ │
│ │ cadr5 ──>┌───────┐<── deriv::fd1
│ └─────────>│ ... │
│ │ dadr3──>┌──────┬───────┐<── b │ ... │
│ │ │ a │ b ││ └───────┘
├──────┴───────┤│
│ │ │ TFV baza ││ cadr6 ──>┌───────┐<── deriv::f2
│ └─────────┼──* dadr1 │└─────────────>│ ... │
└──────────────┘ │ ... │
│ dadr4──>┌───┬────┬─────┐<── d
│ │ a │ b │ c │
├───┴────┴─────┤
│ │ TFV deriv │
└───────────┼──* dadr2 │
Fig. 19
Exemplu de structuri de date+cod generate
Din acest moment apelul unei functii virtuale pentru un anumit obiect NU MAI ESTE facut de catre compilator! Compilatorul va genera cod doar pentru a cauta adresa functiei virtuale apelate in TFV a clasei careia ii apartine obiectul receptor al mesajului. Compilatorul pune in corespondenta numele unei functii virtuale NU cu o adresa de memorie (din segmentul de cod) ci cu un offset (deplasament) in cadrul TFV (numar de ordine al functiei virtuale pentru clasa respectiva).
Observatie
Ordinea in care apar functiile in TFV este "mostenita" de clasa de baza si eventual se extinde cu functiile virtuale proprii (noi). A se vedea figura anterioara.
Apelul unei functii virtuale are acum urmatorul "model" (in doi pasi):
I) la compilare
II)la executie(run-time)
Exemplu
Pentru apelul: d.f2() "pasii" de apel a functiei virtuale sint prezentati in continuare:
I) la compilare
1. se determina adresa din segmentul de date al obiectului receptor (daca e alocat static)
aici: "dadr4"
2. se determina "numarul de ordine" (offset-ul) functiei virtuale apelate, offset in TFV a clasei respective
aici: offset=1
3. se determina lungimea datelor obiectului respectiv
aici: lg=sizeof(a)+sizeof(b)+sizeof(c)
4. se genereaza cod pentru determinarea LA EXECUTIE a adresei functiei virtuale cautate, folosind "datele" determinate la compilare.
II) la rulare
1. se determina adresa obiectului receptor daca e alocat dinamic (adresa "de pe heap").
2. se calculeaza adresa cimpului unde se afla (in obiectul instanta) memorata adresa TFV pentru clasa respectiva.
aici: adrcimp=dadr4+lg
3. se "extrage" adresa TFV
aici: adr TFV=*adrcimp
4. se determina adresa functiei virtuale din adresa TFV determinata anterior si offset-ul calculat de com[pilator:
aici: adrfvirtuala=*(adrTFV+offset)
5. se apeleaza functia de la adresa calculata anterior
aici: call adrfvirtuala
Observatii
1. Se poate face o "paralela" intre C si C++ pentru a evidentia si mai clar "continutul" unui obiect-instanta si a TFV.
Exemplu
typedef struct circle; virtual void draw(void);
};
void circle_draw(void); void circle::draw(void);
/*Tabela Functiilor Virtuale */
int (*circle_vtbl[1])()=
typedef struct cylinder; };
void cylinder_draw(void); void cylinder::draw(void);
/*TFV pentru cylinder */
int (*cylinder_vtbl[1])()=
2. In fiecare obiect instanta cimpul ce contine adresa TFV pentru clasa din care face parte este completat AUTOMAT de catre constructor, "invizibil" pentru programator, prin prologul generat automat de compilator.
Deci acesta este inca un motiv pentru care se generaza implicit pentru orice clasa un constructor (chiar si vid) si pentru care este apelat automat in momentul crearii obiectelor instanta.
3. Fiecare instanta contine un cimp suplimentar (daca apartine unei clase cu functii virtuale) - un pointer; deci functiile virtuale cresc (putin!) dimensiunea zonei de memorie ocupata de date. In plus in aceasta zona se mai generaza si TFV (un tablou de pointeri).
4. Fiecare apel de functie virtuala necesita o regie suplimentara de sistem (calcule, plus 2 dereferiri); deci timpul de executie a unei functii virtuale este superior (putin!) timpului de executie al unei functii statice.
5. Avantajele folosirii functiilor virtuale sint atit de importante incit numai in cazuri EXTREM de reduse ca numar (timp critic de executie) se recomanda transformarea functiilor virtuale in functii statice!!!
4.3. Aspecte speciale privind functiile virtuale
4.3.1. Functii suprapuse si functii virtuale
Ambele tipuri de functii ofera forme de polimorfism, dar care sint esential diferite: pentru functiile suprapuse se utilizeaza legarea statica (early binding) iar pentru functiile virtuale legarea dinamica (late binding). Aceste diferente devin cruciale cind se folosesc pointerii la obiectele clasei derivate.
Pentru a declara functii virtuale corect trebuie respectate regulile:
a) Trebuie folosit cuvintul "virtual" in clasa de baza; se recomanda insa sa se foloseasca in TOATA ierarhia.
b) Toate prototipurile functiei (in diversele clase din ierarhie) trebuie sa fie PERFECT echivalente: numar si tipul argumentelor.
Orice abatere transforma o functie virtuala intr-o functie suprapusa.
Exemplu
class robot
class robot_2001 : public robot
void main(void)
Greseala porneste de la argumentul functiei salut() a clasei robot_2001 care transforma "salut" in functie polimorfica suprapusa; chiar daca "hal" indica spre un obiect "robot_2001" cind se apeleaza "salut" prin "hal" se foloseste versiunea din clasa de baza, in clasa derivata neavind functie de inlocuire pentru functia virtuala "salut".
Atunci se foloseste cea mai "recenta" versiune.
Aceasta proprietate NU se aplica insa obiectelor alocate STATIC. Daca se poate converti argumentul (in cazul de fata char- int) se foloseste versiunea din clasa derivata (!); daca nu se poate converti atunci se genereaza eroare de compilare.
robot_2001 hal2;
hal2.salut('3'); //Robot_2001 51 !!!
4.3.2. Inlantuirea functiilor virtuale
O functie virtuala a unei clasa de baza ce a fost redefinita (corect) intr-o clasa derivata poate fi apelata in clasa derivata folosind operatorul de domeniu (apartenenta la clasa) "::". Aceasta tehnica se numeste "inlantuire" si se foloseste pentru a "pune" functia din clasa de baza sa efectueze operatiile eventual necesare pentru structurile de date mostenite si apoi se continua operatiile pe eventualele structuri particulare clasei derivate.
Exemplu
class rectangle
class box : public rectangle
public:
void display(void)
//etc....
Observatie
Daca nu s-ar fi folosit operatorul de domeniu "::" s-ar fi intrat intr-o bucla infinita - apeluri recursive ale functiei "box::display()".
4.3.3. Cind se utilizeaza functiile virtuale/statice
Din exemple anterioare se naste intrebarea: Cind se folosesc (cind trebuie folosite) metodele statice si cind cele virtuale ?
Regula de baza este data de urmatoarele considerente:
a) Se folosesc obligatoriu metodele statice cind se doreste optimizarea codului obtinut (viteza + memorie).
b) Se folosesc obligatoriu metodele virtuale cind se doreste extensibilitatea.
Daca un obiect "Parinte" are o metoda "Actiune" atunci aceasta metoda este virtuala daca descendentii obiectului parinte modifica aceasta metoda (in intregime sau doar o parte) si se doreste ca obiectul "Parinte" (metodele sale) sa foloseasca aceste modificari.
Exemplu
Parinte = Point contine MoveTo (neschimbata)
Actiune = Show, Hide
"MoveTo" contine referinte la "Show" & "Hide". Acestea vor fi modificate (rescrise) de descendentii lui "Point". "MoveTo" (intrinsec) NU se mai modifica, fiind deci statica si mostenita. Dar pentru a putea folosi VIITOARELE descrieri "Show" & "Hide" acestea trebuie sa fie virtuale. (Referintele in cadrul lui MoveTo sa se rezolve la rulare si NU la compilare !!!)
4.4. Clase abstracte; functii virtuale pure
Proprietatile de mostenire oferite de OOP pot conduce printr-o proiectare corespunzatoare la reducerea dramatica a efortului de dezvoltare a aplicatiilor; dar, pe de alta parte, o proiectare defectuoasa poate conduce la un efort asemanator cu acela din programele nestructurate (cod "spaghetti").
De obicei, in ierarhiile de clase, clasele de "baza" sint atit de "generale" incit nu se foloseste pentru crearea de obiecte ci numai pentru a defini atributele comune mai multor clase (derivate ulterior din aceasta). Astfel de clase se numesc clase abstracte si contin un tip special de functii numite functii virtuale pure.
Aceste functii "nu fac efectiv nimic" ci numai stabilesc un protocol comun pentru toate clasele derivate, adica stabilesc ce set comun de functii au clasele derivate si cum arata argumentele ce se transmit acelor functii.
Aceste functii sint virtuale pentru ca trebuie particularizate (modificate) de clasele derivate si sint "pure" pentru ca nu contin "nimic" in corpul lor ("nu au corp").
Sintactic o functie virtuala pura se declara:
virtual <tip rezultat> nume (<lista parametri>) = 0;
In plus clasele abstracte nu contin variabile (date membre).
Exemplu
O stiva "abstracta", caruia nu ii cunoastem tipul informatiei, dar care are functiile uzuale poate fi definita astfel:
class abstract_stack
virtual ~abstract_stack(void)
virtual int push(int)=0;
virtual int pop(int &)=0;
virtual int isempty(void)=0;
virtual int isfull(void)=0;
Se observa ca functiile constructor si destructor nu pot fi declarate ca functii virtuale pure.
In plus, orice clasa care contine cel putin 1 functie virtuala pura este o clasa abstracta. Compilatorul nu va permite crearea de obiecte din astfel de clase. Se pot crea obiecte numai din clase derivate dar numai daca se "furnizeaza" cod pentru corpul functiilor virtuale pure ce se mostenesc de la clasa abstracta.
In schimb, insa, se pot declara pointeri si referinte la clase abstracte si acest lucru este foarte frecvent in utilizarea obiectelor polimorfice. Acesti pointeri si referinte trebuie sa indice insa numai spre obiecte din clase "neabstracte" (uzuale).
Utilizarea claselor abstracte si a mecanismului functiilor virtuale reprezinta unul dintre avantajele cele mai mari in proiectarea software-ului de buna calitate in maniera OOP.
4.5. Mostenirea multipla
Este singura extensie notabila a vers. 2.0 fata de anterioarele. Mostenirea multipla este capacitatea unei clase derivata de a avea mai mult de un parinte.
Exemplu
Se prezinta o ierarhie ce are ca "scop" afisarea unui mesaj intr-un cerc.
class Location ;
class Point : public Location ;
class Circle : public Point;
class GMessage : public Location ;
class MCircle : Circle, GMessage ;
//Functiile membru pentru MCircle
MCircle::MCircle(int mcircX, int mcircY, int mcircR, int Font,
char* msg)
:Circle(mcircX,mcircY,mcircR),
GMessage(mcircX,mcircY,Font,2*mcircR,msg)
void MCircle::Show(void)
void main(void)
Se observa modalitatea sintactica de derivare multipla: se specifica "parintii" separati de virgula, precedati, optional, de modificatorul de acces (public sau private).
De asemenea, apelul constructorilor de baza se face asemanator (sintactic) cu apelul in cazul obiectelor compuse. La apelul insa al constructorului clasei MCircle, se apeleaza automat sau explicit constructorul pentru Circle, acesta pentru Point, acesta la rindul sau pe Location; apoi constructorul pentru GMessage ce apeleaza si el constructorul Point pentru propria sa copie a membrilor X si Y ai clasei de baza!
Chiar daca pare a fi o imbunatatire (substantiala), mostenirea multipla genereaza si foarte multe probleme, unele dintre acestea deosebit de subtile.
Exemplu
class sifon ;
class sirop ;
class racoritoare : public sifon, public sirop ;
Prima problema ce apare este aceea ca in clasa "racoritore" exista (prin mostenire) 2 membri cu acelasi nume "cant", provenind de la 2 "parinti" diferiti. Referirea "simpla" (uzuala) este considerata (de compilator) EROARE! Se pot insa referi folosind numele clasei de baza si operatorul de domeniu "::".
racoritoare fanta;
fanta.cant; //EROARE!
fanta.sirop::cant; //OK...
fanta.sifon::cant; //OK...
Aceeasi regula se foloseste si in cazul functiilor.
In plus, exista posibilitatea de a mosteni acelasi membru de mai multe ori, de la ACELASI(!) parinte.
Exemplu
class vin : public sifon ;
class sprit : public vin, public racoritoare ;
In acest caz, clasa "sprit" mosteneste "sifon" de 2 ori: o data de la "racoritoare" si inca o data de la "vin". Ca urmare are 2 membri "co2", 2 "bubble" si 3 "cant" !!!
Rezultatul: acesti membri NU POT fi referiti niciodata pentru ca ar fi o referinta ambigua !?
O solutie este oferita, daca se doreste, prin posibilitatea de a avea o singura copie pentru clasa "sifon". Aceasta se poate realiza cu ajutorul notiunii de "clasa de baza virtuala".
O clasa derivata detine doar o singura copie a unei clase de baza virtuale, chiar daca, indirect, o mosteneste de mai multe ori.
Exemplu
class racoritoare : virtual public sifon, public sirop ;
class vin : virtual public sifon ;
class sprit : public vin, public racoritoare ;
Observatie
Cuvintul cheie "virtual" NU are nici o legatura cu functiile virtuale. S-a ales acesta doar pentru a nu mai introduce inca un cuvint cheie in vocabularul limbajului.
Folosire functiilor virtuale combinata cu mostenirea multipla si suprapunerea functiilor conduce la situatii foarte complexe ce se rezolva dupa reguli foarte complexe.
|