Clase si Obiecte

 
 


In loc de "Hello world!" ... o Stiva

Vom incepe discutia despre clase si obiecte prin prezentarea unui exemplu sugestiv, si anume realizarea si utilizarea unei stive in C++:

Modul in care acest program poate fi compilat in UNIX este descris in anexa "Compilarea proiectelor in UNIX. Utilizarea Makefile"
Acest program este sugestiv intrucat cuprinde majoritatea elementelor fundamentale ale unei aplicatii scrise in C++, pe care le vom  discuta in continuare.

In fisierul stack.h vedem felul in care se defineste o clasa in C++. Observam ca declaratia clasei  este impartita in doua sectiuni delimitate de cuvintele-cheie public resp. private, denumite specificatori de acces. Existenta acestor sectiuni face posibila delimitarea clara a interfetei clasei de implementarea sa, realizandu-se astfel materializarea conceptului de incapsulare .
Astfel, tot ceea ce este declarat intr-o sectiune public a unei clase apartine interfetei acelei clase, fiind prin urmare accesibil din toate celelalte functii din program. Din acest motiv sunt declarate aici metodele (functiile membru) prin care se vor efectua operatiile uzuale pentru o stiva (push, pop, top), ce au fost determinate in urma procesului de abstractizare.
Sectiunile privateale unei clase vor contine declaratiile acelor membri ai clasei (date si metode) care contribuie la implementarea clasei si care conform principiului incapsularii trebuie ascunse de "ochii lumii", adica nu au voie sa fie accesate decat din metodele ce apartin respectivei clase. In cazul exemplului nostru au fost declarate aici datele prin care este reprezentata stiva, adica tabloul de numere intregi in care sunt stocate elementele sitivei (int *items) resp. indicatorul varfului de stiva (int sp).

In sectiunea public, pe langa cele trei metode corepunzatoare operatiilor cu stiva, mai sunt declarate trei metode avand o forma speciala:


In fisierul stack.ccavem implementarea metodelor clasei Stack. Primul lucru pe care il observam in semnatura fiecarei functii membru este constructia:

nume_clasa::nume_metoda(...)

Simbolul ":: " este numit operator de specificare a domeniului (scope resolution specifier). Important este ca el spune compilatorului ca aceasta implementare a functiei nume_metoda apartine clasei nume_clasa. Rezulta asadar ca mai multe clase pot folosi acelasi nume de functie. Prin intermediul constructiei de mai sus se obtine o identificare univoca a unei functii in intreg programul.

In implementarea celor doi constructori ai clasei Stack, vedem cele doua tipuri de operatii care se executa in mod normal intr-un constructor:

Privind la implementarea destructorului clasei Stack, vedem ca principala atributie a unui destructor este aceea de "a face curatenie" la distrugerea unui obiect, si anume sa dezaloce toate resursele care au fost alocate dinamic in acel obiect.

Atentie: Constructorii (si nu alte metode) sunt locul in care trebuie plasata initializarea unui obiect. Daca se ignora implementarea constructorilor pentru o clasa, instantele acelor clase vor fi neinitializate! Tot astfel, pentru orice clasa care contine atribute ce au fost alocate dinamic in constructor, trebuie implementat explicit destructorul in care sa aiba loc eliberarea memoriei alocate.
 
 

Programare Structurata vs. Programare Obiectuala. Discutie comparativa.

In cele ce urmeaza vom compara implementarea de mai sus a stivei  cu implementarea intr-o maniera procedurala (in C) a aceleeasi stive.  Cele doua implementari pe baza carora vom purta discutia sunt prezentate aici.

In programarea structurata (procedurala), multe programe au bibliotecile si modulele organizate in jurul unei structuri de date, cum este si stiva din exemplul nostru: datele sunt stocate intr-o structura si exista un numar de functii (proceduri) strans cuplate cu respectiva structura, care opereaza pe variabile "instante" ale structurii. Caracteristica fundamentala a versiunii procedurale a programului o reprezinta centrarea lui in jurul functiilor. Cu alte cuvinte obiectele de date trebuie sa fie "la indemana" functiilor, in loc ca functiile sa fie "la indemana" datelor. Aceasta perspectiva se reflecta la nivelul implementarii prin aceea ca functiilor care opereaza asupra unei structuri de date trebuie sa li se transmita explicit instante ale structurii, ca parametri.

In varianta obiectuala lucrurile stau exact invers, intrucat din cauza cuplarii lor stranse cu structura de date, privim functiile ca apartinand structurii. Astfel, In loc sa mai transmitem structura Stack ca parametru fiecarei functii asociate, plasam aceste functii in interiorul structurii. Deoarece vor apartine structurii, aceste functii se vor numi functii membru.

Observati ca functiile membru nu fac referire la vreun pointer spre structura de date,  si nici macar nu acceseaza explicit vreo instanta a structurii de date, asa cum se intampla in cazul implementarii procedurale. Acest lucru se datoreaza faptului ca interiorul unei clase este considerat un domeniu aparte, iar functiile pot fi privite ca existand inauntrul instantelor structurii.

In programul C++ fiecare declaratie a unei "variabile" de tip Stack aloca spatiu undeva in memorie, la fel cum se face si pentru structurile din C. De exemplu, instantei q din main i se aloca spatiu la executie. Aceasta instanta se numeste obiect al clasei Stack. Functiile membru ale unei clase sunt aplicate obiectelor sale. Expresiile din aceste functii care se refera la membri ai clasei utilizeaza datele alocate fizic pentru obiectul care le apeleaza.
 
 

Clasele in C++

Clasele sunt create utilizandu-se asa cum am vazut deja cuvantul-cheie class. Clasele reprezinta mecanismul fundamental de abstractizare utilizat in C++. Declararea a unei clase defineste un nou tip care uneste cod si date, si care apoi este folosit pentru a declara (instantia) obiecte din clasa respectiva.  Putem spune ca o clasa este o abstractizare logica, in timp ce un obiect are o existenta fizica.

Din punct de vedere sintactic constructia classeste similara cu constructia struct. Iata forma generala a unei declaratii de clasa care nu mosteneste nici o alta clasa:

respectiv modul in care se instantiaza obiecte ale clasei:
nume-clasa lista de obiecte;
Din declaratiile de mai sus se desprind urmatoarele observatii: Notatia UML pentru Clase si Obiecte
Pornind de la ideea ca orice clasa are o identitate, o structura si un comportament, o clasa este reprezentata in UML printr-un dreptunghi impartit in trei compartimente, astfel:

Notatia UML pentru obiecte este data de un dreptunghi simplu, in interiorul caruia apare inscris numele obiectului si/sau a clasei in una din urmatoarele trei forme figurate mai jos:




Specificatorii de Acces
Asa cum am vazut in exemplul de mai sus, pentru a implementa mecanismul de ascundere a informatiei (incapsulare a datelor) au fost introdusi in limbajul C++ un numar de specificatori de acces prin care se stabilesc in mod diferentiat drepturile de acces asupra membrilor unei clase. Acesti specificatori de acces sunt in numar de trei si anume: private, public si protected. Aparitia unuia dintre aceste cuvinte-cheie urmat de simbolul doua-puncte (":") determina gradul de protectie pentru toti membrii care sunt declarati in continuare, pana la sfarsitul clasei sau pana la intalnirea urmatorului specificator de acces. Aceste atribute de protectie au urmatoarea semantica:

Functii Inline
In exemplul prezentat anterior, la implementarea metodei Stack::pop( ), in antetul metodei apare cuvantul-cheie inline. O astfel de functie este numita functie inline.

Ce sunt de fapt functiile inline? Ele sunt functii al caror cod va fi expandat la fiecare apel al lor: compilatorul, in loc sa genereze o secventa clasica de apel, pur si simplu va insera in locul respectiv tot corpul de instructiuni al functiei. Cu alte cuvinte functiile inline vin sa inlocuiasca intr-o maniera eleganta macro-urile din C.
Avantajele folosirii functiilor inline fata de macro-uri sunt:

Utilizarea inteligenta a functiilor inline conduce la o crestere a performantelor programelor d.p.d.v. al timpilor de rulare, datorita eludarii mecanismului de apel al functiilor care este costisitor.

Cand declaram o functie inline? Dezavantajul utilizarii functiilor inline consta in cresterea dimensiunii codului din cauza duplicarii instructiunilor. Asadar vom declara inline doar functii  de dimensiuni reduse si care au un impact major asupra performantelor programului. Are sens sa declaram o functie inline atunci cand executia instructiunilor de apel, respcetiv de revenire din functie ar egala sau depasi ca timp executia instructiunilor propriu-zise ale functiei.
Declararea inline a unei functii membru dintr-o clasa se poate face explicit, ca in exemplul nostru, sau implicit prin includerea definitiei functiei in direct in declaratia clasei, ca mai jos:
 

class Stack {
public:
     ....
  int pop() {
    return items[sp--];
  }
  ....
};

Constructorii
Doua idei importante care au stat in spatele proiectarii limbajului C++ au fost:

Aceste doua idei stau la baza definirii constructorilor si a destructorilor.

Asa cum am vazut deja in primul exemplu o functie constructor este o functie-membru speciala care are intotdeauna acelasi nume cu clasa in care este definita si care are ca principal scop initializarea starii obiectelor la crearea lor.
Fiecare clasa are cel putin 2 (doi) constructori.

Caracteristici
Se pot enumera urmatoarele caracteristici ale constructorilor:

Tipuri de constructori
In primul nostru exemplu apareau doi constructori pentru clasa Stack: unul fara parametri, si celalalt avand un parametru. Ei ilustreaza doua din cele trei tipuri de constructori ce pot exista, aratand in acelasi timp ca distinctia intre diferitele tipuri de constructori este realizata la nivelul listei de parametri. Destructorul unei Clase

Complementul functiei-constructor este destructorul. In multe cazuri, un obiect va trebui sa efectueze o anumita suita de actiuni atunci cand urmeaza sa fie distrus. Aceasta suita de actiuni constituie activitatea destructorului.Un obiect se distruge in una din urmatoarele trei situatii:
                              - cand este parasit blocul in care a fost creat (obiecte locale)
                              - la terminarea programului (obiecte globale)
                              - cand se aplica operatorul delete  (obiecte create dinamic cu new).
In cazul in care nu este definit explicit un destructor, compilatorul va genera automat unul cu corpul de instructiuni vid .
Cand trebuie sa definim un destructor? Definim explicit un destructor atunci cand la crearea obiectului au fost alocate prin program resurse. Cele mai intalnite alocari sunt alocarile dinamice de memorie si deschiderea de fisiere. Astfel, in destructor se dezaloca memoria si se inchid fisierele deschise.

Atentie:  Functiilemalloc, respectiv free utilizate in C pentru alocarea si dezalocarea de memorie nu determina lansarea in executie a constructorilor, respectiv a destructorului! In locul lor, in C++ folosim operatorii new si delete, despre care vom vorbi in lucrarea urmatoare.

Caracteristici
Se pot enumera urmatoarele caracteristici ale destructorilor:


Clase si Structuri
Spuneam mai sus ca exista o relatie de similitudine intre constructiille class si struct, ultima fiindu-ne cunoscuta din C. Limbajul C++ accepta declaratiile de tip struct pastrand atat sintaxa cat si semantica din C. Constructiile de tip struct din C++ au fost insa extinse pentru a se comporta la fel ca si cele de tip class. Singura diferenta intre struct si class este legata de drepturile de acces implicite: daca pentru classmembrii sunt implicit privati, pentru structmembrii sunt considerati implicit publici.

In practica exista o regula clara privind folosirea uneia sau a celeilalte constructii: cuvantul-cheie classe folosit pentru a sublinia definirea unui TDA (tip de date abstract), si nu doar a unei agregari (grupari) de date. Asadar, atunci cand sunt grupate impreuna atat date cat si functii se utilizeaza class; cand se grupeaza doar date se foloseste struct.
 
 

Functii si Clase "Friend"

Este posibil sa permitem unei functii care NU este membru al unei clase sa aiba acces nerestrictiv la toti membrii acelei clase (inclusiv la cei privati), declarand respectiva functie ca prieten (friend) al clasei. Pentru a face acest lucru trebuie ca prototipul functiei, precedat de cuvantul-cheie friend sa fie inclus in declaratia acelei clase.


Vom extinde exemplul cu stiva, de mai sus, incluzand o declaratie ca friend a unei functii care concateneaza doua stive date ca parametru (asezand-o pe cea de-a doua peste prima):
 
class Stack {
public:
     ....
  friend void StackMerge(Stack&, Stack&);
};

....

void StackMerge(Stack &s1, Stack &s2) {
  for(int i = 0; i <= s2.sp; i++)
  s1.items[s1.sp+1+i] = s2.items[i]; 
  s1.sp += (s2.sp + 1); 
}

....

void main(void) {
  Stack q;
  Stack p(5);

  q.push(1); q.push(2);
  p.push(3); p.push(4);
  StackMerge(q, p);
}

Se observa din acest exemplu faptul ca atat implementarea cat si apelul functiei StackMerge nu contin nimic spectaculos, cu exceptia faptului ca in implementarea functiei este violata regula de incapsulare a obiectelor s1 si s2 ale clasei Stack, accesandu-se direct detaliile de reprezentare ale clasei Stack.

Trebuie facuta observatia ca in acest exemplu nu s-a castigat nimic facand ca StackMerge sa fie functie friend, ea putand la fel de bine sa fie o functie membru. Exista insa conditii in care existenta functiilor friend se justifica: este vorba in special de supraincarcarea anumitor operatori (vezi Lucrarea 4 - Supraincarcarea Functiilor si a Operatorilor) sau de crearea anumitor tipuri de functii I/O (vezi Lucrarea 7 - Sistemul de I/O in C++ . Streamuri).

Sa vedem acum CINE poate sa primeasca statutul de friend al unei clase:

class AClass;

class AnotherClass {
    public:
        void AFunction(. . .);
    //. . .
};
class AClass{
    friend void AnotherClass::AFunction(. . .);
    //. . .
};

In acest caz poate sa apara si situatia particulara in care dorim ca TOATE functiile din clasa AnotherClass sa aiba statut de friend pentru clasa AClass. Spunem atunci ca AnotherClass este o clasa friend pentru AClass. Limbajul C++ ne pune la dispozitie o notatie care permite declararea unei intregi clase ca friend, in loc de a insira toate functiile ei explicit:

class AClass{
    friend class AnotherClass;
    //. . .
};
Decizia de a declara o clasa ca friend al altei clase se aplica atunci cand intre cele 2 clase exista relatii de dependenta reciproca, ce nu pot fi modelate cu ajutorul mostenirii. Exemplu: o structura de arbore binar se poate modela cu ajutorul a 2 clase: Nod si Arbore (aceasta din urma ar modela inlantuirea arborescenta). Pentru eficientizarea operatiilor definite in Arbore, se va declara aceasta clasa ca friend al clasei Nod.

Elemente caracteristice ale statutului de friend:


 
 

Membrii Statici
Un tip aparte de membri ai unei clase sunt membrii statici. Atat  functiile membru cat si datele membru (atributele) unei clase pot fi declarate static.

Variabile Membru Statice
Daca declaratia unei variabile membru este precedata de cuvantul-cheie static, atunci va exista o copie unica a acelei variabile care va fi folosita in comun de catre toate obiectele instantiate din respectiva clasa. Spre deosebire de variabilele membru obisnuite, pentru variabilele statice nu sunt create copii individuale ale acestora pentru fiecare obiect in parte. Accesarea unei variabile statice se face folosind numele clasei si operatorul de specificare a domeniului ("::").

Atentie: Cand declarati o data membru ca fiind static intr-o clasa, ea nu este inca definita! Cu alte cuvinte, prin declarare nu se aloca memorie, acest lucru facandu-se doar prin definire. Pentru a defini o variabila membru statica, aceasta trebuie prevazuta cu o definire globala undeva in afara clasei. Variabilele statice (ca si functiile membru statice de altfel) pot fi utilizate indiferent daca exista sau nu instante ale clasei respective. De aceea, initializarea unei variabile statice NU poate cadea in sarcina constructorilor clasei (constructorii, se stie, se executa doar la momentul generarii unei instante). Constructorii pot, insa, sa modifice valorile variabilelor statice (de exemplu, pentru a contoriza numarul instantelor unei clase, create la executia unui program).

Variabilele membru statice sunt folosite cel mai adesea pentru a asigura controlul accesului la o resursa comuna (ex. scrierea intr-un fisier).
Acest tip de variabile se mai folosesc si pentru a stoca informatii comune unei intregi clase de obiecte.

Functii Membru Statice
Si functiile membru pot fi declarate ca statice. In C++ o functie membru statica se comporta asemanator cu o functie globala al carei domeniu este delimitat de clasa in care este definita.

Functiile membru statice pot avea urmatoarele intrebuintari:

Vom ilustra utilizarea atributelor si a metodelor statice prin exemplul unei clase in care se contorizeaza numarul de apeluri ale metodelor acelei  clase. Fiecare functie membru va incrementa o variabila statica de tip contor. Una sau mai multe functii membru statice vor fi utilizate pentru a reseta, returna sau afisa valoarea contorului.
 
class X {
    public:
        void foo() {fooCounter++; . . . }
        static void printCounts() {
            printf("foo called %d times \n", fooCounter);
        }
    private:
        static int fooCounter;
    //. . .
}

int X::fooCounter = 0; //initializarea unei variab. statice

int main() {
  printCounts();    // Eroare! (daca nu exista intr-adevar
                    // o functie globala cu acest nume
  X::printCounts(); // ok!
}


Intrebari si Probleme

    1.  Modificati clasa Stack, renuntand la unul dintre constructorii definiti initial, in asa fel incat aceasta sa nu afecteze programul principal.
    2. Scrieti un program de test in C++ in care sa vizualizati diferitele situatii in care sunt apelate fiecare tip de constructori , precum si mecanismul de apel al destructorului. Vizualizarea se va face prin afisarea de mesaje din corpul constructorilor resp. al destructorului.
    3.  Considerand definitia propusa initial pentru clasa Stack,spuneti care este greseala care va face ca secventa de mai jos sa nu aiba efectul scontat, si anume acela de a obtine doua stive (r si q) cu continut asemanator.
 

main() {
  Stack q(5);  //cream o stiva de cinci elemente

  q.push(1); q.push(2);  // ... si punem in ea 
  q.push(3); q.push(4);  // patru elemente

  Stack r = q; // cream o noua stiva initializata prin 
                //stiva initiala
}

 Modificati clasa Stack in asa fel incat sa se intample ceea ce ne-am propus.
    4. Implementarea stivei propusa mai sus nu trateaza deloc erorile (exceptiile) tipice pentru o stiva, si anume: push( ) intr-o stiva plina, resp. pop( ) pe o stiva goala. Modificati clasa Stack in asa fel incat sa fie tratate cele doua erori, si sa se contorizeze la nivelul clasei si nu al obiectelor numarul de astfel de erori generate in timpul executiei de catre de toate instantele clasei Stack.
 
 

Bibliografie

[Cop94]    J. Coplien  - "Advanced C++ Programming Styles and Idioms",  Addison-Wesley, 1991
[Sch97]    H. Shildt - "C++. Manual complet", Editura TEORA,  1997

Hosted by www.Geocities.ws

1