Cap. 4. Programare avansata utilizand clase

4.1. Controlul accesului la clase

Spre deosebire de limbajele orientate obiect pure, C++ permite controlul accesului la membrii claselor. In acest scop, s-au creat trei specificatori de cotrol al accesului:
  • public, membrul poate fi accesat de orice functie din domeniul declaratiei clasei;
  • private, membrul este accesibil numai functiilor membre si prietene ale clasei;
  • protected, similar cu private, insa accesul se extinde si la functiile membre si prietene ale claselor derivate.
    De remarcat este faptul ca o functie membra a unei clase are acces la toti membrii clasei, indiferent de specificatorul de acces.
    Asadar, sintaxa declaratiei unei clase derivate incluzand controlul accesului este:

    class NumeClasaDerivata : SpecificatorAcces NumeClasaDeBaza


    unde SpecificatorAcces poate fi public sau private.

    Atributul din clasa de baza Modificator de acces Accesul mostenit de clasa derivata Accesul din exterior
    private
    protected
    public
    private
    private
    private
    inaccesibil
    private
    private
    inaccesibil
    inaccesibil
    inaccesibil
    private
    protected
    public
    public
    public
    public
    inaccesibil
    protected
    public
    inaccesibil
    inaccesibil
    accesibil


    Se observa ca pentru a oferi clasei derivate acces la un membru al clasei de baza, acesta trebuie declarat protected sau public. Pentru respectarea principiului incapsularii datelor, datele membre pentru care se ofera acces claselor derivate se declara in clasa de baza cu atributul protected. De asemenea, pentru a conserva dreptul de acces in urma derivarii, se utilizeaza derivarea public. Accesul poate fi stopat pe orice nivel ar ierarhiei de clase printr-o derivare private.
    Stabilirea atributelor de acces ale membrilor unei clase, precum si ale derivarilor, trebuie sa se faca astfel incat dezvoltarea ierarhiei de clase fara a afecta incapsularea datelor.
    Sa reluam in continuare exemplul din capitolul precedent, completat cu specificatori de acces:

    class Point
    {
    protected:
    unsigned x, y;
    public:
    Point();
    Point(unsigned X, unsigned Y);
    ~Point();
    unsigned long Arie();
    unsigned GetX();
    unsigned GetY();
    void SetX(unsigned X);
    void SetY(unsigned Y);
    };

    class GraphicPoint : public Point
    {
    unsigned color;

    public:
    GraphicPoint(unsigned X, unsigned Y, unsigned Color);
    ~GraphicPoint();
    void Draw();
    void SetX(unsigned X);
    void SetY(unsigned Y);
    };


    Se observa ca variabilele membru x si y sunt declarate protected, asa incat vor fi vizibile si vor avea acelasi atribut in clasa GraphicPoint (desi nu sunt utilizate). In mod normal, x si y ar trebui sa fie declarati private, intrucat nu sunt utilizati decat in interiorul clasei Point. Functiile din GraphicPoint nu acceseaza acesti doi membri direct, ci prin intermediul metodelor publice de accesare a lor oferite de clasa Point.
    De notat este faptul ca implicit, daca nu este utilizat nici un specificator de acces, membrii sunt considerati private.

    void main()
    {
    Point *p;

    p = new Point;
    p->x = 5; // operatie imposibila: x este membru privat
    p->y = 8; // operatie imposibila: y este membru privat
    p->SetX(5); // corect: acces la variabila x prin intermediul functiei SetX()
    p->SetY(8);
    printf("Aria = %d\n", p->Aria());
    delete p;
    }


    Am demonstrat prin acest exemplu de program ca din exteriorul unei clase nu pot fi accesate datele membre private sau protected.

    4.2. Functii si clase prietene

    In paragraful precedent, am afirmat ca principiul incapsularii datelor este bine sa fie respectat in cadrul elaborarii ierarhiei de clase. Cu toate acestea, exista situatii in care este greu sa se respecte acest principiu. De aceea, Bjarne Stroustrup a introdus un concept menit sa rezolve si aceste situatii particulare, pentru a oferi solutii elegante in vederea rezolvarii tuturor situatiilor posibile. Acest concept este cel de friend, care permite practic abateri controlate de la ideea protectiei datelor prin incapsulare. Mecanismul de friend este bine sa fie folosit numai in cazul in care nu exista alta solutie!
    Mecanismul de friend (sau prietenie) a aparut datorita imposibilitatii ca o metoda sa fie membru a mai multor clase.
    Functiile prietene sunt functii care nu sunt metode ale unei clase, dar care au totusi acces la membrii privati ai acesteia. Orice functie poate fi prietena a unei clase, indiferent de natura acesteia.
    Sintaxa declararii unei functii prietene in cadrul declaratiei unei clase este urmatoarea:

    friend NumeFunctie


    Iata si un exemplu:

    class Point {
    friend unsigned long Calcul(unsigned X, unsigned Y);
    public:
    friend unsigned long AltaClasa::Calcul(unsigned X, unsigned Y);
    ...
    };

    unsigned long Calcul(unsigned X, unsigned Y)
    {
    return X * Y / 2;
    }

    unsigned long AltaClasa::Calcul(unsigned X, unsigned Y)
    {
    ...
    }


    Dupa cum se vede din exemplul de mai sus, nu are nici o importanta in cadrul carei sectiuni este declarata functia prietena.

    Clasele prietene sunt clase care au acces la membrii privati ai unei clase. Sintaxa declararii unei clase prietene este:

    friend class NumeClasaPrietena


    Iata si un exemplu:

    class PrimaClasa {
    ...
    };

    class ADouaClasa {
    ...
    friend class PrimaClasa;
    };


    In exemplul de mai sus, clasa PrimaClasa are acces la membrii privati ai clasei ADouaClasa.
    Important este sa remarcam ca relatia de prietenie nu este tranzitiva. Daca o clasa A este prietena a clasei B, si clasa B este prietena a unei clase C, aceasta nu inseamna ca A este prietena a clasei C. De asemenea, proprietatea de prietenie nu se mosteneste in clasele derivate.

    4.3. Cuvantul cheie this

    Toate functiile membre ale unei clase primesc un parametru ascuns, pointer-ul this, care reprezinta adresa obiectului in cauza. Acesta poate fi utilizat in cadrul functiilor membre.
    Iata si un exemplu:

    unsigned long Point::Arie()
    {
    return this->x * this->y;
    }


    4.4. Redefinirea operatorilor

    Dupa cum stiti, C/C++ are definite mai multe tipuri de date: int, char, etc. Pentru utilizarea acestor tipuri de date exista definiti mai multi operatori: adunare(+), inmultire (*), etc. C++ permite programatorilor sa isi defineasca acesti operatori pentru a lucru cu propriile clase.
    Sintaxa supraincarcarii unui operator este:

    operator Simbol


    unde Simbol este simbolul oricarui operator C++, exceptand: ., .*, ::, ?:. Aceasta definire se face in cadrul clasei, intocmai ca o functie membra.

    Tipul operatorului Simbolul operatorului Asociativitate Observatii
    Binar
    () [] ->
    ->
    Se definesc ca functii membre
    Unar
    + - ~ * & (tip)
    <-

    Unar
    ++ --
    <-
    Nu se poate distinge intre pre si post
    Unar
    new delete
    <-
    Poate fi supradefinit si pentru o clasa
    Binar
    -> * / % + - & | && ||
    ->

    Binar
    << >> < <= > >= == !=
    ->

    Binar
    = += -= *= /= %= &= ^= |= <<= >>=
    <-
    Se definesc ca functii membre
    Binar
    ,
    ->



    Dupa cum vom vedea in continuare, exista doua variante de definire a operatorilor:
  • ca functie membra a clasei;
  • ca functie prietena a clasei.
    Pentru exemplificare, ne propunem sa extindem clasa Point cu utilizarea unor operatori.

    class Point {
    // ...
    Point& operator += (Point __p);
    Point& operator -= (Point __p);
    Point operator + (Point __p);
    Point operator - (Point __p);
    Point& operator = (Point __p);
    int operator == (Point __p);
    int operator != (Point __p);
    int operator < (Point __p);
    int operator > (Point __p);
    int operator <= (Point __p);
    int operator >= (Point __p);
    };

    Point& Point::operator += (Point __p)
    {
    x += __p.x;
    y += __p.y;
    return *this;
    }

    Point& Point::operator -= (Point __p)
    {
    x -= __p.x;
    y -= __p.y;
    return *this;
    }

    Point Point::operator + (Point __p)
    {
    return Point(x + __p.x, y + __p.y);
    }

    Point Point::operator - (Point __p)
    {
    return Point(x - __p.x, y - __p.y);
    }

    int Point::operator == (Point __p)
    {
    return x == __p.x && y == __p.y;
    }

    int Point::operator != (Point __p)
    {
    return !(*this == __p);
    }

    int Point::operator < (Point __p)
    {
    return x < __p.x && y < __p.y;
    }

    int Point::operator > (Point __p)
    {
    return x > __p.x && y > __p.y;
    }

    int Point::operator <= (Point __p)
    {
    return x <= __p.x && y <= __p.y;
    }

    int Point::operator >= (Point __p)
    {
    return x >= __p.x && y >= __p.y;
    }


    Am utilizat mai sus varianta cu functii membre. In continuare vom descrie implementarea operatorului + folosind cea de-a doua varianta.

    class Point {
    // ...
    friend Point operator + (Point __p1, Point __p2);
    }

    Point operator + (Point __p1, Point __p2)
    {
    return Point(__p1.x + __p2.x, __p1.y + __p2.y);
    }


    Definirea operatorilor ca functii membre a unei clase prezinta o restrictie majora: primul operand este obligatoriu de tipul clasa respectiv.

    Supradefinirea operatorilor este supusa in C++ unui set de restrictii:
  • nu este permisa introducerea de noi simboluri de operatori;
  • patru operatori nu pot fi redefiniti (vezi mai sus);
  • caracteristicile operatorilor nu pot fi schimbate: pluralitatea (nu se poate supradefini un operator unar ca operator binar sau invers), precedenta si asociativitatea;
  • functia operator trebuie sa aiba cel putin un parametru de tipul clasa caruia ii este asociat operatorul supradefinit.

    Programatorul are libertatea de a alege natura operatiei realizate de un operator, insa este recomandat ca noua operatie sa fie apropiata de semnificatia initiala.

    4.4.1. Redefinirea operatorului =

    Operatorul = este deja predefinit in C++, pentru operanzi de tip clasa. Daca nu este supradefinit, atribuirea se face membru cu membru, in mod similar cu initializarea obiectului efectuata de catre compilator. Pot exista situatii in care se doreste o atribuire specifica clasei, ca atare poate fi supradefinit.

    Point& Point::operator = (Point __p)
    {
    x = __p.x;
    y = __p.y;
    return *this;
    }


    4.4.2. Redefinirea operatorului []

    Operatorul de indexare [] se defineste astfel:

    int &operator[](int)


    4.4.3. Redefinirea operatorilor new si delete

    Acesti doi operatori pot fi supradefiniti pentru a realiza operatii specializate de alocare/eliberare dinamica a memoriei. Functia operator new trebuie sa primeasca un argument de tipul size_t care sa precizeze dimensiunea in octeti a obiectului alocat si sa returneze un pointer de tip void continand adresa zonei alocate:

    void *operator new(size_t)


    cu mentiunea ca size_t este definit in stdlib.h. Chiar daca parametrul de tip size_t este obligatoriu, calculul dimensiunii obiectului in cauza si generarea sa se face de catre compilator.

    Functia operator delete trebuie sa primeasca ca prim parametru un pointer de tipul clasei in cauza sau void, continand adresa obiectului de distrus, si un al doilea parametru, optional, de tip size_t. Functia nu intoarce nici un rezultat.

    void operator delete(void *, size_t)


    Trebuie sa mentionam aici ca operatorii new si delete supradefiniti pastreaza toate proprietatile operatorilor new si delete standard.

    4.4.4. Redefinirea operatorilor unari

    Operatorii unari pot fi supradefiniti utilizand o functie membra fara parametri sau o functie prietena cu un parametru de tipul clasa respectiv. Trebuie subliniat ca pentru operatorii ++ si -- dispare distinctia intre utilizarea ca prefix si cea ca postfix, de exemplu intre x++ si ++x, respectiv x-- si --x, dupa cum mentionam si in tabela cu operatori de mai sus.

    4.5. Mostenirea multipla

    Limbajul C++ permite crearea de clase care mostenesc proprietatile mai multor clase de baza. Mostenirea multipla creste astfel flexibilitatea dezvoltarii ierarhiei de clase. Daca derivarea normala duce la construirea unei ierarhii de tip arbore, derivarea multipla va genera ierarhii de tip graf.
    Sintaxa completa pentru operatia de derivare este urmatoarea:

    class NumeClasaDerivata : ListaClaseDeBaza


    unde ListaClaseDeBaza este:

    SpecificatorAcces NumeClasaDeBaza, ...


    4.5.1. Clase virtuale

    Utilizarea mostenirii multiple se poate complica odata cu cresterea dimensiunii ierarhiei de clase. O situatie care poate apare este derivarea din doua clase de baza, Clasa1 si Clasa2, care la randul lor sunt derivate dintr-o clasa comuna, ClasaDeBaza. In acest caz, noua clasa, ClasaNoua, va contine datele membre ale clasei ClasaDeBaza duplicate. Daca prezenta acestor date duplicate este utila, ele pot fi distinse evident cu ajutorul operatorului de rezolutie, ::. Totusi, in cele mai multe cazuri, aceasta duplicare nu este necesara si duce la consum inutil de memorie. De aceea, in C++ a fost creat un mecanism care sa evite aceasta situatie, prin intermediul conceptului de clasa virtuala. Sintaxa este:

    class NumeClasaDerivata : SpecificatorAcces virtual NumeClasaDeBaza


    Aceasta declaratie nu afecteaza clasa in cauza, ci numai clasele derivate din aceasta. Astfel, clasele Clasa1 si Clasa2 considerate vor fi declarate virtuale. Trebuie mentionat faptul ca declararea virtual a acestor clase va afecta definirea constructorului clasei ClasaNoua, deoarece compilatorul nu poate hotari care date vor fi transferate catre constructorul ClasaDeBaza, specificate de constructorii Clasa1 si Clasa2. Constructorul ClasaNoua va trebui modificat astfel incat sa trimita datele pentru constructorul ClasaDeBaza. De asemenea, trebuie precizat ca intr-o ierarhie de clase derivate, constructorul clasei virtuale este intotdeauna apelat primul.

    4.6. Conversii de tip definite de programator

    Dupa cum stiti, in C/C++ exista definit un set de reguli de conversie pentru tipurile fundamentale de date. C++ permite definirea de reguli de conversie pentru clasele create de programator. Regulile astfel definite sunt supuse unor restrictii:
  • intr-un sir de conversii nu este admisa decat o singura conversie definita de programator;
  • se recurge la aceste conversii numai dupa ce se verifica existenta altor solutii (de exemplu, pentru o atribuire, se verifica mai intai supraincarcarea operatorului de atribuire si in lipsa acestuia se face conversia).
    Exista doua metode de a realiza conversii de tip, prezentate mai jos.

    4.6.1. Supraincarcarea operatorului unar "cast"

    Sintaxa este:

    operator TipData()


    respectiv:
    operator (TipData)


    Operatorul "cast" este unar, asadar are un singur parametru, adresa obiectului in cauza, si intoarce un rezultat de tipul operatorului. Ca urmare, prin aceasta metoda se pot defini numai conversii dintr-un tip clasa intr-un tip de baza sau un alt tip clasa.
    De remarcat este faptul ca, in cazul conversiei dintr-un tip clasa intr-un alt tip clasa, functia operator trebuie sa aiba acces la datele membre ale clasei de la care se face conversia, deci trebuie declarata prietena a clasei respective.

    4.6.2. Conversii de tip folosind constructori

    Aceasta metoda consta in definirea unui constructor ce primeste ca parametru tipul de la care se face conversia. Constructorul intoarce intotdeauna ca rezultat un obiect de tipul clasei de care apartine, ca urmare folosind aceasta metoda se pot realiza numai conversii dintr-un tip de baza sau un tip clasa intr-un tip clasa.
    Trebuie mentionat faptul ca, in cazul conversiei dintr-un tip clasa intr-un alt tip clasa, constructorul trebuie sa aiba acces la datele membre ale clasei de la care se face conversia, deci trebuie declarata prietena a clasei respective.

    4.7. Constructorul de copiere

    O situatie care poate aparea deseori este initializarea unui obiect cu datele membre ale unui obiect de acelasi tip. In 4.4.1 am descris supraincarcarea oparatorului de atribuire. Exista totusi situatii in care acest operator nu poate fi utilizat, cum ar fi la transferul unui obiect ca parametru sau la crearea unui instante temporare a unei clase, atunci cand copierea membru cu membru nu este adecvata. Pentru a rezolva aceste situatii, C++ a introdus un constructor special, numit constructorul de copiere.
    Sintaxa este:

    NumeClasa::NumeClasa (NumeClasa &NumeObiectSursa)


    In continuare vom completa clasa Point cu un constructor de copiere:

    Point::Point(Point &p)
    {
    p.x = x;
    p.y = y;
    }


    In cazul in care clasa nu dispune de constructor de copiere, compilatorul genereaza automat un constructor de copiere care realizeaza copierea membru cu membru.

    4.8. Clase abstracte

    In C++ exista posibilitatea de a defini clase generale, care sunt destinate crearii de noi clase prin derivare, ele neputand fi instantiate si utilizate ca atare. Acest gen de clase se numesc clase abstracte. Ele se constituie ca baza in cadrul elaborarii de ierarhii de clase, putand fi folosite, spre exemplu, pentru a impune anumite restrictii in realizarea claselor derivate.
    In vederea construirii unor astfel de clase, s-a introdus conceptul de functie virtuala pura. O astfel de functie este declarata in cadrul clasei, dar nu este definita. O clasa care contine o functie virtuala pura este considerata abstracta. Sintaxa definirii acestor functii este:

    virtual TipData NumeFunctieMembra() = 0


    Se impune aici observatia ca functiile virtuale pure trebuie definite in clasele derivate, altfel si acestea vor fi considerate abstracte.

    4.9. Membri statici ai unei clase

    In mod normal, datele membre ale unei clase sunt alocate in cadrul fiecarui obiect. In C++, se pot defini date membre cu o comportare speciala, numite date statice. Acestea sunt alocate o singura data, existand sub forma unei singuri copii, comuna tuturor obiectelor de tipul clasa respectiv, iar crearea, initializarea si accesul la aceste date sunt independente de obiectele clasei. Sintaxa este:

    static DeclarareMembru


    Functiile membre statice efectueaza de asemenea operatii care nu sunt asociate obiectelor individuale, ci intregii clase. Functiile exterioare clasei pot accesa membrii statici ale acesteia astfel:

    NumeClasa::NumeMembru

    Obiect::NumeMembru


    Functiile membre statice nu primesc ca parametru implicit adresa unui obiect, asadar in cadrul lor cuvantul cheie this nu poate fi utilizat. De asemenea, membrii normali ai clasei nu pot fi referiti decat specificand numele unui obiect.

    In urmatorul capitol va fi prezentata notiunea de stream.


    Realizat de Dragos Acostachioaie, ©1997-98
    http://www.biosfarm.ro/~dragos