Sunteți pe pagina 1din 55

CAPITOLUL 3

BAZELE PROGRAMARII ORIENTATE PE OBIECTE


3.1. Elementele programarii orientate pe obiecte
3.1.1. Privire generala asupra tehnicilor si limbajelor de programare
3.1.2. Abstractizarea datelor
3.1.3. Conceptele Orientarii pe Obiecte
3.1.4. Implementarea tipurilor abstracte de date in C/C++
3.1.5. Modul de accesare a datelor in C++
3.2. Implementarea Programarii Orientate pe Obiecte in C++
3.2.1. Privire generala asupra limbajului C++
3.2.2. Incapsularea, constructori si destructori de obiecte
3.2.3. Polimorfismul si supraincarcarea
3.2.4. Mostenirea si clase derivate
3.2.5. Definirea claselor si obiectelor

3.1. Elementele programarii orientate pe obiecte


3.1.1. Privire generala asupra tehnicilor si limbajelor de programare
In general vorbind, putem distinge urmatoarele trepte de invatare pe care le adopta o
persoana care invata sa programeze:
programare nestructurata;
programare procedurala;
programare modulara;
programare orientata pe obiect.
Programarea nestructurata
De obicei, persoanele invata sa programeze scriind programe mici si simple care constau
doar din programul principal. Aici, program principal inseamna o secventa de comenzi sau
instructiuni care modifica date care sunt globale pe intregul program. Programul principal
opereaza direct asupra datelor globale.
Aceasta tehnica de programare prezinta foarte multe dezavantaje o data ce programul
devine suficient de mare. De exemplu, daca aceeasi secventa de instructiuni este necesara in
diferite locatii din cadrul programului, secventa trebuie copiata. Aceasta a condus la ideea de
a extrage aceste secvente, carora sa li-i se dea o denumire si sa se asigure o tehnica de apelare
si revenire din aceste proceduri.
Programarea procedurala
Prin programarea procedurala se pot combina secventele de instructiuni cu revenire intrun singur loc. Un apel de procedura este utilizat pentru a invoca procedura. Dupa ce secventa
este procesata, fluxul de control continua imediat dupa pozitia din care s-a facut apelul.
Cu introducerea parametrilor ca si a procedurilor de proceduri (subrutine), programele pot
fi scrise acum mai structurat si cu mai putine erori. De exemplu, daca o procedura este
corecta, de fiecare data cand este utilizata ea va produce rezultate corecte. In consecinta, in
cazuri de eroare se poate reduce domeniul de cautare la acele locuri care nu s-au dovedit
corecte.
Acum un program poate fi privit ca o secventa de apeluri de procedura. Programul
principal raspunde de trimiterea datelor ca parametrii spre apelurile individuale, datele sunt
procesate de catre proceduri si, o data programul terminat, sunt prezentate datele rezultate.
Astfel, fluxul de date poate fi ilustrat ca un graf ierarhizat, sau ca un arbore.
In rezumat, acum avem un singur program care este divizat in bucati mici denumite
proceduri. Pentru a activa utilizarea procedurilor generale sau grupurilor de proceduri si in
alte programe, ele trebuie sa fie disponibile separat. Din acest motiv, programarea modulara
permite gruparea procedurilor in module.
Programarea modulara
Prin programarea modulara, procedurile cu o functionalitate comuna sunt grupate in
module separate. Asadar, un program nu mai consta numai dintr-o singura parte. Acum este
impartit in cateva parti mai mici care interactioneaza prin apelurile de procedura si care
formeaza intregul program. Programul principal coordoneaza apelurile de proceduri din
module separate si trimite ca parametrii datele corespunzatoare.
Fiecare modul poate avea propriile sale date. Aceasta permite fiecarui modul sa-si
gestioneze o stare interna care este modificata de apelurile la procedurile acestui modul.
Oricum, exista o singura stare pe modul si fiecare modul exista cel mult o data in intregul
program.

Decuplarea datelor si operatiilor conduce de obicei la o structura bazata pe operatii si nu


pe date. Modulele grupeaza impreuna operatii comune (cum sunt operatiile de tipul list_...()).
Apoi, aceste operatii se utilizeaza prin furnizarea explicita de date pentru ele, date asupra
carora trebuie sa opereze. Structura de modul rezultata este orientata spre operatii si nu spre
datele actuale. Se poate spune ca operatiile definite sprecifica datele de utilizat.
Programarea orientata pe obiecte
In programarea orientata pe obiecte, structura este orgaizata prin date. Se aleg
reprezentarile datelor care se potrivesc cel mai bine cerintelor. In consecinta, programele sunt
structurate pe date si nu pe operatii. Astfel, este exact invers: Datele specifica operatiile
valide. Acum modulele grupeaza impreuna reprezentarile datelor.
Programarea orientata pe obiecte rezolva cateva dintre problemele mentionate anterior. In
contrast cu celelalte tehnici, acum avem o panza de paianjen de obiecte care interactioneaza,
fieare gestionandu-si propria stare. Obiectele programului interactioneaza prin trimiterea de
mesaje unul altuia.
Sa consideram un exemplu de programare care proceseaza liste multiple de date. Aici
problema programarii modulare este ca trebuie creati si distrusi in mod explicit handler-i
listei. Apoi se utilizeaza procedurile fiecarui modul pentru a modifica fiecare dintre handler-i.
Din contra, in programarea orientata pe obiect putem avea oricate obiecte lista dupa
nevoie. In loc sa se apeleze o procedura careia, ii trebuie trimis handler-ul corect, am putea
trimite direct un mesaj obiectului lista in cauza. In general vorbind, fiecare obiect isi
implementeaza propriul modul permitand, de exemplu, coexistenta mai multor liste.
Fiecare obiect este responsabil cu propria initializare si distrugere corecta. In consecinta,
nu mai este necesar apelul in mod explicit al procedurilor de creare si terminare.
Chiar daca ar putea parea tot o programare modulara, exista caracteristici suplimentare ale
orientarii pe obiecte care fac din aceasta o noua tehnica de programare.
Limbaje de programare
Principalele familii de limbaje de programare orientate pe obiecte sunt urmatoarele:
familia SIMULA: limbaje OO textuale;
familia SMALLTALK: limbaje OO netextuale;
limbaje paralele (Actor).
In fig.3.1. se prezinta structura familiilor de limbaje de programare.

Fig.3.1. Structura familiilor de limbaje de programare


3

Familia Simula este o familie de limbaje OO textuale. Din aceasta familie fac parte
limbajele: Simula, C++ si Eiffel.
Simula este derivata din Algol, este cel care introduce notiune de clasa si de compilare.
C++ a fost dezvoltat de catre Stroustrup in 1983 si de atunci a fost cel mai utilizat limbaj
OO. Cu toate acestea, probabil cea mai mare utilizare a sa este in programare structurata. Are
avantajele de a fi un limbaj rapid, eficient si ofera putere (si probleme) programatorului. Ca
dezavantaje amintim inflexibilitatea, tendinta de a fi greu de citit si de a propaga erorile.
Eiffel a fost dezvoltat de Meyer in 1985, fiind un adevarat limbaj OO. Este un limbaj
formal din punct de vedere sintactic (ca ADA) si poate fi compilat in C pentru portabilitate.
Familia Smalltalk este o familie de limbaje OO netextuale. Din aceasta familie fac parte
limbajele: Smalltalk, Objective C si Self.
Smalltalk a fost dezvoltat de Kay&Goldberg din 1972 pana in 1980 ca o derivatie din
Lisp si Simula. Este un limbaj semicompilat, care introduce conceptul de metaclasa (adica,
clasele sunt instante ale unei clase, denumita Metaclasa). Prezinta avantajul claritati si
simplitatii conceptuale si dezavantajul mediului sofisticat (include sistem de operare, editoare
etc.). Smalltalk are patru axiome de baza:
fiecare entitate (inclusiv clasa) este un obiect;
fiecare obiect este o instanta a clasei;
fiecare clasa este subclasa a unei alte clase (cu exceptia clasei Object);
fiecare obiect este activat prin primirea unui mesaj.
Smalltalk are doar patru entitati predefinite:
clasa Object
metaclasa Class
clasa Integer
clasa Block
Objective C a fost dezvoltat de Brad Cox, ca o combinatie intre C si Smalltalk. Poate fi
folosit ca limbaj tiparit/compilat sau netiparit/interpretat.
Self a fost dezvoltat de Ungar&Smith in 1987, ca un limbaj prototip. Nu face distinctie
intre obiecte si clase, adica este un limbaj lipsit de clase. De exemplu, definim o pisica gri
pe nume Moggy, daca avem o alta pisica numita Whiskers care este negru & alb, atunci o
creem spunand Whiskers este ca Moggy dar negru & alb. Nu se face distinctie nici intre stare
si comportament. Se poate realiza dezvoltare rapida, executie rapida, dar foarte sigura.
Limbajele paralele (Actor) sunt limbaje in care actorii sunt fie obiecte fie procese.
Actorii sunt atat stari cat si comportament.
In concluzie, nu exista un limbaj de programare orientat pe obiect ideal. Principalele doua
sunt C++ si Smalltalk. C++ asigura rulare eficienta, dar este dificil de invatat si utilizat corect.
Smalltalk, in schimb, este usor de utilizat, dar se invata greu si ruleaza mai lent.
3.1.2. Abstractizarea datelor
Pentru a prezenta modul de abstractizare a datelor, vom introduce notiunea de tip abstract
de date (ADT = Abstract Data Type).
Tipul de date este o colectie de elemente de date corespunzatoare plus operatiile care pot
fi realizate asupra valorilor tipului de date.
Tipurile pot fi:
predefinite (definite de catre limbaj), cum sunt matricile, structurile, intregistrarile
sau clasele;
definite de programator, ADT, cum sunt listele, stivele sau cozile.
Tipurile abstracte de date pot fi privite din mai multe puncte de vedere astfel:
4

Aplicatie: utilizarea intr-un program particular (cutie variata);


Abstract (Logic): organizare vazuta de utilizator (cutie neagra);
Implementare (fizic): metode de codare utilizate pentru reprezentarea datelor si
operatii (cutie deschisa).
De exemplu o matrice bidimensionala poate fi privita din mai multe puncte de vedere
astfel:
Din punctul de vedere al aplicatiei: labirint, puncte pe o suprafata;
Din punctul de vedere logic: tablou, matrice;
Din punctul de vedere fizic:
o stocat secvential (implica mapare logica si fizica);
o limite de index (L1 .. U1, L2 .. U2);
o lungime = (U1-L1+1)* (U2-L2+1).
Metode de accesare
bazate pe coloane: toate elementele dintr-o coloana sunt stocate unul dupa altul
(de exemplu, limbajul FORTRAN);
bazate pe linii: toate elementele dintr-o linie sunt stocate unul dupa altul (de
exemplu, limbajul C, PASCAL).
In cazul accesarii bazata pe linii, locatia elementului [i][j] se determina cu formula:
+ [(U 2 L 2 + 1) (i L1) + ( j L 2 )] dim ensiunea elementului
unde:
= adresa de baza a matricei;
(U2-L2+1) = dimensiunea liniei;
(i-L1) = numarul de linii peste care se trece;
(j-L2) = numarul de coloane peste care se trece.
In fig.3.2. se prezinta grafic cele doua moduri de adresare.

Fig.3.2. Moduri de adresare


Tipuri noi de date introduse de programator
Tipurile noi de date introduse de programator includ multimi obiecte de date si multimi
de operatii abstracte. ADT-urile sunt necesare pentru:
abstractizarea realitatii, in vederea filtrarii detaliilor neesentiale;
oferirea de ajutor pentru a depasi complexitatea, datorita limitarilor specific
umane;

permiterea focalizarii asupra vederii de ansamblu;


promovarea intelegerii problemei, proiectarii mai curate, mentinerii usoare a
sistemelor.
Obiectele sunt utilizate pentru a modela mai exact realitatea si pentru a reduce prapastia
semantica dintre realitate si model. Astfel, se modeleaza realitatea ca un numar de obiecte ce
interactioneaza intre ele.
Abstractizarea datelor inseamna proiectarea obiectelor de date abstracte si a operatiilor cu
aceste obiecte. Abstractizarea in programare realizeaza separarea proprietatilor logice de
implementarea tipului de date.
Abstractizarea este procesul mental de selectare a unor caracteristici si proprietati si
excluderea altora care nu sunt relevante. Abstractizarea se realizeaza intotdeauna cu un
anumit scop, fiind posibile multe abstractizari diferite ale aceluiasi lucru. Cu toate acestea,
toate abstractizarile sunt descrieri incomplete ale realitatii.
Tipurile de abstractizare sunt:
clasificarea: grupeaza instante similare, adica preia proprietatile comune si le
ignora pe cele unice (fig.3.3.)

Fig.3.3. Clasificarea

agregarea: grupeaza multimi deosebite, adica ignora diferentele dintre partile


componente si se concentreaza asupra faptului ca ele formeaza un intreg (fig.3.4.)

Fig.3.4. Agregarea

generalizarea: grupeaza multimi similare, adica este asemanatoare clasificarii,


dar se aplica asupra multimilor (claselor) (fig.3.5.)

Fig.3.5. Generalizarea

In fig.3.6. se prezinta un exemplu de aplicare a celor trei tipuri de abstractizari.

Fig.3.6. Exemplu de abstractizari


Ascunderea informatiilor este utilizata in proiectarea de functii si tipuri noi de date.
Fiecare componenta trebuie sa ascunda cat se poate de multe informatii fata de utilizatorii
componentei, adica functii. Detaliile de implementare sunt ascunse de utilizator prin
asigurarea accesului printr-o interfata de comunicatii bine definita.
Incapsularea este legarea datelor si actiunilor intr-un asemenea mod incat proprietatile
logice ale datelor si actiunilor sunt separata de detaliile de implementare. Astfel, accesul la
ADT este restrictionat la o multime specificata de operatii posibile. Aceasta inseamna ca
utilizatorii nu au nevoia de a cunoaste si nu pot manipula direct elementele datelor.
Avantajele incapsularii sunt ca modificarile asupra operatorilor sau reprezentarilor de baza nu
afecteaza codul dintr-un client al tipului de date. De asemenea, incapsularea realizeaza
extinderea limbajelor de programare.
In fig.3.7. se prezinta nivele ADT.

Fig.3.7. Nivele ADT


Se pune problema care este diferenta dintre ascunderea informatiilor si incapsulare.
Ascunderea informatiilor este o chestiune ce tine de proiectarea programului si in multe cazuri
este un suport al limbajului prin functii si proceduri. Incapsularea este o chestiune ce tine de
proiectarea limbajului, deoarece o abstractizare este incapsulata efectiv doar cand limbajul
interzice accesul la informatiile ascunse in cadrul abstractizarii.
In fig.3.8. se prezinta un exemplu de ascundere a informatiilor, prin care se ofera o
interfata pentru clasa string. Detaliile de implementare a operatiilor sunt ascunse.

Fig.3.8. Ascunderea informatiilor pentru clasa String


7

Primul lucru cu care se confrunta o persoana atunci cand scrie un program este problema.
Tipic, suntem confruntati cu probleme din viata-reala si dorim sa ne facem viata mai
usoara realizand un program pentru aceste probleme. Oricum, problemele reale sunt neclare
si primul lucru care trebuie facut este sa se incerce intelegerea problemei pentru a separa
detaliile necesare de cele care nu sunt necesare. Adica, se incearca obtinerea propriei viziuni
abstracte, sau modelului, problemei. Acest proces de modelare este numit abstractizare.
Modelul defineste o viziune abstracta a problemei. Aceasta implica faptul ca modelul se
concentreaza doar asupra lucrurilor legate de problema si ca se incearca definirea
proprietatilor problemei. Aceste proprietati includ:
datele care sunt afectate;
operatiile care sunt identificate de catre problema.
Ca exemplu, sa consideram administrarea angajatilor dintr-o institutie. Seful administrativ
cere unui angajat sa creeze un program care permite administrarea angajatilor. Dar aceasta nu
specifica foarte bine ce informatii ii sunt necesare administratiei sau ce sarcini trebuie
realizate. Angajatii sunt persoane reale care pot fi caracterizate prin multe proprietati, cateva
fiind urmatoarele:
nume;
rang;
data nasterii;
numar social;
numar birou;
culoare par;
hobby-uri.
Cu siguranta, nu sunt necesare toate aceste proprietati pentru rezolvarea problemei
administratiei. Doar cateva dintre acestea sunt specifice problemei. In consecinta se poate crea
un model al unui angajat pentru problema. Acest model necesita doar proprietatile care sunt
necesare pentru satisfacerea cerintelor administratiei, de exemplu, nume, data nasterii si
numarul social. Aceste proprietati sunt denumite datele modelului (angajatului). In acest
moment s-au descris persoanele reale cu ajutorul unui model abstract.
Desigur, numai descrierea nu este suficienta. Trebuie sa fie definite cateva operatii cu
care administratia sa poata manipula angajatul abstract. De exemplu, trebuie sa existe o
operatie care sa permita crearea unui nou angajat o data ce este angajata o noua persoana. In
consecinta, trebuie identificate operatiile care trebuie sa poata fi realizate asupra unui angajat
abstract. De asemenea, se poate decide permiterii accesului la datele angatilor doar prin
intermediul operatiilor asociate. Aceasta permite asigurarea ca elementele statelor sunt
intotdeauna in starea corecta. De exemplu, se poate verifica daca este valida o anumita data
calendaristica introdusa.
In rezumat, abstractizarea este structurarea unei probleme neclare in entitati bine definite
prin definirea datelor si operatiilor lor. In consecinta, aceste entitati combina date si operatii.
Ele nu sunt decuplate una de alta.
Definirea si proprietatile ADT
Exemplul de mai sus arata ca abstractizarea creeaza o entitate bine-definita care poate fi
manevrata corect. Aceste entitati definesc structura de date a unei multimi de articole. De
exemplu, fiecare angajat administrat are un nume, o data de nastere si un numar social.
Structura de date poate fi accesata doar cu operatii definite. Acest set de operatii este
denumit interfata si este exportata de catre entitate. O entitate cu proprietatile descrise mai sus
este denumita tip abstract de date (ADT).
In fig.3.9. se prezinta un ADT care consta dintr-o structura abstracta de date si operatiile
aferente. Din exterior sunt vizibile doar operatiile si acestea definesc interfata.
8

Fig.3.9. ADT
O data ce este creat un nou angajat structura de date este umpluta cu valorile
corespunzatoare. Astfel se creeaza o instanta a angajatului abstract. Se pot crea oricate
instante ale unui angajat abstract sunt necesare pentru a descrie fiecare angajat persoana reala.
Definitie ADT
Un tip abstract de date (ADT) este caracterizat de urmatoarele proprietati:
1. Exporta un tip.
2. Exporta un set de operatii. Acest set este numit interfata.
3. Operatiile din interfata sunt singurul mecanism de acces la structura de date
ascunsa a tipului.
4. Axiomele si preconditiile definesc domeniul de aplicare al tipului.
Principiul de ascundere a structurii de date utilizate si asigurarea doar a unei interfete
bine-definite este cunoscut ca incapsulare.
Combinand structurile de date, operatiile si constrangerea de acces la date doar printr-o
intefata bine-definita, se permite alegerea structurilor de date potrivite pentru mediul
aplicatiei.
Deoarece ADT-urile ofera o viziune abstracta pentru a descrie proprietatile seturilor de
entitati, utilizarea lor este independenta de limbajul de programare. Fiecare descriere ADT
consta din doua parti:
Date: Fiecare parte descrie intr-un mod informal structura datelor utilizate in ADT
Operatii: Aceasta parte descrie operatiile valide pentru ADT-ul curent, adica,
descrie interfata sa. Folosim operatia speciala constructor pentru descrierea
actiunilor care se realizeaza o data ce este creata o entitate a ADT-ului curent si
operatia destructor pentru a descrie actiunile care trebuie realizate o data ce o
entitate este distrusa.
ADT-urile permit crearea de instante cu proprietati si comportament bine-definite. In
orientarea pe obiecte ADT-urile sunt denumite clase. Asadar o clasa defineste proprietatile
obiectelor care sunt instantele dintr-un mediu orientat pe obiecte.
ADT-urile definesc functionalitatea prin punerea accentului pe datele implicate, structura,
operatiile, axiomele si preconditiile lor. In consecinta, programarea orientata pe obiecte este
programarea cu ADT-uri: combinarea functionalitatii diferitelor ADT-uri pentru a rezolva o
problema. Asadar instantele (obictele) ADT-urilor (claselor) sunt create, distruse si utilizate in
mod dinamic.
Implementarea tipurilor abstracte de date
ADT-urile sunt o viziune abstracta pentru definirea propritatilor unui set de entitati.
Limbajele de programare orientate pe obiecte trebuie sa permita implementarea acestor tipuri.
In consecinta, o data ce un ADT este implementat ne este disponibila o reprezentare
particulara a sa.
Diferite limbaje de programare aleg diverse reprezentari ale ADT-urilor, POO fiind cea
mai folosita.

Clase
O clasa este o reprezentare a unui ADT. Asigura detalii de implementare pentru structura
de date utilizata si pentru operatii. In continuare se prezinta un exemplu de ADT cu numele
Integer, pentru care se proiecteaza o clasa:
class Integer {
attributes:
int i
metods:
setValue(int n)
Integer addValue(Integer j)
}

In acest exemplu am folosit o notatie care nu este specifica limbajului de programare. In


aceasta notatie class { ... } inseamna definirea unei clase. In intereiorul acoladelor sunt doua
sectiuni attributes: si methods: care definesc implementarea structurii de date si a operatiilor
ADT-ului corespunzator. Din nou, facem distinctie intre cele doua nivele prin termeni diferiti:
la nivel de implementare vorbim de atribute care sunt elemente ale structurii de date la
nivelul ADT-ului. Acelasi lucru se aplica si pentru metode care sunt implementarea
operatiilor ADT-ului.
Definitie
O clasa este implementarea unui tip abstract de date (ADT). Ea defineste atributele si
metodele care implementeaza structura de date si operatiile ADT-ului.
Obiecte
Instantele claselor se numesc obiecte. In consecinta, clasele definesc proprietatile si
comportamentul seturilor de obiecte.
Obiectele sunt identificabile in mod unic printr-un nume. Asadar, pot exista doua obiecte
distincte care au acelasi set de valori.
Definitie
Un obiect este o instanta a unei clase. Poate fi identificat in mod unic prin numele sau si
defineste o stare care este reprezentata prin valorile atributelor sale la un anumit moment.
Starea obiectului se schimba in functie de metodele care ii sunt aplicate. Ne referim la
secventele posibile de modificare a starii ca fiind comportamentul obiectului.
Definitie
Comportamentul unui obiect este definit prin setul de metode care i se pot aplica.
Acum cunoastem doua concepte de baza introduse de orientarea pe obiecte, si anume
clasa si obiectul. Programarea orientata pe obiecte este asadar implementarea tipurilor
abstracte de date sau, cu alte cuvinte, scrierea claselor.
Mesaj
In timpul rularii, instantele acestor clase, obiectele, realizeaza sarcina programului
modificandu-si starile. In consecinta, programul care ruleaza poate fi privit ca o colectie de
obiecte. Problema care se pune priveste modul de interactiune dintre aceste obiecte. Pentru
aceasta se introduce notiunea de mesaj.

10

Un program care ruleaza este o multime de obiecte in care obiectele sunt create, distruse
si interactioneaza intre ele. Aceasta interactiune se bazeaza pe mesaje care sunt trimise de la
un obiect la altul cerandu-i-se destinatarului sa aplice o metoda asupra sa.
Pentru a intelege aceasta comunicare, ne intoarcem la exemplul cu clasa Integer. In
pseudolimbajul de programare putem crea noi obiecte si putem invoca metode pentru ele. De
exemplu, putem folosi:
Integer i;
i.setValue(1);

/* Defineste un nou obiect Integer i */


/* seteaza valoarea sa la 1 */

pentru a exprima acest fapt, ca obiectul Integer i trebuie sa aiba valoarea setata pe 1.
Acesta este mesajul Aplica metoda setValue cu argumentul 1 asupra obiectului i. Aceasta
notatie este utilizata si in C++; alte limbaje orientate pe obiecte ar putea utiliza alte notatii, de
exemplu ->.
Trimiterea unui mesaj care cere unui obiect sa aplice o metoda este similara cu un apel de
procedura din limbajele de programare traditionale. Oricum, in orientarea pe obiecte exista
o viziune a obiectelor autonome care comunica intre ele prin schimbul de mesaje. Obiectele
reactioneaza cand primesc mesaje prin aplicarea metodelor asupra lor. De asemenea, obiectele
pot si sa refuze executarea unei metode, de exemplu daca obiectul apelant nu are voie sa
execute metoda ceruta.
Definitie
Un mesaj este o cerere catre un obiect pentru a invoca una dintre metodele sale. Asadar,
un mesaj contine:
numele metodei
argumentele metodei.
Pentru a vedea un program ca o colectie de obiecte care interactioneaza intre ele este un
principiu fundamental in programarea orientata pe obiecte. Obiectele din aceasta colectie
reactioneaza la receptionarea mesajelor, modificandu-si starea in concordanta cu invocarea
metodelor care ar putea duce la trimiterea unor altor mesaje catre alte obiecte. Aceasta se
prezinta grafic in fig.3.10.

Fig.3.10. Un program continand patru obiecte


In fig.3.10. programul consta din patru obiecte. Aceste obiecte isi trimit mesaje unul
altuia, asa cum se indica prin sageti. Se poate observa ca al treilea obiect isi trimite lui insusi
un mesaj.

11

3.1.3. Conceptele Orientarii pe Obiecte


Orientarea pe obiecte reprezinta o strategie de dezvoltare a software-ului si poate fi
aplicata pentru analiza, proiectare si programare. Aceasta arata ca orientarea pe obiect
constitie o metodologie de baza in ingineria software.
Aceasta metodologie are la baza urmatoarele concepte:
Obiect si clasa;
Instanta si instantiere;
Trimiterea mesajelor si metode;
Abstractizare si incapsulare;
Asociere, mostenire si legare dinamica;
Polimorfism.
Obiectul reprezinta o entitate, are o identitate si incapsuleaza date (atribute) ca starea sa.
Cand i se cere realizeaza operatii, avand o interfata publica si o reprezentare interna privata.
Obiectul este un concept, abstractizare sau lucru cu limite si semnificatie exacta si care
joaca un rol in functie de o cerere de realizare a unei operatii.
Interfata descrie operatiile pe care le poate realiza un obiect.
Starea unui obiect cuprinde proprietatile statice (atributele) ale obiectului. Starile sunt in
numar fix si pot fi mutabile (pot fi modificate) sau nemutabile (nu pot fi modificate).
Comportamentul cuprinde proprietatile diamice (operatiile) ale obiectului. Aceste
proprietati sunt in numar fix. Operatiile modifica starea unui obiect (valorile atributelor).
Operatiile sunt invocate de mesaje de la alte obiecte.
Incapsularea este o modalitate care forteaza o separare intre interfata externa si
implementarea interna a unui obiect, asa cum se vede in fig.3.11.

Fig.3.11. Incapsularea
Incapsularea ascunde detaliile de implementare (ascunderea informatilor), care nu sunt
necesare si asigura modularitatea. Astfel, un obiect are o interfata vizibila (publica) si o
implementare ascunsa (privata).
Obiectele sunt puternic incapsulate. Din exterior un obiect poate fi manipulat doar prin
interfata sa publica, starea sa interna fiind protejata si putand fi modificata doar de obiect.
Incapsularea este o tehnica de minimizare a interdependentelor dintre modulele scrise
separat prin definirea de interfete externe stricte. Interfata externa se comporta ca un contract
intre un modul si clientii sai. Daca clientii depind doar de interfata, modulele pot fi
reimplementate fara a afecta clientul. Astfel efectele modificarilor pot fi limitate. (Synder,
1986)
Proprietatile obiectelor sunt:
fiecare obiect are o identitate unica, nemutabila si independenta de starea sau
comportamentul actual;
obiectele comunica prin raspunderea la stimuli care le invoca operatiile, adica
trimiterea de mesaje;
obiectele aleg cum/daca vor raspunde la un mesaj/stimul, adica obiectele sunt
independente.

12

Clasa defineste structura si comportamentul unei multimi particulare de obiecte si se


comporta ca un sablon sau amprenta. Un obiect trebuie sa fie o instanta o unei singure clase.
O clasa poate avea mai multe obiecte (instante).
Instantierea este activitatea de creare a unui obiect fiindu-i data clasa. Trebuie initializata
structura de date a obiectului (starea).
Obiectele interactioneaza intre ele prin trimiterea de mesaje. Un mesaj cere unui obiect sa
realizeze o operatie sau serviciu. Un mesaj consta dintr-un nume si orice argumente.
Clasa este o descriere a unui grup de obiecte care au comune: starea, comportamentul,
relatiile si semantica. Clasele sunt un mod convenabil de descriere a unei colectii de obiecte si
specifica ce valori sunt acceptabile intr-un context dat. Clasele pot fi vazute ca module foarte
coezive cuplate slab.
Atributele sunt o descriere a valorilor datelor continute de obiectele dintr-o clasa. Fiecare
atribut are:
nume unic in cadrul unei clase;
tip, care specifica domeniul de valori din care poate lua valori un atribut, domeniu
care poate fi continuu sau discret;
vizibilitate, putand fi public (+), protejat (#) sau privat (-);
valoare initiala, care este optionala;
multiplicitate, optionala, implicit fiind 1;
modificabilitate, care poate fi frozen (inghetat), addOnly (doar adaugare), no
constraint (fara constrangeri) (implicita).
O operatie a clasei este o functie sau transformare care poate fi aplicata asupra sau de
catre obiectele dintr-o clasa.
Operatiile sunt invocate de catre un mesaj trimis unui obiect. Semnatura operatiei este
numele operatiei (selector), numarul si tipurile argumentelor si tipul valorilor rezultate.
Vizibilitatea operatiei poate fi: public (+), protejat (#) sau privat (-). Efectele secundare apar
daca executia unei operatii modifica starea unui obiect. De exemplu, operatia de interogare nu
are efecte secundare.
Metoda este descrierea unei operatii (adica, codul program). Metoda este implementarea
unei operatii a unei clase. Este apelata ca raspuns la un mesaj trimis unui anumit obiect.
Metoda are acces complet la starea obiectului. Potrivirea unui nume de mesaj la o metoda se
numeste legare.
Operatia polimorfica este o operatie care poate avea cateva metode diferite. De exemplu,
avand clasa File si operatia Print, putem avea obiectele ASCII file si digitized file care au
operatiile ASCII file print method, respectiv digitized file print method.
In fig.3.12. se prezinta notatia grafica pentru cele prezentate anterior.

Fig.3.12. Notatia grafica a clasei


Legarea dinamica este alegerea metodei de executie a unei operatii pe baza clasei
obiectului. Legarea dinamica inseamna ca acelasi mesaj poate fi trimis spre obiecte diferite si
13

fiecare obiect poate lega mesajul de o metoda diferita, asa cum este definit de clasa sa.
Aceasta reduce cuplarea dintre obiecte si activeaza posibilitatea de includere si substituire.
Abstractizarea separa esentialul de detaliile complete. Clasele reprezinta abstractizari.
Ele asigura o viziune selectiva si simplificata a conceptelor reprezentate ca obiecte.
Clasele sunt utilizate pentru a descrie structura unui sistem. Pentru aceasta clasele trebuie
sa fie relationate sau conectate intre ele. Exista trei relatii de baza:
asocierea;
agregarea (compozitia);
mostenirea.
Clasele definesc structura si comportamentul unui sistem. Obiectele reprezinta sistemul
actual, asa cum se poate vedea in fig.3.13.

Fig.3.13. Clase si obiecte


Asocierea modeleaza o relatie dintre doua clase. De exemplu, asocierea lucreaza_pentru
dintre o clasa persoana si o clasa companie.
Asocierea este o descriere a unui grup de legaturi cu semantici comune, asa cum se
prezinta in fig.3.14. Implicit, asocierile sunt bidirectionale.

Fig.3.14. Asociere
Pot exista mai multe asocierei intre aceleasi doua clase, ca in fig.3.15.a., sau chiar in
aceeasi clasa, ca in fig.3.15.b.

Fig.3.15. Asocieri: a) intre doua clase; b) intr-o singura clasa


Asocierea poate avea mai multe grade, dupa cum urmeaza:
unara, leaga o clasa cu ea insasi (fig.3.16.a);
binara (fig.3.16.b);
ternara (fig.3.16.c);
de gradul n, leaga oricat de multe clase.

14

Fig.3.16. Asocieri: a) unara; b) binara; c) ternara


In practica, marea majoritate a asocierilor sunt binare.
Se pot realiza transformari intre asocieri ternare si asocieri binare, ca in fig.3.17.

Fig.3.17. Exemplu simplu de transformare


Un alt exemplu mai complex este prezentat in fig.3.18., cu solutiile a, b si c.

Fig.3.18. Exemplu complex de transformare


Solutia a nu poate face preciza ce limbaj foloseste Anna pentru fiecare proiect. Solutia b
nu poate certifica daca Anna foloseste doar limbajul Cobol pentru Contabilitate si daca John
foloseste doar C pentru Contabilitate (Accounting). Solutia c nu poate preciza daca Anna
foloseste C doar pentru CAD nu si pentru Contabilitate.
Multiplicitatea asocierii specifica restrictiile asupra numarului de obiecte din fiecare clasa
care pot fi legate de obiecte dintr-o alta clasa. In fig.3.19. se prezinta un exemplu, in care
15

fiecare curs poate fi predat de un singur profesor si un profesor poate sa predea cel mult un
curs intr-un anumit semestru.

Fig.3.19. Exemplu de multiplicitate


In fig.3.20. se prezinta notatia generala a multimplicitatii asocierii.

Fig.3.20. Notatia multiplicitatii asocierii


Cardinalitatea minima (min-card) este:
min-card(C1, A) este numarul minim de legaturi la care fiecare obiect din C1 poate
participa in A;
min-card(C1, A)=0 inseamna participare optionala;
min-card(C1, A)>0 inseamna participare obligatorie.
Cardinalitatea maxima (max-card) este:
max-card(C1, A) este numarul maxim de legaturi la care fiecare obiect din C1
poate participa in A.
Cardinalitati speciale sunt:
max-card=* inseamna limita superioara nelimitata ();
min-card=1 si max-card=1 inseamna ca se poate folosi unul singur;
min-card=* si max-card=* inseamna ca se pot folosi oricate;
max-card(C1, A)=1 si max-card(C2, A)=1 inseamna asociere unu la unu (1:1), asa
cum se prezinta in fig.3.21.a;
max-card(C1, A)=1 si max-card(C2, A)=* inseamna asociere unu la multi (1:N),
asa cum se prezinta in fig.3.21.b;
max-card(C1, A)=* si max-card(C2, A)=* inseamna asociere multi la multi (N:M),
asa cum se prezinta in fig.3.21.c.

Fig.3.21. Multiplicitatea asocierilor: a) 1:1; b) 1:N; c) N:M


Multiplicitatea asocierii de gradul n se defineste in functie de celelalte n-1 terminale.
16

Avand o asociere ternara intre clase (A, B, C), multiplicitatea terminalului C spune cate
obiecte C pot aparea in asocierea cu o anumita pereche de obiecte (A, B). In fig.3.22. se
prezinta un exemplu de asociere ternara, in care un student nu poate participa la acelasi curs
tinut mai multi profesori, dar un student poate participa la mai multe cursuri tinute de acelasi
profesor si un profesor poate preda mai multe cursuri.

Fig.3.22. Exemplu de asociere ternara


Agregarea (compozitia) reprezinta o relatie parte-intreg si este o forma mai puternica de
asociere. Timpul de viata al intregului impune timpul de viata al partilor. De exemplu, un
motor este parte-din o masina (fig.3.33.)

Fig.3.33. Exemplu de agregare


Daca o componenta poate exista independent de obiectul agregat din care este o parte
avem de-a face cu agregarea. In fig.3.34. se prezinta modul de reprezentare a agregarii.
Fig.3.34. Agregare
Daca o componenta nu poate exista independent de obiectul agregat din care este o parte
avem de-a face cu compozitie. In fig.3.35. se prezinta modul de reprezentare a compozitiei.
Fig.3.35. Compozitie
Exista cazuri in care nu este clar daca partile trebuie legate prin agregare sau prin
compozitie. In aceste cazuri alegerea depinde de modul in care se interpreteaza realitatea
si/sau cerintele aplicatiei.
Pentru aceasta alegere se tine seama de urmatoarele aspecte:
se utilizeaza expresia parte din pentru descrierea asocierii sau numele are;
daca operatiile intregului sunt aplicate automat si partilor;
daca valorile unor atribute se propaga de la intreg spre toate sau o parte din
componente;
exista o asimentrie intrinseca a asocierii in care clasa unui obiect este subordonata
celorlalte.
Nu este gresit daca se utilizeaza asocieri in locul agregarii.
Rolul este un terminal al asocierii. Pentru asocierile binare exista doua roluri, ca in
fig.3.36.

17

Fig.3.36. Asociere binara cu doua roluri


Calificatorul este un atribut sau o lista de atribute a caror valori sunt utile pentru
impartirea multimii de obiecte asociate cu un obiect de-a lungul unei asocieri.
Un obiect din clasa sursa impreuna cu o valoare a calificatorului selecteaza in mod unic o
parte dintr-o multime de obiecte ale clasei tinta, asa cum se poate vedea in fig.3.37.

Fig.3.37. Utilizarea calificatorului


Calificatorul se utilizeaza atunci cand dorim sa aratam baza pe care impartim multimea
tinta (in vederea implementarii).
Constragerile pot fi de tipul: ordonare (fig.3.38.a), submultime (fig.3.38.b) si generale
(fig.3.38.c).

Fig.3.38. Constrangeri: a) ordonare; b) submultime; c) generale


Poate fi definita o clasa a asocierii, ca in fig.3.39, careia i se poate asocia un atribut, dar
acesta este necesar in general doar in cazul relatiilor multi la multi.

Fig.3.39. Exemplu de clasa a asocierii


In fig.3.40. se prezinta un exemplu de diagrama de clasa cu asocieri.
Fig.3.40. Exemplu de diagrama de clasa
Mostenirea modeleaza relatia tip-de dintre clase si specifica faptul ca o clasa este o
extensie a unei alte clase. De exemplu, un autobuz este un tip-de vahicul.
O superclasa poate fi mostenita de catre o subclasa. Subclasa preia toate proprietatile
superclasei la care poate adauga si altele.

18

O superclasa este o generalizare si o subclasa este o specializare.


Generalizarea este o relatia intre o clasa si una sau mai multe versiuni rafinate. In fig.3.41.
se prezinta cele doua notiuni introduse mai sus pe un exemplu.

Fig.3.41. Generalizare si specializare


Discriminatorul este un atribut de tip enumerare care arata care proprietate a unei clase
este abstractizata printr-o generalizare.
Rolul mostenirii este sa permita utilizarea de ierarhii de clasificare, sa activeze utilizarea
interfetelor comune si sa activeze share-uirea implementarii prin extensie, si nu prin copiere si
editare.
Mostenirea este o forma a generalizarii care inseamna preluarea de proprietati de catre o
subclasa de la superclasa sa. De aceea, se recomanda extragerea similitudinilor (atribute si
operatii comune), punerea lor intr-o superclasa si mostenirea lor de catre subclase. Aceasta
tactica reduce redundanta, promoveaza reutilizarea, permite definirea proprietatilor si
comportamentului intr-un singur loc si un obiect al unei subclase poate fi substitut pentru un
obiect al superclasei.
In fig.3.42. se prezinta reprezentarea grafica a mostenirii. Se observa ca subclasa poate sa
adauge noi proprietati (atribute, operatii) si sa supraincarce metodele. Rezolvarea conflictelor
se face prin definirea ordinii de catre utilizator sau utilizarea unei ordini predefinite si
redefinirea numelor (doar pentru atribute).

Fig.3.42. Reprezentarea grafica a mostenirii


In fig.3.43. se prezinta cateva exemple de mostenire multipla.

19

Fig.3.43. Mostenire multipla


Clasa abstracta este o clasa care nu are instante directe (fig.3.44.). Este utilizata ca un
container pentru definitii, dar nu se declara instante ale clasei. Si operatiile pot fi abstracte,
adica se poate sa nu se specifice nici o metoda.

Fig.3.44. Clasa abstracta


Constrangerile posibile sunt: suprapunere/disjunct si complet/incomplet si combinatii.
Suprapunerea inseamna ca un obiect al superclasei poate fi un membru a mai mult de o
subclasa (fig.3.45.)

Fig.3.45. Suprapunerea
Disjunctia inseamna ca un obiect al superclasei este un membru a cel mult o subclasa
(fig.3.46.)

Fig.3.46. Disjunctia

20

Complet inseamna ca toate obiectele superclasei sunt membre si ale unor subclase
(fig.3.47.)

Fig.3.47. Complet
Incomplet inseamna ca unele obiecte ale superclasei nu sunt membre ale nici unei
subclase (fig.3.48.)

Fig.3.48. Incomplet
Combinatiile intre acestea pot fi: suprapus si incomplet (fig.3.49.a); suprapus si complet
(fig.3.49.b); disjunct si complet (fig.3.49.c); disjunct si incomplet (fig.3.49.d).

Fig.3.49. Combinatii de constrangeri: a) suprapus si incomplet; b) suprapus si complet;


c) disjunct si complet; d) disjunct si incomplet
Legatura este o relatie fizica sau conceptuala intre instantele obiectului, asa cum se vede
si in fig.3.50.

Fig.3.50. Legatura
21

Polimorfismul este activat de catre legarea dinamica si mostenire si permite trimiterea


unui mesaj catre mai multe tipuri de obiecte. De asemenea, permite interschimbarea
obiectelor care au in comun o interfata publica.
O superclasa poate defini o interfata comuna. Subclasele mostenesc interfata comuna si
specializeaza metodele corespunzatoare. Un obiect al subclasei poate fi utilizat acolo unde a
fost specificat un obiect al superclasei.
In fig.3.51. se prezinta modul de reprezentare grafica a proprietatii de introducere a
obiectelor (pluggability).

Fig.3.51. Pluggability
Componentele noi de tip pluggable pot fi adaugate fara a modifica utilizatorii
componentelor. Codul proiectat sa utilizeze interfetele comune ramane neschimbat.
In concluzie, orientarea pe obiecte este un mod mai eficient de producere a software-ului.
Software-ul trebuie sa fie flexibil si vechile metode structurate nu mai sunt suficient de bune.
Probleme se schimba, insa domeniul acestora tinde sa fie stabil. Cerintele clientului se
modifica, dar mediul sistemului este mai stabil. Deci, trebuie modelate si construite
componente reutilizabile si cu domeniu extensibil.
O buna proiectare orientata pe obiecte se concentreaza asupra construirii de componente
reutilizabile specifice domeniului. Solutiile specifice problemei se realizeaza prin asamblarea
componentelor domeniului.
Dezvoltarea structurata se bazeaza pe principiul descompunerii functionale de sus in jos a
unei probleme, cum se vede in fig.3.52.

Fig.3.52. Proiectarea structurata


Metoda dezvoltarii structurate se concentreaza asupra problemei si nu asupra domeniului
problemei, astfel ca atunci cand se modifica problema, se modifica si intreaga solutie,
deoarece nu exista blocuri solide de constructie.
Descompunerea functionala ne spune ce trebuie sa faca produsul, nu care este domeniul
problemei. Aceasta formeaza fundatia cea mai proasta posibila pentru dezvoltarea software,
deoarece problema se modifica tot timpul.
22

Nu este recomandat sa se bazeze proiectarea software pe problema, deoarece aceasta este


instabila. Sistemele software bazate pe elementele cele mai putin stabile sunt condamnate sa
esueze.
Este recomandata utilizarea abstractizarii si domeniului problemei. O buna abstractizare
necesita intelegere in profunzime a domeniului problemei. Abstractizarea trebuie centrata pe
utilizator, ca sa se construiasca componente reutilizabile. Abstractizarea asigura valoarea
obiectelor, cu detaliile interne protejate prin incapsulare.
3.1.4. Implementarea tipurilor abstracte de date in C/C++
Tipurile abstracte de date asigura siguranta structurala si unitatea exprimarii, deci a
operatiilor. Tipurile abstracte de date ofera echivalenta intre cele doua elemente, datele si
operatiile. Cuvantul cheie class este cel care permite definirea tipurilor abstracte de date.
O clasa reuneste datele si operatiile in cadrul aceluiasi tip abstract de data:
Clasa=Date+Operatii
cu posibilitatea protejarii elementelor componente, atat date cat si functii, nu in vederea
ascunderii acestora de utilizatorii ulteriori, ci impotriva distrugerii lor accidentale (cunoscuta
sub denumirea de incapsulare sau interfatare).
In programarea standard (procedurala), programul este cel care coreleaza datele si
operatiile si nicidecum un tip de data.
Exemplu de tip abstract de date neincapsulat
Sa consideram structura Persoana. Vom implementa acest tip abstract de data in limbajul
C, dupa cum urmeaza:
#include <stdio.h>
#include <conio.h>
struct Persoana
{ int varsta;
char nume[40];
long int telefon;
};
void Arata_pers(Persoana *p)
{ clrscr();
printf(Numele: %s\n,p->nume);
printf(Varsta: %3d Telefonul: %ld\n,p->varsta,p->telefon);
getch();
}
void Cere_pers(Persoana *p)
{ clrscr();
printf(Numele: );
scanf(%40[A-Za-z]s,p->nume);fflush(stdin);
printf(Varsta: );
scanf(%d,&p->varsta);fflush(stdin);
printf(Telefonul: );
scanf(%ld,&p->telefon);fflush(stdin);
}
void main()
{ Persoana p;
Cere_pers(&p);
Arata_pers(&p);
}

23

In cadrul acestui scurt program datele si functiile comunica foarte putin, chiar daca ele au
fost utilizate pentru a da nastere unui aceluiasi obiect, p. La fel se prezinta lucrurile cu orice
structuri neincapsulate.
Exemplu de tip abstract de date incapsulat
Vom studia aceeasi problema prin intermediul claselor. Vom defini un nou tip de clasa
care va reuni atat datele cat si functiile.
#include <stdio.h>
#include <conio.h>
#include <string.h>
class Persoana{
public:
char nume[40];
long int telefon;
void Cere();
void Arata();
};
void Persoana::Arata()
{ for(int i=0;i<80;i++) printf(#);
printf(\nNumele:%40s ,nume);
printf( Telefonul: %ld\n,telefon);
}
void Persoana::Cere()
{ clrscr();
printf(Numele: );
if (scanf(%40[A-Za-z]s,nume))
{
fflush(stdin);
printf(Telefonul: );
scanf(%ld,&telefon);
fflush(stdin);
return 1;
}
fflush(stdin);
return 0;
}
void main()
{ Persoana p;
int t;
p.Cere();
p.Arata();
}

Definitia unei clase consta din declaratia si implementarea sa. Declaratia este o
insiruire a elementelor componente ale unei clase (date si functii), iar implementarea consta
din acea sectiune in care se implementeaza efectiv functiile declarate in clasa. Accesul la
aceste elemente se realizeaza prin intermediul operatorului de apartenenta ..
Cuvantul cheie public indica gradul de libertate de acces la elementele componente ale
clasei, toate elementele sunt disponibile in exteriorul clasei. Prin cuvintele cheie private sau
protected elementele pot fi accesate numai prin functiile membre sau prietene.
3.1.5. Modul de accesare a datelor in C++
Intr-o clasa sunt incapsulate date si functiile care prelucreaza datele. In afara de acestea
accesul la datele din clasa se poate face si altfel. In C++ se pot declara doua categorii de
functii care sa acceseze datele din clasa: functii membre si functii de acces.

24

O functie ce apartine unei clase, deci este functie membra a acelei clase, se specifica prin
plasarea prototipului functiei in cadrul clasei respective. Corpul functiei va fi definit in afara
clasei, aceasta realizandu-se prin intermediul operatorului de scop ::, asa cum s-a vazut
anterior.
Daca o anumita functie este definita chiar in interiorul clasei, aceasta se numeste functie
inline. Nu orice functie poate fi inline, cele care contin bucle sau operatori switch nu apartin
acestei categorii.
Functiile membre si operatorii accepta mai multe implementari in cadrul aceleiasi clase,
fara a exista pericolul ambiguitatilor.
#include <stdio.h>
#include <conio.h>
#include <string.h>
class Persoana{
public:
char nume[40];
long int telefon;
void Arata();
int Cere();
int Cere(char *n,long t=0);
};
void Persoana::Arata()
{ for(int i=0;i<80;i++) printf(#);
printf(\nNumele:%40s ,nume);
printf( Telefonul: %ld\n,telefon);
}
int Persoana::Cere()
{ clrscr();
printf(Numele: );
if (scanf(%40[A-Za-z]s,nume))
{
fflush(stdin);
printf(Telefonul: );
scanf(%ld,&telefon);
fflush(stdin);
return 1;
}
fflush(stdin);
return 0;
}
int Persoana::Cere(char *n,long t)
{ strcpy(nume,n);
telefon=t;
return 1;
}
char *Nume=Ionescu Vasile;
long Telefon=897867;
void main()
{ Persoana p;
int t;
p.Cere(Nume,Telefon);
p.Arata();
}

Parametrii impliciti ai functiei Arata() sunt specificati numai in declaratia clasei, nu si in


cadrul implementarii.

25

Domeniul unei functii membre


Elementele si functiile membre ale unei clase au domenii de valabilitate bine definite,
fiind recunoscute numai in interiorul clasei. Functiile membre deschid accesul spre elementele
clasei, de exemplu, elementele nume si telefon sunt accesate de functiile Cere() si Arata().
Aceste elemente membre ale clasei pot fi accesate si in mod direct cu operatorul de aparteneta
., avand sens forma:
p.Telefon=797979;

Programul urmator tipareste trei numere.


#include <stdio.h>
int a=1,b=2,c=42;
class numere {
public:
int a,b;
void Actiune(int b);
};
void numere::Actiune(int b)
{ a=17; b=55; c=3; }
void main()
{ numere doua_numere;
doua_numere.Actiune(36);
printf(%d %d %d\n,a,b,c);
}

Utilizarea pointerului this


Cu toate ca utilizarea explicita a acestuia este rar intalnita, acesta poate fi mentionat in
cadrul unei functii membre:
void Persoana::Arata()
{ for(int i=0;i<80;i++) printf(#);
printf(\nNumele:%40s ,this->nume);
printf( Telefonul: %ld\n,this->telefon);
}

utilizarea sa in acest caz fiind, de fapt, redundanta.


Exista insa situatii in care se impune utilizarea pointerului this. O astfel de situatie este
cea in care unei functii membre a unui obiect trebuie sa i se transmita adresa obiectului
respectiv.
Ascunderea informatiei
Incapsularea realizeaza pe langa reuniunea dintre date si functii si proprietatea de
ascundere a informatiei. Prin limitarea accesului la datele membre ale unei clase, uneori chiar
functii, aceste componente au o comportare ce ramane ascunsa exteriorului clasei.
Pentru exemplul utilizat pana acum, prezentam o varianta de ascundere a datelor.
class Persoana{
private:
char nume[40];
long int telefon;
public:
void Arata();
int Cere();
int Cere(char *n,long t=0);
};

26

Functii de acces
Singurele elemente ale clasei la care utilizatorul are acces direct sunt functiile membre
declarate publice, prin cuvantul cheie public. Clasa poate fi dotata astfel cu noi funtii.
class Persoana{
private:
char nume[40];
long int telefon;
public:
void Arata();
long Telefon();
char *Nume()
int Cere();
int Cere(char *n,long t=0);
};
long Persoana::Telefon()
{ return telefon; }

// functie de acces

char *Persoana::Nume()
{ return nume; }

// functie de acces

Functiile care returneaza doar valorile unor date private se numesc functii de acces.
Acestea sunt utile datorita faptului ca permit inspectarea continutului datelor, eventual
inaintea unei modificari, dar modificarea unor date private se poate realiza numai prin
intermediul unor functii membre.
Niveluri de acces
In clase exista trei variante de declarare a accesului la elementele componente. Acestea
sunt:
private: elementele sunt accesibile functiilor membre si celor de tip friend;
protected: poseda caracteristicile private si in plus elementele sunt accesibile si
claselor derivate;
public: sunt accesibile oricarei apelari exterioare.
Ordinea in care sunt utilizate aceste trei nivele de accesibilitate nu este impusa, dar
membrii unei clase, neinclusi in nici unul dintre aceste nivele, sunt, in mod implicit, private.
In exemplul urmator toate elementele clasei sunt private, deci nu pot fi accesate de functia
f.init(80), aparand eroare.
class fisier {
FILE *fp;
char *nume;
int lungime_articol;
void init(int);
};
fisier f;
f.init(80);

Functii membre declarate INLINE


Accesul la datele membre se poate realiza prin apelarea unor functii de acces. Acest
aspect, caracteristic incapsularii, este un consumator de timp, aici apelul unei functii este mult
mai ineficient in comparatie cu al unui element membru. Daca mentionam in dreptul functiei
cuvantul inline, se poate solutiona problema local, cu unele exceptii.

27

Alte functii din clasa celor inline sunt cele a caror implementare se realizeaza odata cu
declararea, ca in exemplul urmator:
class Persoana{
private:
char nume[40];
long int telefon;
public:
void Arata();
long Telefon() { return telefon; }
char *Nume() { return nume; }
int Cere();
int Cere(char *n,long t=0);
};

Aceste functii sunt realizate de catre compilator in expresii macro, deci in acel moment
trebuie cunoscut corpul functiei, atat ca forma, cat si din punct de vedere al continutului
(conditie care nu poate fi indeplinita in cazul utilizarii buclelor). Deci utilizarea functiilor
inline are aceleasi restrictii.
3.2. Implementarea Programarii Orientate pe Obiecte in C++
3.2.1. Privire generala asupra limbajului C++
Limbajul C++ este o versiune imbunatatita a lui C. Extensiile lui C++ fata de C au fost
create de catre Bjarne Stroustrup in 1980 in Laboratoarele Bell din Murray Hill, New Jersey.
Initial el a numit noul limbaj C cu clase. Dar, in 1983, numele a fost schimbat in C++.
Predecesorul sau, C, este inca unul dintre cele mai indragite si mai larg utilizate limbaje
de programare profesionale din lume, dar inventarea lui C++ a fost impusa de o importanta
cerinta a programarii: cresterea complexitatii. Actualele programe de calculator au devenit
mai mari si mai complexe. Chiar daca C este un excelent limbaj de programare, inca mai are
limitele sale. In C, daca un program trece de la 25000 la 100000 de linii de cod devine atat de
complex incat este dificil sa fie stapanit ca un intreg. Scopul lui C++, printre altele, este sa
depaseasca aceasta bariera. Esenta sa este sa permita programatorului sau echipei sa inteleaga
si sa administreze programe mari si complexe.
Majoritatea imbunatatirilor aduse de Stroustrup pentru C admit programarea orientata pe
obiecte, numita si OOP (Object Oriented Programming). Stroustrup afirma ca unele dintre
caracteristicile de orientare pe obiecte au fost inspirate din limbajul Simula67.
De la aparitie, C++ a trecut prin trei mari revizuiri, una in 1985, alta in 1989, iar a treia
cand a inceput lucrul la standardul ASCI pentru C++ in 25 ianuarie 1994.
Cand a inventat C++, Stroustrup stia ca este important sa pastreze spiritul initial al
limbajului C, inclusiv eficienta, flexibilitatea si conceptia sa de baza, ca programatorul
conduce jocul, si nu limbajul, adaugandu-i in acelasi timp si suportul pentru programare
orientata pe obiecte. Din fericire, scopurile sale au fost atinse. C++ asigura programatorului
libertatea si controlul din C, impreuna cu puterea obiectelor.
Chiar daca C++ a fost initial proiectat pentru a fi de folos in administrarea programelor
foarte mari, nu exista o limitare a utilizarii sale. De fapt, atributele sale de orientare pe obiecte
pot fi aplicate potential tuturor sarcinilor de programare.
Caracteristicile de baza ale programarii orientate pe obiecte
OOP este o noua abordare a programarii. Modalitatile de programare s-au schimbat imens
de la inventarea calculatorului, in primul rand pentru a se acomoda cresterii complexitatii
programelor. La inceputul calculatoarelor programarea se facea in cod masina, introducanduse instructiuni in masina in cod binar cu ajutorul panoului frontal al calculatorului. Acest lucru

28

a fost convenabil atat timp cat programele aveau doar cateva sute de instructiuni. O data cu
marirea programelor, au fost inventate limbajele de asamblare, astfel incat programatorii se
puteau descurca cu programe mai mari, cu complexitate crescuta, folosind reprezentarea
simbolica a instructiunilor pentru masina. Deoarece programele continuau sa creasca, au fost
introduse limbajele de nivel inalt care ofera programatorului mai multe unelte cu care sa faca
fata complexitatii.
Anii 60 au dat nastere programarii structurate. Aceasta este metoda incurajata de limbaje
cum sunt C si Pascal. Utilizarea limbajelor structurate face posibila scrierea destul de usoara a
unor programe relativ complexe. Totusi, chiar folosind metodele programarii structurate, un
proiect nu mai poate fi controlat o data ce atinge anumite marimi (adica o data ce
complexitatea sa o depaseste pe cea pe care o poate controla un programator). Pentru a
rezolva aceasta problema a fost inventata programarea orientata pe obiecte.
Programarea orientata pe obiecte a preluat cele mai bune idei ale programarii structurate
si le combina cu mai multe concepte noi, mai puternice, care incurajeaza abordarea sarcinii
programarii intr-un mod nou. In general, cand se utilizeaza programarea orientata pe obiecte,
se imparte o problema in subgrupe de sectiuni inrudite, care tin seama atat de codul cat si de
datele corespunzatoare din fiecare grup. Apoi, se organizeaza aceste subgrupe intr-o structura
ierarhica. In sfarsit, acestea sunt transformate in unitati de sine statatoare numite obiecte.
Programarea orientata pe obiecte are trei caracteristici de baza: incapsularea,
polimorfismul si mostenirea.
Incapsularea
Incapsularea este un mecanism care leaga impreuna cod si date si le pastreaza pe ambele
in siguranta fata de interventii din afara si de utilizari gresite. Mai mult, incapsularea este cea
care permite crearea unui obiect. Spus simplu, un obiect este o entitate logica ce incapsuleaza
atat date cat si cod care manevreaza aceste date. Intr-un obiect o parte din cod si/sau date pot
fi particulare (private) acelui obiect si inaccesibile pentru orice din afara sa. In acest fel, un
obiect dispune de un nivel semnificativ de protectie (interfata) care impiedica modificarea
accidentala sau utilizarea incorecta a partilor proprii obiectului de catre sectiuni ale
programului cu care nu are legatura.
Dar un obiect este o variabila de un tip definit de utilizator. La inceput poate sa para
ciudat sa consideram un obiect, care leaga atat cod cat si date, ca fiind o variabila. Totusi, in
programarea orientata pe obiecte asa stau lucrurile. Cand se defineste un obiect, implicit se
creeaza un nou tip abstract de date.
Polimorfism
Limbajele de programare orientate pe obiecte admit polimorfism, care este caracterizat
prin fraza o interfata, metode multiple. Mai clar, polimorfismul este caracteristica ce
permite unei interfete sa fie folosita pentru diverse actiuni, actiunea fiind determinata de
natura precisa a situatiei. De exemplu, se poate avea un program care defineste trei tipuri de
memorie stiva. Una este folosita pentru valori intregi, una pentru valori tip caracter si una
pentru valori in virgula mobila. Datorita polimorfismului, se pot crea trei perechi de functii
numite pune() si scoate() cate una pentru fiecare tip de date. Interfata este generala, pentru a
pune si a scoate date dintr-o memorie stiva. Functiile definesc calea specifica (metoda) care se
foloseste pentru fiecare tip de date. Cand se pun date in memoria stiva, tipul de date va fi cel
care va alege functia particulara a lui pune() corespunzatoare datelor.
Polimorfismul ajuta la reducerea complexitatii permitand aceleiasi interfete sa fie folosita
pentru a specifica o clasa generala de actiuni.

29

C++ foloseste conceptul de clasificare, care pemite gruparea caracteristicilor comune a


unui grup de obiecte in aceeasi grupa denumita clasa. Clasa este deci asemanatoare cu crearea
unei forme (prototip) pentru obiecte fizice, fiind o generalizare abstracta.
Mostenirea
Mostenirea este procesul prin care un obiect poate sa preia caracteristicile (prototipul)
clasei. Majoritatea cunostintelor sunt accesibile deoarece sunt clasificate ierarhic. De
exemplu, un mar ionatan face parte din clasificarea mar, care la randul sau face parte din clasa
fructe, care se afla in marea clasa a hranei. Fara utilizarea claselor, fiecare obiect ar trebui
definit explicitandu-se toate caracteristicile sale. Insa, prin folosirea clasificarilor, un obiect
are nevoie doar de definirea acelor calitati care il fac unic in clasa sa. Mecanismul mostenirii
este acela care face posibil ca un obiect sa fie un exemplar specific al unui caz mai general.
Stilul de programare in C++. Exemplu
Deoarece C++ este un super C, se pot scrie programe in C++ care sa arate exact ca cele
din C. Dar vor fi omise toate avantajele limbajului C++. (Este ca si a privi la televizor cu
butonul de culoare la minim) Astfel, cei mai multi programatori in C++ folosesc stilul si
anumite caracteristici care sunt unice in C++. Majoritatea diferentelor de stil intre programele
in C si cele in C++ se regaseste in avantajul pe care il are C++ prin capacitatile de orientare pe
obiecte. Dar un alt avantaj al folosirii stilului de programare propriu lui C++ este acela ca
ajuta la gandirea in C++ si nu in C, adoptand un stil diferit cand se scriu coduri in C++.
Pentru observarea stilului C++ consideram un exemplu simplu care citeste un intreg de la
tastatura si-l afiseaza pe ecran impreuna cu patratul sau.
#include <iostream.h>
main()
{
int i;
cout << Rezultatul este:\n;
// comentariu pe o linie
/* se pot folosi comentariile
pe mai multe linii in stilul C */
// se introduce un numar folosind operatorul >>
cout << introduceti un numar:;
cin >> i;
// acum se afiseaza rezultatul folosind operatorul <<
cout << i << la patrat este << i*i << \n;
return 0;
}

Dupa cum se poate vedea, acest program arata mult diferit fata de programele din C. La
inceput este inclus fisierul antet IOSTREAM.H. El este definit de C++ si folosit pentru a
admite operatiile de I/O in stilul C++. (IOSTREAM.H este pentru C++ ceea ce este STDIO.H
pentru C).
Prima modificare stilistica se afla in linia:
main()

Lista de parametrii din main() este vida. In C++ aceasta indica faptul ca main() nu are
parametrii, spre deosebire de C, unde o functie care nu are parametrii trebuie sa foloseasca
void in lista sa de parametrii, ca in linia de cod urmatoare:

30

main(void)

Totusi, in C++ utilizarea argumentului void este superflua si inutila. Ca regula generala,
cand o functie nu are parametrii in C++, lista sa de parametrii este pur si simplu goala;
folosirea argumentului void nu este necesara.
Urmatoarea diferenta se afla in linia:
cout << Rezultatul este:\n;

// comentariu pe o linie

Aceasta linie introduce doua noi caracteristici din C++. Prima este instructiunea, care face
sa fie afisat pe ecran mesajul Rezultatul este:, urmat de o combinatie inceput de rand/linie
nou. In C++, << are un rol extins. El este inca operatorul de deplasare la stanga, dar, cand este
folosit ca in exemplul prezentat, este, totodata, un operator de iesire. Cuvantul cout este un
specificator in legatura cu ecranul. (De fapt, ca si C, C++ admite indirectarea I/O, dar in
cadrul acestui exemplu cout se refera la ecran.) Se poate utiliza cout si << pentru a produce la
iesire orice tipuri de date incorporate in C++, precum si siruri de caractere.
Se pot utiliza in continuare printf() sau oricare alte functii de I/O din C in programele in
C++, incluzand STDIO.H. Dar majoritatea programatorilor simt ca utilizand cout << este mult
mai aproape de spiritul lui C++. Mai mult, chiar daca folosirea functiei printf() pentru iesirea
sirului este virtual echivalenta in acest caz cu utilizarea operatorului <<, sistemul de I/O din
C++ poate sa se extinda pentru a efectua operatii asupra obiectelor definite, ceea ce nu poate
face printf().
Ceea ce urmeaza dupa expresia de iesire este un comentariu tip C++. In C++ comentariile
sunt definite in doua moduri. In primul rand se pot folosi comentariile tip C, care lucreaza in
C++ la fel ca in C. Insa, in C++ se poate defini si un comentariu de o singura linie, folosind //.
Cand se incepe un comentariu cu //, orice urmeaza este igorat de compilator pana la sfarsitul
liniei. In general, programatorii in C++ folosesc comentariile tip C cand creeaza comentarii pe
mai multe linii iar pe cele tip C++ cand este suficienta una singura.
In continuare, programul solicita utilizatorului un numar. Numarul este citit de la tastatura
cu urmatoarea instructiune:
cin >> i;

In C++ operatorul >> isi mentine semnificatia de deplasare la dreapta. Dar, cand este
utilizat asa cum s-a aratat, el este si operatorul de intrare pentru C++. Aceasta instructiune
determina ca lui i sa i se dea o valoare citita de la tastatura. Specificatorul cin se refera la
echipamentul de intrare standard, care, de obicei, este tastatura. In general, se poate folosi cin
>> pentru a introduce o variabila de oricare tip de baza, inclusiv una de tip sir.
Chiar daca nu este ilustrat printr-un exemplu, se poate utiliza orice functie de intrare in C,
cum ar fi scanf(), in locul lui cin >>. Totusi, ca si pentru cout, majoritatea programatorilor
simt ca cin este mai aproape de spiritul limbajului C++.
O alta linie interesanta din program este urmatoarea:
cout << i << la patrat este << i*i << \n;

Presupunand ca i are valoarea 10, aceasta instructiune determina sa se afiseze 10 la


patrat este 100, urmat de inceput de rand/linie noua. Asa cum ilustreaza aceasta linie, se pot
asocia mai multe operatii de iesire <<.
Se observa ca programul se incheie cu instructiunea:
return 0;

31

Aceasta determina returnarea unui zero catre procesul care a facut apelarea (care este, de
obicei, sistemul de operare). Returnarea lui zero indica terminarea normala a programului.
Terminarea sa anormala trebuie semnalata prin returnarea unei valori diferite de zero.
Declararea variabilelor locale. Exemplu
O alta diferenta intre codul C si cel C++ este locul in care pot fi declarate variabilele
locale. In C, trebuie declarate toate variabilele locale dintr-un bloc la inceputul acestuia. Nu se
poate declara o variabila locala intr-un bloc dupa ce a aparut o instructiune de actiune.
In C++ se pot declara variabile in orice punct dintr-un bloc, nu doar la inceput. Fiecare
variabila este declarata atunci cand este necesara:
#include <iostream.h>
main()
{
float f;
double d;
cout << Introduceti doua numere in virgula mobila: ;
cin >> f >> d;
cout << Introduceti un sir: ;
char sir[80];
// sir este declarat aici, inainte de prima folosire
cin >> sir;
cout << f << << d << << sir;
return 0;
}

Este la latitudinea programatorului sa declare toate variabilele la inceputul blocului sau in


momentul primei utilizari. Deoarece conceptia lui C++ se bazeaza pe incapsularea codului si a
datelor, este normal sa se declare variabilele aproape de momentul folosirii si nu la inceputul
blocului.
Cand se ruleaza acest program, daca se introduce Acesta este un test, la solicitarea unui
sir, atunci, cand programul va reafisa informatia introdusa, va afisa doar cuvantul Acesta.
Restul sirului nu este afisat deoarece operatorul >> lucreaza cu sirurile in acelasi fel in care o
face si specificatorul %s pentru scanf(). El incheie citirea intrarii cand este intalnit primul
caracter de spatiu liber. De aceea, este un test nu va fi citit niciodata de catre program.
3.2.2. Incapsularea, constructori si destructori de obiecte
Incapsularea este cea mai importanta caracteristica din limbajul C++ si se face prin
definirea claselor de obiecte.
Pentru a crea un obiect in C++ trebuie sa i se defineasca mai intai forma sa generala
(clasa) folosind cuvantul cheie class. O class este similara sintactic cu o structura. Urmatoarea
clasa defineste un tip numit stiva, care este utilizat pentru a crea o memorie stiva:
#define SIZE 100

// dimensiunea stivei

// Aceasta creeaza clasa numita stiva.


class stiva {
int stiv[SIZE];
int tos;
// varful stivei
public:
void init();
void pune(int i);
int scoate();
};

32

O clasa poate contine atat sectiuni private cat si publice. Implicit, toate elementele
definite intr-o clasa sunt private. De exemplu, variabilele stiv si tos sunt private. Aceasta
inseamna ca o functie care nu face parte din acea clasa nu poate avea acces la ele. Iata unul
dintre modurile de realizare a incapsularii accesul la anumite elemente poate fi strict
controlat pastrandu-le private. Chiar daca nu apare in exemplu, se pot defini, de asemenea,
functii private, care apoi pot fi apelate doar de alti membrii ai clasei.
Pentru a crea parti publice ale unei clase (accesibile altor parti ale programului), acestea
trebuie declarate dupa cuvantul cheie public. La toate variabilele sau functiile definite dupa
public pot sa aiba acces toate celelalte functii din program. In esenta, restul programului are
acces la un obiect prin functiile sale publice. Ar trebui mentionat ca, desi pot exista variabile
publice, teoretic ar trebui sa se limiteze sau sa se elimine utilizarea lor. Ar trebui ca toate
datele sa fie private, iar accesul pentru a le controla sa fie permis prin functii publice.
Cuvantul cheie public este urmat de :.
Functiile init(), pune() si scoate() sunt denumite functii membre deoarece ele fac parte din
clasa stiva. Variabilele stiv si tos sunt denumite variabile membre (sau date membre). Un cod
este o grupare de cod si date. Doar functiile membre au acces la membrii particulari ai clasei
in care sunt declarate. De aceea, doar init(), pune() si scoate() pot sa aiba acces la stiv si tos.
O data ce s-a definit o clasa, se poate crea un obiect de acel tip folosind numele clasei. In
esenta, numele clasei devine un nou specificator de tip de date. De exemplu, linia urmatoare
creeaza un obiect numit stiva_mea de tip stiva:
stiva stiva_mea

Forma generala a declararii unei clase este:


class nume_clasa {
date si functii particulare
public:
date si functii publice
} lista de obiecte;

Dintre acestea, lista de obiecte poate fi goala.


In interiorul declaratiei pentru stiva, elementele de tip functie su fost specificate
folosindu-se prototipurile lor. In C++, toate functiile trebuie sa aiba prototipuri. Ele nu sunt
optionale.
Cand vine momentul sa se scrie efectiv codul unei functii care este membru al unei clase,
trebuie transmis compilatorului carei clase ii apartine aceasta, specificand inaintea numelui
sau numele clasei al carei membru este. De exemplu, functia pune() poate fi scrisa astfel:
void stiva::pune(int i)
{
if (vis==SIZE) {
cout << Stiva este plina. ;
return;
}
stiv[vis]=i;
vis++;
}

Operatorul :: este numit operator de specificare a domeniului. Important este ca el ii spune


compilatorului ca aceasta versiune a functiei pune() apartine clasei stiva, sau, altfel spus,
functia pune() face parte din domeniul stiva.
Cand se face referire la un membru al unei clase dintr-o sectiune de cod care nu face parte
din acea clasa, trebuie intotdeauna sa se faca legatura cu un obiect al acelei clase. Pentru

33

aceasta, se foloseste numele obiectului urmat de operatorul punct si de numele acelui


membru. Regula se aplica ori de cate ori se acceseaza o functie membru sau date membru. De
exemplu, urmatoarele instructiuni apeleaza init() pentru obiectul stiva.
stiva stiva1, stiva2;
stiva1.init();

Acest fragment creeaza doua obiecte (stiva1 si stiva2) si initializeaza stiva1. In acest
punct este foarte important sa se inteleaga ca stiva1 si stiva2 sunt doua obiecte distincte.
Aceasta inseamna ca, de exemplu, initializarea lui stiva1 nu determina si initializarea lui
stiva2. Singura legatura dintre stiva1 si stiva2 este aceea ca ele sunt obiecte de acelasi tip.
Programul urmator alatura toate secventele si detaliile necesare si ilustreaza clasa stiva:
#include <iostream.h>
#define SIZE 100
// Se creeaza clasa stiva.
class stiva {
int stiv[SIZE];
int vis;
public:
void init();
void pune(int i);
int scoate();
};
//Se definesc functiile membru
void stiva::init()
{
vis=0;
}
void stiva::pune(int i)
{
if (vis==SIZE) {
cout << Stiva este plina.;
return;
}
stiv[vis]=i;
vis++;
}
int stiva::scoate()
{
if (vis==0) {
cout << Depasire inferioara.;
return 0;
}
vis--;
return stiv[vis];
}
// Programul principal
main()
{
stiva stiva1, stiva2;

// creeaza doua obiecte stiva

stiva1.init();
stiva2.init();
stiva1.pune(1);

34

stiva2.pune(2);
stiva1.pune(3);
stiva2.pune(4);
count
count
count
count

<<
<<
<<
<<

stiva1.scoate()
stiva1.scoate()
stiva2.scoate()
stiva2.scoate()

<<
<<
<<
<<

;
;
;
\n;

return 0;
}

Incapsularea inseamna ca elementele private ale unui obiect sunt accesibile doar functiilor
membre ale acelui obiect. De exemplu, o instructiune ca urmatoarea:
stiva1.vis = 0;

// eroare

nu poate exista in interiorul functiei main() a programului precedent deoarece vis este privat.
Constructori si destructori
Este ceva obisnuit ca unele parti ale unui obiect sa necesite initializare inainte de a putea
fi folosite. De exemplu, pentru a putea fi folosita clasa stiva, prezentata anterior, vis trebuie sa
fie initializat cu zero. Acest lucru a fost realizat prin utilizarea functiei init(). Deoarece
cerintele de initializare sunt atat de uzuale, C++ permite obiectelor sa se initializeze singure,
atunci cand sunt create. Aceasta initializare automata este efectuata prin intermediul unei
functii constructor.
O functie constructor este o functie speciala care este membru al unei clase si are acelasi
nume cu acea clasa. De exemplu, clasa stiva transformata pentru a folosi o functie constructor
pentru initializare este:
// Crearea clasei stiva.
class stiva {
int stiv[SIZE];
int vis;
public:
stiva();
// constructor
void pune(int i);
int scoate();
};

Functia stiva() are urmatorul cod:


// functia costructor pentru stiva
stiva::stiva()
{
vis=0;
cout << Stiva initializata\n;
}

Mesajul Stiva initializata este afisat ca un mod de semnalare a constructorului. In


practica, majoritatea functiilor constructor nu vor afisa si nu vor introduce nimic. Ele vor
efectua pur si simplu diverse initializari.
Un constructor al unui obiect este apelat o singura data pentru obiecte globale sau pentru
cele locale de tip static. Pentru obiecte locale, constructorul este apelat de fiecare data cand
este intalnita declararea acestuia.
Complementul constructorului este destructorul. In multe cazuri, un obiect va trebui sa
efectueze o anumita actiune sau unele actiuni atunci cand este distrus. Obiectele locale sunt
35

create cand se intra in blocul lor si distruse cand blocul este parasit. Obiectele globale sunt
distruse cand se termina programul. Cand este distrus un obiect, este apelat destructorul lui
(daca exista). Exista multe motive pentru care poate fi necesara o functie destructor. De
exemplu, un obiect trebuie sa cedeze memoria care i-a fost alocata, sau sa inchida un fisier pe
care l-a deschis. In C++, cea care trateaza evenimentele de la dezactivare este functia
destructor. Ea are acelasi nume ca si constructorul, dar precedat de caracterul tilda (~). De
exemplu, clasa stiva si functiile sale constructor si destructor sunt:
// Crearea clasei stiva.
class stiva {
int stiv[SIZE];
int vis;
public:
stiva();
// constructor
~stiva();
// destructor
void pune(int i);
int scoate();
};
// functia costructor pentru stiva
stiva::stiva()
{
vis=0;
cout << Stiva initializata\n;
}
// functia destructor pentru stiva
stiva::~stiva()
{
cout << Stiva distrusa\n;
}

Se observa ca, la fel ca functiile constructor, functiile destructor nu au valori de returnare.


In final se prezinta programul stiva, studiat anterior, modificat cu functiile constructor si
destructor. Se observa ca nu mai este necesar init().
#include <iostream.h>
#define SIZE 100
// Se creeaza clasa stiva.
class stiva {
int stiv[SIZE];
int vis;
public:
stiva();
// constructor
~stiva();
// destructor
void pune(int i);
int scoate();
};
// functia costructor pentru stiva
stiva::stiva()
{
vis=0;
cout << Stiva initializata\n;
}
// functia destructor pentru stiva
stiva::~stiva()
{
cout << Stiva distrusa\n;
}

36

void stiva::pune(int i)
{
if (vis==SIZE) {
cout << Stiva este plina.;
return;
}
stiv[vis]=i;
vis++;
}
int stiva::scoate()
{
if (vis==0) {
cout << Depasire inferioara.;
return 0;
}
vis--;
return stiv[vis];
}
// Programul principal
main()
{
stiva a, b; // creeaza doua obiecte stiva
a.pune(1);
b.pune(2);
a.pune(3);
b.pune(4);
count
count
count
count

<<
<<
<<
<<

a.scoate()
a.scoate()
b.scoate()
b.scoate()

<<
<<
<<
<<

;
;
;
\n;

return 0;
}

3.2.3. Polimorfismul si supraincarcarea


Un mod in care C++ realizeaza polimorfismul este acela de supraincarcare a functiilor
(overloading). In C++ doua sau mai multe functii pot sa aiba acelasi nume atat timp cat
declaratiile lor de parametri sunt diferite. In aceasta situatie se spune ca functiile cu acelasi
nume sunt supraincarcate iar procesul este numit supraincarcarea functiilor.
Pentru a vedea de ce sunt importante functiile supraincarcate, vom considera pentru
inceput trei functii care se gasesc, probabil, in biblioteca standard a tuturor compilatoarelor
C/C++: abs(), labs() si fabs(). Functia abs() returneaza valoarea absoluta a unui intreg, labs()
returneaza valoarea absoluta a unei variabile float, iar fabs() pe cea a unei variabile double.
Chiar daca aceste functii efectueaza actiuni aproape identice, in C trebuie sa fie folosite toate
trei numele, putin diferite, pentru a reprezenta aceste sarcini, similare in esenta. Conceptual,
situatia devine mai complexa decat este de fapt. Desi ideea de baza a fiecarei functii este
aceeasi, programatorul trebuie sa-si aminteasca trei lucruri, nu doar unul singur. In C++, insa,
se poate folosi doar un singur nume pentru toate trei functiile, asa cum ilustreaza urmatorul
program:
#include <iostream.h>
// abs este definita in trei feluri
int abs(int i);
double abs(double d);
long abs(long l);

37

main()
{
cout << abs(-10) << \n;
cout << abs(-11.0) << \n;
cout << abs(-9L) << \n;
return 0;
}
int abs(int i)
{
cout << Utilizarea functiei abs() pentru intregi\n;
return i<0 ? i : i;
}
double abs(double d)
{
cout << Utilizarea functiei abs() pentru double\n;
return d<0 ? d : d;
}
long abs(long l)
{
cout << Utilizarea functiei abs() pentru long\n;
return l<0 ? l : l;
}

Acest program creeaza trei functii asemanatoare, dar totusi diferite, numite abs(), fiecare
dintre ele returnand valoarea absoluta pentru argumentul sau. Compilatorul stie ce functie sa
apeleze in fiecare situatie datorita tipului argumentului. Importanta functiilor supraincarcate
este aceea ca ele permit ca seturi de functii inrudite sa fie accesibile printr-un singur nume. De
aceea, numele abs() reprezinta actiunea generala care este efectuata. Este lasat pe seama
compilatorului sa aleaga metoda particulara corecta pentru situatii specifice. Programatorul
trebuie sa-si aminteasca doar actiunea generala care trebuie efectuata. Datorita
polimorfismului, se reduce numarul de lucruri care trebuie retinute de la trei la unul. Acest
exemplu este aproape banal, dar daca se extinde conceptul, se poate vedea cum polimorfismul
poate sa ajute la tratarea programelor foarte complexe.
In general, pentru a supraincarca o functie, se declara pur si simplu diferite versiuni ale ei.
De restul are grija compilatorul. Cand se supraincarca o functie, trebuie tinut seama de
urmatoarea restrictie importanta: tipul si/sau numarul de parametri al fiecarei functii
supraincarcate trebuie sa difere. Nu este suficient ca doua functii sa difere doar prin tipul
returnat de ele. Acestea trebuie sa difere prin tipul sau numarul de parametrii. (Tipul returnat
nu asigura suficiente informatii in toate cazurile pentru ca un compilator sa decida ce functie
sa foloseasca.) Desigur, functiile supraincarcate pot sa difere si prin tipul returnat.
In continuare se prezinta un alt exemplu care utilizeaza functii supraincarcate.
#include <iostream.h>
#include <stdio.h>
#include <string.h>
void adunsir(char *s1, char *s2);
void adunsir(char *s1, int i);
main()
{
char sir[80];
strcpy(sir, Va );

38

adunsir(sir, salut);
cout << sir << \n;
adunsir(sir, 100);
cout << sir << \n;
return 0;
}
// Concateneaza doua siruri
void adunsir(char *s1, char *s2)
{
strcat(s1, s2);
}
// Concateneaza un sir cu un intreg
void adunsir(char *s1, int i)
{
char temp[80];
sprintf(temp, %d, i);
strcat(s1, temp);
}

In acest program, functia adunsir() este supraincarcata. O versiune concateneaza doua


siruri (cum o face si strcat()). Cealalta versiune transforma un intreg intr-un sir si apoi il
adauga altui sir. Aici supraincarcarea este folosita pentru a crea o interfata care adauga la un
sir fie alt sir, fie un intreg.
Supraincarcarea operatorilor
Polimorfismul este realizat in C++ si prin supraincarcarea operatorilor. Dupa cum se stie,
in C++ este posibila folosirea operatorilor << si >> pentru a efectua operatii de I/O de la
consola. Ei pot efectua aceste operatii suplimentare deoarece operatiile sunt supraincarcate in
fisierul antet IOSTREAM.H. Cand un operator este supraincarcat, el capata o semnificatie
suplimentara relativ la o anumita clasa fara a-si pierde vreuna dintre semnificatiile initiale.
In general, se pot supraincarca majoritatea operatorilor din C++ stabilind semnificatia lor
relativ la o anumita clasa. De exemplu, in clasa stiva prezentata in paragraful anterior, se
poate supraincarca operatorul + relativ la obiectele de tipul stiva astfel incat sa adauge
continutul unei memorii stiva la alta de acelasi tip. In acelasi timp, + isi pastreaza semnificatia
sa initial relativ la alte tipuri de date.
3.2.4. Mostenirea si clase derivate
In C++ mostenirea este realizata prin acceptarea ca o clasa sa incorporeze in declararea sa
alta clasa. Mostenirea permite construirea unei ierarhii de clase, trecerea de la cele mai
generale la cele particulare. Procesul implica pentru inceput definirea clasei de baza, care
stabileste calitatile comune ale tuturor obiectelor ce vor deriva din baza. Clasa de baza
reprezinta cea mai generala descriere. Clasele derivate din baza se numesc de obicei clase
derivate. O clasa derivata include toate caracteristicile clasei de baza si, in plus, calitati
proprii acelei clase.
Forma generala a mostenirii este:
class nume_clasa_derivata : acces clasa_de_baza {
// corpul noii clase
}

Acces este aici optional. Totusi, daca este prezent, el trebuie sa fie public, private sau
protected. Deocamdata, toate clasele mostenite vor fi public. Utilizarea modulului public
39

inseamna ca toate elementele publice ale clasei de baza vor fi publice, de asemenea, si in clasa
derivata care o mosteneste. De aceea, membrii clasei derivate au acces la functiile membre
din clasa de baza ca si cum ar fi fost declarate in interiorul ei. Totusi, functiile membre din
clasa derivata nu au acces la partile private din clasa de baza. In acest fel, mostenirea nu
incalca principiul incapsularii, necesar in OOP. O clasa derivata are acces atat la proprii
membri cat si la membrii publici ai clasei de baza.
In continuare se prezinta un program care ilustreaza mostenirea. El creeaza doua clase
derivate pentru cladire folosind mostenirea; unul este casa, iar celalalt este scoala.
#include <iostream.h>
class cladire {
int camere;
int etaje;
int supraf;
public:
void nr_camere(int num);
int cate_camere();
void nr_etaje(int num);
int cate_etaje();
void nr_supraf(int num);
int cate_supraf();
}
// casa este derivat din cladire
class casa : public cladire {
int dormitoare;
int bai;
public:
void nr_dormitoare(int num);
int cate_dormitoare();
void nr_bai(int num);
int cate_bai();
};
// scoala este de asemenea derivat din cladire
class scoala : public cladire {
int saliclasa;
int laborat;
public:
void nr_saliclasa(int num);
int cate_saliclasa();
void nr_laborat(int num);
int cate_laborat();
};
void cladire::nr_camere(int num);
{
camere = num;
}
void cladire::nr_etaje(int num);
{
etaje = num;
}
void cladire::nr_supraf(int num);
{
supraf = num;
}
void cladire::cate_camere();
{
return camere;
}
void cladire::cate_etaje();
{
return etaje;

40

}
void cladire::cate_supraf();
{
return supraf;
}
void casa::nr_dormitoare(int num);
{
dormitoare = num;
}
void casa::nr_bai(int num);
{
bai = num;
}
void casa::cate_dormitoare();
{
return dormitoare;
}
void casa::cate_bai();
{
return bai;
}
void scoala::nr_saliclasa(int num);
{
saliclasa = num;
}
void scoala::nr_laborat(int num);
{
laborat = num;
}
void scoala::cate_saliclasa();
{
return saliclasa;
}
void scoala::cate_laborat();
{
return laborat;
}
main()
{
casa c;
scoala s;
c.nr_camere(12);
c.nr_etaje(3);
c.nr_supraf(4500);
c.nr_dormitoare(5);
c.nr_bai(3);
cout << casa are << c.cate_dormitoare();
cout << dormitoare\n;
s.nr_camere(200);
s.nr_saliclasa(180);
s.nr_laborat(5);
s.nr_supraf(25000);
cout << scoala are << s.cate_saliclasa();
cout << sali de clasa\n;
cout << Aria sa este de << s.cate_supraf();
return 0;
}

Dupa cum arata acest program, marele avantaj al mostenirii este acela ca se poate crea o
clasificare generala care sa fie incorporata in unele particulare. In acest fel, fiecare obiect
poate sa reprezinte exact propria sa clasificare.

41

In C++ pentru a reprezenta relatiile de mostenire sunt folositi, in general, termenii baza si
derivat. Totusi, se pot intalni termenii parinte si copil.
In afara de avantajele clasificarii ierarhice, mostenirea mai ofera, de asemenea, suport
pentru polimorfismul din timpul rularii prin mecanismul functiilor virtuale.
3.2.5. Definirea claselor si obiectelor
Clase
In C++ clasele formeaza baza programarii orientate pe obiecte. Caracteristic este faptul ca
o clasa este folosita pentru a defini natura unui obiect. De fapt, in C++ clasa este unitatea de
baza pentru incapsulare.
Clasele sunt create utilizandu-se cuvantul cheie class. O declarare a unei clase defineste
un nou tip care uneste cod si date. Acest nou tip este apoi folosit pentru a declara obiecte din
acea clasa. De aceea, o clasa este o abstractizare logica, dar un obiect are o existenta fizica.
Cu alte cuvinte, un obiect este un exemplar (o instanta) al unei clase.
O declarare de clasa este similara sintactic cu cea a unei structuri. Forma generala a unei
asemenea declaratii de clasa care nu mosteneste nici o alta clasa este:
class nume_clasa {
date si functii private
specificator de acces:
date si functii
specificator de acces:
date si functii
...
specificator de acces:
date si functii
} lista de obiecte;
Lista de obiecte este optionala. Daca exista, ea declara obiecte din acea clasa. Aici,
specificator de acces este unul dintre urmatoarele cuvinte cheie: public, private si protected.
Implicit, functiile si datele declarate intr-o clasa sunt proprii acelei clase si doar membrii
sai pot avea acces la ele. Totusi, folosind specificatorul de acces public, se permite functiilor
si datelor sa fie accesibile altor sectiuni ale programului. O data utilizat un specificator de
acces, efectul sau dureaza pana cand ori se intalneste altul, ori se ajunge la sfarsitul declaratiei
pentru clasa. Pentru a ne reintoarce la declararea privata, se poate folosi specificatorul de
acces private. Specificatorul protected este necesar cand este implicata mostenirea.
Intr-o declarare de clasa se poate modifica specificatorul de clasa oricat de des se doreste.
Aceasta inseamna ca se poate trece la public pentru unele declaratii si apoi sa se revina la
private. In general, majoritatea programatorilor in C++ vor scrie toate elementele private
grupate impreuna si toate elementele publice grupate separat, ca in exemplul urmator:
class angajat {
char nume[80];
// elemente private
double plata;
public:
// elemente publice
void punenume(char *n);
void ianume(char *n);
void puneplata(double w);
double iaplata();
};

42

Functiile care sunt declarate intr-o clasa sunt numite functii membre. Ele pot sa aiba
acces la orice element al clasei din care fac parte. Aceasta include toate elementele de tip
private. Variabilele care sunt membre ale unei clase sunt numite variabile membre sau date
membre. In general, orice element al unei clase este numit membru al acelei clase.
In general, trebuie ca membrii unei clase sa fie privati in acea clasa. Aceasta este o parte a
modului in care se realizeaza incapsularea. Totusi, pot exista situatii in care va fi necesar sa se
faca una sau mai multe variabile publice. De exemplu, poate fi necesar ca o variabila folosita
intens sa fie accesibila global, pentru a obtine timpi de rulare mai mici. Cand o variabila este
public, ea poate fi accesata direct de orice sectiune a programului. Sintaxa pentru a avea acces
la o data membru public este acceasi ca pentru apelarea unei functii: se specifica numele
obiectului, operatorul punct si numele variabilei. Urmatorul program simplu ilustreaza accesul
direct la variabilele public.
#include <iostream.h>
class clasa_mea {
public:
int i,j,k;
//accesibile intregului program
};
main()
{
clasa_mea a,b;
a.i=100;
a.j=4;
a.k=a.i*a.j;

//accesul direct la i, j si k

b.k=12;
//a.k si b.k sunt diferiti
cout << a.k << << b.k;
return 0;
}

Structuri si clase
C++ a crescut rolul structurii standard din C la acela al unui mod alternativ de specificare
a unei clase. De fapt, singura diferenta dintre o clasa si o structura struct este accea ca,
implicit, toti membrii unei structuri sunt publici, iar cei ai unei clase sunt privati. In toate
celelalte privinte structurile si clasele sunt echivalente. Aceasta inseamna ca in C++ o
structura defineste, de asemenea, un tip de clasa. De exemplu, sa consideram urmatorul scurt
program, care foloseste o structura pentru a declara o clasa ce controleaza accesul la un sir.
#include <iostream.h>
#include <string.h>
struct sirul_meu {
void facesir(char *s);
void aratasir();
private:
char sir[255];
};

//public
//privat

void sirul_meu::facesir(char *s)


{
if (!*s) *sir = \0;
//initializeaza sir
else strcat(sir,s);
}
void sirul_meu::aratasir()
{
cout << sir << \n;
}

43

main()
{
sirul_meu s;
s.facesir();
s.facesir(Va );
s.facesir(salut! );

//initializeaza

s.aratasir();
return 0;
}

Acest program afiseaza mesajul Va salut!.


Se observa ca s este declarat in main(). Deoarece o struct in C++ defineste un tip de clasa,
obiectele de acel tip pot fi declarate folosind doar numele generic al structurii, fara sa fie
precedat de cuvantul cheie struct.
Clasa sirul_meu poate fi rescrisa utilizand class asa cum se arata aici:
class sirul_meu {
char sir[255];
public:
void facesir(char *s);
void aratasir();
};

//privat
//public

In C++ o declarare a unei structuri defineste un alt tip de clasa.


Functii prietene
Este posibil sa se permita unei functii care nu este membru sa aiba acces la membrii
privati ai clasei folosind un friend (prieten). O functie friend are acces la membrii private si
protected ai clasei careia ii este prietena. Pentru a declara o functie friend, se include
prototipul ei in acea clasa, precedat de cuvantul cheie friend. Pentru exemplificare se prezinta
urmatorul program:
#include <iostream.h>
class clasa_mea {
int a, b;
public:
friend int sum(clasa_mea x);
void da_ab(int i, int j);
};

//private
//declararea functiei prietene sum()

void clasa_mea::da_ab(int i, int j) //realizarea functiei publice da_ab()


{
a=i;
b=j;
}
/* Deoarece sum() este functie prietena pentru clasa_mea, ea poate avea acces
direct la a si b. */
int sum(clasa_mea x)
{
return x.a+x.b;
}
main()
{
clasa_mea n;
n.da_ab(3, 4);

44

cout << sum(n);


return 0;
}

In acest exemplu, functia sum() nu este un membru din clasa_mea. Totusi, ea are acces
deplin la membrii privati. De asemenea, sum() este apelata normal. Deoarece ea nu este o
functie membra a clasei, ea nu trebuie (si nici nu poate) sa aiba un nume de obiect.
Chiar daca nu s-a castigat nimic facand sum() sa fie friend in loc de a fi un membru tip
functie al clasei clasa_mea, exista anumite conditii in care functiile friend sunt intr-adevar
valoroase.
In programul urmator se foloseste pentru clasa C2 o declarare ulterioara (numita si
referire ulterioara). Acest lucru este necesar deoarece se declara functia idle() in interiorul lui
C1 cu referire la C2 inainte ca aceasta sa fie declarata. Pentru a crea o declarare ulterioara a
unei clase, se foloseste pur si simplu forma prezentata in acest program. De asemenea, un
friend al unei clase poate fi membru al altei clase.
#include <iostream.h>
#define IDLE 0
#define INUSE 1
class C2;
class C1 {
int stare;
// ...
public:
void da_stare(int cond);
int idle(C2 b);
};

//dezvoltata mai jos

//INUSE daca este pe ecran, IDLE daca nu

//acum un membru al lui C1

class C2 {
int stare;
//INUSE daca este pe ecran, IDLE daca nu
// ...
public:
void da_stare(int cond);
friend int C1::idle(C2 b);
};
void C1::da_stare(int cond)
{
stare=cond;
}
//idle() este un membru al lui C1, dar prieten al lui C2
int C1::idle(C2,b)
{
if (stare||b.stare) return0;
else return 1;
}
main()
{
C1 x;
C2 y;
x.da_stare(IDLE);
y.da_stare(IDLE);
if (x.idle(y)) cout << Ecranul poate fi folosit.\n;
else cout << In curs de folosire.\n;
x.da_stare(INUSE);

45

if (x.idle(y)) cout << Ecranul poate fi folosit.\n;


else cout << In curs de folosire.\n;
return 0;
}

Deoarece idel() este membru al lui C1, ea poate avea acces direct la variabila stare din
obiectele de tip C1. De aceea, doar obiectele de tip C2 trebuie sa ii fie transmise.
Exista doua restrictii importante care se aplica functiilor friend. Prima, o clasa derivata
nu poate mosteni functii friend. A doua, o functie friend nu poate avea un specificator de clasa
de memorare. Aceasta inseamna ca ea nu poate fi declarata ca fiind static sau extern.
Clase prietene
Este posibil ca o clasa sa fie friend pentru alta. Cand este asa, clasa friend are acces la
numele private definite in cadrul celeilalte. Aceste nume private pot include lucruri ca nume
de tipuri si enumerari de constante. De exemplu:
#include <iostream.h>
class monede {
// urmatoarea este o enumerare privata.
enum unitati (penny, nickel, dime, quarter, half_dollar);
friend class cantitate;
};
class cantitate {
monede::unitati bani;
public:
void dam();
int iam();
} ob;

//modul monede::unitati

void cantitate::dam()
{
/* unitatile de enumerare sunt accesibile aici deoarece cantitate este friend
pentru monede */
bani=monede::dime;
}
int cantitate::iam()
{
return bani;
}
main()
{
ob.dam();
cout << ob.iam();

// afiseaza numarul 2

return 0;
}

Aici, clasa cantitate are acces la specificatorul de tip unitati declarat in clasa monede (si la
numele definite in enumerarea unitati) deoarece cantitate este un friend pentru monede.
Este important sa se inteleaga ca atunci cand o clasa este friend pentru o alta, ea are acces
doar la numele definite in interiorul celeilalte. Ea nu mosteneste cealalta clasa. Mai precis,
membrii primei clase nu devin membri ai clasei friend.
Clasele prietene sunt folosite deseori. Ele permit tratarea anumitor situatii speciale.

46

Functii inline
Exista o caracteristica importanta in C++, numita functie inline, care este folosita uzual
impreuna cu clasele.
In C++ se pot crea functii scurte care nu sunt apelate efectiv; in loc de aceasta codul lor se
dezvolta in interior, in fiecare punct in care sunt numite. Acest proces este similar cu folosirea
functiilor macro. Pentru a determina ca o functie sa se dezvolte inline in loc sa fie apelata, se
va preceda definirea ei cu cuvantul cheie inline. De exemplu, in programul urmator, functia
max() este dezvoltata inline in loc sa fie apelata.
#include <iostream.h>
inline int max(int a, int b)
{
return a>b ? a : b;
}
main()
{
cout << max(10, 20);
cout << << max (99, 88);
return 0;
}

In ceea ce priveste compilatorul, programul precedent este echivalent cu acesta:


#include <iostream.h>
main()
{
cout << (10>20 ? 10 : 20);
cout << << (99>88 ? 99 : 88);
return 0;
}

Motivul pentru care functiile inline sunt o facilitate in plus in C++ este ca ele permit
crearea de coduri foarte eficiente. De vreme ce pentru clase este tipic ca, deseori, sa se solicite
executarea frecventa a functiilor de interfata (care asigura accesul la datele private), eficienta
acestor functii este o cerinta esentiala in C++.
Functia inline este, practic, pentru compilator doar o solicitare, nu o comanda.
Compilatorul poate sa aleaga ignorarea ei. De asemenea, unele compilatoare nu pot introduce
inline toate tipurile de functii. De exemplu, in general, un compilator nu o face pentru o
functie recursiva. Va trebui sa se controleze in manualul compilatorului restrictiile pentru
inline. Daca o functie nu poate fi introdusa inline, ea trebuie apelata ca o functie normala.
Functiile inline pot fi membre ale unei clase. De exemplu, urmatorul este un program
perfect valabil in C++:
#include <iostream.h>
class clasa_mea {
int a,b;
public:
void init(int i, int j);
void arata();
};
inline void clasa_mea::init(int i, int j)
{
a=i;
b=j;

47

}
inline void clasa_mea::arata()
{
cout << a << << b << \n;
}
main()
{
clasa_mea x;
x.init(10, 20);
x.arata();
return 0;
}

Este posibil sa se defineasca functii scurte intr-o declarare de clasa. Cand o functie este
definita intr-o declarare a unei clase, ea este automat transformata intr-o functie inline (daca
este posibil). Nu este necesar (dar nici gresit) sa se preceada declararea sa cu cuvantul cheie
inline. De exemplu, programul precedent este rescris aici cu definirile pentru init() si arata()
incluse in declararea lui clasa_mea.
#include <iostream.h>
class clasa_mea {
int a,b;
public:
// inline automat
void init(int i, int j)
{a=i; b=j;}
void arata() {cout << a << << b << \n;}
};

// inline
// inline

main()
{
clasa_mea x;
x.init(10, 20);
x.arata();
return 0;
}

Se observa forma codului functiei din clasa_mea. Deoarece functiile inline sunt, de obicei
scurte, acest stil de codare din cadrul unei clase este aproape tipic. Totusi, se poate folosi orice
forma se doreste. De exemplu, o cale perfect valabila de rescriere a declararii pentru class este
urmatoarea:
#include <iostream.h>
class clasa_mea {
int a,b;
public:
// inline automat
void init(int i, int j)
{
a=i;
b=j;
}

// inline

void arata()
// inline
{
cout << a << << b << \n;
}
};

48

Si functiile constructor si destructor pot fi dezvoltate inline ori implicit, daca sunt
definite in clasa lor, ori explicit.
Functii constructor cu parametrii
Este posibil sa se transmita argumente catre functiile constructor. Tipic, aceste argumente
sunt folosite pentru a contribui la initializarea unui obiect atunci cand este creat. Pentru a crea
un constructor cu parametrii, i se adauga simplu parametrii in acelasi mod ca pentru orice alta
functie. Functia este in asa fel creata incat parametrii sunt folositi pentru a initializa obiectul.
In continuare se prezinta o class simpla care include constructori si destructori cu parametrii:
#include <iostream.h>
class clasa_mea {
int a,b;
public:
// inline automat
clasa_mea(int i, int j)
{a=i; b=j;} // constructor cu parametrii i si j
void arata() {cout << a << << b << \n;}
// inline
};
main()
{
clasa_mea ob(3, 5);
ob.arata();

// initializarea clasei clasa_mea

return 0;
}

In definirea lui clasa_mea(), parametrii i si j sunt folositi pentru a da valori initiale lui a si
b. Programul ilustreaza cea mai uzuala cale de a specifica argumente atunci cand se declara
un obiect care foloseste o functie constructor cu parametrii. Instructiunea:
clasa_mea ob(3, 5);

determina crearea unui obiect numit ob si se paseaza argumentele 3 si 5 catre parametrii i si j


din clasa_mea. Se pot pasa argumente folosind si acest tip de instructiune de declarare:
clasa_mea ob = clasa_mea(3, 5);

Totusi, prima metoda este una general utilizata.


In continuare se prezinta un alt exemplu care foloseste o functie constructor cu parametrii.
Ea creeaza o class care pastreaza informatii despre cartile din biblioteca.
#include <iostream.h>
#include <string.h>
#define IN 1
#define CHECKED_OUT 0
class carte {
char autor[40];
char titlu[40];
int stare;
public:
carte(char *n, char *t, int s);
int ia_stare()
{return stare;}
void da_stare(int s)
{stare = s;}
void arata();
};

49

carte::carte(char *n, char *t, int s)


{
strcpy(autor, n);
strcpy(titlu, t);
stare=s;
}
void carte::arata()
{
cout << titlu << de << autor;
cout << este ;
if (stare==IN) cout << aici.\n;
else cout << data.\n;
}
main()
{
carte b1(Twain,Tom sawyer,IN);
carte b2(Melville,Moby Dick,CHECKED_OUT);
b1.arata();
b2.arata();
return 0;
}

Functiile constructor cu parametrii sunt foarte folositoare deoarece ele permit evitarea
apelurilor in plus a functie doar pentru a initializa una sau mai multe variabile dintr-un obiect.
Fiecare apelare eviatata a functiei face programul mai eficient. De asemenea, functiile
da_stare() si ia_stare() sunt definite in interiorul clasei carte. Aceasta este o practica foarte
uzuala cand se scriu programe in C++.
Daca o functie constructor are doar un parametru, exista o a alta cale de a-i pasa o valoare
initiala. De exemplu, urmatorul program scurt:
#include <iostream.h>
class X {
int a;
public:
X(int j)
int daa()
};

{a=j;}
{return a;}

main()
{
X ob = 99;
cout << ob.daa();
return 0;
}

// paseaza 99 lui j
// afiseaza 99

Asa cum arata exemplul acesta, in cazul in care constructorul are doar un argument, se
poate folosi pur si simplu forma de initializare normala. Compilatorul de C++ va atribui
automat valoarea din dreapta semnului egal parametrului constructorului.
Membrii de tip static ai claselor
Atat functiile membre cat si datele membre ale unei clase pot fi declarate static. In
continuare se explica ce inseamna aceasta, relativ la fiecare tip de membru.

50

Variabile de tip static


Cand se precede o declaratie a unei variabile membru cu static, i se comunica
compilatorului ca va exista o copie a acelei variabile si va fi folosita de catre toate obiectele
clasei. Spre deosebire de membrii obisnuiti, nu sunt create copii individuale ale variabilelor
membre static pentru fiecare obiect. Nu are importanta cate obiecte de acel tip de clasa sunt
create, va exista doar o singura copie a membrilor de tip static. De aceea, toate obiectele
acelei clase folosesc aceeasi variabila. Cand este creat primul obiect, toate variabilele de tip
static sunt initializate cu zero.
Cand se declara o data membru ca fiind static intr-o clasa, aceasta nu se defineste. Cu alte
cuvinte, nu i se aloca memorie. In schimb, trebuie sa realizeze o definire globala a membrilor
de tip date static undeva, in afara clasei. Aceasta se face redeclarand variabila static folosind
operatorul de specificare a domeniului pentru a preciza clasa careia ii apartine; ca urmare se
va aloca memorie pentru variabila.
Pentru a intelege utilizarea si efectul unui membru de tip date static, vom considera
urmatorul program:
#include <iostream.h>
class comun {
static int a;
// parametrul a este static
int b;
public:
void da(int i, int j)
{a=i; b=j;}
void arata();
};
int comun::a
//defineste a, deoarece este static
void comun::arata()
{
cout << Acesta este a static: << a;
cout << \nAcesta este b ne-static: << b;
cout << \n;
}
main()
{
comun x, y;

//declaram obiectele x si y

x.da(1, 1);
x.arata();

//initializeaza pe a cu 1 si pe b cu 1

y.da(2, 2);
y.arata();

//initializeaza pe a cu 2 si pe b cu 2

x.arata();

/* aici a a fost schimbat atat pentru x cat si pentru y


deoarece a este comun pentru ambele obiecte */

return 0;
}

Cand este rulat acest program afiseaza urmatoarea iesire:


Acesta
Acesta
Acesta
Acesta
Acesta
Acesta

este
este
este
este
este
este

a
b
a
b
a
b

static: 1
ne-static: 1
static: 2
ne-static: 2
static: 2
ne-static: 1

51

Numarul intreg a este declarat atat in comun cat si in afara sa. Cum am spus mai devreme,
acest lucru este necesar deoarece declararea lui a in interiorul lui comun nu determina alocare
de memorie.
O variabila membru static exista inainte de a fi creat orice obiect din acea clasa. De
exemplu, in urmatorul scurt program, a este atat public cat si static. De aceea, el poate fi
utilizat direct in main(). Mai mult, deoarece a exista inainte de crearea unui obiect din comun,
lui a i se poate da oricand o valoare. Asa cum ilustreaza programul, valoarea lui a nu este
modificata de crearea obiectului x. Din acest motiv, amandoua instructiunile de iesire afiseaza
aceeasi valoare: 99.
#include <iostream.h>
class comun {
public:
static int a;
};
int comun::a

// parametrul a este static

//defineste a, deoarece este static

main()
{
//initializeaza pe a inaintea crearii oricaror obiecte
comun::a = 99
cout << Aceasta este valoarea initiala a lui a: << comun::a;
cout << \n;
comun x;
cout << Acesta este x.a: << x.a
return 0;
}

Se observa modul de realizare a accesului la a prin folosirea numelui clasei si a


operatorului de specificare a domeniului. In general, cand programul are acces la un membru
static independent de un obiect, trebuie sa precizat folosind numele clasei al carei membru
este.
Una dintre cele mai obisnuite utilizari ale variabilelor membre static este de a asigura
controlul accesului la unele resurse comune. De exemplu, se pot crea mai multe obiecte,
fiecare dintre ele trebuind sa scrie intr-un anumit fisier de pe disc. Este evident, totusi, ca doar
unui singur obiect ii este permis sa scrie intr-un fisier intr-un anumit moment. In acest caz,
este bine sa se declare o variabila static care sa indice cand este folosit fisierul si cand este
liber. Fiecare obiect va controla apoi aceasta variabila inainte de a scrie in fisier. Urmatorul
program arata cum se poate folosi variabila static de acest tip pentru a controla accesul al o
resursa limitata.
#include <iostream.h>
class cl {
static int resursa;
public:
int ia_resursa();
void resursa_libera()
};
int cl::resursa;
int cl::ia_resursa()
{
if(resursa) return 0;

{resursa = 0;}

//defineste resursa

//reusrsa este in lucru

52

else {
resursa = 1;
return 1;

//resursa atribuita acestui obiect

}
}
main()
{
cl ob1,ob2;
if(ob1.ia_resursa()) cout << ob1 are dreptul la resursa\n;
if(!ob2.ia_resursa()) cout << ob2 nu are dreptul la resursa\n;
ob1.resursa_libera();

//o lasa pentru altceva

if(ob2.ia_resursa()) cout << ob2 poate acum sa foloseasca resursa\n;


return 0;
}

Folosind variabilele membre static, virtual ar trebui sa nu mai fie necesara nici o variabila
globala. Problema variabilelor globale pentru OOP este aceea ca ele incalca, aproape
intotdeauna, principiul incapsularii.
Functii membre statice
Functiile membre pot fi, de asemenea, declarate ca static. Exista mai multe restrictii
relativ la acestea. In primul rand, ele pot sa aiba acces doar la alti membri de tip static ai clasei
si, bineinteles, la functiile si datele globale.
In continuare este prezentata o versiune usor modificata a programului anterior. Se
observa ca ia_resursa este acum declarata static. Dupa cum se vede in program, accesul la
ia_resursa este permis fie functiei insesi, fie independent de vreun obiect utilizand numele
clasei si operatorul de specificare a domeniului, fie in legatura cu un obiect.
#include <iostream.h>
class cl {
static int resursa;
public:
static int ia_resursa();
void resursa_libera()
};
int cl::resursa;
int cl::ia_resursa()
{
if(resursa) return 0;
else {
resursa = 1;
return 1;
}
}

{resursa = 0;}

//defineste resursa

//reusrsa este in lucru

//resursa atribuita acestui obiect

main()
{
cl ob1,ob2;
// ia_resursa este static, deci poate fi apelata independent de orice obiect
if(cl::ia_resursa()) cout << ob1 are dreptul la resursa\n;
if(!cl::ia_resursa()) cout << ob2 nu are dreptul la resursa\n;
ob1.resursa_libera();

53

// se poate folosi si apelul cu obiect


if(ob2.ia_resursa()) cout << ob2 poate acum sa foloseasca resursa\n;
return 0;
}

Executia constructorilor si destructorilor


Ca regula generala, un constructor de obiecte este apelat la declararea obiectului, iar
destructorul de obiecte este apelat cand este distrus obiectul. Vom prezenta acum momentul
exact al aparitiei acestor evenimente.
O functie constructor de obicei este executata cand este intalnita instructiunea de
declarare a obiectului. Mai mult, cand doua sau mai multe obiecte sunt declarate in aceeasi
instructiune, constructorii sunt apelati in ordinea in care sunt ele intalnite, de la stanga la
dreapta. Functiile destructor pentu obiecte locale sunt executate in ordine inversa fata de cele
constructor.
Functiile constructor pentru obiectele globale sunt executate inaintea lui main().
Constructorii globali din acelasi fisie sunt executati in ordine, de la stanga la dreapta si de sus
in jos. Nu se poate insa sti ordinea executiei constructorilor globali imprastiati prin mai multe
fisiere. Destructorii globali se executa in ordine inversa dupa ce se incheie main().
Urmatorul program ilustreaza executarea constructorilor si destructorilor.
#include <iostream.h>
class clasa_mea {
public:
int cine;
clasa_mea(int ex);
~clasa_mea();
} glob_ob1(1), glob_ob2(2);

//declaratie constructor
//declaratie destructor

clasa_mea::clasa_mea(int ex)
{
cout << Initializare << ex << \n;
cine = ex;
}
clasa_mea::~clasa_mea()
{
cout << Distrugere << cine << \n;
}
main()
{
clasa_mea local_ob1(3);
cout << Aceasta nu va fi prima linie afisata.\n;
clasa_mea loval_ob2(4);
return 0;
}

El afiseaza aceasta iesire:


Initializare 1
Initializare 2
Initializare 3
Aceasta nu va fi prima linie afisata.
Initializare 4

54

Distrugere
Distrugere
Distrugere
Distrugere

4
3
2
1

Operatorul de specificare a domeniului


Dupa cum se stie, operatorul :: este folosit la asocierea unui nume de clasa cu un nume de
membru pentru a spune compilatorului carei clase apartine acel membru. Dar, operatorul de
specificare are si o alta utilizare asemanatoare: el poate permite accesul la un nume dintr-un
domeniu inchis, nume care este ascuns de o declarare locala a aceluiasi nume. Sa luam, de
exemplu, fragmentul urmator:
int i;

//i este global

void f()
{
int i;

//i este local

i=10;

//foloseste i local

Totusi, se pune problema ce se intampla daca functia f() are nevoie de acces la versiunea
globala a lui i. Poate sa il capete, daca i este precedat de operatorul ::, asa cum se arata in
continuare:
int i;

//i este global

void f()
{
int i;
::i=10;

//i este local


//foloseste i global

Clase locale
O clasa poate fi definita in interiorul unei functii. De exemplu, urmatorul este un program
C++ valid:
#include <iostream.h>
void f();
main()
{
f();
// clasa_mea nu este cunoscuta aici
return 0;
}
void f()
{
class clasa_mea {
int i;
public:
void pune_i(int n) {i=n;}
int da_i()
{return i;}
} ob;
ob.pune_i(10);
cout << ob.da_i();
}

Cand o clasa este declarata intr-o functie, ea este cunoscuta doar acelei functii si
necunoscuta in afara ei.

55