Introducere
Clase
de Baza. Clase Derivate
Intre
diferitele concepte care intervin intr-o aplicatie se pot face ierarhizari.
Mostenirea este mecanismul prin care un limbaj de programare reda relatiile
ierarhice dintre abstractiuni. Utilizand mostenirea, putem construi o clasa
generala care defineste trasaturile comune ale unui set de elemente corelate.
Aceasta clasa poate fi apoi mostenita de alte clase particulare care adauga
doar acele elemente care le sunt proprii. O clasa care este mostenita se
numeste clasa de baza, iar o clasa care mosteneste
este numita clasa derivata. Clasele alfate intr-o relatie
de mostenire formeaza impreuna o ierarhie de clase.
Toate clasele apartinand unei ierarhii (cu exceptia celei din varful ierarhiei) contin toate elementele membru ale claselor pe care le mostenesc, precum si eventuale alte elemente membru specifice clasei respective. Elementele membru ale clasei aflate in varful ierarhiei sunt elemente comune pentru toate clasele respectivei ierarhii de clase.
Utilizari
ale Relatiei de Mostenire
Relatia
de mostenire este utilizata in doua moduri semnificativ diferite:


Mostenirea in C++. Exemplu.
Forma generala a unei clase aflate intr-o relatie de mostenire (simpla) este:
Exemplu
Am
introdus in lucrarea trecuta clasa numerelor complexe, Complex.
Numerele complexe sunt adesea folosite inginerie ca o generalizare a numerelor
imaginare, reale sau rationale. Tocmai de aceea clase ca Double,
Imaginary
etc. reprezinta exemple excelente de derivare a clasei Complex.
| #include
<iostream.h
#include <math.h class
Complex {
class
Imaginary: public Complex {
class
Double: public Complex {
|
Observatii
Analizand
exemplul de mai sus trebuie facute cel putin urmatoarele doua observatii:
Controlul Accesului Vertical
Am denumit setul de reguli care reglementeaza accesul la membrii unei clase de baza din interiorul unei clase derivate, control al accesului vertical, considerand ierarhia de clase orientata vertical, iar clasele de baza aflandu-se "deasupra" claselor derivate.
Membrii
protected
ai unei clase
Cand
un membru al unei clase este declarat protected,
acel membru este inaccesibil altor elemente ale programului care nu sunt
membrii ai clasei, cu o singura exceptie: clasele derivate. Un astfel de
membru este deci perceput ca privat pentru toate celelalte clase, dar este
accesibil din interiorul unei clase derivate.
Atentie: Un membru protected al unei clase de baza nu poate fi accesat dintr-o functie membru sau friend a unei clase derivate prin intermediul unui obiect, pointer sau referinta la clasa de baza, ci doar prin intermediul obiecte, pointeri sau referinte ale clasei derivate! (vezi discutia despre clase friend derivate)
Reguli
de Acces la Clasa de Baza.
Accesul
la membrii unei clasei de baza din interiorul unei clase derivate este
determinat prin specificator-acces.
Specificatorul de acces al clasei de baza trebuie sa fie public,
private
sau protected.
In cazul in care in declaratia clasei nu este prezent nici un specificator,
el este implicit private,
pentru o clasa derivata de tip class,
si este public pentru
clase derivate de tip struct.
Atentie: Pentru ca o clasa derivata sa mosteneasca publicnu uitati ca trebuie sa precizati acest lucru explicit in declaratia clasei derivate! In caz contrar compilatorul va considera relatia de mostenire de tip private!
Specificatorul de acces la clasa de baza modifica statutul in clasa derivata a membrilor din clasa de baza in conformitate cu urmatoarele reguli:
Exista insa cazuri cand una dintre clasele derivate are nevoie in mod exceptional sa acceseze structura interna (declarata privat) a clasei de baza. Ar fi impotriva spiritului incapsularii ca doar din acest motiv sa mutam structura interna in sectiunea protected facandu-o astfel vizibil pentru toate clasele derivate. Pentru a rezolva aceasta situatie, pe langa relatia de mostenire clasa derivata care are nevoie de acces la membrii privati va fi declarata friend in clasa de baza, ca in exemplul de mai jos.
| class
A {
int x, y; protected: int afunction() { ... } public: ... friend class B; }; class
B : public A {
class
C : public B {
|
Regula:
O
clasa ar trebui sa evite sa-si exporte structura interna, chiar si claselor
derivate! Mostenirea nu este o legimitare pentru a viola incapsularea!
Constructorii, Destructorii si Mostenirea
Constructorii si destructorii sunt functii membru speciale care initializeaza automat obiectele la crearea lor si respectiv controleaza procesul de distrugere a obiectelor la terminarea existentei acestora. Un obiect al unei clase derivate contine date membru definite in diferite clase aflate deasupra acesteia in ierarhia de clase; constructorii si destructorii acestor clase de baza contribuie si ei la initializarea si distrugerea obiectelor din clasa derivat. Putem spune ca initializarea si distrugerea obiectelor dintr-o clasa derivata nu se efectueza centralizat, ci distribuit de-a lungul ierahiei de clase.
Ordinea
Executiei Constructorilor
La
creare unui obiect al unei clase derivate se excuta - in ordine cronologica
- urmatoarele operatii:
| Imaginary(double i = 0): Complex(0, i) { } |
| Imaginary(double i = 0) { ipart = i; } |
Parametrii formali ai constructorului clasei derivate pot fi utili si pentru constructorii claselor de baza, intrucat prin intermediul lor programatorul castiga flexibilitatea de a invoca un anumit constructor pentru initializarea clasei de baza. In exemplul de mai sus am specificat explicit ca inainte de executia constructorului Imaginary::Imaginary(double) sa se apeleze nu constructorul implicit al clasei Complex, ci Complex::Complex(double, double).
Invocarea explicita a constructorilor parametrizati ai clasei de baza mai prezinta un avantaj, si anume evitarea initializarilor redundante. Daca in exemplul de mai sus am fi implementat constructorul clasei Imaginary fara apelul explicit al constructorului clasei Complex (vezi a doua varianta) initializarea campului ipart s-ar fi facut de doua ori: prima oara in apelul automat al constructorul implicit pentru clasa Complex si a doua oara prin atribuirea din corpul constructorului Imaginary . Desi acest aspect este nesemnificativ in cazul initializarii unui double, el nu poate fi insa ignorat in cazul in care avem de a face cu o data a carei initializare este mult mai costisitoare.
Invocarea
Explicita a Constructorilor pentru Datele-Membru de tip Obiect
Nu
doar constructorii claselor de baza pot fi apelati explicit. Spuneam mai
sus ca al treilea pas in initializarea unui obiect al unei clase derivate
il constituie initializarea datelor-membru de tip obiect. In mod implicit,
compilatorul va apela constructorul implicit pentru obiectul respectiv.
Putem interveni si asupra acestui fapt, selectand care constructor sa fie
utilizat pentru initializarea obiectului-membru. De obicei initializarea
acestor obiecte se va face pe baza parametrilor formali ai contructorului
clasei in care apare acel obiect. Intrucat acest aspect este independent
de relatia de mostenire fiind o caracteristica generala a constructorilor
il putem ilustra prin urmatorul exemplu:
| class
A {
private int ax, ay; public: A() : ax(0), ay(0) {} A(int x, int y) : ax(x), ay(y) { } }; class
B {
|
Redefinirea Membrilor din Clasa de Baza.
Redefinirea
datelor-membru.
O
data membru a unei clase de baza se poate redefini ca data membru a unei
clase derivate, ca in exemplul de mai jos:
| class
A {
protected: double x; double y; public: A(double xx = 0, double yy = 0) : x(xx), y(yy) {} ... }; class
B : public A {
void
B::afis() const {
|
Redefinirea
functiilor-membru.
Functiile
membru ne-private ale claselor de baza sunt mostenite de clasele derivate.
Astfel, daca A este o clasa de baza si B este o clasa derivata si f este
o functie-membru ne-privata a clasei A, atunci f este o functie membru
si a clasei B. In mod analog, functiile friend ale clasei de baza sunt
functii friend si pentru clasa derivata. Astfel, o functie membru sau friend
din clasa de baza, poate fi utilizata si cu obiecte ale clasei derivate.
Mostenirea functiilor membru si prieten are ca efect faptul ca supraincarcarea operatorilor pentru clasa de baza este valabila si pentru o clasa derivata a clasei respective.
Nu totdeauna o functie mostenita de la o clasa corespunde pentru a fi apelata pentru obiectele clasei derivate. In astfel de situatii functia se va supraincarca pentru clasa derivata. La supraincarcare se poate pastra nu numai numele functiei, ci chiar si numarul si tipul parametrilor. Functiile supraincarcate se selecteaza in acest caz nu numai dupa numarul si tipul parametrilor, ci si dupa obiectul pentru care sunt apelate. Printr-o astfel de supraincarcare, functia omonima din clasa de baza este "ascunsa" pentru obiectele clasei derivate, ea nemaiputand fi invocata direct, ci doar prin intermediul operatorului de specificare a domeniului (::), la fel ca si la redefinirea datelor-membru; spunem de aceea ca o functie a clasei de baza supraincarcata in clasa derivata, este redefinita.
Atentie:
O functie din clasa derivata care supraincarca o functie a clasei de baza
va ascunde functia din clasa de baza, chiar daca numarul si tipul parametrilor
celor doua functii difera. Redefinirea se realizeaza prin numele
functiei nu prin lista de parametrii formali.
Controlul Accesului Orizontal la Clasele Derivate
Am denumit mecanismele care reglementeaza accesul clientilor unei clase derivate la membrii acesteia control al accesului orizontal. In timp ce clasa de baza controleaza care dintre membrii sai vor fi accesibili clasei derivate, clasa derivata controleaza modul in care interfata mostenita de la clasa de baza va putea fi accesata de clientii sai. Cele mai importante mecanisme sunt: mostenirea cu acces private si redefinirea membrilor clasei de baza.
Mostenirea
cu acces private.Restrangerea
Interfetei din Clasa de Baza.
Exista
situatii in care se doreste ca o anumita parte a interfetei dintr-o clasa
de baza sa nu fie accesibila clientilor uneia din clasele derivate. Am
vazut deja ca utilizarea mostenirii cu acces public, face ca interfata
clasei de baza sa fie mostenita integral de catre clasa derivata. Pentru
astfel de situatii, in care dorim restrangerea interfetei mostenite de
la o clasa de baza, vom folosi mostenirea cu acces private.
Sa consideram de exemplu o clasa List care defineste urmatoarea interfata, tipica pentru operatiile cu liste:
| class
List {
... public: void* head(); int count(); long has(void*); void insert(void*); }; |
| class
Set : private
List {
public: void insert(void*); // redefinim operatia de inserare List::count; List::has; ... }; |
Atentie: Specificatorii de acces nu pot modifica drepturile de acces definite pentru membrii clasei de baza, ci pot doar face ca in cazul mostenirii cu acces privat anumiti membri din clasa de baza sa poata fi accesati ca si cand mostenirea ar fi cu acces public. Astfel, daca un membru a fost declarat ca private sau protected in clasa de baza, declararea sa in sectiunea public a clasei derivate nu il va transforma intr-un membru public al acestei clase.
Restrangerea interfetei unei clase de baza intr-o clasa derivata trebuie utilizata cu precautie! De fiecare situatie ne confruntam cu o astfel de situatie trebuie sa ne analizam cu exactitate daca intre clasa de baza si clasa derivata exista intr-adevar o relatie de tip "is-a". In cazul in care am folosit mostenirea intre doua clase doar din comoditatea de a putea reutiliza facil codul clasei de baza, trebuie sa ne punem serios problema reproiectarii relatiei dintre cele doua clase!
Pointeri
si Referinte la Clasa de Baza si la Clasele Derivate. Conversii.
Principala forta a relatiei de mostenire este data de facilitatea de a trata obiectele claselor cuprinse intr-o ierarhie de clase, ca si cum ar fi instante ale clasei din varful ierarhiei. Aceasta este ceea ce se numeste polimorfism (gr. poly = multe; morphe = forma); obiecte avand diferite forme pot fi tratate unitar.
Doua
Avantaje ale Pointerilor
Aceasta
forta de programare este bazata pe conversia intre pointerii la obiecte
ale claselor relationate prin mostenire. Veti observa ca majoritatea programelor
C++ isi acceseaza obiectele prin pointeri. In contextul mostenirii aceasta
prezinta doua avantaje fata de utilizarea obiectelor (alocate static):
Din
prima proprietate trebuie sa intelegem ca daca declaram un pointer la un
obiect al unei clase, putem utiliza acel pointer pentru a pastra adresa
unui obiect al oricarei clase derivate din ea. De exemplu, avand clasa
Patrulater
si clasele derivate din ea Dreptunghi
si Patrat,
definite astfel:
| class
Patrulater {
protected: double laturi[4]; public: Patrulater(double l1=0.0, double l2=0.0, double l3=0.0, double l4=0.0) { laturi[0] = l1; laturi[1] = l2; laturi[2] = l3; laturi[3] = l4; } double perimetru() { return laturi[0]+ laturi[1]+laturi[2]+laturi[3]; } }; class
Dreptunghi : public Patrulater {
class
Patrat : public Patrulater {
|
| Patrulater
*ppater, pater;
Dreptunghi *pdrept, drept; Patrat *ppatrat, patrat; void
f(Patrulater &p) { ... }
pdrept
= &drept;
// dar si atribuirile ... ppater
= &drept; // atribuie adresa
f(drept);
|
| void
f1(Patrat *p) { ... }
pdrept
= &pater;
f1(ppater); |
Cele
doua proprietati enuntate mai sus sunt fundamentale in programarea orientata
pe obiecte. Implicatiie lor vor fi detaliate in lucrarea
urmatoare.
Probleme
1. Compilati programul zoo.cc. Ce se
intampla cand incercati sa-l compilati? Din cauza carui mecanism de protectie
din C++ se intampla acest lucru? Modificati programul astfel incat protectia
sa fie "ocolita"! (2 solutii) [Cop91]
2. Analizati (eventual compilati-l si rulati-l) programul attrib.cc.
Care va fi dupa atribuire valoarea continuta in b1.a ? Ce va spune acest
lucru despre modul in care trebuie sa implementati operatorul de atribuire
atunci cand exista relatii de mostenire? Modificati programul astfel incat,
problema pe care ati constatat-o sa fie eliminata! [Cop91]
3. Sa consideram cele trei clase de mai sus care definesc patrulatere patrulatere.cc
. Se cere sa se modifice aceste clase incat ele sa poata simula comportare
polimorfica fara a face uz de functii virtuale! Comportarea polimorfica
va fi probata astfel: se va declara o colectie eterogena de patrulatere
si o functie globala CalculeazaPerimetre()
care va parcurge colectia si va apela pentru fiecare patrulater din colectie
functia perimetru()
corespunzatoare clasei de care apartine respectivul obiect, asa cum este
descris mai jos (main.cc) :
| #include
<iostream.h>
#include "patrulatere.cc" void
CalculeazaPerimetre(Patrulater *colectie[]) {
main()
{
colectiePatrulatere[i++] = new Patrat(6.5);
CalculeazaPerimetre(colectiePatrulatere);
|
Bibliografie
[Cop91]
J. Coplien - "Advanced C++ Programming Styles and Idioms",
Addison-Wesley, 1991
L. Negrescu - "Limbajele C/C++ pentru Incepatori vol. II", Editura Microinformatica
Cluj, 1994