Relatia de Mostenire

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:

Notatia UML pentru Relatia de Mostenire.
In UML relatia de mostenire intre doua clase este tratata ca o relatie de generalizare si se noteaza astfel:
Mai multe clase pot fi derivate dintr-o clasa de baza comuna. Putem privi aceste clase derivate ca specializari ale clasei de baza. Intr-o clasa de baza pot exista mai multe criterii dupa care sa se faca specializarea, numite discriminanti.  Notatia UML face distinctie intre clasele derivate prin specializarea din acelasi discriminant, si cea realizata din discriminanti diferiti. In figura de mai jos sunt prezentate notatiile UML pentru cele doua cazuri.

Mostenirea in C++. Exemplu.

Forma generala a unei clase aflate intr-o relatie de mostenire (simpla) este:

 class nume-clasa-derivata : specificator-acces nume-clasa-baza {
    // corpul clasei
 };
O clasa poate fi derivata din una sau mai multe clase de baza; in primul caz vorbim de mostenire simpla, iar in cel de-al doilea caz avem de a face cu o mostenire multipla. In lucrarea de fata vom discuta exclusiv despre mostenire simpla.

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 {  
friend class Imaginary;  
friend class Double;  
public:  
    Complex(double r = 0, double i = 0): rpart(r), ipart(i) { }  
    Complex(const Complex &c): rpart(c.rpart), ipart(c.ipart) { }  
    Complex& operator=(const Complex &c) {  
        rpart = c.rpart; ipart = c.ipart; return *this;  
    }  
    Complex operator+(const Complex &c) const {  
        return Complex(rpart + c.rpart, ipart + c.ipart);  
    }  
    friend Complex operator+(double d, const Complex &c) {  
        return c + Complex(d);  
    }  
    ...  
    operator double() {  
       return sqrt(rpart*rpart + ipart * ipart;)   
private:  
    double rpart, ipart;  
};  
  

class Imaginary: public Complex {  
public:  
    Imaginary(double i = 0): Complex(0, i) { }  
    Imaginary(const Complex &c): Complex(0, c.ipart) { }  
    Imaginary& operator=(const Complex &c) {  
        rpart = 0; ipart = c.ipart; return *this;  
    }  
};  
  

class Double: public Complex {  
public:  
    Double(double re = 0): Complex(re) { }  
    Double(const Complex &c): Complex(c.rpart) { }  
    Double& operator=(const Complex &c) {  
        rpart = c.rpart; ipart = 0; return *this;  
    }  
};

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:

Membrii protected vs. Clase friend derivate.
Stim ca declararea unei functii sau a unei clase ca friend pentru o alta clasa, face ca functia (clasa) friend sa aiba acces la toti membrii clasei in care este declarata astfel, incluzand membrii privati ai acelei clase. A declara un membru ca protected intr-o clasa de baza este similar cu a declara ca friend toate clasele derivate din ea. Toate clasele derivate vor avea acces la membrii protected ai clasei de baza, asa incat nu mai este necesara "inregistrarea" in clasa de baza a claselor derivate care doresc sa aiba acces la acesti membrii.

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 {  
public:   
  ...  
  int f() { return x + y + afunction(); }  
};  
  

class C : public B {  
public:  
  ...  
  int f() { return afunction(); }  
};


 

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:

1. Aloca memorie. Se aloca necesarul de memorie pentru o instanta a acelei clase.
2. Apeleaza constructorul clasei de baza. Constructorul clasei derivate, apeleaza automat constructorul clasei sale de baza. Desi acest lucru nu necesita interventia programatorului, acesta poate interveni pentru a controla apelul contructorului de baza. Daca clasa de baza nu se afla in varful ierarhiei, aceasta va apela la randul ei constructorul clasei sale de baza.
3. Initializeaza datele-membru de tip obiect. Constructorul clasei derivate va initializa automat datele-membru de tip obiect definite in clasa. Acest lucru se face implicit prin invocarea constructorului implicit pentru acele obiecte. Acest lucru poate fi modificat, prin intermediul unei liste de initializare.
4. Executa constructorul clasei derivate. Dupa ce au fost initializate obiectele membru ale clasei, se executa corpul constructorului din clasa derivata.
Ordinea Executiei Destructorilor
Destructorii se executa in ordine inversa fata de constructori: 1. Executa destructorul clasei derivate.
2. Distruge datele-mebru nestatice de tip obiect, apeland destructorii pentru aceste obiecte.
3. Apeleaza constructorul clasei de baza.
4. Dezaloca memoria.
Invocarea Explicita a Constructorilor Claselor de Baza
Constructorul clasei Imaginary a fost definit in exemplul de mai sus astfel:
    Imaginary(double i = 0): Complex(0, i) { }
Am fi putut implementa acest constructor si in felul urmator:
Imaginary(double i = 0) { ipart = i; }
In cazul in care am fi procedat astfel compilatorul ar fi invocat construcotrul implicit al clasei de baza (Complex::Complex()) inainte de a se executa corpul constructorului Imaginary::Imaginary , deci inainte de a se executa atribuirea. Aceasta face ca la executia constructorului pentru clasa derivata se putem conta pe faptul ca datele clasei de baza au fost deja initializate.

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 {
private: 
  A a;
  int bz;
public: 
  B(int x=0, int y=0, int z=0) : A(x,y), bz(z) {}
}


 

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 {  
protected:  
 double x;  
  double y;  
public:  
  B(double bx = 0, double by = 0,  
    double ax = 0, double ay = 0): A(ax,ay) { x = bx; y = by; }  
  void afis() const;  
};  

void B::afis() const {  
  cout << "Membrii mosteniti " << A::x << A::y << endl;  
  cout << "Membrii proprii " << x << y << endl;  
}

Observam ca in clasa B am declarat doi membri de tip double care au acelasi nume cu datele mostenite de la clasa A; spunem in acest caz ca datele membru din clasa de baza au fost redefinite (overriden) in clasa derivata. Prin redefinire, membrii clasei de baza redefiniti nu se pierd, ci se "ascund", ei nemaiputand fi accesati direct, ci doar prin intermediul operatorului de specificare a domeniului (::). In functia afis vedem cum sunt afisate atat datele membru ale clasei derivate, cat si datele omonime mostenite de la clasa de baza.

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*);  
};
Dorim acum sa derivam din clasa List, clasa Set, care va implementa operatiile cu multimi. Din interfata clasei List operatia head() (ce returneaza capul listei) nu are sens pentru operatii cu multimi si de aceea dorim sa o excludem din interfata clasei Set. Pentru aceasta vom proceda astfel:
class Set : private List {  
public:  
 void insert(void*); // redefinim operatia de inserare  
 List::count;  
 List::has;  
 ...  
};
Constructia de mai sus, numita specificator de acces spun ca din interfata clasei List doar metodele has()si count() trebuie ?coborate? in interfata clasei Set. Spre deosebire de acestea, metoda head() desi parte a interfetei clasei de baza, va fi tratata in clasa B ca metoda privata (datorita tipului mostenirii).

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):

Observatie: Daca pentru un obiect se defineste si o variabila de tip referinta, iar functiile-membru sunt apelate prin intermediul referintei, atunci selectarea functiei se va face deasemenea la executie si nu la compilare. In schimb apelul functiilor membru prin intermediul operatorului punct (obiect.functie() )  nu ofera aceasta flexibilitate.

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 {
public:
 Dreptunghi(double l1=0.0, double l2=0.0) : Patrulater(l1,l2,l1,l2) {}
 double perimetru() { return 2*(laturi[0]+laturi[1]); }
 double arie() { return laturi[0]* laturi[1]; }
};
 

class Patrat : public Patrulater {
public:
 Patrat(double l1 = 0.0) : Patrulater(l1,l1,l1,l1) {}
 double perimetru() { return 4*laturi[0]; }
 double arie() { return laturi[0]*laturi[0]; }
};

un Patrulater* va putea intotdeauna indica spre  orice obiect de tip Dreptunghi sau Patrat, intrucat toate caracteristicile unui Patrulater sunt "acoperite" de un Dreptunghi resp. Patrat.  Astfel urmatoarele declaratii sunt corecte:
 
Patrulater *ppater, pater;
Dreptunghi *pdrept, drept;
Patrat *ppatrat, patrat;

void f(Patrulater &p) { ... } 
 

pdrept = &drept; 
ppatrat = &patrat;

// dar si atribuirile ...

ppater = &drept; // atribuie adresa
pater = patrat;  // atribuite obiect ( cum?)

f(drept);
 

in timp ce urmatoarele declaratii sunt  eronate, intrucat nu toate caracteristicile unui Patrat sau a unui Dreptunghi sunt comune si pentru un Patrulater (in cazul implementarii propuse, pentru clasa Patrulater nu avem definita o functie care sa calculeze aria):
 
void f1(Patrat *p) { ... }

pdrept = &pater;
patrat = pater; 

f1(ppater);

Un pointer la un obiect al unei clase de baza poate fi convertit la un pointer al unei clase derivate printr-o conversie explicita. Conversia explicita spune compilatorului ca noi stim ceva in plus fata de compilator despre tipul acelui obiect. Conversiile de acest fel sunt utile cand se creaza si se prelucreaza obiecte de tip colectie, colectia fiind un set de obiecte de diferite tipuri abstracte derivate dintr-un tip abstract de baza.

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[]) {
  for(Patrulater *elem = *colectie; elem ; elem = *(++colectie)) {
    cout << (*elem)->perimetru() << endl; // atentie: se supraincarca oper. "->" !!!
  }
}
 

main() {
  Patrulater *colectiePatrulatere[10];
  int i = 0;

  colectiePatrulatere[i++] = new Patrat(6.5);
  colectiePatrulatere[i++] = new Dreptunghi(6.0, 7.5);
  colectiePatrulatere[i++] = new Patrulater(2.0, 6.9, 7.2, 9.0);
  colectiePatrulatere[i++] = new Dreptunghi(2.0, 6.5);
  colectiePatrulatere[i++] = new Dreptunghi(2.0, 6.5);
  colectiePatrulatere[i++] = 0;

  CalculeazaPerimetre(colectiePatrulatere);
  //...
}

Astfel, pentru programul principal de mai sus, la parcurgerea colectiei se va apela mai intai functie perimetru() asa cum este ea definita (de fapt, redefinita) in clasa Patrat, apoi cea redefinita pentru clasa Dreptunghi etc.
 

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

Hosted by www.Geocities.ws

1