Sunteți pe pagina 1din 285

Adonis Butufei

1. INTRODUCERE

CUPRINS
1.Introducere.................................................................................................................................................2
1.1 Roata invatarii....................................................................................................................................2
1.1.1 Intrebare......................................................................................................................................2
1.1.2 Teoria..........................................................................................................................................2
1.1.3 Aplicarea teoriei..........................................................................................................................2
1.1.4 Reflectia......................................................................................................................................3
1.2 Cele cinci dificultati ale programatorului incepator...........................................................................3
1.3 Organizarea cursului...........................................................................................................................4
1.4 Limbaje de programare: tipuri si caracteristici.................................................................................4
1.4.1 Ce este un limbaj de programare?..............................................................................................4
1.4.2 Generatiile limbajelor de programare.........................................................................................4
1.5 Ce este C++?......................................................................................................................................6
1.5.1 Modelele de programare suportate de C++................................................................................6
1.5.2 Documentatia online pentru limbajul C++.................................................................................7
1.6 Ciclul de viata al programelor software.............................................................................................8
1.6.1 Procesul programarii...................................................................................................................8
1.6.2 Metodologii de dezvoltare a programelor..................................................................................8
1.6.2.1 Programeaza si depaneaza .................................................................................................8
1.6.2.2 Spirala.................................................................................................................................9
1.7 Medii de dezvoltare integrata (IDE)...................................................................................................9
1.8 Scrierea primului program...............................................................................................................10
1.8.1 Crearea proiectului în Visual C++............................................................................................10
1.8.2 Scrierea codului........................................................................................................................14
1.8.3 Compilarea programului...........................................................................................................15
1.9 Sumar...............................................................................................................................................15
1.10 Intrebari si exercitii........................................................................................................................15
1.11 Bibliografie.....................................................................................................................................16

1
1
Adonis Butufei

1. INTRODUCERE

1.1 Roata invatarii


Invatarea unui limbaj de programare presupune pe langa o implicare activa in lectura cartilor de
specialitate si activitati practice de scriere a codului si depanare a programelor. Simpla intelegere
intelectuala a conceptelor nu este suficienta. Pentru fixarea cunostintelor si formarea deprinderilor este
necesara aplicarea acestor cunostinte atat in timpul studiului dar mai ales dupa aceea. Fara o repetare
periodica a conceptelor notiunile invatate sunt uitate.
Roata invatarii este un sistem format din patru pasi care permite asmilarea si fixarea cunostintelor in mod
eficient: intrebarea, teoria, testarea teoriei si reflectia. Aceste etape se pot aplica pentru intregul studiu cat
si pentru fiecare concept in parte.

1.1.1 Intrebare
In aceasta etapa se clarifica scopul invatarii si permite canalizarea eforturilor intr-o directie precisa.
• De ce vreau sa invat un limbaj de programare? Ce anume vreau sa stiu? (stabilit cat mai detaliat). Ce
anume trebuie sa fac pentru a invata? Cat de multe resurse sunt dispus sa aloc pentru invatare? Cum
stiu ca am invatat?
• Ce stiu deja despre acest subiect? Scopul acestei intrebari este de a constientiza ce stiu si ce am nevoie
sa invat.
• De unde pot afla ceea ce nu stiu? Care sunt sursele corecte de informare de care dispun?

1.1.2 Teoria
Dupa ce am stabilit scopul si am decis ce material se potriveste cu nivelul de intelegere este necesara
studierea teoriei din acele materiale. Studiul cartilor de obicei se refera la citirea si intelegerea textului.
Studiul programarii presupune scrierea de cod. Pentru a putea scrie cod este necesara mai intai invatarea
sintaxei1 si apoi citirea unor exemple.

Urmatorul set de intrebari ne ajuta sa participam activ in parcurgerea materialului intr-un timp mai scurt:
• Cum este organizat materialul? Care este ideea prinpcipala? Care sunt termenii si conceptele? Ce informatie este importanta aici?
• Ce intrebari imi ridica aceasta informatie (Cine? Ce? Unde? Cand? De ce? Cum?)
• Cum pot reformula si sumariza informatia?
• Cum pot reorganiza informatia pentru a raspunde nevoilor mele?
• Cum pot vizualiza informatia? (harti mentale 2, diagrame, tabele)
• Cum se incadreaza aceasta informatie in ceea ce stiu deja?

1.1.3 Aplicarea teoriei


• Rularea exemplelor. Aceasta etapa presupune scrierea manuala a exempelor (fara copy/paste). In
momentul scrierii codului mintea poate asimila detaliile. Este posibil sa apara erori de compilare3
datorita unor greseli de scriere. Aceasta este un prilej foarte bun de invatare a sintaxei.

1 Scrierea programelor presupune respectarea anumitor reguli similare cu cele gramaticale.


2 Mind-map.
3 Compilarea reprezinta transformarea codului scris in fisier care poate fi executat de calculator.
2
2
Adonis Butufei

• Folosirea debuggerului4 pentru rularea programelor pas cu pas si verificarea codului scris.
• Scrierea codului propriu, schimb exemplele, vad ce se intampla.
• Identificarea lucrurilor pe care nu le inteleg. Reformularea lor in cuvinte proprii. Adresarea intrebarilor
detaliate.

1.1.4 Reflectia
Aceasta etapa ajuta la organizarea intelegerii si la internalizarea cunoasterii. Urmatorul set de intrebari
poatre ajuta la definirea contextului:
• Care este sintaxa?
• Care este tiparul aici?
• Exista cazuri speciale?
• Daca schimb asta ce altceva se mai schimba?
• In cate feluri diferite pot rezolva aceasta problema?
• Exista o solutie mai simpla?
• Se pot rezolva si alte probleme cu ce am invatat?

1.2 Cele cinci dificultati ale programatorului incepator


1. Materialele necesare:
1.1. Alegerea limbajului de programare.
1.2. Alegerea unui mediu de dezvoltare.
1.3. Alegerea unui tutorial.

2. Formarea modului de gandire al programatorului:


2.1. La inceput prin imitarea pasilor prezentati in tutorial si aplicand roata invatarii.
2.2. Separarea unei probleme complexe in probleme mai simple.
2.3. Recunoasterea unor tipare similare in probleme diferite.

3. Mesajele de eroare
3.1. La inceput necesita un efort de intelegere.
3.2. Cu timpul ele devin elemente ajutatoare care permit scrierea programelor la un standard de
calitate profesional.

4. Depanarea programelor
4.1. Este o abilitate importanta care se poate froma.
4.2. Permite verificarea corectitudinii.

4 Debuggerul este un program care permite rularea programului pas cu pas.


3
3
Adonis Butufei

5. Designul programelor
5.1. Cum se organizeaza codul programelor pentru a obtine functionalitatea dorita.
5.2. Cum se descompun problemele complexe intr-un set minimal de probleme simple.
5.3. Este o activitate care se dezvolta odata cu formarea modului de gandire al programatorului.

1.3 Organizarea cursului


Cursul este organizat in 15 capitole. Fiecare capitol cuprinde un set de intrebari si exercitii care vor
permite asimilarea si evaluarea cunostintelor. De asemenea, pe masura introducerii conceptelor vor fi
oferite si recomandari practice care vor asigura scrierea unui cod de calitate. Pentru o mai buna asimilare,
notiunile vor fi introduse de la ansamblu catre detalii.

In primul capitol sunt prezentate notiunile introductive legate de limbaje de programare si mediu de
dezvoltare. Vom incepe prin a defini limbajele de programare. Apoi vom explora generatiile limbajelor de
programare, vom analiza modelele de programare suportate de C++, vom trece in revista cateva elemente
legate de ciclul de viata al programelor software. La final vom crea primul program folosind mediul de
dezvoltare Visual C++ Express Edition.

1.4 Limbaje de programare: tipuri si caracteristici

1.4.1 Ce este un limbaj de programare?


Un limbaj de programare contine un set de reguli si expresii care permit scrierea programelor. Programele
descriu pasii pentru rezolvarea unor probleme. Fiecare pas este exprimat in comenzi care sunt executate
de calculator.

1.4.2 Generatiile limbajelor de programare


Prima generatie
Programele din aceasta generatie erau scrise la inceput in limbaj binar, care putea fi procesat direct de
calculatoare.
Pentru imbunatatirea productivitatii au fost create limbajele de asamblare. Aceste tipuri de limbaje
contineau un set de instructiuni specifice tipului de calculator. Pentru a transfera programul pe alt tip de
calculator era necesara rescrierea lui folosind un nou set de instructiuni.

A doua generatie
Aceasta generatie a aparut cu limbajul FORTRAN. El permitea scrierea programului folosind un set de
instructiuni care erau mai usor de scris si depanat si erau independente de platforma hardware pe care
lucrau.
Pentru a putea fi rulat pe o masina, codul scris de programator trebuia transformat in instructiuni binare
(cod obiect) si aceasta se realiza cu ajutorul unui program numit compilator.
Deoarece codul sursa putea avea mai multe fisiere, dupa compilare rezultau mai multe fisiere binare care
trebuiau grupate impreuna pentru a crea programul. Aceasta se realiza cu ajutorul unui program numit
linker. Pentru a putea fi transferat pe o alta platforma hardware programul trebuia recompilat cu un
4
4
Adonis Butufei

compilator specific acelei platforme.

A treia generatie
Daca a doua generatie a imbunatatit structura logica a limbajelor, a treia generatie a devenit mult mai usor
accesibila dezvoltatorilor. Multe limbaje de uz general folosite astazi, BASIC, C, C++, Java, C# apartin
acestei generatii.

Brian Kernighan si Denis Richie au creat limbajul de programare C. Acest limbaj a fost folosit pentru
rescrierea sistemului de operare UNIX care avut un succes deosebit. Multe platforme hardware au adoptat
variante ale acestui sistem de operare. Dupa raspandirea acestui sistem de operare, scrierea programelor a
devenit mult mai usoara pentru ca dezvoltatorii nu mai erau nevoiti sa interactioneze direct cu
particularitatile platformei hardware ci cu resursele oferite de sistemul de operare.

In aceasta generatie, pe langa compilatoare, care transformau intregul program in comenzi binare, au
aparut interpretoarele care executau programul instructiune cu instructiune. Acest mod de lucru permitea
obtinerea raspunsului imediat fara a mai fi necesara compilarea. Este potrivit pentru interactiunea cu
sistemul de operare. Probabil fiecare am deschis un comand prompt in Windows si am executat comenzi
pentru a rezolva anumite probleme simple.

Deoarece procesul de compilare pentru programele mari era consumator de timp, dezvoltatorii s-au gandit
oare nu putem sa facem ceva invers? In loc sa compilam codul in limbaj binar, sa dezvoltam un program
care sa fie capabil sa execute comenzi complexe si compilarea sa se execute in momentul executiei. Asa
au aparut masinile virtuale care erau capabile sa execute comenzi si compilatoarele Just in time folosite
de limbajele Small Talk, Java si mai tarziu C#. In acest mod era suficient sa se implementeze o masina
virtuala pentru fiecare platforma iar codul scris in acel limbaj putea, cel putin teoretic, sa fie rulat pe toate
platformele.

A patra generatie
Cu limbajele de uz general din a treia generatie puteau fi dezvoltate o multitudine de programe. Insa
pentru anumite domenii specifice cum ar fi interogarea bazelor de date, calcule statistice si altele nu era
eficienta folosirea acestor limbaje. Din acest motiv, limbajele din a patra generatie s-au concentrat pe
rezolvarea problemelor specifice unor domenii.
Scopul acestor limbaje este sa reduca eforturile si costurile de dezvoltare a programelor, oferind un nivel
inalt de abstractizare al domeniului respectiv.
Cateva exemple de limbaje: FoxPro, SQL, PL/SQL, LabView, S, R, Mathematica, ABAP.

A cincea generatie
Aceste limbaje sunt dezvoltate cu intentia de a lasa programul sa rezolve probleme fara interventia
programatorului si sunt folosite in cercetarile legate de inteligenta artificiala. Eforturile in acest domeniu
au ramas inca in faza cercetarilor. Cateva exemple de limbaje: Prolog, OPS5, Mercury.

5
5
Adonis Butufei

1.5 Ce este C++?


Asa cum am vazut in paragraful anterior C++ este un limbaj care apartine generatiei a treia. A fost creat
de Bjarne Strostrup in anul 1980. Dupa cum o sugereaza si numele, este bazat pe limbajul C si ofera
imbunatatiri radicale acestui limbaj. In mai bine de trei decenii de existenta limbajele C si C++ s-au
influentat si completat reciproc.

De ce a fost ales C ca limbaj de baza pentru C++?


• Pentru ca este un limbaj eficient si permite dezvoltarea apropiata de nivelul masina.
• Pentru ca oferea solutia optima pentru cele mai multe activitati de programare.
• Pentru ca ruleaza pe orice platforma hardware.

1.5.1 Modelele de programare suportate de C++


Programare procedurala
Aceasta model imparte functionalitatea programului in proceduri (functii) si foloseste algoritmii optimi
pentru implementarea functiilor. Atentia principala este indreptata catre transmiterea si returnarea
rezultatelor.

Programare modulara
Pe masura ce dimensiunea si complexitatea programelor creste, apare necesitatea unei organizari la nivel
mai inalt. Functionalitatea este impartita in module. Aceste module contin datele si procedurile care
lucreaza cu aceste date.

Abstractizarea datelor
Progamarea modulara este un aspect pentru toate programele mari de succes. Pe masura ce aceste
programe cauta sa rezolve probleme reale, exprimarea conceptelor este destul de dificila folosind doar
tipurile de date oferite de limbajul de programare. Pentru a rezolva acest aspect C++ asigura
dezvolatorilor posibilitatea definirii propriilor tipuri de date. Acestea se numesc tipCuri abstracte de date
sau tipuri definite de utilizator.

Programare obiectuala
Abstractizarea datelor este un element esential al designului de calitate. Totusi, doar tipurile utilizator nu
rezolva o serie de aspecte importante ale programelor reale.

Primul aspect important al programarii obiectuale este incapsularea. Aceasta se realizeaza prin gruparea
datelor care descriu un concept cu functiile care acceseaza acele date intr-o clasa. Mai mult, accesul la
unele date este permis numai functiilor clasei.
Este foarte usor sa intelegem acest concept daca privim in jur la toate dispozitivele inconjuratoare. De
exemplu o telecomanda: toata complexitatea functionarii este „ascunsa” utilizatorului care o foloseste.
Acesta poate interactiona prin intermediul tastaturii (interfetei) oferite pentru indeplinirea obiectivelor.

6
6
Adonis Butufei

Al doilea aspect important al programarii obiectuale este mostenirea. Mostenirea permite extinderea unei
clase si adaugarea de alte functionalitati. Acest mecanism, folosit corect, permite reutilizarea codului
intr-un grad mult mai mare decat era posibil folosind modelele anterioare.
De exemplu putem avea o clasa Angajat. Deoarece un manager este un angajat dar are atributii diferite de
ale angajatului putem deriva clasa Manager si adauga functionalitatea specifica, asa cum este schitat in
exemplul de mai jos. Clasa Angajat se numeste clasa de baza, sau parinte pentru clasa Manager. Clasa
Manager se numeste clasa derivata.

Al treilea aspect important al programarii obiectuale este polimorfismul. Acest mecanism permite
claselor derivate sa specializeze comportamentul claselor de baza. De exemplu tramvaiul si autobuzul
sunt ambele mijloace de transport în comun. Toate mijoloacele de transport în comun se deplaseaza intre
statii. Modul în care se realizeaza aceasta deplasare difera si polimorfismul ne ajuta sa implementam
diferit deplasarea intre statii pentru autobuz si tramvai.

Programare generica
Aplicarea programarii obiectuale in productie a permis reducerea timpului de dezvoltare a proiectelor de
la luni la saptamani si de la ani la trimestre. De asemenea, a permis dezvoltarea mai eficienta a versiunilor
urmatoare ale programelor lansate pe piata.
Exista tipuri de prelucrari care sunt similare indiferent de tipul de date asupra carora se executa aceste
prelucrari. O stiva de caramizi si o stiva de farfurii folosesc acelasi tipar de organizare a elementelor.
Folosind programarea generica putem implementa tiparul, stiva, o singura data si apoi sa-l folosim pentru
diferite tipuri de date.

1.5.2 Documentatia online pentru limbajul C++


Acest curs prezinta elementele de baza ale limbajului. Pentru informatii detaliate despre limbaj,
aprofundarea cunostintelor si rezolvarea problemelor practice este necesara consultarea documentatiei
online. In aceasta sectiune sunt recomandate sursele de informare utile:
1. Documentatia standard C++ poate fi consultata la: http://www.cplusplus.com/. Acest site contine
informatii de referinta legate in C++.
2. O alta sursa importanta de informatii despre programarea C++ se gaseste la:
http://www.cprogramming.com/
3. Informatii introductive pot fi gasite la: http://www.cpp4u.com/
4. C++ FAQ5 pot fi gasite la: http://www.parashift.com/c++-faq-lite/
5. Danny Kalev are un blog cu informatii utile despre C++ care poate fi accesat la:
http://www.informit.com/guides/guide.aspx?g=cplusplus

5 Intrebari frecvente despre C++, este o versiune online a unei carti excelente C++ FAQ de Marshall Cline
7
7
Adonis Butufei

1.6 Ciclul de viata al programelor software

1.6.1 Procesul programarii


Dezvoltarea programelor nu trebuie sa se desfasoare intamplator. Progamarea este mai mult decat scrierea
codului. Intelegerea ciclului de viata este importanta pentru ca in profesia de programator numai o mica
parte din timp este petrecuta cu scrierea codului nou. O parte insemnata a timpului este petrecuta cu
modificarea, depanarea codului existent. Programele trebuie documentate, mentinute, dezvoltate si
vandute.

Etapele majore ale dezvotarii unui program sunt:


1. Design
Programatorul va face designul codului. Designul va contine algoritmii principali, clasele,
modulele, formatul fisierelor si structurile de date. Modularitatea si incapsularea prezentate
anterior sunt elementele cheie ale unui design bun.
2. Implementarea
In aceasta etapa se scrie codul care implementeaza designul.
3. Testarea
Programatorul trebuie sa defineasca un plan de testare si sa-l foloseasca pentru testarea codului.
Acesta este primul nivel al testarii. Urmatorul este trimiterea codului echipei testare a calitatii.
4. Depanarea
Datorita complexitatii programelor este o foarte mica sansa ca implementarea sa fie fara defecte.
Depanarea este procesul de corectare a defectelor programului.
In practica sunt necesare mai multe treceri prin fiecare etapa pana la finalizarea unui proiect.

1.6.2 Metodologii de dezvoltare a programelor


De-a lungul timpului, in dezvoltarea programelor au fost folosite mai multe metodologii de dezvoltare.
Vom examina mai jos doua dintre cele mai raspandite.

1.6.2.1 Programeaza si depaneaza


Acest model este foarte raspandit. Daca nu folositi in mod explicit alt model probabil ca acesta este cel
implicit.
Acest model porneste cu o idee despre ce trebuie implementat. Este posibil sa aveti o specificatie formala
sau nu. Dupa aceea, se foloseste orice combinatie de design, implementare, depanare si testare pana
produsul este gata de lansare.

Avantajele acestui model:


Nu prezinta nicio complicatie deoarece nu se consuma timp cu planificare, documentatie, testarea
calitatii. Singura activitate este scrierea codului.
Necesita foarte putina expertiza. Oricine a scris vreodata un program il poate folosi.

Acest model poate fi util pentru: proiecte foarte mici, de verificare a unui concept, demo, sau prototipuri
care se abandoneaza.
8
8
Adonis Butufei

Pentru orice alt tip de proiect, acest model este periculos. Poate sa nu aduca nicio complicatie dar, de
asemenea, nu aduce nicio modalitate de evaluare a progresului, calitatii sau identificarii riscurilor. Este
posibil ca dupa aproape un an de munca sa constatam ca designul este fundamental gresit si singura
solutie este sa aruncam codul si sa o luam de la inceput pe cand alte modele ar fi putut detecta aceasta
mult mai devreme.

1.6.2.2 Spirala
Acest model constituie una din cele mai bune abordari practice a dezvoltarii programelor deoarece se
orienteaza pe reducerea riscurilor. Fiecare proiect este descompus in mini proiecte. Fiecare miniproiect
adreseaza unul sau mai multe riscuri majore pana ce toate riscurile au fost adresate.
In acest context riscurile pot fi reprezentate de intelegerea defectuoasa a cerintelor, arhitecturii,
problemelor de performanta, problemelor tehnologice etc.

Fiecare iteratie este compusa din cinci pasi:


1. Determinarea obiectivelor, alternativelor si constrangerilor
2. Indentificarea si adresarea riscurilor
3. Evaluarea alternativelor
4. Implementarea codului pentru iteratie si verificarea corectitudinii
5. Planificarea iteratiei urmatoare

2. identificare si adresare
riscuri
1. Determinare Progres
obiective

3. Evaluare alternative

3 2 1

Livrare

5. Planificarea noii faze 4. Implementare si


verificare

9
9
Adonis Butufei

Acest model se poate combina cu alte modele in mai multe moduri. Se poate incepe proiectul cu cateva
iteratii ale spiralei pentru reducerea riscurilor dupa aceea se schimba modelul. Se pot incorpora alte
modele in iteratiile spiralei.

1.7 Medii de dezvoltare integrata (IDE)


Mediile de dezvoltare integrata sunt programe care reduc eforturile de dezvoltare. Ele cuprind printre
altele functionalitati de editare a codului, organizarea fisierelor, compilare, depanare.
Pentru acest curs vom folosi varianta free a mediului Visual C++ 2010 Express edition. Aceasta varianta
se poate downloada de la http://www.microsoft.com/visualstudio/en-us/products/2010-editions/visual-
cpp-express.
Dupa instalare, la prima rulare, este necesara introducerea unui cod de inregistrare care se obtine de pe
site-ul Microsoft urmarind indicatiile.
Documentatia detaliata pentru Visual C++ se poate accesa online de la adresa:
http://msdn.microsoft.com/en-us/library/60k1461a.aspx.
Fisierele programelor, in Visual C++ sunt organizate intr-un proiecte. Unul sau mai multe programe pot fi
organizate in solutii. Solutiile sunt fisiere create de Visual C++ care au extensia sln.

10
10
Adonis Butufei

1.8 Scrierea primului program

1.8.1 Crearea proiectului în Visual C++


In incheierea acestei lectii vom scrie un program care afiseaza clasicul Hello World! pe ecran.
Pentru aceasta vom lansa Visual C++.

Din meniul File alegem New project, selectam Win32 din lista de Project Templates si Win32 Console
Application.

11
11
Adonis Butufei

Apoi introducem numele proiectului HelloWorld si apasam butonul OK6.

Apasam next in dialogul care se afiseaza

6 In cazul proiectelor de laborator, pentru primul proiect vom selecta Create directory for solution si vom introduce numele
laboratorului in campul Solution Name
12
12
Adonis Butufei

Si apoi verificam daca avem selectat Console application, selectam Empty Project si apasam finish.

13
13
Adonis Butufei

Acum va trebui sa cream fisierul programului. Pentru aceasta selectam folderul Source Files si dam click
cu butonul din dreapta.

Alegem C++ file (.cpp), introducem numele HelloWorld.cpp si apasam Add.

14
14
Adonis Butufei

1.8.2 Scrierea codului


Acum trebuie sa scriem urmatorul cod in HelloWorld.cpp.
1: #include <iostream>
2: using namespace std;
3:
4: int main ()
5: {
6: cout << "Hello World!\n";
7: return 0;
8: }

Prima linie este o directiva preprocesor7, aceasta directiva se copieza continutul fisierului iostream in
fisierul HelloWorld.cpp. Fisierul iostream este necesar pentru a putea scrie mesaje pe ecran.
A doua linie este o directiva de folosire a spatiilor de nume, aceasta ne ajuta sa folosim mai ușor
continutul fisierului iostream8. Daca aceasta linie lipseste, pentru scrierea mesajelor pe ecran trebuie sa
folosim std::cout în loc de cout.
In liniile 4 – 8 este scris codul programului reprezentat de functia main9. Aceasta functie este apelata de
sistemul de operare atunci cand lansam programul.
Cu ajutorul comenzii cout scriem mesajul din dreapta pe ecran. Mesajele sunt incadrate intre ghilimele.
Pentru mutarea cursorului pe urmatoarea linie se adauga caracterul ( \n ) la sfarsitul mesajului.

1.8.3 Compilarea programului


Selectam meniurile Build\Build Solution sau tasta F7 si am terminat primul program!
Pentru executie avem mai multe variante: folosind meniul Debug\Start Without Debugging, combinatia
de taste Ctrl + F5, din linia de comanda.
Daca rulam din Windows Explorer sau din Visual C++ cu debugger (apasand F5) programul se executa
foarte repede si fereastra in care apare mesajul dispare inainte de a putea vedea mesajul.

1.9 Sumar
• Limbajele de progamare permit transmiterea instructiunilor catre calculator.
• Exista 5 generatii de limbaje de programare.
• Primele trei generatii au reprezentat evoulutia tehnologica pentru limbajele de programare de uz
general. A patra generatie de limbaje de programare a cuprins limbaje specializate pe anumite domenii
cum ar fi interogarea bazelor de date. A cincea generatie a ramas doar în domeniul cercetarii.
• Limbajul C++ a fost creat de Bjarne Strostrup în anul 1980 si are la baza limajul C. El suporta patru
modele de scriere a programare: procedurala, modulara, obiectuala si generica.
• Programarea procedurala imparte functionalitatea unui program în functii si date asupra carora se

7 Toate liniile de cod care incep cu caracterul # sunt directive preprocesor si se executa inainte de compilare. Vom discuta
despre directivele preprocesorului în capitolul 5.
8 Spatiile de nume vor fi studiate în capitolul 8.
9 Functiile, dupa cum vom vedea în capitolul 8 ajuta la organizarea codului.
15
15
Adonis Butufei

exercita actiunea functiilor.


• Programarea modulara permite gruparea functiilor si datelor în module.
• Programarea obiectuala ofera 3 mecanisme pentru cresterea productivitatii: incapsularea, mostenirea si
polimorfismul.
• Programarea generica ajuta la implementarea unei functionalitati comune pentru tipuri diferite de date.
• Procesul dezvoltarii programelor are 4 etape de baza: designul, implementarea, testarea si depanarea.
Pentru realizarea programelor practice aceste etape se parcurg de mai multe ori.
• Dezvoltarea programelor de calitate necesita o metodologie care ajuta descompunerea problemelor
complexe în probleme mai simple care pot fi analizate, planificate si dezvoltate.
• Pentru cresterea productivitatii se folosesc mediile de dezvoltare integrata.

1.10 Intrebari si exercitii


1. Care este rolul limbajelor de programare?
2. Care sunt imbunatatirile aduse de limbajele din generatia a 3 a?
3. Enumerati cateva limbaje de programare din generatia a 3 a.
4. Care este diferenta intre un limbaj compilat si un limbaj interpretat?
5. De ce acest limbaj a fost denumit C++?
6. Care au fost motivele pentru care a fost ales ca limbaj de baza C-ul?
7. Care sunt modelele de dezvoltare suportate de C++?
8. Care sunt cele trei aspecte importante ale programarii obiectuale? Dati exemple din viata de zi de
zi unde ar putea fi folosite aceste aspecte.
9. Instalati mediul Visual C++ Express edition si modificati programul anterior pentru a va afisa
numele.
10. Care metodologie de dezvoltare ati prefera sa o folositi? Ce anume v-a determinat sa o alegeti?

1.11 Bibliografie
• Practical C++ Programming, O'Reily, Steve Oualline, Cap 7
• Rapid Development, Microsoft Press, Steve McConnell, Cap 7

16
16
Adonis Butufei

2. ELEMENTE DE BAZA C++

CUPRINS
2.Elemente de baza C++...............................................................................................................................3
2.1 Comentariile.......................................................................................................................................3
2.1.1 Comentarii pe mai multe linii.....................................................................................................3
2.1.2 Cel specific C++ care este este pe o singura linie. ....................................................................3
2.1.3 Recomandari...............................................................................................................................3
2.2 Variabile si constante - introducere....................................................................................................3
2.2.1 Ce sunt variabilele......................................................................................................................3
2.2.2 Tipul variabilelor........................................................................................................................4
2.2.3 Declararea variabilelor...............................................................................................................4
2.2.4 Atribuirea valorilor.....................................................................................................................5
2.2.5 Citirea valorilor de la tastatura...................................................................................................5
2.2.6 Constante....................................................................................................................................6
2.2.7 Declararea constantelor simbolice..............................................................................................6
2.2.8 Constante de tip enumerare........................................................................................................7
2.2.9 Sugestii pentru denumirea constantelor......................................................................................8
2.3 Instructiuni, operatori si expresii introducere....................................................................................8
2.3.1 Instructiuni..................................................................................................................................8
2.3.1.1 Instructiuni compuse sau blocuri........................................................................................8
2.3.2 Operatori si expresii....................................................................................................................8
2.3.2.1 Operatorul = .......................................................................................................................9
2.3.2.2 Expresiile ...........................................................................................................................9
2.3.2.3 Operatorii aritmetici............................................................................................................9
2.3.2.4 Incrementarea / decrementarea variabilelor......................................................................10
2.3.2.5 Operatorii de comparare...................................................................................................10
2.4 Elemente de control al executiei......................................................................................................10
2.4.1 Secventele.................................................................................................................................10
2.4.2 Deciziile....................................................................................................................................10
2.4.2.1 Instructiunile if, else, else if..............................................................................................11
2.4.2.2 Instructiunea switch..........................................................................................................14
2.4.3 Bucle.........................................................................................................................................17
2.4.3.1 Bucle while.......................................................................................................................17
2.4.3.2 Bucle do while..................................................................................................................18
2.4.3.3 Bucle for...........................................................................................................................19
2.4.4 Instructiunea break...................................................................................................................20
2.4.5 Instructiunea continue...............................................................................................................21
2.5 Functii...............................................................................................................................................21
2.5.1 Structura functiilor....................................................................................................................21
2.5.2 Headerul functiilor....................................................................................................................22
2.5.3 Prototipul functiilor..................................................................................................................22
2.5.4 Apelul functiilor........................................................................................................................22
2.5.5 Exemple de functii....................................................................................................................22
2.5.6 Executia programelor...............................................................................................................24
2.6 Elemente de depanarea programelor: breakpoint, watches..............................................................24
2.6.1 Setarea breakpointurilor...........................................................................................................25

1
17
Adonis Butufei

2.6.2 Rularea in mod debugger a programelor..................................................................................25


2.6.2.1 Rularea programului pas cu pas........................................................................................25
2.6.2.2 Rularea programului pana la urmatorul breakpoint..........................................................25
2.6.2.3 Rularea progamului pana in dreptul cursorului................................................................25
2.6.3 Adaugarea watches...................................................................................................................26
2.7 Exemplu practic................................................................................................................................26
2.8 Anexa: cuvintele cheie standard pentru C++ ...................................................................................26
2.9 Sumar...............................................................................................................................................27
2.10 Intrebari si exercitii........................................................................................................................27
2.11 Bibliografie:....................................................................................................................................29

2
18
Adonis Butufei

2. ELEMENTE DE BAZA C++


Dupa parcurgerea acestui capitol:
• Vom putea sa scriem programe simple care ne vor permite explorarea practica a conceptelor
invatate de acum inainte.
• Vom putea organiza codul in functii pentru o mai buna organizare logica.
• Vom putea folosi variabilele si constantele.
• Vom intelege folosirea operatorilor si expresiilor.
• Vom intelege elementele de control al executiei unui program.
• Vom intelege elementele de baza ale depanarii programelor.

2.1 Comentariile
Comentariile sunt explicatii care ajuta programatorul sa inteleaga mai usor anumite aspecte ale codului.
Pentru a putea fi ignorate de compilator comentariile necesita folosirea unor caractere de idenficare.
Exista doua tipuri de comentarii: pe mai multe linii si pe o singura linie.

2.1.1 Comentarii pe mai multe linii


Cel preluat din C si care se poate extinde pe mai multe linii, cuprins intre /* si */. Exemplu:
/* Acesta este un exemplu de comentariu
pe mai multe linii si
este preluat din C. */

2.1.2 Cel specific C++ care este este pe o singura linie.


// Acest comentariu este specific C++.

2.1.3 Recomandari
Comentariile nu trebuie sa clarifice ce face codul ci de ce este aleasa o anumita abordare.
Atunci cand se schimba codul este important sa actualizam si comentariile aferente.
Trebuie sa fie concise pentru a reduce eforturile de mentenanta.

2.2 Variabile si constante - introducere

2.2.1 Ce sunt variabilele


In C++ variabilele sunt zone de memorie in care se stocheaza datele. Putem gandi un program ca avand
doua parti importante: partea de cod care contine instructiunile scrise de programator si partea de date
partea asupra careia se exercita actiunile instructiunilor.
3
19
Adonis Butufei

2.2.2 Tipul variabilelor


Tipul unei variabile specifica dimensiunea zonei de memorie rezervata pentru ea. In C++ este necesara
declararea tipului pentru fiecare variabila folosita. Aceasta ofera doua avantaje majore: eficienta si
identificarea posibilelor erori in timpul compilarii. Eficienta este realizata prin folosirea unei cantitati
optime de memorie pentru date.

In tabelul de mai jos sunt prezentate doar tipurile de variabile necesare pentru a scrie programele de
invatare iar in capitolul urmator vom prezenta toate tipurile de date fundamentale din C++ cu detaliile
aferente.
Tip de date folosit Cuvant cheie Detalii
Numere intregi int

Numere reale double

Valoare de adevar bool

O singura litera char

Siruri de caractere string Trebuie adaugat la inceputul fisierului daca nu exista:

#include <string>
using namespace std;

2.2.3 Declararea variabilelor


Pentru declararea variabilelor sunt necesare doua elemente: tipul variabilei si numele. Tipul este necesar
pentru a informa compilatorul cata memorie este necesara pentru acea variabila si numele este necesar
pentru a putea lucra cu acea zona de memorie.

In C++ este necesar sa declaram variabilele inainte de a le putea folosi. Declararea unei variabile
informeaza compilatorul sa rezerve un spatiu de memorie din zona de date si sa-i asocieze un nume.
Variabilele pot fi gandite precum scaunele dintr-o sala de spectacol. Atunci cand cumparam un set de
bilete rezervam un numar de locuri din acea sala. Prin aceasta actiune ne declaram intentia de a participa
la acel spectacol.

Pentru numele variabilelor este necesar sa respectam urmatoarele reguli si sugestii:


• Trebuie sa inceapa cu o litera sau caracterul (_)1.
• Trebuie sa nu fie un cuvant cheie pentru C++2. Este necesara consultarea documentatiei mediului
(IDE) folosit pentru a verifica daca nu au fost rezervate si alte cuvinte.
• Trebuie sa fie expresive pentru a putea intelege usor scopul lor.
• Trebuie respectata o conventie de stabilire a numelor in mod consecvent pentru o mai usoara
mentenanta a programului. Pe parcursul acestui curs numele variabilelor incepe cu litera mica.
• Fiecare variabila este util sa se foloseasca pentru un singur scop. Aceasta previne aparitia unor defecte.
• Declararea variabilelor trebuie sa fie cat mai apropiata de locul unde sunt folosite.
1 In limba engleza caracterul (_) se numeste underscore.
2 O lista a cuvintelor cheie standard C++ este prezentata la sfarsitul capitolului.
4
20
Adonis Butufei

• Numele sunt case sensitive, aceasta inseamna ca totalCount si totalcount sunt procesate ca doua
variabile diferite.

In cazul in care primele doua reguli nu sunt respectate apar erori la compilare.

Important
Variabilele trebuie sa fie unice in contextul de executie al programului. Daca doua variabile cu acel nume
sunt declarate in acelasi context compilatorul genereaza mesaje de eroare.

Exemple de declarare:
int contor; // declararea unei singure variabile pe linie

double celsius, fahrenheit; // declararea a doua variabile pe linie

bool isFinished;

char terminator;

string nume;

2.2.4 Atribuirea valorilor


Atunci cand este declarata o variabila compilatorul aloca memoria necesara pentru acea variabila insa
valoarea acelei variabile este nedeterminata. Atribuirea permite setarea valorilor pentru variabile folosind
simbolul =.
Exemplu:
int contor;
contor = 0;

Important
Pentru a elimina potentialele erori se recomanda setarea unei valori in momentul declararii variabilelor
aceasta se numeste initializare.

Exemplu:
int contor = 0;

char start = 'A';

bool isFinished = false;

double temperaturaCelsius = 32.5;

string mesaj = "Buna ziua\n";

2.2.5 Citirea valorilor de la tastatura


Citirea valorilor se realizeaza cu cin care este complementarul lui cout.

5
21
Adonis Butufei

Exemplu:
#include <iostream>
using namespace std;

int main()
{
string nume;
cout << "Introduceti numele: \n";
cin >> nume; // variabila nume este initializata de la consola

string prenume;
cout << "Introduceti prenumele: \n";
cin >> prenume; // variabila prenume este initializata
// de la consola;

cout << "Numele complet este: " << nume << " ";
cout << prenume << "\n";
}

In acest exemplu am citit numele si prenumele in doua variabile de tip string. Apoi am afisat informatia in
consola. Observam ca putem scrie mai multe informatii la consola pe aceeasi linie.

2.2.6 Constante
Cunstantele sunt variabile a caror valoare nu se schimba pe parcursul rularii programului. Se intalnesc in
doua forme de constante: literale si simbolice.
O constanta este literala atunci cand este folosita direct valoarea ei. Exemplu:
int lungimea = 35; // In acest caz 35 este o constanta literala.
// valoarea ei nu se poate schimba.

Acest tip de constanta este folosita de regula la initializarea variabileor .

2.2.7 Declararea constantelor simbolice


Exista doua modalitati de declarare a constantelor: folosind directiva de preprocesor #define sau
folosind cuvantul cheie const.
Exemplu de constanta definita cu directiva preprocsor.
#define PI 3.14

Aceasta directiva spune preprocesorului sa inlocuiasca toate aparitiile lui PI cu valoarea 3.14 in codul
sursa.

Exemplu de constanta declarata folosind cuvantul cheie const.


const double PI = 3.14;

Dupa introducerea in limbaj a specificatiei constantelor acesta este stilul recomandat pentru constante.
Prezinta marele avantaj ca erorile pot fi descoperite in timpul compilarii.

6
22
Adonis Butufei

Constantele trebuie initializare la declarare, omiterea acestui fapt genereaza erori de compilare.
Exemplu
const double FACTOR_CONVERSIE; // genereaza eroare de compilare

2.2.8 Constante de tip enumerare


C++ permite definirea unui tip de date care poate avea un set predefinit si restrans de valori. De exemplu
zilele saptamanii pot lua doar 7 valori, semaforul de circulatie are 3 culori etc. Pentru acest tip de date se
folosesc constantele enumerate.

Exemplu
enum CuloareSemafor {ROSU, GALBEN, VERDE};

Implicit compilatorul trateaza tipul enumerat ca un caz particular de intreg, initializand crescator fiecare
componenta, pornind de la valoarea 0.
Exemplu
#include <iostream>
using namespace std;

enum CuloareSemafor { ROSU, GALBEN, VERDE };

int main()
{
cout << "ROSU= " << ROSU << "\n";
cout << "GALBEN= " << GALBEN << "\n";
cout << "VERDE= " << VERDE << "\n";

return 0;
}

La rularea acestui program obtinem urmatoarele mesaje in consola:


ROSU= 0
GALBEN= 1
VERDE= 2
Press any key to continue . . .

Daca dorim putem specifica valoarea numerica pentru fiecare element din set sau pentru o parte a setului.
In ultimul caz, compilatorul continua incrementarea cu 1 pornind de la ultima valoare specificata ca in
exemplul urmator.

#include <iostream>
using namespace std;

7
23
Adonis Butufei

enum CuloareSemafor { ROSU, GALBEN = 10, VERDE };

int main()
{
cout << "VERDE= " << VERDE << "\n";
return 0;
}

La rularea acestui program se obtine urmatorul mesaj:


VERDE= 11
Press any key to continue . . .

2.2.9 Sugestii pentru denumirea constantelor


Pentru a putea fi usor de identificat este recomandabila scrierea cu majuscule a numelor
constantelor. Celelalte recomandari de la variabile se aplica si aici.

2.3 Instructiuni, operatori si expresii introducere

2.3.1 Instructiuni
Instructiunile controleaza secventa de executie, evalueaza expresiile sau nu fac nimic (in cazul
instructiunii nule care contine doar carcterul ; ). Caracterul (;) indica sfarsitul instructiunii.
Sa examinam putin urmatoarea instructiune:
a = b + c;

Aceasta poate fi citita in modul urmator: atribuie variabilei a valoarea sumei b + c. Operatorul = atribuie
ce se afla in partea dreapta variabilei din stanga.

2.3.1.1 Instructiuni compuse sau blocuri


Mai multe instructiuni pot fi grupate intr-un bloc. Acest bloc formeaza o instructiune compusa. Un bloc
de instructiuni incepe cu caracterul ({) si se termina cu caracterul (}). La sfarsitul blocului nu se pune
terminatorul (;).
Exemplu
{
temp = x;
x = y;
y = temp;
}

2.3.2 Operatori si expresii


Operatorii sunt simboluri care determina compilatorul sa calculeze un rezultat. Operanzii sunt datele
asupra carora se exercita actiunea operatorilor. In C++ sunt mai multe categorii de operatori. In acest
8
24
Adonis Butufei

capitol vom studia operatorul de atribuire, operatorii aritmetici si operatorii de comparare.

2.3.2.1 Operatorul =
Acest operator seteaza valoarea din partea dreapta variabilei care se afla in partea stanga a semnului egal.

2.3.2.2 Expresiile
In C++ o expresie este orice combinatie de operatori, constante, apeluri de functii care calculeaza o
valoare.
Exemple:
4.5; // returneaza valoarea 4.5
a = b+c;
d = a = b + c;

Sa analizam ultima expresie, care este evaluata in ordinea urmatoare:


1. calculeaza suma b + c
2. atribuie rezulatul lui a
3. atribuie rezultatul lui a = b + c lui d

2.3.2.3 Operatorii aritmetici


In tabelul de mai jos sunt prezentati operatorii aritmetici
Operator Explicatie Exemplu
+ adunare result = 4 + 5; // result = 9
- scadere result = 20 – 15; // result = 5
* inmultire result = 8 * 4; // result = 32
/ impartire result = 36 / 9; // result = 4
% modulo (restul impartirii intregi) result = 10 % 4; // result = 2

Important
Rezultatul impartirii depinde de tipul operanzilor: daca ambii termeni sunt de tip intreg, compilatorul
efectueaza impartirea intreaga chiar daca in stanga egalului este o variabila de tip real. In cazul in care cel
putin unul din termenii impartirii este real atunci compilatorul efectueaza impartirea reala si rezultatul
este cel asteptat.

Exemplu
double impartireIntreaga = 3 / 4; // rezultatul evaluarii este 0
// deoarece ambii termeni sunt
// numere intregi.

double impartireReala = 3.0 / 4; // rezultatul evaluarii este 0.75

9
25
Adonis Butufei

2.3.2.4 Incrementarea / decrementarea variabilelor


Este un caz particular intalnit frecvent in programare: valoarea unei variabile este adunata sau scazuta cu
o unitate. Exista doua tipuri de incrementare / decrementare: cu prefixare atunci cand operatorul apare
inaintea operandului si cu postfixare atunci cand operatorul apare dupa operand. In capitolul 4 vom
discuta in detaliu diferentele dintre acesti operatori.
Exemple:
int a = 0;
int b = 1;
++a; // operatorul cu prefixare.
b++; // operatorul cu postfixare.

2.3.2.5 Operatorii de comparare


Acesti operatori compara doua numere pentru a determina relatia dintre ele si returneaza o valoare true
sau false.
Operator Exemplu Explicatii
== x == y Se testeaza egalitatea valorilor lui x si y
!= x != y Se testeaza daca x si y au valori diferite
> x > y Se testeaza daca valoarea lui x este mai mare decat valoarea lui y
< x < y Se testeaza daca valoarea lui x este mai mica decat valoarea lui y
>= x >= y Se testeaza daca valoarea lui x este mai mare sau egala cu valoarea lui y
<= x <= y Se testeaza daca valoarea lui x este mai mica sau egala cu valoarea lui y

2.4 Elemente de control al executiei


Daca analizam activitatile zilnice vom observa niste tipare care se repeta. Sunt activitati secventiale pe
care le incepem si le continuam pana la sfarsit fara intrerupere. De asemenea sunt activitati in care trebuie
sa evaluam anumite alternative si ceea ce va urma depinde de rezultatul evaluarii. Mai sunt activitati pe
care le repetam periodic atunci cand sunt indeplinite anumite conditii.

Deoarece activitatea de programare presupune in primul rand rezolvarea unor probleme din viata reala
aceste elemente le putem exprima si in cod cu ajutorul a trei elemente: secventa, decizia si buclele.

2.4.1 Secventele
Exista probleme a caror rezolvare presupune o succesiune de pasi. In acest caz instructiunile programului
sunt scrise intr-o secventa.

2.4.2 Deciziile
In multe dintre programele reale se evalueaza valoarea unor variabile si in functie de aceasta evaluare
executia se ramifica. Putem gandi partea de decizii ca o intersectie, executia va continua in functie de
directia pe care o alegem.

10
26
Adonis Butufei

2.4.2.1 Instructiunile if, else, else if


Prima forma este cea mai simpla decizie:
if(conditie)
{
// instructiuni executate doar daca este indeplinita conditia
}

Pe prima linie avem testarea conditiei cu instructiunea if(conditie). Aici este important de remarcat ca am
folosit o instructiune compusa. In acest mod se delimiteaza foarte clar instructiunile care se executa atunci
cand este indeplinita conditia.

Exemplu urmatoarea functie testeaza daca parametrul are valoare para.


1: bool EsteNumarPar(int val)
2: {
3: int rest = val % 2;
4: if(0 == rest)
5: {
6: return true;
7: }

8: return false;
9: }

Frecvent atunci cand se foloseste instructiunea if se uita scrierea celui de-al doilea egal al operatorului.
Atunci cand expresia se compara cu o constanta eroarea poate fi identificata la compilare daca se scrie
constanta in partea stanga ca in linia 4.

A doua forma de decizie foloseste ambele ramuri ale deciziei. De exemplu folosind functia de mai sus in
urmatorul program:

#include <iostream>
using namespace std;

bool EsteNumarPar(int val)


{
//...
}

int main ()
{

cout << "Introduceti un numar: \n";


11
27
Adonis Butufei

int numar;
cin >> numar;
if(EsteNmarPar(numar))
{
cout << "Numarul este par\n";
}
else
{
cout << "Numarul este impar\n";
}
}

Exista cazuri cand trebuie sa testam mai multe alternative. O solutie poate fi sa tratam in interiorul
blocului else variantele ramase. Ca in exemplul urmator unde citim culoarea semaforului de pietoni:
r pentru rosu si v pentru verde de la tastatura. Apoi afisam mesajele "Stop!" pentru rosu,
"Puteti trece." pentru verde si "Culoare invalida." pentru orice alta valoare.

#include <iostream>
using namespace std;

int main()
{
cout << "Introduceti culoarea semaforului: r,v\n";

char culoareSemafor;

cin >> culoareSemafor;

if(culoareSemafor == 'r')
{
cout << "Stop!\n";
}
else
{
if(culoareSemafor == 'v')
{
cout << "Puteti trece.\n";
}

else

12
28
Adonis Butufei

{
cout << "Culoare invalida.\n";
}
}

return 0;
}

In cazul in care avem mai multe variante logica acestor conditii devine greu de urmarit si inteles. Din
aceste motive pot apare multe defecte in timpul mentenantei.

O solutie mai buna se poate obtine folosind a treia forma in care folosim si combinatia else if ca in
exemplul de mai jos.
#include <iostream>
using namespace std;

int main()
{
cout << "Introduceti culoarea semaforului: r,v\n";

char culoareSemafor;

cin >> culoareSemafor;

if(culoareSemafor == 'r')
{
cout << "Stop!\n";
}
else if(culoareSemafor == 'v')
{
cout << "Puteti trece\n";
}
else
{
cout << "Culoare invalida\n";
}

return 0;
}

Din punct de vedere logic implementarile sunt echivalente insa aceasta forma este mult mai clara si este

13
29
Adonis Butufei

de preferat primeia.

De evitat
1. Adaugarea terminatorului (;)

if(conditie); // ; aici este gresit daca este indeplinita conditia


{ // instructiunea nula, adica (;) pus din greseala
// aceste instructiuni se executa mereu!
}

2. Folosirea operatorului = in loc de ==


if( x = y)
{
//...
}

Dupa compilare este echivalent cu urmatorul cod:


x = y;
if( x != 0)
{
// ...
}

3. Scrierea instructiunilor pe aceeasi linie cu if


if( contor < MAX) contor++;

Aici avem doua instructiuni pe aceeasi linie si este mai greu de intretinut si inteles.

4. Evaluarea variabilelor ne initializare.


int x;
if( 0 == x)
{
// ...
}

In acest caz valoarea variabilei x este nedeterminata si functionarea programului este aleatoare!

2.4.2.2 Instructiunea switch


Atunci cand exista mai multe optiuni se poate folosi instructiunea switch daca evaluarea acelor optiuni
se poate face cu o expresie care returneaza o valoare de tip intreg , enum sau char. Folosirea se face ca in
exemplul de mai jos:

#include <iostream>
#include <string>
14
30
Adonis Butufei

using namespace std;


int main()
{
cout << "Introduceti culoarea semaforului: r,v\n";
char culoareSemafor;
cin >> culoareSemafor;
switch(culoareSemafor) // inceputul blocului switch, tipul variabilei care
{ // se evalueaza trebuie sa fie char, int sau enum.
case 'r': // constante cu care se compara valoarea variabilei.
cout << "Stop!\n"; // cod care se executa cand variabila are valoarea 'r'
break; // transfera executia dupa blocul switch.
case 'v':
cout << "Stop!\n";
break;
default:
cout << "Culoare invalida\n";
break;
}

return 0;
}

Observam ca blocul incepe cu instructiunea switch care evalueaza expresia. In interiorul blocului avem
o succesiune de alternative reprezentate de cuvintele cheie case.
Valorile acestor alternative sunt constante. Dupa aceea pentru fiecare valoare avem una sau mai multe
instructiuni. In momentul cand am terminat de tratat acea alternativa se iese din bloc cu instructiunea
break.
Optional exista alternativa default care este aleasa daca nici una dintre celelalte nu a fost rezultatul
evaluarii expresiei.

Important
In absenta instructiunii break din alternativa curenta, se continua executarea secventei de instructiuni din
interiorul blocului switch pana la intalnirea primului break sau pana la sfarsitul blocului switch.
Acest aspect trebuie tratat cu atentie pentru ca exista cazuri cand acest comportament este necesar, dar si
cazuri in care s-a omis instructiunea break din greseala.

Exemplu de identificare a vocalelor si consoanelor (pentru simplitate am omis diacriticele).


#include <iostream>
using namespace std;

int main()

15
31
Adonis Butufei

cout << "introduceti un caracter\n";


char ch;
cin >> ch;

switch(ch)
{
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
cout << "caracterul este vocala\n"; // se executa pentru atunci cand ch
break; // are una din valorile specificate de
// case (a, e, i, o sau u)
default:
cout << "caracterul este consoana\n";
break;
}

return 0;
}

In exemplu vocalele sunt selectate fiecare in parte si se executa un cod comun pentru ele iar pentru
consoane se executa codul din sectiunea default.

Declararea variabilelor si blocul switch


Compilatorul Visual C++ genereaza eroare de compilare atunci cand variabilele sunt declarate in
interiorul blocului switch ca in exemplul de mai jos:
switch(tipCitire)
{
case 0:
int iVal; // eroare de complilare deoarece iVal este declarata in acest bloc.
cin >> iVal;
cout << iVal;
break;
}

Pentru rezolvare sunt doua solutii:


1. Se declara variabila iVal inainte de blocul swicth bloc de cod
int iVal;
switch(tipCitire)
{
16
32
Adonis Butufei

case 0:
cin >> iVal;
cout << iVal;
break;
}

2. Se declara variabila in interiorul unui bloc local in interiorul case:


switch(tipCitire)
{
case 0:
{ // Se foloseste un bloc local pentru declararea iVal
int iVal;
cin >> iVal;
cout << iVal;
}
break;
}

2.4.3 Bucle
Permit executarea unei secvente de instructiuni atat timp cat este indeplinita o conditie. Conceptual exista
trei tipuri de bucle: while, do while si for.

2.4.3.1 Bucle while


Acest tip de bucla efectueaza testul inainte de executia secventei. Sintaxa pentru aceasta bucla este:
while(conditie)
{
// secventa de instructiuni
}

Un exemplu simplu de folosire este programul urmator care calculeaza factorialul unui numar.
1: #include <iostream>
2: using namespace std;
3:
4: int main()
5: {
6: cout << "Introduceti un numar intreg\n";
7:
8: int n;
9: cin >> n;
10:
11: int factorial = 1;
12: while(n > 1)

17
33
Adonis Butufei

13: {
14: factorial = factorial * n;
15: n--;
16: }
17:
18: cout << "n! = " << factorial << "\n";
19: return 0;
20: }

In linia 6 se afiseaza mesajul pe ecran. Valoarea variabilei este citita de la tastatura in linia 9. In linia 11
valoarea factorialului este initializata cu 1. In linia 14 se calculeaza produsul intre valoarea anterioara si
n. In linia 15 valoarea lui n este decrementata. Procesul se continua pana cand n devine egal cu 1 moment
in care bucla se termina si se afiseaza rezultatul pe ecran.

2.4.3.2 Bucle do while


Acesta este un tip de bucla in care testul se face dupa prima iteratie. Sintaxa pentru aceasta bucla este:
do
{

// secventa instructiuni

} while (conditie);

Urmatorul program calculeaza factorialul unui numar folosind bucla do while.


1: #include <iostream>
2: using namespace std;
3:
4: int main()
5: {
6: cout << "Introduceti un numar intreg\n";
7:
8: int n;
9: cin >> n;
10:
11: int factorial = 1;
12: if (n > 0)
13: {
14: do
15: {
16: factorial = factorial * n;
17: --n;
18: }while(n > 1);
19: }

18
34
Adonis Butufei

20: cout << "n! = " << factorial << "\n";


21: return 0;
22: }

Singura diferenta intre acest tip de bucla si precedentul este ca aici testul conditiei se face la final.
Secventa se executa prima data indiferent de valoarea conditiei.

2.4.3.3 Bucle for


Aceasta este o bucla cu incrementare. Sintaxa pentru aceasta bucla este:
for(initializare; conditie; increment)
{
// secventa instructiuni.
}

Urmatorul program calculeaza factorialul unui numar folosind bucla for.


1: #include <iostream>
2: using namespace std;
3:
4: int main()
5: {
6: cout << "Introduceti un numar intreg\n";
7:
8: int n;
9: cin >> n;
10:
11: int factorial = 1;
12:
13: for(int i = n; i > 1; --i)
14: {
15: factorial = factorial * i;
16: }
17:
18: cout << "n! = " << factorial << "\n";
19: return 0;
20: }

Executia programului pana la linia 13 este similara cu exemplele anterioare. Executia buclei for se face in
modul urmator:
1. La prima iteratie se declara si se intializeaza variabila i cu valoarea lui n.
2. Se evalueaza conditia in cazul nostru i > 1.
3. In caz afirmativ porneste executia seventei din corpul buclei.
4. Altfel se merge la urmatoarea instructiune dupa bucla.
19
35
Adonis Butufei

5. Se actualizeaza factorial cu valoarea factorial * i


6. Se decrementeaza i
7. Se repeta secventa de la punctul 2.
Dupa terminarea buclei se afiseaza rezultatul pe ecran.
Observatii
• Pentru intelegerea codului este recomandat ca expresiile de initilalizare, conditie si increment sa fie cat
mai simple.
• Oricare din aceste expresii poate lipsi. Daca a doua expresie, cea pentru conditie, compilatorul
considera conditia indeplinita.

2.4.4 Instructiunea break


Exista cazuri frecvente in care este necesara terminarea executiei buclei inainte de verificarea testului.
Pentru aceasta se foloseste instructiunea break. In exemplul urmator se testeaza daca numrarul introdus
de la tastatura este numar prim.
1: #include <iostream>
2: using namespace std;
3:
4: int main()
5: {
6: cout << "Introduceti un numar intreg\n";
7: int n;
8: cin >> n;
9:
10: int i=2;
11: for(;i < n; i++)
12: {
13: if(0 == n % i )
14: {
15: cout << "Numarul nu este prim\n";
16: break;
17: }
18: }
19: if(i == n)
20: {
21: cout << "Numarul este prim\n";
22: }
23: return 0;
24: }

In acest exemplu am definit variabila contor in linia 9 in afara buclei deoarece este utilizata pentru
verificarea conditiei din linia 19. In cazul in care a fost gasit un divizor pentru numar, altul decat 1 sau n
numarul nu este prim si bucla se opreste in linia 16.

20
36
Adonis Butufei

2.4.5 Instructiunea continue


Exista cazuri cand este necesara executia pentru iteratia urmatoare fara a executa toti pasii iteratiei
curente. Pentru aceasta se foloseste instructiunea continue. La intalnirea ei programul revine la inceputul
buclei sarind peste instructiunile care o urmeaza3.
In urmatorul exempul se afiseaza doar numerele pare in intervalul 0 – 10 pe ecran.
1: for(int i = 0; i < 10; i++)
2: {
3: if(0 == i % 2)
4: {
5: continue;
6: }
7: cout << i << "\n";
8: }

Atunci cand i este numar par conditia din linia 3 este indeplinita si executia se contiuna de la inceputul
buclei.

2.5 Functii4
O functie este un bloc de cod care este executat cand este apelat din program. Functiile permit
descompunerea problemelor complexe intr-un set de probleme simple care pot fi usor de implementat.
Functiile scrise corespunzator ascund detaliile de implementare. Sunt folosite pentru evitarea scrierii
repetate a aceluiasi cod intr-un program sau in programe diferite.

2.5.1 Structura functiilor


Functiile au urmatoarea sintaxa de scriere:
1: tip Nume (parametru1, parametru2, ...)
2: {
3: // instructiuni
4: }

• tip – reprezinta tipul de date returnat dupa executia functiei5.


• Nume – reprezinta numele functiei.
• Lista de parametri: (parametru1, parametru2, ...) – contine atati parametri cati sunt
necesari pentru executia secventei. Fiecare parametru este reprezentat de un tip si un nume care poate
fi gandit ca o variabila apartinand functiei. Parametri permit transferul datelor catre functie.
• Corpul functiei – blocul de cod care contine instructiunile functiei (liniile 2 – 4).

3 In cazul buclelor while si do while trebuie acordata atentie deoarece uzual incrementarea / decrementarea contorului se face
la sfarsit.
4 In capitolul 8 vor fi prezentate functiile in detaliu.
5 Functiile care nu returneaza date folosesc void ca si tip de return.
21
37
Adonis Butufei

2.5.2 Headerul functiilor


Prima linie din exemplul de mai sus se numeste headerul functiei.
1: tip Nume (parametru1, parametru2, ...)

2.5.3 Prototipul functiilor


Prototipul functiei furnizeaza informatii compilatorului pentru a determina daca functia este folosita
corect sau nu. El contine aceeasi informatie ca si headerul functiei plus terminatorul de instructiune (;)
tip Nume (parametru1, parametru2, ...);

Important:
• Ca si variabilele functiile trebuiesc declarate sau definite inainte de a fi folosite.
• Declararea functiilor presupune scrierea prototipului.
• Definirea functiilor presupune scrierea headerului si a corpului functiei.
• O functie poate fi declarata de mai multe ori insa trebuie sa fie definita o singura data.

2.5.4 Apelul functiilor


Apelul functiilor reprezinta executarea codului functiei. Acesta se realizeaza in scriind numele functiei si
transmiterea valorilor parametrilor pentru care dorim executia functiei.

2.5.5 Exemple de functii


In exemplul de mai jos este prezentata definirea functiei Suma (liniile 1 – 3) care calculeaza suma a doua
variabile de tip int si returneaza rezultatul. In linia 8 este apelata functia Suma pentru valorile 3 si 5. Dupa
executia acestei linii de cod variabila s va contine rezultatul returnat de functia Suma.
1: int Suma(int x, int y)
2: {
3: return x + y;
4: }
5:
6: int main()
7: {
8: int s = Suma(3, 5);
9: return 0;
10: }

Daca dorim sa definim functia suma dupa functia main este necesar sa folosim prototipul functiei ca in
exemplul de mai jos:
In linia 1 este declarata functia Suma, apelul funciei este in linia 5 iar definitia functiei este dupa corpul
functiei main in liniile 9 – 12.
Fara declararea functiei in linia 1 obtinem eroare de compilare deoarece compilatorul nu are informatii
pentru apelul functiei Suma.
1: int Suma(int x, int y);

22
38
Adonis Butufei

2:
3: int main()
4: {
5: int s = Suma(3, 5);
6: return 0;
7: }
8:
9: int Suma(int x, int y)
10: {
11: return x + y;
12: }

In exemplul de mai jos este prezentata o functie care nu returneaza rezultate.


1: #include <iostream>
2: #include <string>
3: using namespace std;
4:
5: void AfisareMesaj(string mesaj)
6: {
7: cout << mesaj << "\n";
8: }
9:
10: int main()
11: {
12: AfisareMesaj("Hello");
13: return 0;
14: }

Recomandari
• Pentru usurinta depanarii si intelegerii programelor este recomandata scrierea unei singure instructiuni
pe linie.
• O functie trebuie sa faca un singur lucru (de exemplu sa citeasca variabile, sa calculeze, sa afiseze
rezultate etc).
• Numele functiei este recomandabil sa inceapa cu litera mare pentru a putea distinge functiile de
variabile.
• Numele functiei trebuie sa reprezinte prelucrarea realizata de functie.
• Este recomandabil ca o functie sa aiba cel mult 7 parametri.

23
39
Adonis Butufei

2.5.6 Executia programelor


Executia programului se face parcurgand secventa de instructiuni din interiorul functiei main.
In urmatorul exemplu se afiseaza doua mesaje succesive pe ecran.

#include <iostream>
using namespace std;

int main()
{
cout << "Aceasta este prima instructiune a programului.\n";
cout << "Aceasta este a doua instructiune a programului\n";

return 0;
}

Practic toate instructiunile unui program pot fi scrise in interiorul functiei main. Pentru cazurile practice
acest mod nu este recomandabil deoarece un program scris in acest mod este aproape imposibil de
mentinut si inteles.

Pentru o organizare logica mai eficienta functionalitatea unui program se imparte in mai multe functii
care sunt mai usor de inteles, implementat si mentinut.

2.6 Elemente de depanarea programelor: breakpoint, watches


Depanarea programelor este o deprindere necesara unui programator. Programele reale rareori
functioneaza corect de la prima rulare. De multe ori defectele sunt introduse in perioada de mentenanta
sau de adaugare de noi functionalitati. Chiar daca programele ar functiona corect de la prima rulare tot
este necesara verificarea corectitudinii pentru a fi siguri de asta.
Programele pot contine erori de sintaxa si erori de logica. Erorile de sintaxa sunt detectate automat de
compilator care precizeaza fisierul, locul in fisier si tipul erorii. Acestea sunt cel mai usor de fixat si cele
mai sigure pentru ca nu putem compila programul fara rezolvarea lor.
Am putea crede ca dupa ce am reusit sa compilam cu succes programul, jobul nostru s-a terminat. In
realitate insa lucrurile nu stau deloc asa. Pentru a fi siguri ca programul functioneaza corect este necesara
executarea programului pas cu pas si verificarea comportamentului prin toate ramurile logice.

Multe din programele comerciale „crapa” atunci cand utilizatorul doreste sa faca o actiune normala
deoarece echipa de dezvolatare nu a reusit sa verifice executia prin acea ramura logica.

Pentru a verifica un program dezvoltatorul poate rula programul din mediul de dezvoltare in mod
debbugger. Debuggerul este o componenta a mediului de dezvoltare care ofera printre altele posibilitatea
executarii programului pas cu pas, setarea unor puncte de intrerupere (breakpoint) in care se opreste
executia, examinarea valorilor variabilelor (watches).

In acest paragraf vom invata urmatoarele elemente:

24
40
Adonis Butufei

1. Setarea unui breakpoint


2. Executarea pas cu pas a programului
3. Setarea watch-urlilor pentru examinarea valorilor unei variabile.

2.6.1 Setarea breakpointurilor


Se realizeaza din meniul Debug/Toggle Breakpoint sau folosind tasta F9 atunci cand ne aflam cu cursorul
pe linia dorita. Setarea breakpointurilor se poate face inainte de lansarea in executie sau in timpul
executiei. Pentru a rula un program in mod debugger executia unui program, de regula, este necesar sa
avem cel putin un breakpoint in program.

2.6.2 Rularea in mod debugger a programelor


Pentru rularea programului avem urmatoarele modalitati: rularea pas cu pas, rularea pana la urmatorul
breakpoint sau rularea pana in dreptul cursorului.

2.6.2.1 Rularea programului pas cu pas.


Rularea pas cu pas trateaza in trei moduri diferite apelurile de functii: step over, step into si step out.
In modul step over, executia programului continua in fisierul curent la urmatoarea instructiune dupa apel.
In modul step into, executia continua in fisierul unde se afla acea functie daca acesta este disponibil,
In modul step out executia continua in fisierul de unde s-a apelat functia curenta.

Aceste moduri se pot alege din meniul Debug, butoanele dedicate din toolbarul Debug sau tastele: F11
pentru step into, F10 pentru step over si Shift + F11 pentru step out.

2.6.2.2 Rularea programului pana la urmatorul breakpoint


Acesta metoda permite deplasarea rapida intre doua breakpointuri succesive folosind meniul
Debug/Continue, buttonul dedicat din toolbarul Debug sau tasta F5.

2.6.2.3 Rularea progamului pana in dreptul cursorului


Aceasta metoda permite executarea programului pana la locul unde se afla cursorul si oprirea executiei in
acest punct. Aceasta se poate realiza pozitionand cursorul in fisierul sursa in pozitia dorita, Meniu
Contextual/Run to cursor sau cu combinatia de taste Ctrl + F10. Aceasta comanda poate porni
debuggerul daca nu ruleaza deja.

Nota
Executia se va opri inainte de a ajunge la cursor in urmatoarele conditii:
1. Exista un breakpoint intre punctul curent de executie si pozitia cursorului,
2. Exista o zona de cod care solicita interactiunea cu utilizatorul intre punctul curent de executie si
pozitia cursorului.

25
41
Adonis Butufei

2.6.3 Adaugarea watches


Adaugarea de watches pentru inspectarea valorilor variabilelor se face doar in timpul executiei
programului cu ajutorul debuggerului in modul urmator:
Selectam variabila de inspectat din fisierul sursa si din meniul contextual selectam Add Watch.

Nota
Pentru a putea examina valorile variabilelor este necesar ca programul sa fie compilat in configuratia
Debug. Acest tip de compilare adauga informatii aditionale in executabil care faciliteaza operatiile de
rulare in mod interactiv a executiei. Aceasta se realizeaza din meniul Build/Configuration Manager.

2.7 Exemplu practic


La sfarsitul acestui capitol vom implementa practic exemplul de calculare a factorialului cu bucla for si
vom trasa executia programului in debugger urmarind pasii de mai jos:
1. Pornim Visual C++ si creem un nou proiect asa cum am prezentat in capitolul precedent.
2. Adaugam un nou fisier cu numele factorial.cpp si scriem codul prezentat la bucla for.
3. Compilam pentru a verifca daca nu avem erori de sintaxa. Punem cursorul pe linia primei instructiuni
din functia main.
4. Adaugam un breakpoint folosind tasta F9.
5. Apoi executam programul cu ajutorul tastei F5.
6. In acest moment executia programului se opreste pe aceasta linie.
7. Adaugam watch pentru variabilele n, factorial si i.
8. In acest moment putem vedea informatiile despre cele trei variabile in fereastra watch a mediului.
9. Executam pas cu pas programul folosind tasta F10 si examinam valorile in fereastra watches.

2.8 Anexa: cuvintele cheie standard pentru C++


asm auto bool break case
catch char class const const_cast
continue default delete do double
dynamic_cast else enum explicit export
extern false float for friend
goto if inline int long
mutable namespace new operator private
protected public register reinterpret_cast return
short signed sizeof static static_cast
struct switch template this typedef
typeid typename union unsigned using
virtual void volatile wchar_t while

26
42
Adonis Butufei

2.9 Sumar
• Comentariile ajuta programatorul sa inteleaga mai usor anumite aspecte ale programului. Ele nu
trebuie sa explice de ce s-a ales o anumita solutie.
• Variabilele sunt locatii de memorie in care se stocheaza date. Variabilele trebuiesc declarate inainte de
folosire. Declararea variabilelor are doua elemente tipul si numele.
• Atribuirea permite setarea unei valori intr-o variabila.
• Constantele sunt variabile ale caror valori nu se pot schimba in timpul executiei.
• Pentru transmiterea comenzilor se folosesc instructiunile.
• Folosind acoladele se pot grupa instructiunile intr-un bloc.
• Programele au trei categorii elemente de control a executiei: secventele de instructiuni, deciziile si
buclele.
• Pentru decizii se folosesc instructiunile if, if/else, if/else if/ else si switch.
• Sunt trei tipuri de bucle: while, do while si for.
• Functiile sunt folosite pentru evitarea scrierii repetate a aceluiasi cod.
• Pentru verificarea executiei programele se executa in mod debugger. Aceasta permite executia pas cu
pas, verificarea valorilor variabilelor.

2.10 Intrebari si exercitii


1. Care este diferenta dintre o variabila si o constanta?
2. Care este diferenta dintre tipurile de comentarii // si /* */?
3. Urmatorul program are erori. Care sunt acelea?

#include <iostream>
using namespace std;

main ()
{
cout << Care sunt erorile?\n”;
}

4. Corectati urmatorul program:


#include <iostream>

int main()
{
cout << Introduceti anul nasterii\n;

27
43
Adonis Butufei

cin >> anNastere;

cout << Introduceti anul curent\n;


cin >> anCurent;

varsta = anCurent – anNastere;

return 0;
}

5. Rezultatul urmatoarei functii este incorect. Unde este greseala?

double Division(int a, int b)


{
return a /b;
}

6. Evaluati urmatoarele expresii:


a) 10 % 3
b) 8 * 9 + 2
c) 6 * 3 /4
d) 3/4 * 8

7. Rezultatul urmatoarei functii este incorect. Unde este greseala?


int Sum(int range)
{
int result;

for(int i =0; i < range; i++)


result = result + i;

return result;
}

8. Scrieti o functie care primeste numarul de ore si minute ca parametri si returneaza valoarea acelui
interval in minute. Exemplu: pentru 1 ora si 30 minute va returna 90 minute.
9. Scrieti o functie care primeste un interval in minute si tipareste pe ecran numarul de ore si minute.
Exemplu: pentru 90 minute va afisa o ora si 30 minute.
10. Scrieti un program care citeste varsta in ani si calculeaza numarul de luni corespunzatoare acestor ani.
28
44
Adonis Butufei

2.11 Bibliografie:
• Sams Teach Yourself C++ in One Hour a Day, sixth edition, Sams, Jesse Liberty, Siddhartha Rao,
Bradley L. Jones; Cap 2 – 5, 7
• C++ Without Fear, second edition, Prentice Hall, Brian Overland; Cap 1, 2.

29
45
Adonis Butufei

3. VARIABILE

CUPRINS
3.Variabile.....................................................................................................................................................2
3.1 Dimensiunea variabilelor...................................................................................................................2
3.1.1 Biti si octeti.................................................................................................................................2
3.1.2 Determinarea numarului de octeti pentru variabile - operatorul sizeof .....................................2
3.1.3 Reprezentarea numerelor in baza 2 (binar)................................................................................3
3.1.4 Reprezentarea numerelor in baza 16 (hexazecimala)................................................................3
3.1.4.1 Initializarea variabilelor intregi cu valori hexazecimale.....................................................4
3.1.4.2 Unde se foloseste reprezentarea hexazecimala...................................................................4
3.1.5 Reprezentarea numerelor in baza 8 (octal).................................................................................5
3.2 Domeniul de viata al variabilelor.......................................................................................................5
3.2.1 Locul declararii variabilelor si influenta acestuia asupra domeniului de viata..........................5
3.2.1.1 Mascarea variabilelor..........................................................................................................7
3.2.1.2 Domeniul de viata al variabilelor si apelul functiilor.........................................................8
3.2.2 Calificatorii de context...............................................................................................................8
3.2.2.1 Calificatorul auto................................................................................................................8
3.2.2.2 Calificatorul register...........................................................................................................9
3.2.2.3 Calificatorul extern.............................................................................................................9
3.2.2.4 Calificatorul static...............................................................................................................9
3.3 Variabile numerice............................................................................................................................10
3.3.1 Variabile intregi........................................................................................................................10
3.3.1.1 Calculul dimensiunii necesare pentru stocarea variabilelor intregi pozitive....................10
3.3.1.2 Procesarea semnului..........................................................................................................11
3.3.1.3 Calificatori de semn..........................................................................................................11
3.3.1.4 Calificatori de marime......................................................................................................11
3.3.1.5 Limitele variabilelor intregi..............................................................................................12
3.3.1.6 Depasirea limitelor............................................................................................................13
3.3.2 Variabile reale...........................................................................................................................14
3.3.2.1 Tipuri de variabile reale suportate de C++ .......................................................................14
3.3.2.2 Moduri de scriere a numerelor reale.................................................................................14
3.3.2.3 Intializarea variabilelor reale............................................................................................15
3.3.2.4 Testarea egalitatii pentru variabilele reale........................................................................15
3.3.2.5 Limitele variabilelor reale.................................................................................................17
3.4 Variabile de tip caracter....................................................................................................................18
3.4.1.1 Afisarea codurilor ASCII pe ecran....................................................................................18
3.4.1.2 Citirea valorilor tip caracter de la tastatura.......................................................................19
3.4.1.3 Categorii de coduri ASCII................................................................................................19
3.5 Sumar...............................................................................................................................................21
3.6 Intrebari si exercitii..........................................................................................................................22
3.7 Bibliografie......................................................................................................................................24

1
46
Adonis Butufei

3. VARIABILE
In acest capitol vom discuta despre:
• Dimensiunea variabilelor: cum se determina dimensiunea variabilelor folosind operatorul sizeof, ce
legatura este intre dimensiunea variabilelor si intervalul de valori cu care lucreaza acea variabila, baze
de numeratie.
• Variabile de tip caracter, codurile ASCII, categorii de caractere si secvente escape.
• Variabile de tip numeric, procesarea semnului, limitele intervalelor de valori pentru fiecare tip de date
standard folosite in C++ si conversia tipurilor de date.
• Contextul de acces al variabilelor.

3.1 Dimensiunea variabilelor


In capitolul precedent am vazut ca o variabila reprezinta o zona de memorie. Cantitatea de memorie
rezervata de compilator pentru acea variabila este determinata de tipul de variabila. In acest paragraf vom
studia in detaliu corelatia dintre valoarea datelor si dimensiunea variabilelor.

3.1.1 Biti si octeti


Calculatoarele au fost construite sa proceseze informatia binara. Elementul cel mai mic de informatie
binara este bitul. Acesta poate avea doua valori: 1 sau 0. Pentru a putea reprezenta date reale este necesara
folosirea mai multor biti simultan.
Deoarece reprezentarea textului a fost una dintre primele probleme adresate de calculatoare si pentru
reprezentarea unui caracter au fost necesari 8 biti acesta a devenit etalonul pentru reprezentarea datelor.

O grupare de 8 biti reprezinta un octet sau un byte.


Din motive de arhitectura hardware dimensiunea variabilelor este reprezentata prin multiplii intregi de
octeti. Limbajul C++ are tipuri predefinite de variabile care au uzual 1, 2, 4, 8 octeti.

3.1.2 Determinarea numarului de octeti pentru variabile - operatorul sizeof


Pentru a determina numarul de octeti asociat unei variabile se foloseste operatorul sizeof, ca in urmatorul exemplu:

#include <iostream>
using namespace std;

int main()
{
cout << sizeof(int) << "\n";
}

2
47
Adonis Butufei

3.1.3 Reprezentarea numerelor in baza 2 (binar)


In activitatile zilnice suntem obisnuiti cu reprezentarea zecimala a numerelor. Inainte de a explora
reprezentarea binara a numerelor sa aruncam o privire asupra modului in care folosim baza zecimala.
Orice numar de 4 cifre, de exemplu, se poate reprezenta in modul urmator:

3 2 1 0
abcd 10 =a∗10 + b∗10 + c∗10 + d∗10

Unde a, b, c si d pot lua valori in intervalul [0 – 9].


Acelasi numar se poate reprezenta in baza 2 cu ajutorul formulei:
(abcd )10=b n∗2 n+ b n−1∗2n−1+ ⋯+ b1∗21+ b0∗20 =( bn b n−1⋯b1 b 0)2

Unde b n , b n−1⋯ , b0 apartin intervalului [0, 1].


Bitul b n se numeste cel mai semnificativ bit1 iar bitul b 0 se numeste cel mai putin semnificativ bit2.

Exemple:
(0)10=0∗2 1+ 0∗2 0=(00)2
(1)10=0∗21+ 1∗20 =( 01)2
(2)10=1∗21+ 0∗20=(10)2
(3)10=1∗21+ 1∗20=(11)2

3.1.4 Reprezentarea numerelor in baza 16 (hexazecimala)


In paragrafele anterioare am vazut ca pentru reprezentarea datelor se folosesc unul sau mai multi octeti.
De asemenea, am inteles ca aceste date sunt reprezentate in baza 2 pentru a putea fi procesate de
calculator. Ne-am putea intreba care este utilitatea folosirii bazei 16? Sa examinam reprezentarea binara a
unui octet:
b 7 b6 b5 b 4 b3 b2 b 1 b 0
Aceasta forma este greu de urmarit (pentru oameni) si din acest motiv a fost organizata in doua grupuri de
4 biti. Fiecare grup de 4 biti poate fi reprezentat foarte usor in baza 16 dupa cum urmeaza:
∣b 7 b6 b5 b 4∣∣b 3 b 2 b1 b0∣≡h 1 h 0
Codurile numerelor pentru reprezentarea zecimala, binara si hexazecimala sunt prezentate in tabelul de
mai jos.
Zecimal Binar Hexazecimal
0 0000 0
1 0001 1

1 In literatura de specialitate il putem intalni sub acronimul MSB most significant bit.

2 In literatura de specialitate il putem intalni sub acronimul LSB least significant bit.
3
48
Adonis Butufei

Zecimal Binar Hexazecimal


2 0010 2
3 0011 3
4 0100 4
5 0101 5
6 0110 6
7 0111 7
8 1000 8
9 1001 9
10 1010 A
11 1011 B
12 1100 C
13 1101 D
14 1110 E
15 1111 F

Exemple:
Zecimal Binar Hexazecimal

255 11111111 FF = 15∗161 + 15∗160

62 00111110 3E

74 01001010 4A

Se observa ca plecand de la codurile hexazecimale se poate verifica foarte usor reprezentarea binara.

3.1.4.1 Initializarea variabilelor intregi cu valori hexazecimale


Initializarea se poate face folosind notatia 0xValoareHexa ca in exemplele urmatoare:
short flag = 0x00F1; // Initializarea unei variabile de 2 octeti.
char mask = 0xA2; // Initializarea unei variabile de un octet.

3.1.4.2 Unde se foloseste reprezentarea hexazecimala


Cel mai des, reprezentarea hexazecimala se foloseste pentru:
- Specificarea adreselor de memorie. Aceasta este utila pentru depanarea programelor care manipuleaza
adresele variabilelor.

- Definirea constantelor intregi care sunt folosite ca filtre pentru operatiile pe biti. In capitolul viitor le
4
49
Adonis Butufei

vom intalni cand vom explora operatorii pe biti.

Exemplu:
const short OK = 0x00;

- Definirea valorilor pentru constantele enumerate, de obicei cu puterile lui 2, care permit operatii de
concatenare pe biti3.
Exemplu:
enum Flag { OPTIUNE_1 = 0x0001,
OPTIUNE_2 = 0x0002,
OPTIUNE_3 = 0x0004 };

int val = OPTIUNE_1 | OPTIUNE_2; // Ultimii biti sunt 11 (avem ambele optiuni)

3.1.5 Reprezentarea numerelor in baza 8 (octal)


Aceasta reprezentare se poate gandi in mod asemanator cu reprezentarea binara si cea hexazecimala.
A fost folosita inaintea notatiei hexazecimale. In prezent este utilizata pentru setarea drepturilor de acces
in sistemele de operare UNIX/Linux si probabil pentru intretinerea programelor mai vechi.

Initializarea variabilelor se face folosind 0 inaintea valorii, ca in exemplul de mai jos:


int octalExample = 0123;

3.2 Domeniul de viata al variabilelor


O variabila are un domeniu de viata care reprezinta zona de cod in care este accesibila pe parcursul
executiei unui program.
Contextul de acces al variabilelor este determinat de modul in care se face declararea acelor variabile.
Declararea variabilelor influenteaza domeniul de viata prin locul unde se face declararea si prin
calificatori.

3.2.1 Locul declararii variabilelor si influenta acestuia asupra domeniului


de viata
Dupa locul de declarare, exista doua tipuri de variabile: locale si globale.
Variabilele locale sunt declarate in interiorul unui bloc de instructiuni si sunt accesibile in acel bloc si in
blocurile incluse in acesta. Atunci cand executia programului depaseste blocul in care au fost definite,
memoria ocupata este eliberata automat si numele acelor variabile este desfiintat.

3 Pentru aceste valori doar un singur bit are valoarea 1 si operatiile pe biti se pot combina fara a altera valoarea anterioara
asa cum vom vedea in capitolul viitor.
5
50
Adonis Butufei

Variabilele globale sunt declarate in afara oricarui bloc de instructiuni si sunt accesibile pe toata durata
executarii programului.
Exemplu:
int global = 0; // Aceasta variabila este globala,
// ea este accesibila pe toata durata
// executarii programului.

int main()
{
int total = 0; // Aceasta variabila este accesibila
// in interiorul functiei main.

for(int i = 0; i < 10; i++)


{
// Variabila i este accesibila in interiorul blocului for.
total = total + i; // putem folosi variabila total
// deoarece blocul for apartine
// functiei main.
}
return 0;
}

Daca incercam sa folosim o variabila in afara domeniului ei obtinem o eroare de compilare ca in exemplul
de mai jos:
int main()
{
int total;
for(int i = total; i < 10; i++)
{
total = total + i;
}

int lastIndex = i; // se obtine eroare de compilare

return 0;
}

Recomandare
Pentru reducerea defectelor programelor este bine ca variabilele sa aiba cel mai mic domeniu de viata.
Acesta este un mod defensiv care asigura eliberarea memoriei nefolosite si reduce cuplajul prin date4

4 Cuplajul prin date apare atunci cand mai multe functii folosesc aceleasi variabile globale.
6
51
Adonis Butufei

care poate crea defecte greu de depistat.


Este ca si cum am privi variabilele ca pe niste „bogatii” ale programului pe care trebuie sa le protejam de
accesul exterior nedorit. Cu cat este mai mic domeniul de viata, cu atat mai mic este riscul accesului
nedorit.

3.2.1.1 Mascarea variabilelor


Apare atunci cand declaram intr-un bloc o variabila cu nume identic cu cel al unei variabile declarate
intr-un bloc parinte. Atunci cand se intampla acest lucru, compilatorul afiseaza doar un mesaj de
avertizare (warning), insa putem compila si rula programul.
Exemplu:
1: #include <iostream>
2: using namespace std;
3:
4: int main()
5: {
6: int total = 0;
7:
8: {
9: int total = 0; // mascheaza variabila total
// declarata in blocul parinte.
10:
11: for(int i = 0; i < 10; i++)
12: {
13: total = total + i;
14: }
15:
16: cout << total << "\n"; // avem rezultatul asteptat
17: }
18:
19: cout << total << "\n"; // aceasta variabila are valoarea 0
20: return 0;
21: }

In linia 6 am declarat o variabila total care are domeniul de viata functia main. Pe linia 9 am definit o alta
variabila total care are ca domeniu de viata acest bloc si blocurile descendente si care mascheaza variabila
total definita in linia 6.
Bucla dintre liniile 11 – 14 lucreaza cu aceasta variabila, Atunci cand se termina executia blocului,
memoria pentru variabila declarata aici este eliberata si rezultatul se pierde.

Mascarea variabilelor poate genera defecte foarte greu de depanat si trebuie evitata. Folosind variabile
cu domenii de viata cat mai mici este usor de identificat acest fenomen.

7
52
Adonis Butufei

3.2.1.2 Domeniul de viata al variabilelor si apelul functiilor


Functiile pot accesa variabile globale si locale. Interactiunea cu functia se face prin intermediul
parametrilor si al valorii returnate. Cuplajul prin intermediul variabilelor globale este bine sa fie evitat.
Exemplul de mai jos ilustreaza aceste aspecte:
1: int total;
2:
3: int sum( int a, int b)
4: {
5: return a + b;
6: }
7:
8: void badsum(int a, int b)
9: {
10: total = a + b;
11: }
12:
13: int main()
14: {
15: int result = sum(2,3);
16: badsum(4,5);
17: }

In prima linie am declarat o variabila globala total. Functia sum calculeaza suma parametrilor a si b si
returneaza rezultatul. Functia badsum atribuie rezultatul sumei variabilei globale creand un cuplaj prin
date.
Daca o alta functie schimba valoarea acestei variabile intre punctul de apel si cel in care se foloseste
rezultatul, executia este incorecta.

Parametri transmisi unei functii sunt considerati variabile locale si pot fi folositi ca si cum ar fi fost
declarati in corpul functiei.

3.2.2 Calificatorii de context


In capitolul anterior am discutat ca pentru declararea variabilelor sunt necesare doua elemente: tipul
variabilei si numele acesteia. Acum vom examina al treilea element care este reprezentat de calificatori.
Pentru specificarea locului unde se va stoca variabila, exista patru calificatori: auto, static, extern
si register.

3.2.2.1 Calificatorul auto


Este calificatorul implicit. Atunci cand nu este specificat, compilatorul foloseste acest calificator.

8
53
Adonis Butufei

Exemplu:
auto int test;

3.2.2.2 Calificatorul register


Acest calificator este folosit pentru variabile care sunt tinute in registrii procesorului. Folosirea unei variabile in registrul
procesorului poate imbunatati performanta executiei5.
Exemplu:
register int processorRegister = 0;

3.2.2.3 Calificatorul extern


Acest calificator spune compilatorului ca variabila respectiva a fost definita in alt fisier. In acest fel putem
folosi variabile globale definite in alte fisiere.
Exemplu:
extern int totalIncome;

3.2.2.4 Calificatorul static


Acest calificator are doua intelesuri:
1. Daca este folosit pentru variabilele globale, el restrange domeniul de viata la fisierul in care sunt
declarate.
2. Daca se refera la variabilele locale, atunci memoria variabilei nu mai este dealocata dupa executia
codului.

Urmatoarele exemple ilustreaza folosirea calificatorului static:

1: static int variabilaGlobala; // accesibila doar in fiserul curent.


2:
3: int main()
4: {
5: // instructiuni.
6: return 0;
7: }

Variabila globala definita in acest fisier are domeniul de viata in acest fisier, Daca se incearca accesarea ei
din alt fisier folosind o declaratie: extern int variabilaGlobala; se obtine o eroare de compilare.

1: #include <iostream>
2: using namespace std;
3:

5 Se recomanda masurarea precisa a performantei si identificarea portiunilor care necesita optimizarea performantei.
Optimizarea prematura a performantei este o sursa de probleme si poate rezulta intr-un cod greu de mentiut.
9
54
Adonis Butufei

4: void ExempluVariabilaStatica()
5: {
6: static int nrApeluri = 1; // Se executa doar la primul apel.
7:
8: cout << "Numarul apelurilor functiei:"
9: << nrApeluri << "\n";
10:
11: nrApeluri++;
12: }
13:
14: int main()
15: {
16: ExempluVariabilaStatica();
17: ExempluVariabilaStatica();
18:
19: return 0;
20: }

In acest exemplu linia 6 se executa doar la prima rulare. Apelurile succesive ale functiei incrementeaza
variabila si, in acest caz, putem calcula numarul apelurilor.

3.3 Variabile numerice


Pana acum au fost prezentate principiile generale de reprezentare a variabilelor in memorie si domeniul
lor de viata. Acum vom examina in detaliu tipurile de variabile intregi si reale, intervalele de valori si
dimensiunea zonei de memorie in octeti necesara pentru aceste variabile.

3.3.1 Variabile intregi

3.3.1.1 Calculul dimensiunii necesare pentru stocarea variabilelor intregi pozitive


Cati biti sunt necesari pentru a reprezenta numerele intregi?
Observam din exemplele anterioare ca pentru reprezentare numerelor de la 0 la 15 sunt suficienti 4 biti.
Se poate demonstra matematic urmatoarea relatie:
Cu ajutorul a n biti putem reprezenta numere intregi pozitive in baza 10 in intervalul:
[0−(2 n−1)]

Exemplu:
In cazul unui octet putem stoca numere pozitive cuprinse intre 0 si 2 8−1 adica in intervalul [0, 255].

Ce se intampla daca o variabila are valoarea maxima si o incrementam? Ce ar indica kilometrajul


automobilului dupa ce toate cifrele au ajuns la 9? Am incepe de la 0. Acelasi lucru se intampla si cu
variabila.

10
55
Adonis Butufei

Important
• Atunci cand una dintre limitele intervalului este depasita cu o unitate prin incrementare sau
decrementare urmatoarea valoare va fi cealalta limita a intervalului.
• Putem gandi valorile unui interval dispuse pe un cerc. Dupa o rotatie completa in oricare sens valorile
se repeta.
• Acest aspect este foarte important la bucle: daca valoarea din conditia de oprire a buclei nu este in
intervalul variabilei utilizate pentru testul buclei, vom avea fie o bucla infinita, fie secventa buclei nu
se va executa niciodata.

3.3.1.2 Procesarea semnului


In momentul cand vrem sa lucram cu numere pozitive si negative, vom translata intervalul calculat cu
formula anterioara catre stanga astfel incat jumatate din numere sa fie negative si jumatate pozitive.
Aceasta presupune injumatatirea limitei maxime.

Exemplu:
Pentru un octet am avea limita maxima pozitiva de 2 7−1 adica 127, iar limita inferioara va fi de
7
−2 adica -128. Pe un octet putem stoca numere cuprinse in intervalul [-128, 127].

Important
Sa presupunem ca avem o variabila de marimea unui octet care are valoarea 127 si o incrementam. Si in
aceasta situatie se aplica observatiile anterioare. Noua valoare va fi -128. Iar daca variabila are valoarea
-128 si o decrementam noua valoare va fi 127.

3.3.1.3 Calificatori de semn


In mod implicit variabilele sunt cu semn. Pentru a informa compilatorul ca dorim ca variabila noastra sa
fie fara semn trebuie sa folosim calificatorul unsigned ca in exemplul de mai jos.
unsigned int contor;
In acest caz variabila va avea numai valori pozitive.

3.3.1.4 Calificatori de marime


Am vazut ca tipul variabilei determina intervalul de valori pe care o variabila le poate lua. Pentru a
informa compilatorul cati octeti dorim pentru variabila noastra intreaga folosim calificatorii prezentati in
tabelul de mai jos:
Calificator Dimensiune in octeti Exemplu
short 2 short intreg16b = 16;
int 4 int intreg32b = 32;
long 4 long intreg32b = 32;
long long 8 long long intreg64b = 64

11
56
Adonis Butufei

3.3.1.5 Limitele variabilelor intregi


Am discutat pana acum despre limitele variabilelor si am vazut ca ele sunt de terminate de numarul de
octeti si de modul in care se proceseaza semnul. Aceste limite sunt definite in fisierul <climits> si sunt
prezentate in tabelul de mai jos:

Tip Limta Valoare Explicatii


char SCHAR_MIN (-128) valoare minima a tipului
char
char SCHAR_MAX 127 valoare maxima a tipului
caracter cu semn
char UCHAR_MAX 0xff valoare maxima a tipului
caracter fara semn
char CHAR_MIN SCHAR_MIN sau 0 valoare minima a tipului
caracter
char CHAR_MAX SCHAR_MAX sau valoare maxima a tipului
UCHAR_MAX caracter (cu semn sau
fara
short SHRT_MIN (-32768) valoare minima a tipului
intreg (cu semn) short
short SHRT_MAX 32767 valoare maxima a tipului
intreg (cu semn) short
unsigned short USHRT_MAX 0xffff valoare maxima a tipului
intreg (fara semn) short
int INT_MIN (-2147483647 - 1) valoare minima a tipului
intreg (cu semn)
int INT_MAX 2147483647 valoare maxima a tipului
intreg (cu semn)
unsigned UINT_MAX 0xffffffff valoare maxima a tipului
intreg (fara semn)
long LONG_MIN (-2147483647L - 1) valoare minima a tipului
long (cu semn)
long LONG_MAX 2147483647L valoare maxima a tipului
long (cu semn)
unsigned long ULONG_MAX 0xffffffffUL valoare maxima a tipului
long (fara semn)

12
57
Adonis Butufei

long long LLONG_MAX 9223372036854775807i64 valoare maxima a tipului


long long (cu semn)
long long LLONG_MIN (- valoare minima a tipului
9223372036854775807i64 - long long (cu semn)
1)
unsigned long long ULLONG_MAX 0xffffffffffffffffui64 valoare maxima a tipului
long long (fara semn)

Urmatorul exemplu afiseaza valorile limitelor pentru variabile de tip int.


#include <iostream>
#include <climits>
using namespace std;

int main()
{
cout << "Valoarea minima pentru int: " << INT_MIN << "\n";
cout << "Valoarea minima pentru int: " << INT_MAX << "\n";
cout << "Valoarea maxima pentru unsigned int: " << UINT_MAX << "\n";

return 0;
}

3.3.1.6 Depasirea limitelor6


Cand vrem sa stocam intr-o variabila o valoare care depaseste limitele intervalului specificat pentru acea
variabila, valoarea va fi trunchiata si programul va functiona incorect.

Exemplu:
1: unsigned short v1;
2: // ...
3: short v2 = v1;

Atribuirea din linia 3 necesita multa atentie. Daca valoarea variabilei v1 depaseste limita SHRT_MAX,
atunci este posibil ca functionarea sa fie defectuoasa.

Acest fenomen poate apare atunci cand se face atribuirea intre doua variabile de tip diferit sau cand o
variabila este folosita pentru a calcula suma sau produsul unei secvente de valori.

Cunoasterea acestui aspect este esentiala pentru alegerea tipurilor de variabile si scrierea codului
de calitate.

6 In limba engleza se foloseste termenul overflow.


13
58
Adonis Butufei

3.3.2 Variabile reale7


Daca ar fi sa folosim numai variabile intregi pentru efectuarea calculelor este ca si cum am incerca sa
construim o sfera din caramizi. La nivel de detaliu ar lipsi finetea reprezentarii.

Pe de alta parte, daca am folosi numai variabile reale pentru efectuarea calculelor este ca si cum am
incerca sa construim un cub din plastilina. Oricat de mult ne-am stradui nu am obtine muchiile drepte si
precise.

Pentru efectuarea calculelor cu numere reale au trebuit rezolvate doua probleme importante: posibilitatea
de a lucra cu intervale foarte mari de valori si reprezentarea acestor valori intr-o dimensiune predefinita
de memorie.

Pentru adresarea acestor necesitati nu se putea folosi o reprezentare exacta ca in cazul variabilelor intregi
ci s-a folosit o reprezentare aproximativa suficient de precisa a numerelor reale folosind un numar
standard de octeti8.

Deoarece analiza reprezentarii in memorie a variabilelor reale depaseste scopul cursului, in continuare
vom schita aspectele necesare intelegerii principiilor de functionare a numerelor reale.
Variabilele reale au trei elemente: semnul, o fractie zecimala si un exponent:
exponent
y=± f.f 1 f 2 … f n x 10
Unde f.f 1 f 2 … f n este fractia zecimala.

3.3.2.1 Tipuri de variabile reale suportate de C++


Pentru lucrul cu numere reale C++ ofera trei tipuri de date: float, double si long double.
Caracteristicile acestor tipuri sunt prezentate in tabelul de mai jos:
Tip Explicatie Numar de octeti
float numar real in precizie simpla 4
double numar real in precizie dubla 8
long double9 similar cu double 8

3.3.2.2 Moduri de scriere a numerelor reale


Pentru scrierea numerelor reale in C++ se folosesc doua moduri: modul stiintific si modul zecimal sau
notatia E.

Pentru a separa partea intreaga de partea zecimala, in C++ folosim caracterul punct (.) in loc de virgula
(,).

7 In limba engleza se foloseste termenul floating point pentru acest tip de variabile.
8 Reprezentarile uzuale sunt de 4, 8 sau 16 biti in functie de sistemul de operare.
9 Implementarile curente pentru Windows folosesc aceeasi reprezentare pentru long double si pentru double.
14
59
Adonis Butufei

Exemple:
3.54
0.37
-42.8

Modul stiintific foloseste forma ±abE±cd care este echivalenta cu ±ab x 10±cd . In aceasta notatie
putem folosi ambele litere (E) sau (e).

Exemple:
3e+2 = 3x102 = 300
3.24e-3= 3.24x10−3

De retinut
Nu trebuie sa existe spatii intre caractere atunci cand se foloseste aceasta notatie. Urmatoarele valori sunt
invalide:
3 e+4
-4.5 e-2

3.3.2.3 Intializarea variabilelor reale


Initializarea variabilelor se face direct pentru variabilele double, iar pentru cele float trebuie pus f la
sfarsitul valorii.

Exemple:
float temp = 1.5f;
float y = -3.2e-5f;
double sum = 0.0;
double x = 1.23e5;

3.3.2.4 Testarea egalitatii pentru variabilele reale


Datorita aproximarii valorilor conditiile de egalitate nu se pot verifica folosind operatorul (==).
In exemplul urmator observam cum adunand de 10 ori valoarea 0.1 aceasta nu devine egala cu 1.0 asa
cum ne-am astepta.
#include <iostream>
using namespace std;

int main()

15
60
Adonis Butufei

{
double sum = 0.0;
for(int i =0; i < 10; i++)
{
sum = sum + 0.1;
}

if(1.0 == sum)
{
cout << "suma este 1.0\n";
}
else
{
cout << "suma nu este 1.0\n";
}
return 0;
}

La rulare acest program intra pe ramura else si mesajul afisat este:


suma nu este 1.0

Pentru a compara variabilele reale este necesar sa calculam diferenta absoluta a valorii celor doua
variabile si daca aceasta diferenta este mai mica decat un nivel de eroare considerat acceptabil atunci
numerele sunt considerate egale. Programul urmator exemplifica aceasta idee:
#include <iostream>
#include <cmath>
using namespace std;

bool AreEqual(double x, double y)


{
const double EPSILON = 1e-10;
return abs(x - y) < EPSILON;
}

int main()
{
double sum = 0.0;
for(int i =0; i < 10; i++)
{
sum = sum + 0.1;
}

16
61
Adonis Butufei

if(AreEqual(1.0, sum))
{
cout << "suma este 1.0\n";
}
else
{
cout << "suma nu este 1.0\n";
}
}

Aici am inclus fisierul <cmath> pentru a putea folosi functia abs;


Important
Datorita erorilor de aproximare nu este recomandata folosirea variabilelor reale in programe pentru
calcule financiare.

3.3.2.5 Limitele variabilelor reale


Pentru a folosi aceste limite este necesara includerea fisierului <cfloat>. In urmatorul tabel sunt prezentate
constantele uzuale care pot fi utilizate pentru verificarea conditiilor.
Constante pentru double

Constanta Valoare Explicatie

DBL_DIG 15 Numar de cifre semnificative

DBL_EPSILON 2.2204460492503131e-016 Cea mai mica valoare pentru care


1.0+DBL_EPSILON != 1.0

DBL_MAX 1.7976931348623158e+308 Valoarea maxima

DBL_MAX_10_EXP 308 Exponentul maxim in vaza 10

DBL_MIN 2.2250738585072014e-308 Valoarea pozitiva minima

DBL_MIN_10_EXP (-307) Exponentul minim in baza 10

Constante pentru float

FLT_DIG 6 Numar de cifre semnificative


FLT_EPSILON 1.192092896e-07F Cea mai mica valoare pentru care
1.0F+FLT_EPSILON != 1.0

FLT_MAX 3.402823466e+38F Valoarea maxima

FLT_MAX_10_EXP 38 Exponentul maxim in vaza 10

FLT_MIN 1.175494351e-38F Valoarea pozitiva minima

FLT_MIN_10_EXP (-37) Exponentul minim in baza 10

17
62
Adonis Butufei

3.4 Variabile de tip caracter


Cu toate ca tipul char este un tip intreg care are dimensiunea de 1 octet, el este folosit de obicei in mod
diferit de variabilele de tip intreg. Variabilele de tip char contin fie o valoare numerica fie un cod definit
de standardul ASCII (American Standard of Code Interchange).
Pentru a initializa variabilele cu codurile ASCII este necesar ca litera sa fie delimitata de caracterul (').
Exemplu:
char x = 'a'; // Atribuie variabilei x codul ASCII
// corespunzator literei a.

Este important de facut diferenta intre valoarea numerica si codul ASCII corespunzator unei cifre.
Urmatoarele doua variabile au valori diferite. In primul caz variabila are valoarea 5 in al doilea caz
variabila are valoarea codului asociat pentru caracterul 5 care este 53.
char x = 5; // x are valoarea 5
char y = '5'; // y are valoarea codului ASCII pentru '5' este 53.

3.4.1.1 Afisarea codurilor ASCII pe ecran


Pentru a putea afisa pe ecran valoarea codului ASCII este necesara convertirea variabilei la int. Urmatorul
exemplu afiseaza codurile ASCII pentru toate valorile posibile ale variabilelor de tip char.
1: #include <iostream>
2: using namespace std;
3:
4: int main()
5: {
6: for(int i =0; i < 256; i++)
7: {
8: cout << (char) i << " " << i << "\n";
9: }
10: return 0;
11: }

Ca si contor al buclei am folosit o variabila de tip int. Pentru a putea tipari ultima valoare a intervalului
trebuia sa fixez limita la 256. Daca foloseam o variabila unsigned char intervalul posibil de valori era [0 –
255]. Atunci cand i lua valoarea 255 se executa codul buclei apoi se incrementa valoarea care in acest caz
ar fi devenit 0. In acest moment totul se repeta si obtineam o bucla infinita.

Pe linia 8 observam expresia (char)i aceasta converteste tipul variabilei i de la intreg la caracter si se
numeste castare. Vom analiza conversiile in detaliu in capitolul urmator cand vom discuta despre
operatori.

18
63
Adonis Butufei

3.4.1.2 Citirea valorilor tip caracter de la tastatura


Pentru a citi valorile de tip caracter de la tastatura se foloseste functia cin. Citirea se opreste in momentul
cand am apasat tasta Enter. In cazul in care am introdus mai multe caractere, variabila va fi initializata cu
primul caracter introdus de la tastatura.
Exemplu:
#include <iostream>
using namespace std;

int main()
{
char c;
cout << "Introduceti un caracter\n";
cin >> c;

cout << "Ati introdus caracterul " << c << "\n";


return 0;
}

3.4.1.3 Categorii de coduri ASCII


Standardul ASCII s-a dezvoltat cu mult timp inainte de aparitia calculatoarelor. Din acest set nu toate
valorile se folosesc pentru reprezentarea caracterelor. Primele 32 de valori erau folosite ca si valori de
control si nu au un corespondent grafic care se afiseaza pe ecran.

Exista o categorie speciala de valori care este utilizata pentru formatarea textului. Fiecare valoare din
aceasta categorie incepe cu caracterul (\) pentru a preveni procesarea implicita a caracterului care
urmeaza. Acest grup de doua caractere se numeste secventa escape .
Valoare Explicatie
'\0' marcheaza sfarsitul unui sir de carcatere
'\a' produce un sunet
'\b' muta cursorul un spatiu inapoi
'\t' insereaza un tab orizontal
'\n' muta cursorul pe linia urmatoare
'\v' insereaza un tab vertical
'\f' muta cursorul pe pagina urmatoare
'\r' muta cursorul la inceputul linei

19
64
Adonis Butufei

Valoare Explicatie
'\'' afiseaza caracterul (')
'\"' afiseaza caracterul (")
'\\' afiseaza caracterul (\)

Valorile mai pot fi impartite logic si in alte categorii: caracterele care reprezinta cifre ('0' - '9'), litere
(majuscule, minuscule) etc. Pentru a testa apartenenta unui caracter la o categorie se folosesc functiile
declarate in fisierul <ctype.h>.
Tabelul de mai jos contine o scurta prezentare a acestor functii:
Functie Explicatie
isalpha(c) returneaza 1 daca c este litera mare sau mica A-Z, a-z
isupper(c) returneaza 1 daca c este litera mare A-Z
islower(c) returneaza 1 daca c este litera mica a-z
isdigit(c) returneaza 1 daca c este cifra 0-9
isxdigit(c) returneaza 1 daca c este cifra hexa 0-9,a-f,A-F
isspace(c) returneaza 1 daca c este spatiu ' ','\t','\n','\r','\f' sau '\v'
ispunct(c) returneaza 1 daca c este character de punctuatie
isalnum(c) returneaza 1 daca c este litera sau cifra
isprint(c) returneaza 1 daca c este afisabil cu spatiu
isgraph(c) returneaza 1 daca c este afisabil fara spatiu
iscntrl(c) returneaza 1 daca c este caracter de control

Urmatorul exemplu afiseaza semnele de punctuatie si codurile lor ASCII:


1: #include <iostream>
2: #include <ctype.h>
3: using namespace std;
4:
5: int main()
6: {
7: for(int c = 0; c < 256; c++)
8: {
9: if(ispunct(c))

20
65
Adonis Butufei

10: {
11: cout << c << " " << (char)c << "\n";
12: }
13: }
14:
15: return 0;
16: }

3.5 Sumar
• Cea mai mica unitate de informatie este bitul.
• Un grup de 8 biti formeaza un octet.
• Variabilele se masoara in octeti.
• Pentru a determina numarul de octeti ocupati de o variabila se foloseste operatorul sizeof.
• Pe langa reprezentarea zecimala, variabiele intregi mai pot fi reprezentate in baza 8, reprezentare
octala, sau baza 16, reprezentare hexazecimala.
• Domeniul de viata al variabilelor reprezinta zona de cod in care acestea pot fi utilizate in cadrul
programului. Dupa ce sunt executate instructiunile din domeniul de viata al unei variabile, acea
variabila nu mai poate fi accesata si memoria aferenta este eliberata. Exceptie in cazul variabilelor
statice!
• Pentru a reduce cuplajul prin date este recomandabil ca variabilele sa aiba un domeniu de viata cat mai
redus.
• Calificatorul de context este utilizat pentru a accesa variabile declarate in alt fisier.
• Calificatorul de context static are doua caracteristici:
- variabilele globale statice pot fi accesate doar din fisierul curent
- variabilele locale statice sunt alocate in memorie la prima executie si valoarea lor se pastreaza pe
toata perioada rularii programului.
• In C++ sunt definite urmatoarele tipuri de variabile intregi: int, long si short. Primele doua tipuri ocupa
4 octeti, ultimul tip ocupa 2 octeti.
• Variabilele intregi pot avea semn sau nu. In cazul celor fara semn toate valorile sunt pozitive.
• Limitele pentru variabilele intregi sunt definite in fisierul <climits>
• Variabilele reale pot lua valori in intervale largi. Ele folosesc o reprezentare aproximativa a numerelor
datorita faptului ca au un numar finit de cifre.
• Dimensiunea variabilelor reale este de 4 sau 8 octeti.
• Pentru variabilele reale se folosesc doua notatii: zecimala si stiintifica. Cea zecimala este de forma
24.5 iar cea stiintifia este de forma 2.45 * 101 .
• Pentru testarea egalitatii variabilelor reale, datorita aproximarii, nu se poate folosi operatorul == . Se
defineste cea mai mica diferenta acceptabila. Daca diferenta celor doua numere este mai mica in
valoare absoluta decat diferenta acceptabila atunci sunt considerate egale.

21
66
Adonis Butufei

• Limitele variabilelor reale sunt definite in fisierul <cfloat>.


• Tipul char foloseste codurile ASCII pentru reprezentarea informatiei ca si text.

3.6 Intrebari si exercitii


1. Care este diferenta dintre un bit si un octet?
2. Cum se poate determina numarul de octeti folosit de compilator pentru variabilele de tip double?
3. Scrieti un program care afiseaza dimensiunea variabilelor de tip bool, char, int si double.
4. Unde se foloseste reprezentare in baza 16 a numerelor intregi?
5. Care este avantajul reprezentarii in baza 16?
6. Cum se reprezinta in binar numarul 21?
7. Care este reprezentarea in hexazecimal a numarului 75?
8. Care este rezultatul urmatoarei operatii in hexazecimal: AE + 21?
9. Care este rezultatul urmatoarei operatii in hexazecimal: BD – A3?
10. Care este cea mai mare valoare care se poate reprezenta pe 32 de biti?
11. Care este diferenta dintre o variabila globala si o variabila locala?
12. Ce se intampla cu variabilele dupa ce executia programului depaseste contextul de acces?
13. Prin ce difera o variabila statica globala de o variabila statica locala?
14. Ce este mascarea variabilelor si de ce trebuie evitata?
15. Ce se afiseaza pe ecran la rularea urmatorului program:

#include <iostream>
using namespace std;

int main()
{
for(int i = 0; i < 3; i++)
{
static int a = 1;
int b = 1;

cout << "a= " << a << " b= " << b << "\n";
a++;
b++;
}

return 0;
}

16. Care este diferenta dintre o variabila de tip int si una de tip short?

22
67
Adonis Butufei

17. Care este diferenta dintre o variabila cu semn si una fara semn?
18. Cum se declara o variabila fara semn de tip long long?
19. Care este valoarea variabilei dupa executarea codului:
int x = INT_MIN;
x--;

20. Cand este indeplinita conditia de iesire din bucla?


int result = 0;
for(short i =0; i < USORT_MAX; i++)
{
result = result + i;
}

21. Ce fisier trebuie inclus pentru a putea lucra cu limitele variabilelor de tip intreg?
22. Extindeti programul de afisare a dimensiunii variabilelor pentru toate tipurile de variabile discutate
pana acum.
23. Extindeti programul de afisare a limitelor pentru toate valorile intregi.
24. Scrieti reprezentarea zecimala pentru urmatoarele valori:
1.345e-2
45.89e-1
1234.789E-3
-324.758e-2

25. Scrieti reprezentarea stiintifica pentru urmatoarele valori astfel incat partea intreaga sa aiba 2 cifre:
234.78
0.756
-2.546
-2459.576
26. Modificati exemplul initial pentru compararea numerelor reale astfel incat sa foloseasca variabile de
tip float. Apar si in acest caz erorile de aproximare?
27. Ce fisier trebuie inclus pentru a folosi limitele variabilelor reale?
28. Scrieti un program care afiseaza valorile limitelor pentru variabilele de tip float si double.
29. Implementati un program care afiseaza codul ASCII si valoarea pentru toate literele si cifrele.
30. Programul urmator ruleaza in bucla infinita. Care este eroarea?
17: #include <iostream>
18: using namespace std;
19: int main()
20: {
21: for(unsigned char = 0; c < 256; c++)

23
68
Adonis Butufei

22: {
23: cout << c << " " << (int)c << "\n";
24: }
25:
26: return 0;
27: }

31. Modificati codul anterior pastrand tipul datelor, dar folosind o bucla do /while
32. Implementati o functie care primeste un caracter si daca acesta este litera o transforma in majuscula folosind tabelul
codurilor ASCII prezentat in bibliografie.
33. Prin ce difera conversiile implicite de conversiile explicite?
34. Care este rezultatul urmatoarei conversii?
double a = 4.5;
int x = int (a);

3.7 Bibliografie
• Practical C++ Programming, second edition, O'Reily, Steve Oualline, Cap 5, Cap 9.
• C++ Primer, sixth edition, Addison-Wesley Professional, Stephen Prata, Cap3.
• Reprezentarea numerelor reale http://steve.hollasch.net/cgindex/coding/ieeefloat.html
• Acuratete vs. precizie:
http://www.cprogramming.com/tutorial/floating_point/understanding_floating_point.html
• Reprezentarea double pe 64 biti: http://en.wikipedia.org/wiki/Double_precision_floating-point_format
• Codurile ASCII http://www.asciitable.com/

24
69
Adonis Butufei

4. OPERATORI

CUPRINS
4.Operatori....................................................................................................................................................2
4.1 Operatorul de atribuire: =...................................................................................................................2
4.2 Asociativitatea si precedenta operatorilor..........................................................................................3
4.3 Operatori de semn: + -........................................................................................................................3
4.4 Operatori aritmetici si operatori de incrementare..............................................................................4
4.4.1 Operatori aritmetici compusi: += -= *= /= %=...........................................................................4
4.4.2 Operatori de incrementare / decrementare: ++ --.......................................................................5
4.4.2.1 Operatorii de prefixare........................................................................................................5
4.4.2.2 Operatorii de postfixare......................................................................................................5
4.5 Operatori logici: || && !....................................................................................................................6
4.5.1 Tabele de adevar pentru operatorii logici...................................................................................8
4.5.1.1 Operatorul || (sau logic).......................................................................................................8
4.5.1.2 Operatorul && (si logic).....................................................................................................8
4.5.1.3 Operatorul ! (negare logica)................................................................................................9
4.5.2 Precedenta operatorilor logici.....................................................................................................9
4.5.3 Efecte secundare.........................................................................................................................9
4.6 Conversii si operatorii cast..............................................................................................................10
4.6.1 Conversii implicite...................................................................................................................10
4.6.2 Conversii explicite: operatorii cast...........................................................................................10
4.6.3 Recomandari pentru conversii..................................................................................................11
4.7 Operatorul conditional: ? .................................................................................................................11
4.8 Operatori pe biti: | & ^ << >> ~.......................................................................................................12
4.8.1 Operatorii compusi: |= &= ^= <<= >>=...................................................................................14
4.9 Operatorul: , ....................................................................................................................................15
4.10 Precedenta operatorilor..................................................................................................................16
4.11 Intrebari si exercitii........................................................................................................................16
4.12 Bibliografie.....................................................................................................................................18

1
70
Adonis Butufei

4. OPERATORI
Stim din capitolul 2 ca operatorii sunt simboluri care determina compilatorul sa interactioneze cu
variabilele. In acest capitol vom discuta in detaliu despre:
• Operatorul de atribuire.
• Asociativitatea operatorilor.
• Operatorii compusi +=, -=, *=, /= si %= care permit scrierea mai concisa a expresiilor.
• Folosirea operatorilor de incrementare si decrementare in expresii.
• Operatorii logici.
• Conversii si operatorii de cast.
• Operatori pe biti.
• Precedenta operatorilor.

4.1 Operatorul de atribuire: =


Atribuie variabilei care se afla in stanga operatorului valoarea din dreapta.
Exemplu:
double a = 3.5;

In cazul in care in partea dreapta avem o functie acea functie se apeleaza si rezultatul returnat se atribuie
variabilei din stanga.
Exemplu:
double radical = sqrt(45);

De asemenea, daca in partea dreapta avem o expresie acea expresie se evalueaza si se atribuie rezultatul
variabilei din stanga.
Exemplu:
int sum = 4 + 5;

Nota
Cand sunt mai multe variabile declarate pe aceeasi linie atribuirea se face numai pentru ultima variabila
ca in exemplul urmator:
int x,y, z = 0; // numai z are valoarea 0 celelalte au
// valoare nedeterminata.

Varianta corecta este:


int x, y, z; // aici se declara toate variabilele
x = y = z = 0; // aici se initializeaza toate variabilele.

2
71
Adonis Butufei

Este important de remarcat ca orice exista in partea stanga1 a operatorului = se poate muta in partea
dreapta insa invers nu este intotdeauna corect.
Exemplu:
x = 5; // este corect
5 = x; // nu este corect deoarece 5 este o constanta
// literala si nu-si poate schimba valoarea.

4.2 Asociativitatea si precedenta operatorilor


Asociativitatea operatorilor defineste ordinea in care se evalueaza operanzii. In C++ avem asociativitate
de la dreapta la stanga cum este cazul operatorului de atribuire si de la stanga la dreapa cum este cazul
operatorilor aritmetici si al altor operatori despre care vom discuta in acest capitol.

Precedenta operatorilor determina ordinea in care se executa operatiile unei expresii. In C++ fiecare
operator are un nivel de precedenta. Opearatorii care au un nivel mai mic de precedenta se executa
inaintea celor cu precedenta mai mare.

Pentru a asigura evaluarea expresiilor operatorul = se executa in majoritatea cazurilor ultimul. De aceea el
are un nivel de precedenta mare.

In exemplul de mai jos ordinea operatiilor este cea cunoscuta: intai se calculeaza inmultirea apoi
adunarea.
int x = 3 + 4*5; // x are valoarea 23.

Nota
La sfarsitul acestui capitol este prezentat un tabel cu precedenta si asociativitatea operatorilor prezentati
in curs.

In cazul in care dorim sa efectuam mai intai adunarea si dupa aceea inmultirea este necesara folosirea
parantezeor ca in exemplul de mai jos:
int x = (3 + 4)*5; // x are valoarea 35

Important
Ca sa fim siguri de ordinea operatiilor este recomandabila folosirea parantezelor in expresiile care contin
mai mult de 3 operanzi.

4.3 Operatori de semn: + -


Acesti operatori ii intalnim in special in expresiile de atribuire.
Exemplu:

1 Operandul din partea stanga a operatorului = se numeste l-value iar operandul din partea dreapta se numeste r-value.
3
72
Adonis Butufei

int y = -34;
double t = -45.2;
int z = +23;

Deoarece semnul + este cel implicit el este omis in cele mai multe cazuri.

4.4 Operatori aritmetici si operatori de incrementare


Asociativitatea operatorilor aritmetici este de la stanga la dreapta. Nivelul de precedenta pentru operatorii
* / % este mai mic decat nivelul de precedenta pentru operatorii + - .

In exemplul urmator se evalueaza mai intai valoarea modulo apoi se face adunarea:
int x = 5 + 7 % 2;

Insa urmatoarea expresie este mai clara:


int x = 5 + (7%2);

4.4.1 Operatori aritmetici compusi: += -= *= /= %=


Exista situatii frecvente cand este necesar sa efectuam o operatie asupra unei variabile si sa stocam
rezultatul in aceeasi variabila. De exemplu, daca vrem sa incrementam variabila x cu valoarea 10 vom
scrie:
int x = x + 10;

Pentru a usura scrierea, in C++ sunt definiti operatorii compusi care permit scrierea mai concisa a
expresiilor. Expresia de mai sus este echivalenta cu:
int x += 10;

In partea dreapta poate sa fie o expresie complexa al carei rezultat este evaluat si atribuit lui x.

In tabelul de mai jos este prezentata forma concisa pentru operatorii aritmetici2:
Operator Exemplu Echivalenta

+= x += y; x = x + y;

-= x -= y; x = x - y;

*= x *= y; x = x * y;

/= x /= y; x = x / y;

%= x %= y; x = x % y;

2 Pentru simplificare se considera ca variabilele au fost declarate anterior.


4
73
Adonis Butufei

Acesti operatori au acelasi nivel de precedenta cu operatorul = si asociativitatea este de la dreapta spre
stanga.

4.4.2 Operatori de incrementare / decrementare: ++ --


Acesti operatori se pot plasa fie inaintea variabilei, caz in care ei apartin categoriei prefixare sau dupa
variabila, caz in care apartin categoriei de postfixare.
Desi in ambele cazuri se realizeaza incrementarea sau decrementarea variabilei pozitia in care se afla
operatorul determina momentul in care aceasta operatie se realizeaza.

4.4.2.1 Operatorii de prefixare


Pentru operatorii de prefixare incrementarea sau decrementarea se realizeaza inaintea evaluarii
rezultatului expresiei.
Exemplu:
int x =0;
int y = ++x;

In acest caz mai intai se realizeaza incrementarea, apoi se evalueaza expresia y = x si la final ambele
variabile au valoarea 1.

Asociativitatea operatorilor de prefixare este de la dreapta la stanga.


In exemplul de mai jos variabila y va avea valoarea 2 deoarece mai intai se face incrementarea si apoi se
efectueaza adunarea.
int x =0;
int y = ++x + x;

Operatorul de prefixare are un nivel de prioritate mai mic decat operatiile aritmetice si se efectueaza
inaintea lor. In exemplul urmator se efectueaza mai intai operatia de incrementare si dupa aceea operatia
de adunare datorita precedentei si variabila y va fi initializata cu valoarea 2.
int x = 0;
int y = x + ++x;

4.4.2.2 Operatorii de postfixare


Asociativitatea operatorilor de postfixare este de la stanga la dreapta, precedenta este mai mare decat
operatiile aritmetice insa incrementarea se realizeaza dupa evaluarea expresiei.
Exemplu:
int x = 0;
int y = x++;

In acest caz mai intai se evalueaza expresia y = x, apoi se incrementeaza variabila x si la final variabila x
are valoarea 1 si variabila y are valoarea 0.

5
74
Adonis Butufei

Pentru cazul:
int x = 0;
int y = x++ + x;

lucrurile se intampla asemanator: mai intai se evalueaza expresia x + x a carei valoare se atribuie
variabilei y si la final se incrementeaza variabila x.

Acelasi lucru se intampla si in cazul urmator:


int x = 0;
int y = x + x++;

Recomandari
Pentru a asigura o intelegere a codului si a evita defecte care consuma timp pretios pentru depanare se
recomanda:
• folosirea operatorilor de incrementare in expresii cat mai simple
Exemplu:
Este preferabila expresia
factorial *= n;
n--;

in locul
factorial *= n--;

• in expresie o variabila trebuie sa suporte o singura operatie de incrementare. Urmatorul exemplu este
de evitat:
int x = 0;
int result = x++ + x + ++x; // Complicat! La final x == 2 si result == 3

4.5 Operatori logici: || && !


Operatorii logici sunt folositi in expresii pentru calcularea unor valori de adevar. In C++ sunt definiti 3
operatori logici: &&, || si !. Primul exprima un si logic, al doilea un sau logic iar al treilea o negare.
In activitatile zilnice ii folosim frecvent.
Exemple:
Daca am introdus pin-ul corect si am sold suficient pot sa achit cu cardul.
Daca clientul este pensionar sau elev se aplica un discount de 20%.
Daca semaforul nu are culoarea rosie putem traversa.

Din exemplele anterioare observam ca exista un tipar: avem un element de decizie „daca”, o conditie care
are mai multe criterii si o actiune. Acesta este scenariul uzual pe care il intalnim si in programe.
Exemplele de mai sus pot fi exprimate in C++ in modul urmator:
6
75
Adonis Butufei

Exemplul 1:3

bool pinCorect = ValideazaPIN();


bool soldSuficient = CitesteSold() > pretProdus;
if (pinCorect && soldSuficient)
{
AchitaProdus(pretProdus);
}

In acest exemplu validarea PIN-ului se realizeaza prin apelul ValideazaPIN(). Apoi se verifica situatia
soldului. In final daca ambele conditii sunt evaluate cu true se achita produsul prin apelul functiei
AchitaProdus().

Exemplul 2:
const double DISCOUNT = 0.8;
bool pensionar = VerificaPensionar(numeClient);
bool elev = VerificaElev(numeClient);

if(pensionar || elev)
{
pret = pret * DISCOUNT;
}

In acest exemplu am folosit o constanta simbolica pentru definirea discountului4. Apoi verificam daca
clientul este pensionar sau elev si la final daca una din conditii este indeplinita aplicam discountul.

Codul se poate scrie mai compact in modul urmator:

if(VerificaPensionar(numeClient) || VerificaElev(numeClient))
{
pret = pret * DISCOUNT;
}

Pentru o mai usoara intelegere am separat apelul functiilor de conditia if. De cele mai multe ori in practica
se foloseste aceasta varianta.

Exemplul 3:
3 In acest exemplu variabila pretProdus este declarata anterior, functiile ValideazaPIN(), CitesteSold() sunt implementate
anterior.
4 Folosirea constantelor simbolice ajuta la intelegerea codului si reduce eforturile de mentenanta. Valoarea constantei poate fi
folosita in mai multe locuri in cod. Daca este necesara actualizarea procentului de discount si am folosit o constanta, atunci
este suficienta o singura schimbare. Altfel ar fi trebuit sa cautam toate locurile in cod unde am folosit constanta literala 0.8
si sa o inlocuim cu noua valoare.
7
76
Adonis Butufei

if( ! EsteSemaforRosu())
{
TraverseazaStrada();
}

4.5.1 Tabele de adevar pentru operatorii logici


Asa cum pentru folosirea corecta operatorilor aritmetici este necesara cunoasterea precedentei si
principiilor de calcul pentru adunare, scadere, inmultire si impartire la fel pentru folosirea corecta a
operatorilor logici este necesara cunoasterea tabelelor de adevar pentru fiecare operator. Aceste tabele de
adevar specifica rezultatul evaluarii expresiilor.

4.5.1.1 Operatorul || (sau logic)


Acest operator foloseste doi operanzi, are asociativitatea de la stanga la dreapta si tabela de adevar este
prezentata mai jos:

Rezultat x y

true true true

true true false

true false true

false false false

Observam ca rezultatul are valoarea true doar daca daca unul din operanzi are valoarea true.

4.5.1.2 Operatorul && (si logic)


Acest operator foloseste doi operanzi, are asociativitatea de la stanga la dreapta si tabela de adevar este
prezentata mai jos:
Rezultat x y

true true true

false true false

false false true

false false false

Observam ca rezultatul are valoarea true doar daca ambii operanzi au valoarea true.

4.5.1.3 Operatorul ! (negare logica)


Acest operator are un singur operand si tabela lui de adevar este prezentata mai jos:
8
77
Adonis Butufei

Rezultat x

true false

false true

4.5.2 Precedenta operatorilor logici


In cazurile practice exista expresii in care pot apare o combinatie a acestor operatori. Precedenta
operatorului (!) este cea mai mica, el se evalueaza primul, urmeaza operatorul (&&), iar ultimul se
evalueaza operatorul (||).
Exemplu:
bool x = false;
bool y = false;
bool z = true;

bool result = x && y || z;

In acest exemplu rezultatul evaluarii este true deoarece se evalueaza x && y care este false. Cu acest
rezultat se evalueaza || z care este true.

Recomandare:
Pentru a evita erorile este de preferat folosirea parantezelor pentru specificarea succesiunii operatiilor.
Expresia anterioara se poate scrie mai clar in modul urmator:
bool result = (x && y) || z;

4.5.3 Efecte secundare


In cadrul expresiilor logice pot apare apeluri de functii. Aceste functii sunt apelate pana in momentul in
care compilatorul poate evalua expresia.
Exemplu:
bool testSau = f1() || f2();

In acest caz daca f2 returneaza true f2 nu mai este apelata deoarece rezultatul expresiei s-a evaluat la true.

bool testSi = f1() && f2();

In acest caz daca f1() returneaza false f2 nu mai este apelata deoarece rezultatul expresiei s-a evaluat la
false.

Nota
In aceste cazuri este necesar sa determinam, in functie de cerintele programului, daca apelul celei de-a

9
78
Adonis Butufei

doua functii este necesar sau nu.

4.6 Conversii si operatorii cast


In rezolvarea problemelor concrete apare deseori necesitatea efectuarii unor calcule cu variabile de tipuri
diferite, datorita unei operatii de atribuire explicita, evaluarii unei expresii sau apelului unei functii. In
toate aceste cazuri spunem ca valoarea este convertita de la un tip la altul.
Exista doua tipuri de conversie: implicita si explicita.

4.6.1 Conversii implicite


Conversia implicita se face automat de catre compilator pentru tipurile de date standard suportate de
limbaj5.
Exemplu de conversie implicita prin atribuire:
short a = 10;
int b = a; // valoarea lui a este convertita la int.

Exemplu de conversie implicita in cadrul unei expresii:


double result = 12.0/ 3;

Exemplu de conversie implicita prin apel de functie:


double AriaDreptunghi(double x, double y)
{
return x * y;
}

int main ( )
{
double aria = ArieDreptunghi(3,5);
}

4.6.2 Conversii explicite: operatorii cast


In acest caz programatorul specifica tipul la care se doreste conversia. Aceasta operatie se numeste cast.

Exemplu:
double result = 45.6;
int parteIntreaga = (int)result;

5 Exista operatori de conversie pentru clase care sunt implementati de utilizator. Vom discuta despre acest subiect intr-un
capitol viitor.
10
79
Adonis Butufei

Notatia de mai sus este preluata din C. In C++ putem face conversia introducand valoarea intre paranteze
ca in exemplul urmator:
double result = 45.6;
int parteIntreaga = int(result);

4.6.3 Recomandari pentru conversii


• Evitarea pe cat posibil a conversiilor intre variabile cu semn si cele unsigned pentru ca pot
produce defecte greu de identificat.
• Este importanta verificarea limitelor pentru tipul in care se realizeaza conversia.

4.7 Operatorul conditional: ?


Acest operator permite scrierea mai compacta a unei conditii if / else. Este singurul operator care lucreaza cu 3
operanzi.
Exemplu:
int y = x > 0 ? 1 : -1;

Executarea acestei linii presupune urmatorii pasi:


Se evalueaza expresia x > 0
In caz afirmativ se atribuie variabilei y valoarea 1.
Altfel se atribuie valoarea -1.

Codul de mai sus este echivalent cu:


int y;
if( x > 0)
{
y = 1;
}
else
{
y = -1;
}

Important
Desi acest operator poate fi folosit in expresii complexe pentru scrierea unui cod usor de inteles si mentinut este
recomandabil sa fie folosit numai pentru initializarea variabilelor in expresii similare cu exemplul prezentat mai sus.

11
80
Adonis Butufei

4.8 Operatori pe biti: | & ^ << >> ~

Exista situatii in care este necesara efectuarea operatiilor la nivel de bit asupra variabilelor intregi. Pentru
a adresa aceasta cerinta C++ foloseste operatorii pe biti. In tabelul de mai jos sunt prezentati operatorii pe
biti.
Operator Descriere

| sau pe biti

& si pe biti

^ sau exclusiv

<< deplasare la stanga

>> deplasare la dreapta

~ negare (inversiunea bitilor)

Operatorii |, & si ^ compara fiecare bit al primului operand cu bitul corespunzator al celui de-al doilea
operand si seteaza valoarea bitului de pe aceeasi pozitie din variabila rezultat. Modul de calcul al valorii
individuale a bitilor din variabila rezultat este prezentat in tabelul urmator:
Bit operand Bit rezultat

I II | & ^

0 0 0 0 0

0 1 1 0 1

1 0 1 0 1

1 1 1 1 0

Exemplu:
1: #include <iostream>
2: using namespace std;
3:
4: int main()
5: {
6: cout << hex << showbase;
7:
8: // 1110 | 0011 -> 1111 (0xf).
9: cout << "0xe | 0x3 = " << (0xe | 0x3 ) << "\n";
10:
11: // 1110 & 0011 -> 0010 (0x2).
12
81
Adonis Butufei

12: cout << "0xe & 0x3 = " << (0xe & 0x3 ) << "\n";
13:
14: // 1110 ^ 0011 -> 1101 (0xd).
15: cout << "0xe ^ 0x3 = " << (0xe ^ 0x3 ) << "\n";
16:
17: return 0;
18: }

Pentru a putea vizualiza valorile in reprezentare hexazecimala si a afisa baza am folosit setarile din linia
66.

Operatorii de deplasare << si >> muta toti bitii catre dreapta respectiv catre stanga cu un numar
specificat de pozitii.
Deplasarea la stanga cu n biti este echivalenta cu inmultirea valorii cu 2 n iar deplasarea la dreapta cu n
biti este echivalenta cu impartirea valorii cu 2 n .
Exemplu:
#include <iostream>
using namespace std;

int main()
{
int x = 1;
cout << x << " << 1 = " << (x << 1) << "\n";
cout << x << " << 2 = " << (x << 2) << "\n";
cout << x << " << 3 = " << (x << 3) << "\n";

cout << "\n";

int y = 8;
cout << y << " >> 1 = " << (y >> 1) << "\n";
cout << y << " >> 2 = " << (y >> 2) << "\n";
cout << y << " >> 3 = " << (y >> 3) << "\n";

return 0;
}

Operatorul de negare ~ inverseaza valoarea bitilor: din 1 in 0 si din 0 in 1.


Exemplu:
#include <iostream>

6 Detaliile de formatare le vom discuta in detaliu in capitolul 13 care este dedicat notiunilor de intrare si iesire.
13
82
Adonis Butufei

using namespace std;

int main()
{
unsigned short x = 0xa;
unsigned short y = ~x; // y = 0xfff5

cout << hex << showbase << "~" << x << " = " << y << "\n";

return 0;
}

4.8.1 Operatorii compusi: |= &= ^= <<= >>=


Ca si in cazul operatorilor aritmetici, pentru scrierea cat mai compacta a expresiilor, in C++ s-au definit
operatorii compusi pe biti. Prezentarea lor este in tabelul de mai jos:

Operator Exemplu Echivalenta

|= x |= y x=x|y

&= x &= y x=x&y

^= x ^= y x=x^y

>>= x >>= n x = x >> n

<<= x <<= n x = x << n

4.9 Operatorul: ,
Acest operator este folosit pentru a separa doua sau mai multe expresii. Aceste expresii sunt evaluate de la
stanga la dreapta si rezultatul expresiei de la marginea dreapta este cel considerat.
Exemplu:
int x = (b = 5, b +3);

In acest caz valoarea lui x este 8 iar a lui b este 5.

Un caz tipic de folosire este in buclele for, ca in exemplul urmator:


int x;
double y;

for(x = 0, y = 5.5; x < 10 && y < 1e6; x++, y *= 4.2)

14
83
Adonis Butufei

{
cout << x << " " << y << "\n";
}

Deoarece variabilele sunt de tipuri diferite este necesara declararea lor in afara buclei for.
In partea de initializare se executa ambele atribuiri. La verificarea conditiei se testeaza ambele conditii si
atunci cand una din conditii nu mai este evaluata ca true se opereste executia.

Bucla afiseaza valoarea curenta pentru x si y apoi pentru actualizarea ambelor valori ale variabilelor am
folosit din nou operatorul (,) .
Actualizarea variabilei in interiorul lui for se face si in cazurile in care executia codului din bucla
intalneste o instructiune continue.

Exemplu:
int x;
double y;

for(x = 0, y = 5.5; x < 10 && y < 1e6; x++)


{
cout << x << " " << y << "\n";

if ( conditie)
{
continue; // in acest caz nu se mai actualzeaza y!
}

y *= 4.2;
}

4.10 Precedenta operatorilor


In tabelul de mai jos sunt prezentati toti operatorii discutati pana acum cu precedenta si asociativitatea
corespunzatoare. Operatorii aflati pe o linie se aplica inaintea tuturor operatorilor de sub ei7.

Precedenta Operator Descriere Asociativitate

2 ++ -- incrementare, decrementare cu postfixare de la stanga la dreapta


() apel de functie

3 ++ -- incrementare, decrementare cu prefixare de la dreapta la stanga


! ~ negare logica si negare pe biti
+ - operatori de semm

6 + - adunare scadere de la stanga la dreapta


7 C++ are si alti operatori care vor fi prezentati pe parcurs si acestor operatori le corespund nivelele de precedenta 1,4,5 si 17.
15
84
Adonis Butufei

Precedenta Operator Descriere Asociativitate

7 << >> deplasare la stanga, dreapta

8 < <= comparare


> >=

9 == != egalitate, diferenta

10 & si pe biti

11 ^ sau exclusiv pe biti

12 | sau pe biti

13 && si logic

14 || sau logic

15 ?: operatorul conditional de la dreapta la stanga

16 = operatorul de atribuire
+= -= *= operatori de atribuire compusi
/= %= <<=
>>= &= ^=

|=
18 , virgula de la stanga la dreapta

4.11 Intrebari si exercitii


1. Care este valoarea variabilei x dupa executia urmatoarei secvente?
int x = 10;
x += 20;

2. Care este valoarea variabilei y din urmatoarea expresie?


int x, y, z = 110;

3. Care este rezultatul evaluarii urmatoarei expresii?


8 + 6 * 2 /3;

4. Care este valoarea variabilei z dupa executia urmatoarei secvente?


int z = 10;
z *= 3 + 4;

5. Care este valoarea variabilelor x si z dupa executia urmatoarei secvente?


int z =0;
int x = z++;

6. Care este valoarea variabilelor x si y dupa executia urmatoarei secvente?


16
85
Adonis Butufei

int y = 0;
int x = ++y;

7. Care este valoarea variabilei x in cazul in care n = 25?


int x;
if( ! (n %4 == 0) )
{
x = 35;
}
else
{
x = 24;
}

8. Care este valoarea variabilei x dupa evaluarea expresiei de mai jos?


int x = ~FF;

9. Care este valoarea finala a variabilei y?


int y = 2;
y << = 2;

10. Care este valoarea variabilei x?


int x = 0x0008 & 0xFFFF;

11. Care este valoarea variabilei y?


int y = 0x0004 | 0x0000;

12. Care este valoarea finala a variabilei y in cazul in care n = 24?


int y = 10;
if( (n % 3 == 0 ) && (n %8 == 0))
{
y = n;
}
else
{
--y ;
}

13. Care este valoarea finala a variabilei x in cazul in care n =36?


int x = 12;
if( n % 5 == 0)
{
x = 1;
}
else if( n%6 == 0)
{

17
86
Adonis Butufei

x = 2;
}
else
{
x = 3;
}

14. Care este valoarea variabilei z in cazul in care n = 18?


int z = (n % 3 == 0) ? 10 : 20;

4.12 Bibliografie
• http://en.cppreference.com/w/cpp/language/operator_precedence

18
87
Adonis Butufei

5. PREPROCESORUL C++

CUPRINS
5.Preprocesorul C++.....................................................................................................................................2
5.1 Ce este preprocesorul?.......................................................................................................................2
5.2 Directiva #define................................................................................................................................2
5.2.1 Macroinstructiuni complexe.......................................................................................................4
5.3 Compilare conditionata #ifdef, #else, #endif.....................................................................................4
5.4 Directiva #include..............................................................................................................................5
5.5 Programe cu mai multe fisiere...........................................................................................................5
5.5.1 Fisierele header...........................................................................................................................6
5.5.2 Fisierele sursa.............................................................................................................................9
5.6 Sumar.................................................................................................................................................9
5.7 Intrebari si exercitii............................................................................................................................9
5.8 Bibliografie.......................................................................................................................................10

1
88
Adonis Butufei

5. PREPROCESORUL C++
In primul capitol am intalnit directiva de preprocesare #include. Atunci cand am discutat despre
constante am intalnit a doua directiva: #define. In acest capitol vom discuta urmatoarele aspecte legate
de preprocesor:
• Ce este preprocesorul?
• Directivele #define si #include
• Compilarea conditionata
• Proiecte cu mai multe fisiere
• Folosirea fisierelor header

5.1 Ce este preprocesorul?


Preprocesorul este un simplu editor de text specializat care este rulat inaintea compilarii cu scopul de a
modifica fisierele sursa.
Sunt patru categorii importante de modificari realizate de preprocesor: includerea fisierelor, inlocuirea de
text, expandarea macroinstructiunilor si compilarea conditionata.
Instructiunile executate de preprocesor se numesc directive si ele incep cu caracterul (#) si se termina fara
a folosi caracterul (;). Pentru instructiunile pe mai multe randuri este necesara folosirea caracterului
escape (\) la sfarsitul liniei.
Sintaxa preprocesorului este complet diferita de cea a limbajului C++ si, de multe ori, erorile care apar
sunt datorita ignorarii diferentelor de sintaxa.

5.2 Directiva #define


Poate fi folosita pentru declararea de macroinstructiuni. Forma cea mai simpla de macroinstructiune este:
#define NUME text

Aceasta forma este frecvent folosita pentru declararea constantelor pe care am intalnit-o in capitolul 2.
Preprocesorul substituie in codul sursa NUME cu text.
Codul scris inainte de introducerea in standardul C++ a constantelor utilizeaza aceasta modalitate.
Exemplu:
#define MAX 10

Atunci cand folosim preprocesorul el inlocuieste peste tot unde gaseste MAX cu valoarea specificata in
directiva #define. Acest proces de inlocuire a textului se numeste expandare.

In cazul in care folosim calificatorul const compilatorul verifica sintaxa si semnaleaza eventualele erori
care in cazul anterior ar fi identificate in cadrul executiei. Din acest motiv pentru constante este preferata
folosirea cuvintelor cheie.
Exemplu:

2
89
Adonis Butufei

const int MAX = 10;

Este important de remarcat ca atunci cand folosim directiva #define nu adaugam terminatorul (;) in
dreapta valorii deoarece poate introduce efecte secundare ca in urmatorul exemplu:
#define MAX_VAL 10;
#define DELTA = MAX_VAL – 2;

int main()
{
int x = DELTA;
cout << x << "\n"; // afiseaza 10 in loc de 8!
return 0;
}

In acest caz, dupa expandarea DELTA initializarea lui x este realizata in modul urmator:
int x = 10; - 2; Deci valoarea lui x este 10 nu 8.

Varianta corecta este:


#define MAX_VAL 10
#define DELTA = MAX_VAL – 2

int main()
{
int x = DELTA;
cout << x << "\n";
return 0;
}

Important
Inlocuirea numelui nu se face in interiorul sirurilor. In exemplul de mai jos pe ecran va apare mesajul
original!

#include <iostream>
using namespace std;
#define FOO bar

int main()
{
string mesaj = "Test substituire FOO\n";
cout << mesaj;
}

3
90
Adonis Butufei

5.2.1 Macroinstructiuni complexe


Exista cazuri practice cand o secventa de cod se repeta frecvent, dar nu poate fi scrisa intr-o functie
datorita sintaxei C++. Pentru acest tip de probleme se folosesc macroinstructiunile care contin acea
secventa si sunt apelate ca si cum ar fi o functie.
Formele standard pentru macroinstructiuni sunt:
#define NUME() definitie
#define NUME(parametri) definitie

Sunt cateva aspecte importante in definirea macroinstructiunilor:


• Nu trebuie sa existe spatiu intre NUME si paranteza. In cazul in care exista spatiu, paranteza este
interpretata ca parte a unei directive simple de inlocuire.
• Se recomanda ca parametrii sa fie introdusi intre paranteze in definitie pentru evaluare corecta.
Exemplu:
#define PATRAT(x) ((x) * (x))

Daca in acest caz am fi omis parantezele si expresia ar fi fost (x* x) atunci o expresie de forma
y = PATRAT(x + 1) ar fi fost inlocuita cu: y = (x + 1 * x +1) ceea ce este diferit de (x + 1) * (x + 1).

Atunci cand avem o expresie mai complicata este de preferat organizarea ei pe mai multe linii. Pentru
continuarea macroinstructiunii pe linia urmatoare se foloseste caracterul (\) la sfarsitul liniei curente.
Acest caracter se aplica pana la penultima linie a macroinstructiunii.
Exemplu:
#define PATRAT(x,y) \
((x) * (y))

Recomandari
Macroinstructiunile ofera un mecanism puternic de mentenanta a codului. Deoarece ele au o sintaxa
diferita de C++ si depanarea lor este putin mai dificila este bine sa fie folosite cu multa atentie.

Atunci cand putem obtine acelasi rezultat folosind cod C++, de exemplu implementand o functie care
contine o secventa ce se repeta. este de preferat evitarea macroinstructiunilor.

Macroinstructiunile trebuie sa fie cat mai simple pentru a reduce efortul de intelegere si mentenanta.

5.3 Compilare conditionata #ifdef, #else, #endif


De obicei versiunea de dezvoltare a unui program contine cod suplimentar pentru testarea functionarii,
cod care trebuie sa nu apara in versiunea finala a produsului. Pentru a putea compila diferit cele doua
versiuni se foloseste compilarea conditionata.
Exemplu:
#ifdef _DEBUG
cout << "Versiune de dezvoltare\n";
#endif

4
91
Adonis Butufei

Constanta _DEBUG este definita de mediul de dezvoltare atunci cand selectam configuratia curenta.

Exista cazuri in care dorim sa tratam ambele ramuri ca in urmatorul exemplu:


#ifdef _DEBUG
cout << "Versiune de dezvoltare\n";
#else
cout << "Versiune de productie\n";
#endif

Important
Indiferent care este varianta folosita trebuie ca sectiunea sa aiba la sfarsit directiva #endif

5.4 Directiva #include


Stim din capitolulul 2 ca aceasta directiva include continutul fiserului specificat in fisierul curent.
Exista doua modalitati de a specifica fisierul care trebuie inclus:
1. Atunci cand folosim caraterele < respectiv > pentru a delimita numele fisierului preprocesorul va cauta
fisierul intr-una din caile predefinite in variabila sistem path. Aceasta metoda este folosita, de regula,
pentru fisierele specifice mediului C++.
2. Pentru fisiere din directorul proiectului se poate folosi varianta:

#include "myfile"

Daca fisierul se afla intr-un director al proiectului este necesara specificarea caii catre acel fisier. Exemplu:

#include "myfolder/myfile"

Remarca
Daca preprocesorul nu poate include fisierul specificat cu instructiunea #include vom obtine un mesaj de eroare 1.

Recomandare
Cu toate ca este posibil sa folosim prima varianta si pentru fisierele proprii, pentru usurinta depanarii este de preferat sa
folosim prima varianta exclusiv pentru fisierele mediului C++ si cea de-a doua pentru fisierele proprii.

5.5 Programe cu mai multe fisiere


Programele scrise pana acum aveau doar fisierul continand functia main. In cazul programelor simple si
de dimensiuni mici acest mod este eficient. In cazurile practice insa este foarte dificil de inteles si
mentinut un program care are tot codul scris intr-un fisier.

1 In Visual C++ se poate verifica daca fisierele pot fi incluse folosind meniul contextual Open Document care este
afisat atunci cand am pozitionat cursorul editorului pe linia respectiva.
5
92
Adonis Butufei

Pentru o mai buna organizare, codul sursa al programelor este organizat in mai multe fisiere. In C++
avem doua categorii importante de fisiere: fisierele header si fisierele sursa.

5.5.1 Fisierele header


Fisierele header au de regula extensia h2 si contin prototipurile functiilor, tipurile de date definite de
utilizator, directive de compilare, declararea variabilelor externe si declararea constantelor.
Rolul acestor fisiere este de a separa conceptele programului de detaliile de implementare. De exemplu,
pentru a putea apela o functie trebuie sa stim care este semnatura acelei functii, adica tipurile parametrilor
si tipul valorii returnate. Pentru aceasta este necesara includerea fisierului header in care este declarata
acea functie. Nu avem insa nevoie sa stim detaliile de implementare.

Exemplu de fisier header pentru functia Factorial():


// Fisierul factorial.h
// Prototipul functiei factorial
int Factorial(int n);

Un fisier header este inclus de regula in cel putin doua fisiere sursa: fisierul sursa care contine
implementarea si fisierul sursa in care se face apelul. Deoarece fisierul header contine declaratii de
variabile includerea lui in mai multe fisiere sursa poate genera erori de redefinire daca nu se pun directive
de preprocesare care previn aceste erori. Aceste directive se numesc garda headerului.
Mai jos este prezentata garda headerului pentru calculul factorialului.
// Fisierul factorial.h
#ifndef FACTORIAL_H
#define FACTORIAL_H

// Prototipul functiei factorial


int Factorial (int n);

#endif //FACTORIAL_H

Din acest exemplu observam 3 elemente:


Prima linie testeaza daca a fost definit numele FACTORIAL_H (putem folosi orice denumire unica, dar
pentru simplitate se recomanda folosirea numelui fisierului).

In cazul in care nu a fost definit, se face definirea cu directiva #define. Preprocesorul copiaza in fisierul
sursa tot textul dintre directiva #define si directiva #endif asociata cu #ifndef.
Daca acest header este inclus a doua oara, atunci numele FACTORIAL_H a fost deja definit si
preprocesorul ignora textul pana la urmatoarea directiva #endif din acest header.

2 Urmatoarele extensii sunt folosite mai rar pentru fisierele header: h, hpp, hxx, hm, inl, inc
6
93
Adonis Butufei

Nota
Etapele adaugarii unui fiser header in proiectul curent, in Visual C++:
Se selecteaza Header Files din fereastra Solution Explorer.

Din meniul contextual se selecteaza Add/New Item

In dialog se selecteaza Header File


7
94
Adonis Butufei

Se introduce numele si se apasa OK.

5.5.2 Fisierele sursa


Fisierele sursa au, de regula, extensia cpp3 si contin implementarea functiilor declarate in header si apelul
acestor functii. Fisierele cu care am lucrat in programele scrise pana acum sunt fisiere sursa.

5.6 Sumar
• Preprocesorul este un editor de text specializat care este rulat inaintea compilarii. El are o sintaxa
diferita de C++ si este folosit pentru modificarea codului sursa folosind instructiuni care se numesc
directive.
• Directivele incep cu caracterul # si se termina la sfarsitul liniei curente.
• Macroinstructiunile ofera un mecanism puternic si flexibil pentru modificarea fisierelor sursa,
compilarea contitionata si definirea unor functionalitati care nu pot fi implementate direct in limbajul
C++.
• Pentru organizare eficienta codul programelor este impartit in doua categorii de fisiere: fisiere header
si fisiere sursa. Prima categorie contine declaratiile, iar a doua contine detaliile de implementare.

3 Urmatoarele extensii sunt folosite mai rar pentru fisierele sursa: cpp, cc, cxx. Atunci cand proiectul foloseste ambele
limbaje C si C++ putem avea in proiecte si fisiere sursa C care au extensia c.
8
95
Adonis Butufei

• Pentru fisierele header se folosesc directive care previn erorile de compilare atunci cand fisierul este
inclus de mai multe ori.

5.7 Intrebari si exercitii


1. Ce este preprocesorul?
2. Ce sunt directivele de compilare?
3. Cand se executa directivele de compilare?
4. Care este motivul pentru care codul programelor este organizat in mai multe fisiere?
5. Care sunt categoriile principale de fisiere utilizate in programe?
6. Cum se folosesc fisierele header?
7. Care este rolul compilarii conditionate? Dati exemple.
8. Cum se realizeaza compilarea conditionata?
9. Sa se scrie un program care calculeaza factorialul unui numar intreg. Programul are urmatoarele
fisiere:
factorial.h – contine prototipul functiei factorial.
factorial.cpp – implementeaza functia factorial.
main.cpp – contine functia main in care se citeste de la tastatura numarul pentru care se calculeaza
factorialul, apeleaza functia de calcul a factorialului, afiseaza rezultatul.

10. Sa se scrie un program care face conversia intre mile si km folosind urmatoarea relatie 1 mila =
1.609344 km. Programul are urmatoarele fisiere:
conversii.h – contine prototipul functiei double Mile2Km(double miles).
conversii.cpp – implementeaza functia double Mile2Km(double miles).
main.cpp – contine functia main in care se citeste de la tastatura numarul de mile, se apeleaza functia
de conversie, afiseaza rezultatul.

11. Sa se extinda programul anterior adaugand conversia inversa Km2Mile folosind aceleasi fisiere.
12. Sa se comenteze garda de include din fisierul header de la exercitiul anterior si sa se includa a doua
oara in fisierul main. Care sunt mesajele de eroare?

5.8 Bibliografie
• Practical C++ Programming, Second edition, O'Reilly Steve Oualline, Cap 10.
• http://www.ebyte.it/library/codesnippets/WritingCppMacros.html

9
96
Adonis Butufei

6. TIPURI COMPUSE DE DATE

CUPRINS
6.Tipuri compuse de date..............................................................................................................................2
6.1 Variabile de tip tablou (array).............................................................................................................2
6.1.1 Declararea tablourilor.................................................................................................................2
6.1.2 Initializarea tablourilor...............................................................................................................2
6.1.3 Accesarea elementelor ...............................................................................................................3
6.1.4 Tablouri cu mai multe dimensiuni .............................................................................................4
6.2 Siruri de caractere...............................................................................................................................5
6.2.1 Operatii cu siruri de caractere.....................................................................................................6
6.2.1.1 Determinarea lungimii unui string .....................................................................................6
6.2.1.2 Copierea sirurilor de caractere............................................................................................7
6.2.1.3 Concatenarea sirurilor de caractere.....................................................................................8
6.2.1.4 Compararea sirurilor de carctere.........................................................................................9
6.3 Structuri de date...............................................................................................................................10
6.3.1 Initializarea structurilor............................................................................................................10
6.3.2 Accesul la membrii structurii....................................................................................................11
6.3.3 Atribuirea structurilor...............................................................................................................11
6.4 Uniuni...............................................................................................................................................12
6.5 Sumar...............................................................................................................................................13
6.6 Intrebari si exercitii..........................................................................................................................14
6.7 Bibliografie.......................................................................................................................................15

1
97
Adonis Butufei

6. TIPURI COMPUSE DE DATE


In capitolele anterioare am discutat despre tipurile de variabile simple suportate de C++. In acest capitol
vom discuta despre tipuri complexe de date folosite in programele practice:
Tipuri compuse de date: siruri de caractere si tablouri.
Tipuri abstracte de date: structuri si uniuni.

6.1 Variabile de tip tablou (array)


In activitatile curente folosim clasamente si statistici pentru o gama variata de activitati: rezultate scolare,
sportive, evolutia preturilor etc. In aceste cazuri lucram cu valori care apartin aceleiasi categorii de
elemente. De exemplu, in cazul rezultatelor la examenele de admitere avem o lista care contine numele
participantilor, nota la fiecare proba si media. Pentru a determina candidatii admisi, lista se sorteaza
descrescator dupa media notelor.

Pentru a scrie programe care sa lucreze cu acest tip de date avem nevoie sa folosim tipul de date numit
tablou. Acest tip de date este o colectie secventiala de locatii de memorie de acelasi tip. Fiecare locatie se
numeste element.

6.1.1 Declararea tablourilor


Pentru declararea tablourilor specificam tipul elementelor, numele si, intre paranteze drepte, dimensiunea.

Exemplu:
int tablou[3];

In acest exemplu am declarat un tablou de trei elemente de tip intreg.

6.1.2 Initializarea tablourilor


Initializarea se poate realiza specificand valorile la declarare.
Exemplu:
int tablou[5] = { 1, 2, 3, 4, 5 };

In acest exemplu am initialzat un tablou de 5 elementete intregi.

tablou[0] tablou[1] tablou[2] tablou[3] tablou[4]


tablou 1 2 3 4 5
int

Observatii:

2
98
Adonis Butufei

• Se poate omite dimensiunea tabloului la declarare daca se initializeaza toate elementele. In exemplul
de mai jos se initializeaza un tablou de 5 elemente.
int tablou [] = {1, 2, 3, 4, 5};

• Daca numarul de elemente folosite pentru initializare depaseste dimensiunea tabloului se obtine o
eroare de compilare:
int tablou [3] = { 1, 2, 3, 4};

• In cazul in care sunt mai putine valori pentru initializare, ele vor fi folosite pentru initializarea primelor
elemente din tablou. Dupa ce s-au folosit toate valorile, elementele ramase vor fi initializate cu 0:
int tablou[3] = { 1, 2} ;// tablou[2] are valoarea 0.

• Pentru initializarea tuturor elementelor cu 0 este suficient sa folosim o singura valoare 0 in dreapta
egalului:
int tablou [3] = {0}; // echivalent cu tablou[3] = { 0, 0, 0};

6.1.3 Accesarea elementelor


Accesarea elementelor unui tablou se face folosind o variabila intreaga numita index care reprezinta
offsetul fata de inceputul tabloului. Indexul porneste de la zero. Folosind exemplul anterior, primul
element este tablou[0], al doilea este tablou [1] iar ultimul este tablou[2].
Exemplu:
1: #include <iostream>
2: using namespace std;
3:
4: int main()
5: {
6: const int SIZE = 3;
7: int tablou[SIZE];
8:
9: for(int i = 0; i < SIZE; i++)
10: {
11: cout << "tablou[" << i << "]=";
12: cin >> tablou [i];
13: }
14:
15: for(int i= 0; i < SIZE; i++)
16: {
17: cout << i <<": " << tablou [i] << "\n";

3
99
Adonis Butufei

18: }
19:
20: return 0;
21: }

In acest tablou am initializat elementele unui vector de elemente intregi si am afisat valorile pe ecran.
In linia 6 am declarat o constanta care reprezinta dimensiunea tabloului. Aceasta se foloseste pentru
declararea tabloului in linia 7 si pentru conditiile de limita din bucle in liniile 9 si 15.
Valorile tabloului sunt citite de la tastatura (liniile 9 – 13) si afisate pe ecran (liniile 15 – 18).

Important
Deoarece indexul tablourilor porneste de la 0, valoarea indexului corespunzator ultimului element este
SIZE -1. Omiterea acestui fapt este o sursa frecventa de erori.

6.1.4 Tablouri cu mai multe dimensiuni


Exista probleme care se pot rezolva mai usor folosind tablouri cu mai multe dimensiuni. De exemplu
pentru a rezolva un sistem de 2 ecuatii cu 2 necunoscute trebuie sa folosim tablouri de doua linii si doua
coloane care contin valorile coeficientilor.

Declararea tablourilor cu mai multe dimensiuni se face asemanator cu declararea tablourilor cu o singura
dimensiune. In plus, pentru fiecare dimensiune se mai adauga o pereche de paranteze drepte.
Exemplu de declarare a unei matrici de 2 x 2 elemente:
double matrice[2] [2];

Initializarea tuturor elementelor cu 0 se realizeaza folosind o singura valoare in dreapta operatorului =.


Exemplu:
double matrice [2][2] = {0.0};

0 1
matrice 0 0.0 0.0
1 0.0 0.0

matrice[0][1]

Modul de lucru cu tablouri cu mai multe dimensiuni este prezentat in exemplul de mai jos.
1: #include <iostream>
2: using namespace std;
3:

4
100
Adonis Butufei

4: int main()
5: {
6: const int SIZE1 = 2;
7: const int SIZE2 = 3;
8:
9: // Declarare si initializare
10: int matrice [SIZE1][SIZE2] = { {1,2,3} ,
11: {4,5,6} };
12:
13: for(int i = 0; i < SIZE1; i++)
14: {
15: for(int j = 0; j < SIZE2; j++)
16: {
17: cout << "matrice[" << i << "][" << j << "]=";
18: cout << matrice[i][j] << "\n";
19: }
20: }
21: return 0;
22: }

Initializarea tabloului este realizata in liniile 10 si 11. Pentru accesarea elementelor am folosit doua
bucle, fiecare pentru o dimensiune a tabloului. In linia 18 se acceseaza valorile corespunzatoare
indecsilor.

6.2 Siruri de caractere


Sirurile de caractere sunt un tip special de tablouri care contin coduri ASCII folosite pentru lucrul cu
mesaje de tip text. Au fost preluate din C si le intalnim frecvent in multe programe.
Exemplu:
char mesaj [] = "Hello World!";

Spre deosebire tablourile prezentate anterior, sirurile de caractere contin pe ultima pozitie caracterul de
control '\0', care reprezinta terminatorul sirului. Acest caracter este introdus automat de compilator,
insa este necesar ca tabloul sa aiba alocat spatiu pentru el.
In urmatorul exemplu obtinem eroare de compilare deoarece mesajul are 12 caractere si compilatorul nu
mai are spatiu pentru terminator.
char mesaj[12] = "Hello World!"; // Tabloul trebuie sa aiba 13
// caractere.

Din acest motiv, pentru mesajele care nu se modifica pe parcursul executiei programului este
recomandabila prima varianta de declarare, deoarece dimensiunea tabloului se calculeaza automat.

5
101
Adonis Butufei

Terminatorul sirului nu este printabil si orice caracter printabil1 pozitionat dupa acest carcter nu este
afisat.
Exemplu:
#include <iostream>
using namespace std;

int main()
{
char mesaj [] = "Hello\0 World!";
cout << mesaj << "\n";
return 0;
}

In acest exempul pe ecran va fi afisat numai mesajul Hello.

6.2.1 Operatii cu siruri de caractere


Modificarea variabilelor simple se face prin atribuire. Pentru modificarea variabilelor de tip tablou este
necesara scrierea unor functii. In cazul sirurilor de caractere, pentru copierea, concatenarea, aflarea
lungimii sirurilor etc, se folosesc functii specifice definite si implementate in fisierul cstring.

6.2.1.1 Determinarea lungimii unui string


Se face cu functia strlen. Aceasta functie returneaza doar numarul de caractere al mesajului,
terminatorul sirurului nu este luat in calcul.
Exemplu:
#include <iostream>
#include <cstring>
using namespace std;

int main()
{

char mesaj [] = "Hello World!";


cout << mesaj << " are " << strlen(mesaj) << " caractere.\n";
return 0;
}

In acest exemplu am inclus headerul cstring pentru a putea apela functia strlen. Mesajul din acest
exemplu are 12 caractere.

Important
1 Caracterele printabile sunt caractere care se afiseaza pe ecran si au fost prezentate in capitolul 3.
6
102
Adonis Butufei

Atunci cand calculam spatiul necesar pentru un tablou trebuie sa incrementam valoarea returnata de
strlen pentru a obtine spatiul necesar pentru terminator.

6.2.1.2 Copierea sirurilor de caractere


In cazul variabilelor simple pentru schimbarea valorii este suficient sa folosim operatorul =. In cazul
sirurilor de caractere este necesar sa copiem fiecare caracter si pentru asta folosim functiile strcpy.
Exemplu:
#include <iostream>
#include <cstring>
using namespace std;

int main()
{

char mesaj1 [] = "Hello World!";


char mesaj2 [50];

strcpy(mesaj2, mesaj1);

cout << mesaj1 << "\n" << mesaj2 << "\nCopiere cu success.\n";
return 0;
}

In acest exemplu, dupa apelul functiei strcpy, ambele variabile vor contine acelasi mesaj.

Important
Variabila destinatie trebuie sa aiba suficient spatiu pentru a putea copia tot mesajul inclusiv terminatorul
de caractere.

Daca vrem sa copiem numai primele n caractere putem folosi functia strncpy, ca in exemplul urmator:
1: #include <iostream>
2: #include <cstring>
3: using namespace std;
4:
5: int main()
6: {
7: char mesaj1 [] = "Hello World!";
8: char mesaj2 [6];
9:
7
103
Adonis Butufei

10: strncpy(mesaj2, mesaj1, 5);


11: mesaj2[5] = '\0';
12:
13: cout << mesaj1 << "\n" << mesaj2 << "\nCopiere cu
success.\n";
14: return 0;
15: }

In acest exemplu am copiat doar primele 5 caractere din mesaj1. Este important de remarcat adaugarea
terminatorului in linia 11. Acesta este necesar deoarece dupa copierea primelor 5 caractere din variabila
mesaj1 sirul trebuie sa aiba terminatorul setat corect.

6.2.1.3 Concatenarea sirurilor de caractere


Exista situatii frecvente in care avem nevoie sa combinam continutul a doua siruri de caractere. Aceasta
se realizeaza cu ajutorul functiei strcat.
Exemplu:
1: #include <iostream>
2: #include <cstring>
3: using namespace std;
4:
5: int main()
6: {
7:
8: char nume [] = "Popescu";
9: char prenume [] = "Vasile";
10: char id [60] = {'\0'};
11:
12: strcpy(id,nume);
13: strcat(id,".");
14: strcat(id,prenume);
15:
16: cout << nume << "\n";
17: cout << prenume << "\n";
18: cout << id << "\n";
19: return 0;
20: }

In acest exemplu am format un identificator de persoana format din Nume.Prenume.


In linia 12 am copiat numele, apoi am adaugat caracterul '.' pentru a separa numele de prenume. La final
am adaugat prenumele.

8
104
Adonis Butufei

Important
Sirul destinatie trebuie sa aiba suficient spatiu pentru copierea tuturor caracterelor plus terminatorul de sir.

6.2.1.4 Compararea sirurilor de carctere


Deoarece sirurile de caractere sunt un tip de date compus, pentru comparare se foloseste functia strcmp
(nu operatorul ==). Aceasta functie returneaza 0 daca sirurile au aceleasi caractere, o valoare pozitiva
daca primul caracter care difera are valoare mai mare, o valoare negativa in caz contrar.
Exemplu:
#include <iostream>
#include <cstring>
using namespace std;

int main()
{

char inventator [] = "Edison";


char raspuns [30];

cout << "Cine a inventat becul cu incandescenta?\n";


cin >> raspuns;

if(0 == strcmp(inventator, raspuns))


{
cout << "Corect\n";
}
else
{
cout << "Incorect\n";
}

return 0;
}

Important
Compararea sirurilor de caractere este case sensitive, adica literele mari sunt considerate diferite de cele
mici: A != a. Pentru ca doua siruri de caractere sa fie egale trebuie sa contina acelasi tip de litere.

6.3 Structuri de date


Acesta este un tip de date care grupeaza variabilele intr-o singura entitate. Organizarea datelor in acest

9
105
Adonis Butufei

mod permite scrierea unui cod mai usor de inteles si mentinut. Variabilele grupate in interiorul structurii
se numesc membri sau atribute.
Exemplu:
struct Complex
{
double re;
double im;
};

In acest exemplu am grupat partea reala si imaginara intr-o structura Complex care poate fi folosita pentru
calcule cu numere complexe.

6.3.1 Initializarea structurilor


Structurile se pot initializa specificand valoarea fiecarei variabile componente. In exemplul de mai jos se
initializeaza o variabila de tip complex.
Complex c1 = {1.2, 3.4};

In acest caz varloarea variabilei re este 1.2 iar valoarea variabilei im este 3.4.

6.3.2 Accesul la membrii structurii


Pentru accesul la membrii structurii se foloseste operatorul punct (.) aplicat membrilor structurii.
Exemplu:
1: #include <iostream>
2: using namespace std;
3:
4: struct Complex
5: {
6: double re;
7: double im;
8: };
9:
10: int main()
11: {
12:
13: Complex c1 = {2.0,3.5};
14:
15: cout << "c1.re = " << c1.re << "\n";
16: cout << "c1.im = " << c1.im << "\n";
17:
18: return 0;

10
106
Adonis Butufei

19: }

In acest exemplu in liniile 15 si 16 se afiseaza valorile partii reale si imaginare ale variabilei complex c1.

6.3.3 Atribuirea structurilor


Pentru structurile de acelasi tip putem folosi operatorul de atribuire. Aceasta operatie copiaza continutul
memoriei de la prima structura la a doua structura bit cu bit.
Exemplu:
Complex c1 = {2.5, 3.4};
Complex c2 = c1;

Dupa executarea instructiunii de atribuire din acest exemplu variabila c2 va avea aceleasi valori pentru re
si im

6.4 Uniuni
Structurile permit definirea tipurilor de date care contin mai multe variabile membru. Fiecare membru
ocupa o zona separata de memorie. Uniunile permit definirea unui tip de date in care o zona de memorie
este partajata de mai multe variabile membru.

Exemplu:
union Valoare
{
int iVal;
double dVal;
};

In acest exemplu avem o zona de memorie partajata intre o variabila intreaga si una reala.

Putem gandi o structura ca o cutie cu mai multe compartimente, iar uniunea ca o cutie cu un singur
compartiment. In cazul structurilor, membrii nu interactioneaza intre ei. In cazul uniunilor, numai un
singur membru este activ la un moment dat: acela caruia i s-a atribuit o valoare.

Pentru a lucra corect cu o uniune este necesara o variabila auxiliara care sa indice membrul activ.
Mai jos este prezentat un exemplu tipic de folosire a uniunilor.
1: #include <iostream>
2: using namespace std;
3:
4: union Valoare
5: {
6: int i;
7: double d;
8: };
9:

11
107
Adonis Butufei

10: enum CampActiv { INT, DOUBLE};


11:
12: void AfiseazaValoare(Valoare val, CampActiv camp)
13: {
14: switch(camp)
15: {
16: case INT:
17: cout << val.i << "\n";
18: break;
19: case DOUBLE:
20: cout << val.d << "\n";
21: break;
22: default:
23: cout << "Camp invalid\n";
24: break;
25: }
26: }
27:
28: int main()
29: {
30: CampActiv camp = DOUBLE;
31: Valoare val;
32: val.d = 3.5;
33: AfiseazaValoare(val,camp);
34:
35: val.i = 10;
36: camp = INT;
37: AfiseazaValoare(val,camp);
38: return 0;
39: }

Pentru identificarea campului activ am folosit o variabila enumerata.


In momentul cand am schimbat campul activ a trebuit actualizata si valoarea acestei variabile (linia 36).

Uniunile au fost preluate din limbajul C unde sunt folosite pentru transmiterea in mod uniform a
parametrilor catre functii atunci cand au tipuri diferite.
Prezinta un avantaj pentru portabilitatea programelor. Cand se adauga un nou camp la membrii uniunii
semnatura functiilor ramane neschimbata, doar detaliile de implementare sunt actualizate.
In C++ acest tip de date este utilizat mai rar deoarece mecanismele programarii obiectuale pe care le vom
studia in capitolele urmatoare ofera solutii mai simple pentru obtinerea aceluiasi rezultat.

12
108
Adonis Butufei

6.5 Sumar
• Tabloul (array) reprezinta o serie de date de acelasi tip plasate in locatii succesive de memorie.
• Fiecare dintre aceste locatii de memorie se poate accesa prin incrementarea unui index atasat numelui
tabloului.
• Indexul porneste de la 0.
• Declararea unui tablou: tip numeTablou[elemente]
• Initializarea: tip numeTablou[] = {element1, element2, element3}
• Accesarea elementelor: numeTablou[index]
• Valoarea indexului corespunzatoare ultimului element este: dimensiunea tabloului (numarul de
elemente) – 1.
• Pentru anumite probleme se pot folosi tablouri cu 2 sau mai multe dimensiuni.
• Pentru tablourile cu mai mult de 2 dimensiuni trebuie tinut cont de memoria consumata.
• Declarare: tip nume[elemente1][elemente1]
• Un tip particular de tablou este sirul de caractere.
• In acest caz elementele tabloului sunt codurile ASCII folosite pentru mesaje de tip text.
• Declarare si initializare: char mesaj [] = "Hello World!";
• Sirurile de caractere se termina cu un caracter de control '\0'. Atentie la alocarea spatiului pentru acest
terminator!
• Deoarece sirurile de caractere sunt un tip de date compus modificarea lor se face cu ajutorul unor
functii.
• In C++ aceste functii sunt implementate in fisierul cstring:
strlen () - lungimea sirului (fara terminator)
strcpy() - copiere
strncpy() – copierea primelor n caractere
strcat() - concatenare
strcmp() - comparare
• Structura (struct) este un tip de date care grupeaza mai multi membri, de diferite tipuri, sub acelasi
nume.
• Fiecare membru al unei structuri ocupa o zona de memorie proprie.
• Uniunea (union) este un tip de date care grupeaza mai multe elemente de diverse tipuri. Acestea ocupa
acelasi spatiu fizic de memorie.
• Pentru a lucra corect cu o uniune este necesara o variabila auxiliara care sa indice membrul activ.

6.6 Intrebari si exercitii


1. Urmatoarea linie de cod este incorecta. Care este greseala?
13
109
Adonis Butufei

int test [4] = {1,2,3,4,5};

2. Cum se declara un tablou de tip double care are trei dimensiuni si poate contine 4 elemente pentru
prima dimensiune, 5 elemente pentru a doua dimensiune si 10 elemente pentru a treia dimensiune?

3. Care este indexul pentru primul element al unui tablou?

4. Care este indexul ultimului element al tabloului de mai jos?


int m [5];

5. Care este valoarea celui de-al treilea element al urmatorului tablou?


int pos [4] = { 1,2};

6. Cum se initializeaza toate elementele unui tablou cu o anumita valoare?

7. Sa se scrie un program care calculeaza determinantul unei matrici de doua linii si doua coloane.

8. Sa se scrie un program care citeste de la tastatura un mesaj de maxim 20 de caractere si afiseaza


lungimea sirului de carctere.

9. Sa se scrie un program care citeste de la tastatura numele, prenumele si calculeaza adresa de email
dupa urmatoarea formula prenume.nume@mailserver.com.
10. Sa se scrie un program care foloseste o structura de tip persoana cu urmatorii membrii: nume,
prenume, varsta. Programul citeste de la tastatura valorile pentru acesti membrii.
11. Care este diferenta intre o structura si o uniune?
12. Care sunt asemanarile dintre o structura si o uniune?

6.7 Bibliografie
• C++ Without Fear, Second Edition, Prentice Hall, Brian Overland, Cap 7.
• Practical C++ Programming, Second Edition, O'Reilly, Steve Oualline, Cap 12.

14
110
Adonis Butufei

7. VARIABILE SPECIALE

CUPRINS
7.Variabile speciale.......................................................................................................................................2
7.1 Referinte.............................................................................................................................................2
7.2 Pointeri...............................................................................................................................................4
7.2.1 Determinarea adresei variabilelor, operatorul &........................................................................4
7.2.2 Declararea pointerilor si stocarea adreselor variabilelor............................................................5
7.2.3 Accesarea valorii unei variabile folosind pointerii.....................................................................5
7.2.4 Calificatorul const pentru pointeri .............................................................................................6
7.2.4.1 Pointeri catre constante ......................................................................................................6
7.2.4.2 Pointeri constanti................................................................................................................7
7.2.4.3 Pointeri constanti catre constante.......................................................................................7
7.2.5 Pointeri si vectori........................................................................................................................7
7.2.5.1 Aritmetica pointerilor..........................................................................................................8
7.2.6 De ce se folosesc pointerii?........................................................................................................8
7.2.6.1 Transferul parametrilor prin adresa.....................................................................................9
7.2.6.2 Alocare dinamica a memoriei: operatorii new si delete...................................................10
7.2.6.3 Selectia datelor membru pentru structuri..........................................................................11
7.2.7 Pointerul NULL........................................................................................................................12
7.2.8 Pointerul void...........................................................................................................................12
7.2.9 Pointeri vs. Referinte................................................................................................................13
7.3 Sumar ..............................................................................................................................................13
7.4 Intrebari si exercitii..........................................................................................................................13
7.5 Bibliografie.......................................................................................................................................15

1
111
Adonis Butufei

7. VARIABILE SPECIALE
Exista situatii cand este util sa se lucreze cu adresele locatiilor de memorie unde sunt stocate variabilele.
Pentru a adresa aceste situatii, in acest capitol sunt discutate urmatoarele notiuni:
• Referinte
• Pointeri

7.1 Referinte
Referintele sunt un tip special de variabile. Cu ajutorul referintelor putem atasa unul sau mai multe nume
aceleiasi locatii de memorie.
Referintele se declara folosind tipul variabilei si operatorul &.

Exemplu:
1: #include <iostream>
2: using namespace std;
3:
4: int main()
5: {
6: int valoare = 10;
7: int &test = valoare;
8: cout << "Valoare initiala: " << valoare << "\n";
9:
10: test = 25;
11: cout << "Valoare finala: " << valoare << "\n";
12:
13: return 0;
14: }

In linia 7 am definit o referinta pentru variabila valoare. Din acest moment test si valoare folosesc aceeasi
zona de memorie.
10
valoare
test

Important
Referintele odata setate nu mai pot fi schimbate. Folosirea operatorului = pentru o referinta determina
executia unei atribuiri, nu schimbarea referintei.
Exemplu:
1: #include <iostream>
2: using namespace std;
3:
2
112
Adonis Butufei

4: int main()
5: {
6: int v1 = 10;
7: int &r1 = v1;
8:
9: cout << "v1 " << v1 << "\n";
10: cout << "r1 " << r1 << "\n";
11:
12: int &r2 = v1;
13:
14: int v2 = 20;
15:
16: r2 = v2;
17:
18: cout << "v1 " << v1 << "\n";
19: cout << "r1 " << r1 << "\n";
20:
21: return 0;
22: }

In acest exemplu atribuirea din linia 16 nu schimba referinta catre variabila v2, ci atribuie valoarea
variabilei v2 zonei de memorie conectata cu referinta r2. La final variabila v1 va avea valoarea 20.

Referintele sunt foarte utile pentru transmiterea parametrilor catre functii. Am vazut in capitolul despre
variabile ca parametrii functiilor se pot considera variabile locale functiei. O modificare asupra
parametrilor nu schimba valoarea variabilelelor cu care s-a apelat functia.
In cazul in care dorim ca functia sa modifice variabilele care sunt transmise ca parametri, folosim
referintele ca in exemplul de mai jos:
1: #include <iostream>
2: using namespace std;
3:
4: void Swap(int &x, int &y)
5: {
6: int temp = x;
7: x = y;
8: y = temp;
9: }
10:
11: int main()
12: {
13: int x = 10;
14: int y = 20;

3
113
Adonis Butufei

15:
16: cout << "x: " << x << "\n";
17: cout << "y: " << y << "\n";
18:
19: Swap(x,y);
20:
21: cout << "x: " << x << "\n";
22: cout << "y: " << y << "\n";
23:
24: return 0;
25: }

In acest exemplu avem o functie care inverseaza valorile variabilelor x si y. In linia 6 salvam valoarea
variabilei x intr-o variabila locala. Dupa executia liniei 7 variabila x va avea valoarea variabilei y. Apoi,
dupa executia liniei 8 variabila y va avea valoarea initiala a variabilei x.
Programul afiseaza valorile initiale si finale dupa apelul functiei Swap.

7.2 Pointeri
Pointerii sunt variabile care contin adrese de memorie. Se spune ca o variabila de tip pointer pointeaza
catre o adresa de memorie.
Memoria calculatorului este organizata intr-o secventa de pozitii. Fiecare variabila ocupa o pozitie unica
de memorie. Aceasta pozitie reprezinta adresa variabilei.
Prin analogie putem gandi variabilele ca fiind cladirile dintr-un oras iar adresele acestor cladiri reprezinta
adresele din memorie. Pentru a identifica orice cladire indiferent de dimensiune, adresele trebuie sa
contina numele strazii si numarul. In acelasi mod adresele oricaror variabile se reprezinta prin numere
scrise in hexazecimal.

7.2.1 Determinarea adresei variabilelor, operatorul &


Pentru determinarea adresei variabilelor se foloseste operatorul &.
Exemplu:

#include <iostream>
using namespace std;

int main()
{
int x = 10;

cout << "Adresa variabilei x: " << &x << "\n";

return 0;
}

4
114
Adonis Butufei

In acest exemplu adresa variabilei x este tiparita pe ecran. In functie de varianta sistemului de operare
putem obtine o adresa pe 32 sau pe 64 de biti.

7.2.2 Declararea pointerilor si stocarea adreselor variabilelor


Declararea pointerilor este similara cu declararea referintelor insa pentru pointeri folosim simbolul * in
loc de &.
Exemplu:
int *p = 0;

In acest exemplu am declarat un pointer care poate stoca adresele variabilelor intregi.
Important
Deoarece in momentul declararii valoarea variabilei este nedeterminata, in cazul pointerilor este esentiala
initializarea variabilei in momentul declararii. De obicei se foloseste adresa 0 sau valoarea NULL1. In caz
contrar, adresa aleatoare pe care o contine acel pointer poate creea defecte greu de identificat.
Stocarea adreselor variabilelor in pointeri se face prin atribuirea adresei unei variabile pointer ca in
exemplul urmator:
int x = 10;
int *p = &x;

Pointerul p contine adresa variabilei x.


0x1000 0x3045
10 0x1000
x p
Important
Pot exista mai multi pointeri care contin adresa aceleiasi variabile:
int x = 5;
int *p1;
int *p2;
p1 = &x;
p2 = p1;

7.2.3 Accesarea valorii unei variabile folosind pointerii


Pointerii ofera un mecanism puternic deoarece cunoscand adresa unei variabile putem modifica valoarea
acelei variabile. Aceasta se realizeaza cu ajutorul operatorului de indirectare2 (*).
Exemplu:
1: int x = 10;
2: int *p = &x;
3: int y = *p;

In acest exemplu in linia 2 se atribuie pointerului p adresa lui x apoi in linia 3 variabila y este initializata

1 Compilatorul C++ are definit pointerul NULL ca avand adresa 0.


2 Se numeste operator de indirectare deoarece accesam valoarea variabilei indirect pornind de la adresa acelei variabile.
5
115
Adonis Butufei

cu valoarea catre care pointeaza pointerul p. Dupa executarea acestei linii y va avea valoarea 10.

Important
Simbolul (*) este folosit in doua moduri in lucrul cu pointerii:
• La declararea unei variabile pointer, caz in care simbolul urmeaza dupa tipul de date a carui adresa o
stocheaza.
Exemplu:
int *p = 0;

• La indirectare pentru accesarea valorii de la adresa de memorie.


Exemplu:
*p = 10;

7.2.4 Calificatorul const pentru pointeri


In practica exista cazuri frecvente cand este necesar sa folosim calificatorul const pentru variabile de tip
pointer. Deoarece prin intermediul pointerilor putem lucra cu adresele de memorie si prin indirectare cu
valorile de la acele adrese, calificatorul const poate fi folosit in oricare din aceste aspecte sau in ambele.

7.2.4.1 Pointeri catre constante


Se folosesc in doua cazuri:
1. Cand locatia de memorie contine o constanta
Exemplu:
const int x = 10;
int *p = &x; // Eroare de compilare.

In acest exemplu daca incercam sa atribuim pointerului p adresa constantei x obtinem eroare de
compilare. Varianta corecta este prezentata mai jos.
const int x = 10;
const int* p = &x;

In acest caz pointerul p este catre o constanta de tip intreg.


2. Cand dorim sa interzicem modificarea valorii de la acea locatie
Exemplu:
int x = 10;
const int *p = &x;

// ...

*p = 20; // Genereaza eroare de compilare!

In acest caz daca incercam sa schimbam valoarea obtinem eroare de compilare.

6
116
Adonis Butufei

7.2.4.2 Pointeri constanti


Exista cazuri practice cand dorim ca pointerii sa contina o adresa fixa, valoarea de la acea adresa se poate
modifica insa pointerul nu se poate schimba.
Exemplu:
int x = 10;
int y = 20;
int * const p = &x;
*p = 30; // corect, schimb valoarea locatiei catre care pointeaza p.
p = &y; // genereaza eroare de compilare.

In acest caz pointerul este constant, locatia nu este constanta. Atribuirea altei adrese, din ultima linie,
genereaza eroare de compilare.

7.2.4.3 Pointeri constanti catre constante


Acesta este cazul in care atat adresa cat si locatia de la acea adresa sunt constante. In acest caz,
calificatorul const apare in ambele locuri.
Exemplu:
int x = 10;
int y = 20;
const int * const p = &x;
*p = 30; // eroare pentru ca locatia este constanta.
p = &y; // eroare pentru ca pointerul este constant.

7.2.5 Pointeri si vectori


In C++ numele unui tablou este un pointer constant care contine adresa primului element al tabloului.
Putem folosi pointerii constanti in locul tablourilor si invers.
Exemplu:
#include <iostream>
using namespace std;

int main()
{
int x [] = {1,2,3};
int * p = x;

cout << "&x[0] " << &x[0] << "\n";


cout << "p " << p << "\n";

return 0;

7
117
Adonis Butufei

La rularea acestui program obtinem aceeasi valoare pentru adresa primului element si pentru pointerul p.

7.2.5.1 Aritmetica pointerilor


Elementele unui tablou ocupa locatii succesive de zone de memorie. Am vazut din exemplul anterior cum
se poate initializa un pointer cu adresa primului element. Incrementand acel pointer putem accesa orice
element al tabloului. De exemplu:
1: #include <iostream>
2: using namespace std;
3:
4: int main()
5: {
6: const int MAX = 3;
7: int v [] = {1,2,3};
8: int * p = v;
9:
10: for (int i = 0; i < MAX; i++)
11: {
12: cout << i << ": " << *(p + i) << "\n";
13: }
14:
15: return 0;
16: }

In acest exemplu la linia 12 expresia p + i deplaseaza pointerul la pozitia i, apoi se extrage valoarea de
la acea adresa si se afiseaza pe ecran.

0x5000 0x5000 v[0] 0x5000 v[0]


p 0x5001 v[1] 0x5001 0x5001 v[1]
0x5002 v[2] p+1 0x5002 v[2]

7.2.6 De ce se folosesc pointerii?


Pointerii se folosesc in mod frecvent pentru:
• Transferul parametrilor prin adresa.
• Alocarea dinamica a memoriei.
• Selectia datelor membru.

8
118
Adonis Butufei

7.2.6.1 Transferul parametrilor prin adresa


Transferul parametrilor prin adresa ofera urmatoarele avantaje:
• Eficienta sporita in cazul variabilelor care ocupa multa memorie deoarece lucreaza direct asupra
acestor locatii.
• Posibilitatea de a returna mai multe rezultate. In cazul in care o functie modifica anumiti parametri se
spune ca acesti parametri sunt de iesire.

Exemplu:
1: #include <iostream>
2: using namespace std;
3:
4: void Swap(int *x, int *y)
5: {
6: int temp = *x;
7: *x = *y;
8: *y = temp;
9: }
10:
11: int main()
12: {
13: int x = 10;
14: int y = 20;
15:
16: cout << "Valori initiale\n";
17: cout << "x " << x << "\n";
18: cout << "y " << y << "\n";
19:
20: Swap(&x , &y);
21:
22: cout << "Valori finale\n";
23: cout << "x " << x << "\n";
24: cout << "y " << y << "\n";
25:
26: return 0;
27: }

In acest exemplu functia Swap schimba valoarea celor doua variabile. Deoarece parametrii sunt transmisi
prin adresa functia lucreaza cu locatiile de memorie declarate in locul apelului.

9
119
Adonis Butufei

7.2.6.2 Alocare dinamica a memoriei: operatorii new si delete


Toate variabilele folosite pana acum erau declarate in momentul alocarii si compilatorul se ocupa de
alocarea si dealocarea lor din memorie.
Aceasta presupune cunoasterea dimensiunii necesare variabilelor in momentul compilarii. Exista cazuri
practice in care nu putem cunoaste spatiul de memorie necesar unei variabile. De exemplu, putem avea un
tablou al carui numar de elemente se detemina in momentul rularii programului.

Pentru gestionarea acestor situatii, compilatorul foloseste un macanism numit stiva. Cand o variabila este
alocata este pusa pe stiva. Cand domeniul ei de viata se termina, variabila este stearsa din stiva si zona de
memorie este eliberata.

Cand se foloseste alocarea dinamica a memoriei programatorul este responsabil cu dealocarea memoriei
atunci cand variabilele respective nu mai sunt necesare.
Alocarea dinamica a variabilelor simple
Exemplu:
int *p = new int;

// folosirea variabilei

delete p; // dealocarea memoriei

In acest exemplu este alocata dinamic o zona de memorie pentru o variabila de tip int si adresa acestei
zone de memorie este atribuita pointerului p. Cand variabila p nu mai este necesara, se dealoca memoria
de la adresa respectiva.

Alocarea dinamica a tablourilor


Exemplu:
int nrElemente = 10;
int *p = new int [nrElemente];

// folosirea variabilei.

delete [] p; // dealocarea memoriei

In acest exemplu am alocat un tablou de tip int cu 10 elemente si la final am eliberat memoria.
Important
Pentru dealocarea variabilelor simple se foloseste delete, iar pentru dealocarea variabilelor tablou se
foloseste delete [].

Memorie irosita (Memory leaks)


Memoria alocata dinamic nu are domeniu de viata: o zona alocata dinamic ramane ocupata pana in
momentul eliberarii explicite sau pana la terminarea executiei programului. Pointerii utilizati pentru
alocarea unei zone de memorie au un domeniu de viata similar cu al altor variabile. Atunci cand domeniul

10
120
Adonis Butufei

de viata al unui pointer s-a terminat insa memoria alocata nu a fost eliberata se creaza un memory leak.
Exemplu:
void Functie()
{
int *p = new int;
}

In acest exemplu la apelul funciei se aloca o zona de memorie pentru o variabila de tip int.
Dupa terminarea executiei acestei funcii domeniul de viata al pointerului care a fost utilizat pentru alocare
se termina si zona de memorie ramane ocupata insa nu mai poate fi accesata, generand un memory leak.

Al doilea caz in care apar irosiri de memorie (memory leaks) este atunci cand pointerului utilizat i se
atribuie o alta adresa.
Exemplu:
1: int x = 10;
2: int *p = new int;
3: p = &x;

Zona de memorie alocata in linia 2 nu mai este accesibila dupa executia liniei 3.

Important
Pentru fiecare instructiune new trebuie sa existe o instructiune delete asociata.
Trebuie tinuta evidenta pointerilor catre zone de memorie alocate dinamic si aceste zone trebuiesc
eliberate dupa ce si-au indeplinit scopul.

7.2.6.3 Selectia datelor membru pentru structuri.


Exemplu:
1: #include <iostream>
2: using namespace std;
3:
4: struct Complex
5: {
6: double re;
7: double im;
8: };
9:
10: int main()
11: {
12: Complex c1 = {2.0, 3.5};
13: Complex * pC1 = &c1;

11
121
Adonis Butufei

14: (*pC1).im = 10;


15:
16: return 0;
17: }

In linia 14 este selectat membrul imaginar. Datorita precedentei operatorilor este necesarea folosirea
parantezelor. Mai intai se face indirectarea pointerului apoi se selecteaza membrul structurii.
In C++ se poate folosi operatorul -> pentru a simplifica selectia membrilor. Linia 14 din exemplul de mai
sus se poate scrie in modul urmator:
pC1->im = 10;

7.2.7 Pointerul NULL


Pointerul NULL corespunde adresei 0. Se foloseste pentru initializarea pointerilor atunci cand nu exista o
alta alternativa disponibila sau dupa ce zona de memorie a fost eliberata.
Exemple:
int *p = NULL;

int *p = new int;


// ...
delete p;
p = NULL;

In acest exemplu, dupa eliberarea zonei de memorie valoarea lui p contine aceeasi adresa insa continutul
memoriei aflate la acea adresa nu mai este valid. Accesul ulterior al acelei adrese are efecte nedeterminate
si determina defecte greu de fixat! Prin atribuirea pointerului NULL aceste probleme sunt eliminate.

Folosind sistematic acest principiu de initializare putem verifica foarte usor daca pointerul contine o
adresa de memorie valida.

7.2.8 Pointerul void


Pointerii void sunt pointeri generici, ei pot sa contina adresa oricarui tip de date.

Exemplu:
int x = 10;
double y = 2.5;

void *p = &x;
p = &y;

In acest exemplu p este initializat cu adresa variabilei x de tip int apoi lui p i se atribuie adresa lui y care
are tipul double.

12
122
Adonis Butufei

Datorita faptului ca tipul nu este cunoscut, nu se poate face indirectarea pointerului pentru accesul valorii.
Pentru aceasta este necesara operatia de cast la tipul dorit.
Exemplu:
int x = 10;
void *p = &x;

int *m = (int *)p;

Folosind pointerii void se ocoleste mecanismul de verificare al tipului datelor folosit de compilator. Din
acest motiv este recomandabila folosirea pointerilor void numai atunci cand este necesar, pentru
mentinerea compatibiliatii.

7.2.9 Pointeri vs. Referinte


Pointerii si refereintele ofera doua mecanisme inrudite pentru lucrul cu adresele variabilelor si este
necesara stabilirea unor criterii pentru a decide cand folosirea uneia dintre alternative este mai potrivita.
Folosirea referintelor este de preferat:
• Pentru variabilele de pe stiva deoarece codul este mai sigur si mai usor de inteles.
• Pentru transmiterea parametrilor la functii.

Pointerii sunt solutia optima:


• Atunci cand memoria se aloca dinamic pentru variabile.
• Pentru programele care necesita accesul direct la memorie (in cazul driverelor hardware sau pentru
optimizarea performantei).

7.3 Sumar
• Referintele sunt un tip special de variabile, care actioneaza ca un nume alternativ pentru variabile.
• Referintele se declara folosind tipul variabilei si operatorul &.
• Referintele sunt foarte utile pentru transmiterea parametrilor catre functii.
• Memoria calculatorului poate fi privita ca o succesiune de celule de memorie numerotate consecutiv.
La declararea unei variabile se aloca o cantitate de memorie necesara stocarii variabilei la o anumita
locatie in memorie - adresa de memorie.
• Variabilele care contin adrese de memorie se numesc pointeri.
• Pointerii ofera posibilitatea de a lucra direct cu memoria.

7.4 Intrebari si exercitii


1. Care este valoarea variabilei x dupa executia urmatoarelor linii?
int x = 10;

13
123
Adonis Butufei

int & ref1 = x;


int & ref2 = ref1;
ref2 = 35;

2. Care este valoarea variabilei x dupa executia urmatoarelor linii?


int x = 10;
int & y = x;
int z = 20;
y = z;

3. Care este diferenta intre o structura de date si o uniune?

4. Sa se scrie o functie care are 2 parametri de tip referinta la structura Complex (definita in acest
capitol) si returneaza o struncura de tip Complex.

5. Urmatoarea linie de cod este incorecta. Care ste greseala?


int x = 20;
int * p = x;

6. Cum se pot seta membrii urmatoarei structuri?


Complex * pX = new Complex;

7. Ce se intampla cu prima locatie de memorie?


int *p = new int;
*p = 200;
p = new int;

8. Ce se intampla la executia urmatorului cod?


int *p;
if(p != NULL)
{
delete p;
}

9. Urmatorul cod este incorect. Unde este greseala?


int * m = new int [3];
m [2] = 4;
delete m;

10. Care este valoarea obtinuta prin indirectarea pointerului urmator?


int z [] = {23, 35, 46};
int *p = z;

14
124
Adonis Butufei

p+= 2;

11. Dupa apelul urmatoarei functii valorile sunt neschimbate. Care este greseala?
void Swap(int x, int y)
{
int temp = x;
x = y;
y = temp;
}

7.5 Bibliografie
• Teach yourself C++ In an Hour A day, Sixth Edition, Sams, Jesse Liberty, Cap 9.
• Practical C++ Programming, Second Edition, O'Reilly, Steve Oualline, Cap15
• Teach yourself C++ In an Hour A day, Sixth Edition, Sams, Jesse Liberty, Cap8

15
125
Adonis Butufei

8. FUNCTII SI SPATII DE NUME

CUPRINS
8.Functii si spatii de nume............................................................................................................................2
8.1 Functii.................................................................................................................................................2
8.1.1 Declararea, definirea si apelul functiilor....................................................................................3
8.1.2 Parametri si argumente...............................................................................................................5
8.1.3 Transferul parametrilor...............................................................................................................5
8.1.3.1 Transfer prin valoare...........................................................................................................5
8.1.3.2 Transfer prin referinta.........................................................................................................5
8.1.3.3 Transfer prin adresa............................................................................................................7
8.1.4 Returnarea rezultatului ..............................................................................................................9
8.1.4.1 Prin valoare.........................................................................................................................9
8.1.4.2 Prin referinta.......................................................................................................................9
8.1.4.3 Prin adresa.........................................................................................................................11
8.1.5 Parametri cu valori implicite....................................................................................................11
8.1.6 Supraincarcarea functiilor.........................................................................................................12
8.1.7 Functii inline.............................................................................................................................12
8.1.8 Functii recursive.......................................................................................................................14
8.1.9 Recomandari pentru scrierea functiilor....................................................................................15
8.2 Spatii de nume..................................................................................................................................15
8.2.1 Accesarea entitatilor dintr-un namespace.................................................................................15
8.2.2 Spatiul de nume global.............................................................................................................16
8.2.3 Declararera spatiilor de nume...................................................................................................17
8.2.4 Spatii de nume ierarhice...........................................................................................................17
8.3 Sumar...............................................................................................................................................18
8.4 Intrebari si exercitii..........................................................................................................................19
8.5 Bibliografie.......................................................................................................................................21

1
126
Adonis Butufei

8. FUNCTII SI SPATII DE NUME


In acest capitol vom discuta despre doua mecanisme eficiente pentru organizarea codului programelor:
functiile si spatiile de nume.
Pentru rezolvarea problemelor practice este necesara descompunerea problemelor complexe in sub-
probleme mai simple si organizarea acestor sub-probleme intr-un mod care este usor de mentinut si
extins.
Similar, ne putem gandi la organizarea personalului unei companii in echipe si departamente: fiecare
echipa este responsabila pentru anumite activitati si fiecare departament grupeaza echipele pentru
atingerea obiectivelor.

Organizarea codului in functii asigura descompunerea problemelor complexe in sub-probleme mai mici
care sunt usor de inteles, implementat si mentinut.

Gruparea functiilor, variabilelor si tipurilor de date definite de utilizator in spatii de nume usureaza
eforturile de scriere a codului. Aceasta este folosita si in activitatile curente cand pentru toti membrii unei
familii folosim numele de familie pentru a-i diferentia de alte familii.

8.1 Functii
Functiile permit gruparea codului intr-un bloc compact care poate fi folosit in mai multe locuri. De
exemplu, daca dorim calcularea pretului final pentru zece produse, putem sa scriem formula de zece ori
sau putem sa scriem o functie care face acelasi lucru si sa o apelam de zece ori.
Ultima varianta nu reduce numai efortul initial de scriere ci si eforturile de mentenanta. Daca peste un
timp dorim sa adaugam discount procentual pentru produse va trebui sa facem o singura modificare in loc
de zece.

Fiecare functie are un nume si cand acest nume este intalnit in timpul rularii programului, se executa
codul acelei functii. Aceasta se numeste apelarea functiilor.

Functiile proiectate corect realizeaza un singur obiectiv, specific, usor de inteles si identificat de numele
functiei.

Exemplu:
int result = Factorial(10);

In acest exemplu am apelat functia factorial pentru a calcula 10! si am initializat variabila result cu
valoarea calcului.
Problemele complicate trebuiesc descompuse in probleme mai simple care sunt implementate cu ajutorul
functiilor. Prin apelul acestor functii se rezolva problemele initiale.

Exista doua categorii de functii: cele care apartin compilatorului1 si cele dezvoltate de utilizator.
Dupa ce executia unei functii se termina, programul continua din locul in care a fost apelata functia. Daca
1 In literatura se foloseste termenul built in functions.
2
127
Adonis Butufei

functia a returnat o valoare acea valoare este folosita in expresia din apel.
Exemplu:
int result = Factorial(3) ;

In acest exemplu dupa ce executia functiei Factorial se termina variabila result se initializeaza cu
rezultatul returnat de functia Factorial si se continua codul care urmeaza apelului.

8.1.1 Declararea, definirea si apelul functiilor


Folosirea functiilor in programe implica trei etape: declararea, definirea si apelul.
Ca si variabilele, functiile trebuiesc declarate inainte de a fi folosite.
Declararea functiei spune compilatorului numele functiei, tipul valorii returnate si lista de parametri.
Declararea unei functii se numeste prototip sau semnatura.
Exemplu:
int Factorial(int n);

In acest exemplu am declarat prototipul functiei Factorial care are un parametru de tip int si tipul valorii
returnate este int.

Pentru functii care nu au parametri prototipul este similar cu cel din exemplul urmator:
int FunctieFaraParametrii();

Prototipul unei functii care nu returneaza rezultate folosesc cuvantul cheie void ca in exemplul urmator.
void FunctieCareNuReturneazaValori();

Definirea unei functii contine codul care implementeaza functia.

Exemplu:
int Factorial (int n)
{
int result = 1;
for(int i = 2; i < = n ; i++)
{
result *= i;
}
return result;
}

In acest exemplu este definita functia de calcul a factorialului.

Sunt trei moduri de a declara o functie:


1. Scrierea prototipului intr-un fiser header si apoi folosirea directivei #include pentru a o folosi in
program.
3
128
Adonis Butufei

Exemplu:
#ifndef FACTORIAL_H
#define FACTORIAL_H
int Factorial(int n);
#endif

In acest exemplu am declarat functia factorial in fisierul factorial.h si este necesara folosirea directivei
#include in fisierele in care vrem sa apelam functia Factorial.

2. Scrierea prototipului in fisierul in care este folosita functia.


Exemplu:
int Factorial(int n);

int main ()
{
int result = Factorial(10);
}

3. Definirea functiei inainte de apel. In acest caz definirea joaca rolul de prototip.
Exemplu:
int Factorial (int n)
{
int result = 1;
for(int i = 2; i < = n ; i++)
{
result *= i;
}
return result;
}

int main ()
{
int result = Factorial(10);
}

In acest exemplu functia Factorial este definita apoi in functia main este apelata.

8.1.2 Parametri si argumente


Parametrii si argumentele sunt termeni care pot fi folositi unul in locul celuilalt. Pentru simplificare, pe
parcursul acestui curs, vom considera cele doua notiuni echivalente.
Transferul datelor catre functii este realizat cu ajutorul parametrilor. Parametrii unei functii sunt variabile
locale functiei. Dupa executia functiei domeniul de viata al parametrilor se termina.

4
129
Adonis Butufei

In momentul apelului functiei valorile de apel se atribuie parametrilor.

8.1.3 Transferul parametrilor


Exista trei metode de transfer al parametrilor: prin valoare, prin referinta si prin adresa. Fiecare parametru
al functiei are modul de transfer propriu. Putem intalni functii in care toti parametrii sunt transferati prin
valoare si alte functii in care o parte dintre parametrii sunt transferati prin adresa sau prin referinta etc.

8.1.3.1 Transfer prin valoare


Acesta este cel mai simplu transfer si este folosit pentru variabilele simple. Apelul functiei Factorial
foloseste acest mod de transfer. Daca functia modifica valorile parametrilor aceste modificari nu se
transmit variabilelor care au fost utilizate pentru apel.

Exemplu:
#include <iostream>
using namespace std;

void Test(int n)
{
n++;
cout << "n modificat in functia Test" << n << "\n";
}

int main()
{
int a = 2;
cout << "valoare inainte apel" << a << "\n";
Test(a);
cout << "valoare dupa apel" << a << "\n";
}

8.1.3.2 Transfer prin referinta


In cazul variabilelor care ocupa o cantitate mare de memorie, transferul parametrilor prin valoare este
ineficient. De asemenea, exista cazuri cand avem nevoie ca modificarile parametrilor sa se propage in
codul apelant. In aceste scenarii se foloseste transferul prin referinta.
Exemplu:
#include <iostream>
using namespace std;

void Test(int& n)
{
n++;
cout << "n modificat in functia Test" << n << "\n";
}

5
130
Adonis Butufei

int main()
{
int a = 2;
cout << "valoare inainte apel" << a << "\n";
Test(a);
cout << "valoare dupa apel" << a << "\n";
}

In acest caz parametrul este un alias al variabilei a. Modificand valoarea lui n modificam si valoarea lui a.

Important
Daca folosim transferul prin referinta pentru optimizarea operatiilor cu memoria dar vrem sa prevenim
modificarea variabilei in interiorul functiei este necesara folosirea referintelor constante in semnatura
functiei.
Exemplu:
void Test (const int& n)
{
n--; // genereaza eroare de compilare
}

Folosirea directivei const ofera urmatoarele avantaje:


• Previne modificarile parametrilor datorate erorilor de implementare.
• Informeaza dezvoltatorul care apeleaza functia ca valoarea parametrului nu va fi schimbata prin apelul
functiei.

Recomandare
Atunci cand se foloseste transferul prin referinta, utilizati directiva const pentru toate cazurile care nu
necesita modificarea valorii parametrului.

Avantajele transferului prin referinta:


• Permite schimbarea valorii parametrilor.
• Deoarece parametrul nu copiaza valoarea, este de preferat atunci cand se folosesc variabile care ocupa
multa memorie.
• Putem folosi referinte constante pentru a preveni modificarea neintentionata a valorilor.
• Putem returna mai multe valori dintr-o functie. Parametrii care sunt modificati pot fi ganditi ca si
valori returnate de functie.

Dezavantajele transferului prin referinta:


• Deoarece nu putem crea referinte catre constante literale, parametrii trebuie sa fie variabile normale.

6
131
Adonis Butufei

• Este greu de identificat daca un parametru transferat prin referinta este folosit pentru intrare2, pentru
rezultat sau pentru amandoua.
• Din apelul unei functii nu putem determina care dintre parametri sunt transferati prin referinta si care
parametri sunt transferati prin valoare fara a cunoaste semnatura functiei.

8.1.3.3 Transfer prin adresa


Acest mod de transfer foloseste adresa parametrilor. Functiile pentru care transferul este prin adresa
folosesc pointeri in semnatura.
Exemplu:
#include <iostream>
using namespace std;

void Test(int* n)
{
(*n)++;
cout << "n modificat in functia Test" << n << "\n";
}

int main()
{
int a = 2;
cout << "valoare inainte apel" << a << "\n";
Test(&a);
cout << "valoare dupa apel" << a << "\n";
}

In acest exemplu, deoarece parametrul este pointer este necesara indirectarea pentru a accesa valoarea
care se afla la acea adresa de memorie.
Transferul prin adresa este folosit frecvent pentru variabile alocate dinamic si pentru tablouri. Urmatorul
exemplu afiseaza toate elementele unui tablou:
#include <iostream>
using namespace std;

void AfiseazaElemente(int* tablou, int lungime)


{
for(int i = 0; i < lungime; i++)
{
cout << tablou[i] << "\n";
}
}

2 Parametrii de intrare sunt sunt folositi pentru calculele interne functiei. Parametrii de iesire sun rezultate returnate de
functie. Un parametru poate avea ambele roluri atunci cand este folosit si pentru calculele interne si la sfarsit i se atribuie o
valoare pentru rezultat.
7
132
Adonis Butufei

int main()
{
const int lungime = 3;
int tbl [lungime] = { 1 , 2 , 3};

AfiseazaElemente(tbl, lungime);
return 0;
}

Si in acest caz se pot folosi pointeri catre constante pentru a preveni modificarile neintentionate ale
parametrilor.

Avantajele transferului prin adresa:


• Permite schimbarea valorii parametrilor.
• Deoarece parametrul nu copiaza valoarea, este de preferat atunci cand se folosesc variabile care ocupa
multa memorie.
• Putem folosi pointeri catre constante pentru a preveni modificarea neintentionata a valorilor.
• Putem returna mai multe valori dintr-o functie.
• Deoarece pentru apelul functiei se folosete explicit operatorul adresa (&), putem identifica direct
parametrii transferati prin valoare de cei transferati prin adresa.

Dezavantajele transferului prin adresa:


• Deoarece nu putem crea pointeri catre constante literale, parametrii trebuie sa fie variabile normale.

8.1.4 Returnarea rezultatului


Interactiunea unei functii poate fi gandita ca un schimb de date dinspre apelant catre functie si dinspre
functie catre apelant. Parametrii in general sunt utilizati pentru transferul de date dinspre apelant catre
functie3. Majoritatea functiilor, dupa ce termina prelucrarea datelor, returneaza o valoare apelantului.

Ca si la transferul parametrilor, returnarea rezultatului se realizeaza in trei moduri: prin valoare, prin
referinta si prin adresa.

8.1.4.1 Prin valoare


Returnarea prin valoare este cea mai simpla si am folosit-o in toate exemplele de pana acum.
Exemplu:
int Patrat(int n)
{
return n * n;

3 Parametrii de iesire care sunt modificati de functie se considera rezultate returnate.


8
133
Adonis Butufei

Atunci cand vrem sa returnam un rezultat calculat cu variabile locale functiei, returnarea prin valoare este
indicata.

Pentru variabilele care ocupa multa memorie, precum tablourile si tipurile de date definite de utilizator,
acest mod de returnare a rezultatului este lent.

8.1.4.2 Prin referinta


Aceasta modalitate returneaza o referinta apelantului care poate modifica variabila. Returnarea prin
referinta este rapida pentru tablouri si tipuri de date definite de utilizator.

Important
Trebuie evitata returnarea referintelor la variabilele locale deoarece domeniul de viata se termina odata cu
executia functiei.
Exemplu:
int & Patrat(int n)
{
int result = N * n;
return result;
}

In acest exemplu domeniul de viata al variabilei result se termina dupa terminarea executiei functiei.
Apelantul va obtine o referinta la o variabila inexistenta.

Returnarea prin referinta este utilizata, de obicei, pentru returnarea parametrilor transmisi prin referinta
inapoi catre apelant.

Exemplu:
1: #include <iostream>
2: using namespace std;
3:
4: struct Complex
5: {
6: double re;
7: double im;
8: };
9:
10:
11: double& Im(Complex& c)

9
134
Adonis Butufei

12: {
13: return c.im;
14: }
15:
16: int main()
17: {
18: Complex c1 = { 2.0, 3.5};
19:
20: Im(c1) = 4.11;
21:
22: cout << c1.im << "\n";
23:
24: return 0;
25: }

In acest exemplu functia Im returneaza o referinta la membrul imaginar al numarului complex. Din acest
motiv in linia 20 se poate schimba valoare membrului imaginar pentru variabila c1.

8.1.4.3 Prin adresa


Folsind acest tip de returnare apelantul obtine adresa unei variabile. Ca si returnarea prin referinta, acest
tip este rapid deoarece se transmite numai adresa care este un numar.

Important
Ca si in cazul precedent trebuie sa evitam returnarea unei adrese la o variabila locala functiei deoarece
domeniul de viata al aceasteia se termina odata cu executia functiei.

Acest tip de returnare este folosit pentru returnarea unui pointer la zona de memorie nou alocata catre
apelant.
Exemplu:
int * AlocaTablou(int lungime)
{
return new int [lungime];
}

int main()
{
int * tablou = AlocaTablou(10);
// lucreaza cu tabloul
delete [] tablou;
return 0;
}

10
135
Adonis Butufei

In acest exemplu se aloca un tablou de o dimensiune specificata cu ajutorul functiei AlocaTablou.


Dupa ce se termina lucrul cu tabloul memoria folosita se elibereaza.

8.1.5 Parametri cu valori implicite


In practica exista cazuri in care valoarea unui parametru are frecvent o anumita valoare. Pentru cresterea
productivitatii, in C++ este posibila specificarea valorii implicite a parametrului in semnatura functiei.
In cazul in care utilizatorul doreste sa foloseasca valoarea implicita nu mai scrie aceasta valoare in apel.

Exemplu:
#include <iostream>
using namespace std;

double ArieDreptunghi(double lungime, double latime = 1.0);

int main()
{

cout << "Prima arie: " << ArieDreptunghi(10) << "\n";


cout << "A doua arie: " << ArieDreptunghi(10,2) << "\n";
return 0;
}

double ArieDreptunghi(double lungime, double latime)


{
return lungime * latime;
}

In acest exemplu primul apel foloseste valoarea implicita pentru latime. In al doilea apel sunt specificate
valorile pentru ambii parametrii.

Important
Toti parametrii care urmeaza dupa un parametru cu valori implicite trebuie sa aiba valori implicite.

8.1.6 Supraincarcarea4 functiilor


Supraincarcarea functiilor este un mecanism oferit de C++ care permite crearea mai multor functii cu
acelasi nume, cu conditia ca setul de parametri sa fie unic pentru fiecare functie.

4 In limba engleza se foloseste denumirea de function overloading


11
136
Adonis Butufei

Este util atunci cand avem de efectuat acelasi tip de operatie cu tipuri de date diferite.

Exemplu:
int Add(int x, int y);
double Add(double x, double y);

Tipul valorii returnate poate sa fie acelasi sau sa difere.

Permite simplificarea conceptelor si intelegerea mai usoara a codului.

8.1.7 Functii inline


Atunci cand se apeleaza o functie, compilatorul opreste secventa curenta de executie si continua executia
cu codul acelei functii. Timpul de apel al unei functii este mai mare decat executia unei secvente locale de
instructiuni. Pentru unele functii care sunt foarte mici, o linie sau doua de cod, se poate imbunatati viteza
de executie daca am putea inlatura apelul lor.

Daca o functie este declarata folosind cuvantul cheie inline, compilatorul nu creaza o functie ci copiaza
codul acelei functii direct in locul fiecarui apel. In acest caz performanta este imbunatatita insa creste
dimensiunea programului.

Este indicat sa se foloseasca functiile inline doar in cazul in care contin una sau doua linii de cod si sunt
apelate dintr-un numar redus de locuri. In caz contrar, datorita cresterii programului este posibil ca
performantele globale ale programului sa fie mai slabe cand se folosesc functiile inline.

Exemplu:
#include <iostream>
using namespace std;

inline double ArieDreptunghi(double lungime, double latime);

int main()
{
cout << "Aria dreptunghiului: " << ArieDreptunghi(10,2) << "\n";
return 0;
}

double ArieDreptunghi(double lungime, double latime)


{
return lungime * latime;
}

Observatii

12
137
Adonis Butufei

Optimizarea performantei programelor este o provocare dificila si multi programatori nu pot identifica
sectiunea de cod care necesita optimizari. Modalitatea adecvata pentru optimizare este studierea
comportamentului programului folosind programe speciale5 pentru generarea statisticilor precum timpul
consumat de o anumita functie, numarul de apeluri etc. Aceste statistici ajuta programatorul sa-si
concentreze atentia acolo unde este adevarata problema.
Din acest motiv este mult mai utila scrierea unui cod clar si usor de inteles decat un cod care sa contina
presupunerile referitoare la ce ar putea sa mearga incet sau rapid, dar sa fie mai greu de inteles.
Optimizarea unui cod bine organizat este mult mai usoara decat mentinerea unui cod cu optimizari bazate
doar pe presupuneri.

Folosirea directivei inline este o sugestie pentru compilator ca vrem ca functia sa fie inline. In functie de
implementare si de setari compilatorul poate ignora aceasta directiva.

8.1.8 Functii recursive


O functie recursiva este o functie care se apeleaza pe ea insasi.
Exemplu:
unsigned int Factorial(unsigned short n)
{
if (n < 2)
{
return 1;
}

else
{
return (n * Factorial(n-1));
}
}

In acest exemplu se calculeaza factorialul unui numar folosind apeluri recursive.


Daca n == 1 sau n == 0, rezultatul este 1. Daca n > = 2, se intra pe ramura recursiva si la fiecare apel
parametrul este decrementat cu 1 si se continua pana se ajunge la n == 1, cand se returneaza valoarea 1.
Dupa terminarea apelului pentru n == 0, se termina pentru n == 1 etc pana la valoarea initiala a variabilei
n.

Este important de realizat ca atunci cand o functie se apeleaza pe ea insasi, o noua copie a functiei
ruleaza. Variabilele locale din a doua functie sunt independente de variabilele locale din prima functie si
nu se pot influenta reciproc.

Functiile recursive necesita o conditie de stop. Ceva trebuie sa se intample pentru a opri apelul recursiv.

5 Profilers.
13
138
Adonis Butufei

In exemplul anterior aceasta conditie de stop este in blocul in care se testeaza daca n < 2.

Important
Functiile recursive permit o implementare mai simpla pentru o categorie de probleme insa este necesar ca
numarul de apeluri recursive trebuie sa fie redus pentru a nu consuma excesiv memoria si a degrada
performanta. In cazul in care sunt necesari un numar mari de pasi pentru apelul recursiv este recomandata
implementarea echivalenta folosind bucle in loc de apel recursiv.

8.1.9 Recomandari pentru scrierea functiilor


• Functiile trebuie sa aiba un singur scop.
• Numele functiei trebuie sa reflecte care este scopul functiei.
• Implementarea functiei trebuie sa nu depaseasca 3 – 4 ecrane pentru a putea fi usor inteleasa.
• Este recomandabil ca numele functiilor sa inceapa cu litera mare iar parametrii si variabiele sa inceapa
cu litera mica pentru a le putea diferentia6.
• Atunci cand o secventa de cod se repeta in mai multe locuri atunci acea secventa poate fi grupata intr-o
functie si apelata in acele locuri.

8.2 Spatii de nume


Dimensiunea programelor a crescut in timp. Datorita acestui fapt devine din ce in ce mai dificil de gasit
nume unice pentru variabile globale, pentru functii si pentru tipuri de date definite de utilizator. O solutie
pe termen scurt este folosirea unor prefixe pentru numele acestor entitati7. Aceasta strategie este
consumatoare de timp.

In C++ se poate rezolva aceasta problema impartind programul in spatii de nume. In activitatile curente
folosim spatiile in activitatile curente. In discutiile zilnice folosim prenumele pentru a discuta cu
persoanele apropiate. Daca in acelasi loc sunt doua persoane cu acelasi prenume atunci folosim si numele
de familie pentru a diferentia, de exemplu, pe Stefan Vasilescu de Stefan Georgescu.

Toate entitatile declarate intr-un spatiu de nume se considera a fi membrii aceleiasi familii sau spatiu de
nume.

Exemplu:
namespace CalculFinanciar
{
double profitTrimestrial;
double taxeLunare;
};

6 Se spune ca pentru functii se foloseste PascalCase adica fiecare cuvant din numele functiei incepe cu litera mare, exemplu
CalculeazaSalarii(), iar pentru parametri se foloseste camel case, adica primul cuvant incepe cu litera mica si celelalte cu
litera mare, exemplu CaluleazaSalarii(int lunaCurenta);
7 Prin entitate se poate intelege o functie, variabila sau tip de date definit de utilizator.
14
139
Adonis Butufei

In acest exemplu am declarat doua variabile profitTrimestrial si taxeLunare in spatiul de nume


CalculFinanciar.

8.2.1 Accesarea entitatilor dintr-un namespace


Se poate face in doua moduri: ca in exemplul primului program folosind directiva using
nume_namespace; sau folosind numele complet adica nume_namespace::nume_entitate.

Exemplu: folosind directiva using.Implicit C++ foloseste spatiul de nume global. Acesta se mai numeste
si spatiul de nume anonim. Toate entitatile care nu au specificat un spatiu de nume sunt puse automat aici.
#include <iostream>
using namespace std;

int main()
{
cout << "Hello World!\n";
return 0;
}

Exemplu: folosind numele complet.


#include <iostream>

int main()
{
std::cout << "Hello World!\n";
return 0;
}

In exemplele de mai sus am folosit spatiul de nume standard (std) definit de C++.
Functionalitatile limbajului C++ sunt organizate in mai multe spatii de nume si ele vor fi introduse pe
masura ce vom folosi entitati din acele spatii de nume.

Remarca
Scrierea codului este mai eficienta folosind directiva using deoarece este scrisa o singura data si permite
folosirea directa a numelui entitatilor in contextul in care a fost declarata. Numele complet se foloseste
doar atunci cand avem suprapunerea numelor pentru entitati din spatii de nume diferite.

Important
Daca omitem directiva using si folosim doar numele entitatii vom obtine o eroare de compilare.
15
140
Adonis Butufei

8.2.2 Spatiul de nume global


Implicit C++ foloseste spatiul de nume global. Acesta se mai numeste si spatiul de nume anonim. Toate
entitatile care nu au specificat un spatiu de nume sunt puse automat aici.

8.2.3 Declararera spatiilor de nume


Atunci cand dezvoltam programe putem defini propriile spatii de nume. Aceasta se se face in modul
urmator:
namespace HumanResources
{
// entitati pentru Human resource
// ...
}

namespace Development
{
// entitati pentru Development.
int x, y;
}

8.2.4 Spatii de nume ierarhice


In programele mai mari putem folosi o structura ierarhica de spatii de nume ca in exemplul urmator:
namespace Companie
{
// ...

namespace Contabilitate
{
// ...
}

namespace Management
{
// ...
}

// ...
}

Pentru a folosi entitatile din spatiile de nume Contabilitate respectiv Management vom folosi

16
141
Adonis Butufei

directiva using in modul urmator::


using namespace Companie::Contabilitate;
using namespace Companie::Management;

8.3 Sumar
• Functiile permit gruparea codului intr-un bloc compact care poate fi folosit in mai multe locuri.
• O functie este identificata printr-un nume, tipul rezultatului returnat, lista de parametri.
• Functiile sunt declarate, implementate si apelate.
• Interactiunea unei functii poate fi gandita ca un schimb de date dinspre apelant catre functie si dinspre
functie catre apelant.
• Parametrii, in general, sunt utilizati pentru transferul de date dinspre apelant catre functie.
• Parametrii se pot transfera prin valoare, referinta si adresa.
• Parametrii de iesire, care sunt modificati de functie, se considera rezultate returnate.
• Majoritatea functiilor, dupa ce termina prelucrarea datelor, returneaza o valoare apelantului. Pentru
functiile care nu returneaza un rezultat se foloseste termenul void scris inainte de numele functiei.
• Returnarea rezultatului se poate face prin valoare, referinta, adresa.
• Pentru imbunatatirea vitezei de executie a unui program se pot folosi functii inline.
• Cand avem de implementat acelasi tip de prelucrare pentru tipuri de date diferite se poate folosi mecanismul de
supraincarcare a functiilor (function overloading).
• In cazul programelor complexe apare problema gasirii unor nume unice si semnificative in acelasi timp
pentru variabile, functii, tipuri de date. O rezolvare in C++ a acestei probleme este impartirea
programului in spatii de nume.
• Declararea spatiului de nume se face astfel:
namespace nume_namespace
{
entitati
}
• Accesarea entitatilor dintr-un spatiu de nume se poate face in doua moduri: folosind directiva using
nume_namespace; sau folosind numele complet adica nume_namespace::nume_entitate.

• Implicit C++ foloseste spatiul de nume global. Acesta se mai numeste si spatiul de nume anonim. Toate entitatile care
nu au specificat un spatiu de nume sunt puse automat aici.
• Pentru programele complexe se pot folosi structuri ierarhice de spatii de nume.

17
142
Adonis Butufei

8.4 Intrebari si exercitii


1. Continuati exemplul de mai jos implementand functia de scadere a doua numere complexe.

#include <iostream>
using namespace std;

struct Complex
{
double re;
double im;
};

void Add(const Complex& c1, const Complex& c2, Complex& result);

void AfiseazaComplex(const Complex& c);

int main()
{
Complex a = {2.0, 3.0};
Complex b = {4.0, 1.0};

Complex result;

Add(a,b, result);

AfiseazaComplex(result);

return 0;
}

void Add(const Complex& c1, const Complex& c2, Complex& result)


{
result.re = c1.re + c2.re;
result.im = c1.im + c2.im ;
}

void AfiseazaComplex(const Complex& c)


{
cout << c.re;

if(c.im > 0)
{

18
143
Adonis Butufei

cout << " + " << c.im;


}
else
{
cout << " - " << c.im;
}

cout << "i\n";


}

2. Scrieti o functie care returneaza conjugatul8 unui numar complex.


3. Modificati exemplul precedent astfel incat rezultatul sa fie returnat prin valoare.
4. Modificati exemplul precedent astfel incat parametrii sa fie transmisi prin adresa.
5. Implementati o functie recursiva care calculeaza valoarea functiei Fibonacci:
Fibonacci (n)= Fibonacci( n−1)+ Fibonacci (n)
Fibonacci (0)=0
Fibonacci (1)=1
6. Implementati o functie care calculeaza valoarea functiei Fibonacci folosind o bucla.
7. Care este numele complet al variabilei total din urmatorul exemplu?
namespace Sales
{
int total;
}

8. Care este numele complet al variabilei x din urmatorul exemplu?


namespace Graphic
{
namespace Drawing
{
int x;
}
}

9. Scrieti directiva using pentru exemplul de la problema anterioara.


10. Urmatorul cod genereaza erori de compilare. Care este greseala?
#include <iostream>

int main()
{
cout << "Hello World\n";
}

8 Conjugatul numarlui complex a + ib este a – ib.


19
144
Adonis Butufei

8.5 Bibliografie
• Teach yourself C++ In an Hour A day, Sixth Edition, Sams, Jesse Liberty, Cap 6.
• Practical C++ Programming, Second Edition, O'Reilly, Steve Oualline, Cap 9

20
145
Adonis Butufei

9. CLASE SI OBIECTE

CUPRINS
9.Clase si obiecte...........................................................................................................................................2
9.1 Clase, membri si obiecte....................................................................................................................2
9.1.1 Declararea claselor.....................................................................................................................2
9.1.2 Definirea obiectelor....................................................................................................................2
9.1.3 Care este diferenta dintre clase si obiecte?.................................................................................3
9.2 Incapsulare si metode de access.........................................................................................................3
9.2.1 Tipuri de access..........................................................................................................................3
9.2.2 Metode de acces la atributele claselor........................................................................................4
9.3 Constructorii.......................................................................................................................................5
9.3.1 Constructorul de copiere.............................................................................................................7
9.4 Destructorii.........................................................................................................................................8
9.5 Metode constante................................................................................................................................8
9.6 Atribute constante...............................................................................................................................9
9.7 Atribute statice..................................................................................................................................10
9.8 Metode statice..................................................................................................................................10
9.9 Metodele inline.................................................................................................................................11
9.10 Organizarea codului........................................................................................................................11
9.11 Structuri de date si clase................................................................................................................12
9.12 Sumar.............................................................................................................................................13
9.13 Intrebari si exercitii........................................................................................................................13
9.14 Bibliografie.....................................................................................................................................15

1
146
Adonis Butufei

9. CLASE SI OBIECTE
In acest capitol vom discuta despre clase si obiecte, diferenta dintre o clasa si un obiect.
Vom invata primul mecanism al programarii obiectuale: incapsularea.
Vom organiza codul clasei in fisiere header si fisiere sursa.
Vom invata cum sa lucram cu obiectele.

9.1 Clase, membri si obiecte


Practic se pot scrie programe complexe folosind doar variabile numerice si siruri de caractere, insa efortul
de scriere, intelegere si mentenanta este foarte mare.
Inainte de aparitia programarii orientate pe obiecte functiile si datele erau separate. Se foloseau structuri
de date pentru crearea unor tipuri definite de utilizator, insa functiile care lucrau cu aceste structuri erau
separate.
In activitatile curente obiectele cu care interactionam au proprietati si multe dintre obiecte au si
comportament.
In programarea obiectuala nu ne gandim la structuri de date si la functiile care lucreaza cu aceste
structuri, ci ne gandim la modelarea obiectelor, ca si cum ar fi reprezentarea obiectelor din lumea reala cu
datele si comportamentul lor. De exemplu, cand ne gandim la un automobil, nu ne gandim la niste
specificatii si cum se poate lucra cu aceste specificatii. Ne gandim la un obiect care arata si actioneaza
intr-un mod precis.

Clasele permit combinarea datelor cu functiile care lucreaza cu aceste date. Elementele claselor, datele si
functiile se numesc membri. Pentru datele membru se foloseste termenul atribute si pentru functiile
membru se foloseste termenul metode.

9.1.1 Declararea claselor


Declararea claselor este similara cu declararea structurilor.
Exemplu:
class Complex
{
// metode si atribute ...
};

In acest exemplu este declarata o clasa de tip complex.


Important
Declararea unei clase se termina intotdeauna cu caracterul (;). Omiterea lui genereaza erori de compilare.

9.1.2 Definirea obiectelor


Dupa declararea unei clase o putem folosi pentru a declara variabile care au tipul acestei clase.
Exemplu:
Complex c1;

In acest exemplu c1 este un obiect de tip complex.


2
147
Adonis Butufei

9.1.3 Care este diferenta dintre clase si obiecte?


Putem gandi o clasa ca fiind planul unei case iar obiectul ca fiind o casa reala cu pereti, usi etc, creat dupa
planul respectiv. Clasa este un tip de date pe cand obiectul este o variabila de acel tip care are un domeniu
de viata. Obiectele se numesc si instante ale claselor.

9.2 Incapsulare si metode de access


Mecanismul de incapsulare permite gruparea datelor si metodelor. Acest mecanism ascunde detaliile de
implementare ale unui obiect oferind un set de functii prin care pot interactiona cu acel obiect.

Pentru accesul membrilor se foloseste operatorul punct (.) ca si la structuri.


Exemplu:
Complex c1;
c1.re = 2.5;

9.2.1 Tipuri de access


Metodele si atributele claselor au trei tipuri de acces: public, protected si private. Pentru
specificarea tipului de acces se folosesc directive cu acelasi nume (public, protected si private).
Atunci cand nu este specificata nicio directiva, se foloseste implicit tipul de acces private.

Exemplu:
class TipuriDeAccess
{
// membri privati

public:
// membri publici

protected:
// membri protected

private:
// membri privati
};

Aceasta clasa prezinta toate tipurile de acces.

Important
• Directivele de acces definesc zone in declararea clasei. O zona incepe pe linia in care se afla directiva
si se termina pe linia in care apare o noua directiva sau pe linia unde se termina declararea clasei.

3
148
Adonis Butufei

• Toti membrii declarati intr-o zona au tipul de acces al zonei respective.


• Putem avea mai multe zone cu acelasi tip de acces in declararea unei clase.

Membrii cu tipul de acces public pot fi apelati din afara clasei. Cei cu tipul de acces private pot fi apelati
doar din metodele clasei, iar tipul protected doar din metodele clasei sau claselor derivate1.

Exemplu:
class Complex
{
double _re;
double _im;
};

In acest exemplu este prezentata clasa Complex care are doua atribute private.
Nota
Pentru a diferentia atributele clasei de variabilele locale sau parametri am folosit prefixul (_).
O incercare de acces a acestor atribute in afara clasei genereaza eroare de compilare.
Exemplu:
Complex c1;
c1._re = 1.5; // genereaza eroare

9.2.2 Metode de acces la atributele claselor


Este recomandabil ca atributele sa fie private sau protected pentru a preveni accesul neautorizat.
Pentru a accesa din exterior atributele se pot folosi metode de acces.
Exemplu:
1: class Complex 12: };
2: {
3: double _re;
4: double _im;
5:
6: public:
7: double Re();
8: void Re(double val);
9:
10: double Im();
11: void Im(double val);

1 Clasele derivate si mostenirea vor fi tratate intr-un capitol viitor.


4
149
Adonis Butufei

13: double Complex::Re() 26: void Complex::Im(double val)


14: { 27: {
15: return _re; 28: _im = val;
16: } 29: }
17: 30:
18: double Complex::Im() 31: int main()
19: { 32: {
20: return _im; 33: Complex c1;
21: } 34: c1.Re(2.0);
22: void Complex::Re(double val) 35: c1.Im(3.5);
23: { 36:
24: _re = val; 37: return 0;
25: } 38: }

Codul acestui exemplu a fost prezentat pe doua coloane pentru economie de spatiu.
Clasa complex are o pereche de metode pentru fiecare atribut: cate o metoda care returneaza valoarea
curenta2 si o metoda care modifica valoarea curenta a atributului3.
Dupa declararea clasei urmeaza definirea metodelor. Observam si aici operatorul rezolutie (::) pe care
l-am intalnit si in capitolul 8 la spatii de nume pe care il folosim in acelasi mod pentru a specifica
apartenenta metodelor la clasa.

9.3 Constructorii
Variabilele simple pot fi initializate folosind operatorul =. Initializarea asigura faptul ca variabila va avea
intotdeauna o valoare determinata. Pentru initializarea atributelor unei clase se folosesc metodele
constructor care au acelasi nume ca si clasa. Constructorii nu au nici un tip de returnat. In momentul
declararii unui obiect, compilatorul apeleaza constructorul clasei care initializeaza atributele.

Putem adauga la clasa Complex4 prezentata anterior un constructor fara parametri care initializeaza
ambele atribute cu 0. Constructorul fara parametri al unei clase se numeste constructor implicit.
class Complex Complex::Complex()
{ {
public: _re = 0.0;
Complex(); _im = 0.0;
}; }

In momentul declararii unei variabile de tip complex este apelat acest constructor.
Exemplu:

2 Aceasta metoda se numeste getter.


3 Aceasta metoda se numeste setter.
4 Pentru simplitate in exemplele din acest capitol vom prezenta numai modificarile aduse clasei complex.
5
150
Adonis Butufei

#include <iostream>
using namespace std;

int main()
{
Complex c; // este apelat constructorul
cout << c.Im() << " + " << c.Re() << "i\n";
return 0;
}

La rularea acestui program pe ecan va apare mesajul 0 + 0i.


Exista cazuri frecvente cand dorim sa intializam obiectele unei clase in moduri diferite. De exemplu
pentru clasa complex ar fi util daca am putea initializa ambele atribute in momentul definirii variabilei.
Pentru aceasta vom folosi supraincarcarea constructorului cu parametrii necesari.
Exemplu:
class Complex Complex::Complex(double re,
double im)
{
{
public:
_re = re;
Complex(double re, double im);
_im = im;
};
}

Deoarece am folosit prefixul (_) diferentierea parametrilor de atributele clasei este evidenta.
Folosind acest constructor putem initializa un obiect de tip complex cu valorile dorite intr-o singura linie
de cod.
Exemplu:
1: int main()
2: {
3: Complex c(2.5, 3.4);
4: return 0;
5: }

In acest exemplu in linia 3 este apelat al doilea constructor care initializeaza atributele _re si _im cu
valorile 2.5 respectiv 3.4.

Important
In standardul C++ curent un constructor nu poate apela alt constructor al aceleiasi clase. Urmatorul cod
genereaza erori de compilare.
Complex::Complex() : Complex(0.0, 0.0)
{
}

Atunci cand avem o sectiune de cod duplicata in constructorii clasei se poate muta acea sectiune intr-o
functie privata care poate fi apelata din fiecare constructor.
Exemplu:
6
151
Adonis Butufei

In cazul clasei Complex codul pentru initializarea atributelor este similar in ambii constructori.
Declaram metoda Init in sectiunea privata cu urmatorul prototip:
void Init(double re = 0.0, double im = 0.0);

Folosim 0.0 valoare implicita pentru ambii parametri. Implementarea este foarte simpla, practic se
copiaza codul din al doilea constructor. Deoarece am specificat valorile implicite in prototipul functiei nu
mai este necesar sa le scriem si la implementare.
void Complex::Init(double re, double im)
{
_re = re;
_im = im;
}

Actualizam constructorii:
Complex::Complex()
{
Init();
}
Complex::Complex(double re, double im)
{
Init(re,im);
}

Folosind aceasta strategie am eliminat duplicarea codului.

9.3.1 Constructorul de copiere


Constructorul de copiere permite crearea unor instante cu valori identice pentru atribute. El are ca
parametru o referinta constanta de tipul clasei.
In exemplul de mai jos este prezentat prototipul constructorului de copiere pentru clasa Complex.
Complex(const Complex& src);

Mai jos este prezentata implementarea constructorului de copiere. Initializam noua clasa cu valorile
atributelor _re si _im din clasa sursa.
Complex::Complex(const Complex& src)
{
Init(src._re, src._im);
}

Constructorul de copiere este apelat explicit atunci cand folosim un obiect, sau implicit atunci cand
apelam o functie care are in lista de parametri un obiect de acel tip sau cand folosim pentru a crea o noua
instanta ca in exemplul de mai jos:
Complex c1(2.5, 3.7);
Complex c2(c1); // apeleaza constructorul de copiere.
Complex c3 = c1; // apel implicit al constructorului de copiere.
Complex c4 = Add(c1, c2); // Unde Add are prototipul:

7
152
Adonis Butufei

// Complex Add(Complex c1, Complex c2);

Pentru evitarea copierii obiectelor, functiile si metodele folosesc transferul prin referinta sau prin adresa
pentru parametrii care sunt obiecte.

9.4 Destructorii
Destructorii sunt apelati de compilator in momentul cand un obiect a ajuns la sfarsitul domeniului de
viata, in scopul eliberarii resurselor folosite de clasa (eliberarea memoriei, inchiderea unui fisier etc). O
clasa poate avea mai multi constructori insa un singur destructor.
Caracteristicile destructorului:
• Numele destructorului foloseste prefixul (~) urmat de numele clasei.
• Destructorul nu are parametri.
• Destructorul nu are tip de returnare.

Exemplu:
class Complex Complex::~Complex()
{ {
}
public:
~Complex();
};

9.5 Metode constante


In capitolele anterioare am folosit const pentru a declara variabile a caror valoare nu se poate schimba. O
metoda constanta este o metoda care nu modifica atributele clasei si nu apeleaza alte metode care nu sunt
constante.
Pentru a declara o metoda constanta folosim cuvantul cheie const dupa inchiderea parantezelor
prototipului.
Exemplu:
class Complex
{
public:
double Re() const;
double Im() const;
};

Implementarea acestor functii este:

double Complex::Re()const double Complex::Im()const


{ {
return _re; return _im;
} }
8
153
Adonis Butufei

Modificarile valorilor atributelor in metodele declarate constante genereaza erori de compilare.

9.6 Atribute constante


Sa presupunem ca avem urmatoarea clasa care are un atribut constant.
class Buffer
{
const int _size;
public:
Buffer ();
};

Initializarea acestui atribut nu se poate face in declararea clasei ca in exemplul de mai jos.
class Buffer
{
const int _SIZE = 1024; // Genereaza eroare de compilare.
//....
};

De asemenea setarea valorii in constructor genereaza eroare de compilare.


Buffer::Buffer()
{
_SIZE = 1024; // Genereaza eroare de compilare.
}

Pentru a putea initializa acest atribut trebuie sa folosim lista de initializare a constructorului. Aceasta lista
de initializare se executa inainte de executia constructorului. Aici compilatorul aloca memoria pentru
atributele clasei si acesta este momentul cand putem seta valorile atributelor constante. Sintaxa este
prezentata in exemplul urmator:
Buffer::Buffer () : _SIZE(1024)
{
}

Se folosesc doua puncte dupa paranteza ) constructorului apoi urmeaza atributele pe care dorim sa le
initializam, separate prin virgula. Lista de initializare se poate folosi si in cazul atributelor normale si
poate aduce o imbunatatire a performantei daca atributele se initializeaza in ordinea in care au fost
declarate.

In capitolul doi am intalnit tipurile de constante enumerate. Aceste constante sunt folosite frecvent in
cazul in care tipul constantei este intreg deoarece permite o initializare mai simpla. Putem modifica
exemplul anterior in modul urmator pentru a folosi constantele enumerate:
class Buffer
{
enum { _SIZE = 1000};
public:
//..

9
154
Adonis Butufei

};

9.7 Atribute statice


Cum am putea numara toate instantele de tip complex prezente in memorie la un moment dat? O solutie
ar fi sa folosim o variabila globala pe post de contor. In constructor este incrementata si in destructor este
decrementata.
Aceasta modalitate nu functioneaza. Incapsularea nu mai este respectata deoarece folosim o variabila
globala care contine informatii despre starea instantelor clasei.

In C++ aceasta problema se poate rezolva foarte simplu folosind un atribut static in loc de o variabila
globala.

Exemplu:
Declaram un atribut static privat:
class Complex
{
private:
static int _nrInstante;
};

Definim atributul:
int Complex::_nrInstante = 0;

Apoi modificam constructorii si destructoul sa incrementeze si sa decrementeze aceasta variabila.


Atributele obisnuite apartin obiectelor insa atributele statice apartin clasei. Deoarece avem o singura
definitie a clasei exista o singura variabila care este actualizata de toate instantele.

9.8 Metode statice


Am vazut anterior ca atributele statice apartin clasei, nu instantelor acelei clase. Ar trebui sa existe un
mecanism care sa ne permita interactiunea cu aceste variabile fara a crea instante noi. In C++ pentru acest
scop se folosesc metodele statice.
Exemplu:
Declaram o metoda statica in sectiunea publica:
class Complex
{
public:
static int NrInstante();
};

Definim metoda:
int Complex::NrInstante()
10
155
Adonis Butufei

{
return _nrInstante;
}

Deoarece aceasta metoda apartine clasei, nu instantelor, ea poate fi apelata folosind numele clasei:
cout << Complex::NrInstante() << "numere complexe sunt in memorie\n";

9.9 Metodele inline


Pentru a declara o metoda inline avem doua variante: folosim cuvantul cheie inline
inline double Complex::Re()
{
return _re;
}

sau scriem implementarea in fisierul header


class Complex
{
public:
double Re () { return _re; }
};

9.10 Organizarea codului


Ca si la functii codul claselor se poate organiza in fisierele sursa si fisierele header. In mod uzual fiecare
clasa are un fisier header si un fisier sursa. Ambele fisiere au numele clasei si extensiile corespunzatoare.
Exemplu:
Clasa Complex este declarata in fisierul Complex.h care are urmatorul continut:
#ifndef _COMPLEX_H_
#define _COMPLEX_H_
class Complex
{
double _re;
double _im;
public:
Complex(double re = 0.0, double im = 0.0);
Complex(const Complex& src);

double Re() const;


void Re(double val);

double Im() const;


void Im(double val);

11
156
Adonis Butufei

};
#endif

si fisierul Complex.cpp care contine implementarea metodelor si are urmatoru continut:


#include <iostream>
#include "Complex.h"
using namespace std;

Complex::Complex(double re , double im ) :
_re(re), _im(im)
{
cout << "Complex::Complex(re,im)\n";
}

Complex::Complex(const Complex& src)


{
_re = src._re;
_im = src._im;
cout << "Complex::Complex(const Complex& src)\n";
}

double Complex::Re()const
{
return _re;
}

double Complex::Im()const
{
return _im;
}

void Complex::Re(double val)


{
_re = val;
}

void Complex::Im(double val)


{
_im = val;
}

9.11 Structuri de date si clase


Exista o asemanare foarte mare intre structurile de date si clase. In C++ singura diferenta dintre o
12
157
Adonis Butufei

structura de date si clase este ca tipul de acces implicit este privat pentru clase si public pentru structuri de
date.
Putem defini metode pentru o structura de date in acelasi mod ca si pentru clase si daca folosim
directivele de acces prezentate anterior putem obtine aceeasi functionalitate.

9.12 Sumar
Clasele creaza noi tipuri de date prin gruparea datelor si functiilor care lucreaza cu aceste date.

Obiectele sunt variabile care au tipul definit de clase. Ca si analogie, clasa reprezinta planul unei case iar
obiectele sunt cladirile construite folosind acel plan.

Incapsularea permite ascunderea detaliilor de implementare, folosind directivele de acces, si ofera un set
de metode prin care putem interactiona cu obiectele.

Constructorii sunt metode speciale care sunt apelate de compilator in momentul crearii obiectelor pentru
initializarea datelor. Un constructor nu poate apela alt constructor.
Destructorii sunt metode speciale care sunt apelate de compilator in momentul in care domeniul de viata
al unui obiect s-a terminat, pentru eliberarea resurselor. O clasa poate avea un singur destructor.

Folosind metode de acces putem interactiona cu datele obiectelor.

Metodele constante nu modifica atributele obiectelor. Folosind directiva const pentru metode putem
identifica foarte usor erorile de modificare neautorizata a atributelor.

Atributele statice apartin claselor. Ele pot fi accesate de toate instantele acelei clase.

Metodele statice pot accesa doar atributele statice ale clasei si se pot apela fara a crea o instanta a acelei
clase.

Metodele inline se pot declara folosind cuvantul cheie sau scriind implementarea in fisierul header.

Declararea claselor se scrie in fisiere header si implementarea in fisiere sursa.

Structurile de date sunt echivalente cu clasele in C++. Singura diferenta este ca pentru clase tipul de acces
implicit este privat iar pentru structuri este public.

9.13 Intrebari si exercitii


1. Creati un proiect in Visual C++ si implementati clasa Complex prezentata in acest capitol.

13
158
Adonis Butufei

2. Rulati fiecare exemplu prezentat in capitol.


3. Care este eroarea din exemplul de mai jos?
class Fractie
{
private:
int _numarator;
int _numitor;
public:
Fractie(int numarator, int numitor);
}

4. Dupa corectarea declaratiei clasei anterioare urmatorul cod genereaza erori de compilare. Care este
motivul?
Fractie f;

5. Declarati o clasa Persoana care are urmatoarele atribute private: _nume, _prenume, _varsta. Scrieti
metode de acces (de setare si citire a acestor atribute).

6. Declarati si implementati constructorul de copiere pentru clasa Persoana.

7. Urmatorul cod genereaza erori de compilare. Care este motivul?


class Numar
{
int _val;
Numar(int val = 0);
};

Numar::Numar(int val)
{
_val = val;
}

int main()
{
Numar a(10);
}

8. Urmatorul cod genereaza erori de compilare. Care este eroarea?


class A
{
int _test;
public:
static int X();

14
159
Adonis Butufei

};

int A::X()
{
return _test;
}

9. Scrieti implementarea constructorului care foloseste lista de initializare pentru urmatoarea clasa:
class Punct
{
double _x;
double _y;
double _z;
public:
Punct(double x = 0.0,double y = 0.0, double z = 0.0);
};

10. Urmatorul cod genereaza erori de compilare. Care este greseala?


class A
{
~A();
};

int main()
{
A a;
}

9.14 Bibliografie
• Teach yourself C++ In an Hour A day, Sixth Edition, Sams, Jesse Liberty, Cap 10.
• Practical C++ Programming, Second Edition, O'Reilly, Steve Oualline, Cap 13 - 14.
• C++ Without Fear, Second Edition, Prentice Hall, Brian Overland, Cap 11 - 12.

15
160
Adonis Butufei

10. SUPRAINCARCAREA OPERATORILOR

CUPRINS
10.Supraincarcarea operatorilor....................................................................................................................2
10.1 Ce sunt operatorii in C++?...............................................................................................................2
10.2 Functii, clase si operatori friend.......................................................................................................3
10.3 Operatorii unari................................................................................................................................3
10.3.1 Operatorii de incrementare / decrementare..............................................................................4
10.3.1.1 Operatorul de incrementare cu prefixare..........................................................................5
10.3.1.2 Operatorul de incrementare cu postfixare.........................................................................5
10.3.2 Operatorii de conversie.............................................................................................................6
10.4 Operatorii binari...............................................................................................................................6
10.4.1 Supraincarcarea operatorilor aritmetici....................................................................................7
10.4.2 Supraincarcarea operatorilor aritmetici cu atribuire.................................................................8
10.4.3 Supraincarcarea operatorilor de comparare..............................................................................8
10.4.4 Supraincarcarea operatorilor >, >=, < si <=.............................................................................9
10.4.5 Supraincarcarea operatorilor de indexare operator []...............................................................9
10.5 Operatorii functie operator ().........................................................................................................10
10.6 Operatorul de atribuire operator =..................................................................................................11
10.7 Operatori care nu pot fi supraincarcati...........................................................................................12
10.8 Metode si operatori definiti automat..............................................................................................12
10.8.1 Copierea superficiala vs. copierea completa..........................................................................12
10.9 Sumar.............................................................................................................................................13
10.10 Intrebari si exercitii......................................................................................................................13
10.11 Bibliografie...................................................................................................................................14

1
161
Adonis Butufei

10. SUPRAINCARCAREA OPERATORILOR


C++ ofera posibilitatea utilizarii operatorilor pentru clasele definite de utilizator. Aceasta permite scrierea
si intelegerea mai usoara a codului.
In acest capitol vom discuta despre:
• Categoriile de operatori: unari, binari, de conversie si operatorii care nu pot fi supraincarcati.
• Operatorii unari.
• Operatorii binari.
• Metodele si operatorii generati automat.
• Functii si clase friend.

10.1 Ce sunt operatorii in C++?


Operatorii sunt metode speciale ale claselor. Folosesc cunvantul cheie operator. Declararea
operatorilor este similara cu cea a functiilor:
tip_returnat operator simbol (... lista de parametri ...);

Simbolul poate fi + pentru adunare, == sau != pentru comparare etc.


Care este avantajul folosirii operatorilor in locul metodelor normale? Sa presupunem ca avem o clasa
Fractie si vrem sa implementam operatia de adunare. Daca am folosi o metoda obisnuita ea ar avea
urmatorul prototip:
Fractie Add(const Fractie& src);

Apelul acestei medode ar arata in modul urmator:


Fractie result = a.Add(b);

Unde a si b sunt fractii definite anterior.

Daca am folosi operatorul + in locul metodei Add prototipul ar arata in modul urmator:
Fractie operator + (const Fractie& src);

Apelul ar arata in modul urmator:


Fractie result = a + b;

Operatorii se pot clasifica dupa mai multe criterii.


In functie de numarul de operanzi: operatori unari care au un singur operand, operatori binari care au doi
operanzi.
Operatorii de conversie ajuta conversia unui tip de clasa la alt tip. Putem, de exemplu, converti
reprezentarea datei calendaristice la un sir de caractere.
Nu toti operatorii limbajului pot fi supraincarcati pentru tipurile de date definite de utilizator.

2
162
Adonis Butufei

10.2 Functii, clase si operatori friend


Atributele claselor au nivelul de acces protected sau private pentru a asigura incapsularea. Accesul la
aceste atribute din exteriorul clasei se realizeaza prin intermediul metodelor publice.
Exista cazuri practice in care o functie, un operator sau o alta clasa are nevoie sa acceseze atributele
private sau sa apeleze metodele private ale unei clase. In C++ aceasta se realizeaza cu ajutorul directivei
friend.
Exemplu:
friend Complex operator + (double x, const Complex& y);

In acest exemplu am declarat un operator friend pentru clasa Complex.


Ca si in viata, prietenii au acces la informatii care, de obicei, nu sunt accesibile in mod public. Din acest
motiv este important sa folosim cu precautie directiva friend asa cum ne alegem cu precautie prietenii.

10.3 Operatorii unari


Operatorii unari se pot implementa folosind functii friend sau metode.
Declararea unui operator unar ca functie friend
friend1 return operator simbol (parametru);

Exemplu:
friend Complex operator – (const Complex& src);

Acest operator creaza un nou obiect de tip complex care are semnul schimbat pentru partea reala si partea
imaginara. El este apelat in urmatorul tip de expresii:
Complex c = -c1;

Unde c1 este un obiect de tip Complex definit anterior.


Implementarea operatorului este prezentata in exemplul de mai jos:
Complex operator -(const Complex& src) const
{
return Complex(-src._re, - src._im);
}

Declararea pentru varianta cu metoda este:


Complex operator -() const;

Iar implementarea operatorului este similara cu precedenta:


Complex Complex::operator -() const
{
return Complex(-_re, -_im);
}

Deoarece metoda modifica instanta curenta nu mai este necesar un parametru ca in cazul implementarii
functiei.
Recomandare

1 Directiva friend este necesara numai in cazul in care se acceseaza atribute, metode cu nivel de acces protected sau private.
3
163
Adonis Butufei

Este preferabila folosirea metodelor pentru implementarea operatorilor deoarece nu ofera acces la datele
membru din afara. Cazurile in care este necesara folosirea functiilor friend vor fi prezentate in acest
capitol.

In tabelul de mai jos sunt prezentati operatorii unari.


Operator Nume

++ Incrementare

-- Decrementare

* Indirectarea pointerului

-> Selectia membrilor

! Negare logica

& Adresa

~ Complement fata de 1

+ Plus unar

- Minus unar

Operatori de conversie
In cele ce urmeaza vom discuta operatorii frecvent utilizati in practica.

10.3.1 Operatorii de incrementare / decrementare


Exista doi operatori de incrementare / decrementare ca si la variabilele numerice: operatorul prefixare si
operatorul post fixare. Pentru prezentarea operatorilor de incrementare / decrementare vom folosi o clasa
Contor declarata in fisierul Contor.h si implementata in fisierul Contor.cpp.
class Contor Contor::Contor(int valoare)
{ {
private: _valoare = valoare;
int _valoare; }

public:
Contor::Contor(const Contor& src)
Contor(int valoare = 0) ;
{
Contor(const Contor& src);
_valoare = src._valoare;
};
}

Clasa are un singur atribut _valoare, un constructor pentru initializarea valorii si un constructor de
copiere.

4
164
Adonis Butufei

10.3.1.1 Operatorul de incrementare cu prefixare


Operatorul de incrementare cu prefixare are urmatorul prototip:
Contor& operator ++ ();

Returneaza o referinta de tip Contor. Implementarea acestui operator este prezentata mai jos. Mai intai se
incrementeaza valoarea si apoi se returneaza o referinta la instanta curenta.
Contor& Contor::operator ++()
{
_valoare++;
return *this;
}

Codul urmator prezinta apelul acestui contor:


Contor c;
Condor c1 = ++c;

10.3.1.2 Operatorul de incrementare cu postfixare


Prototipul acestui operator este urmatorul:
Contor operator ++ (int);

Observam ca acest operator returneaza un obiect de tip Contor si nu o referinta, deoarece se returneaza
valoarea dinaintea incrementarii.
Implementarea acestui operator este prezentata mai jos:
1: Contor Contor::operator ++ (int)
2: {
3: Contor result = *this;
4: _valoare++;
5: return result;
6: }

In linia 3 se creaza un obiect care contine valoarea initiala. In linia 4 se incrementeaza valoarea. La final
se returneaza obiectul cu valoarea initiala.

Observatii:
Acest operator are un parametru de tip int care nu este folosit. Acesta este necesar compilatorului pentru
a-l diferentia de operatorul de incrementare cu prefixare.
Varianta de prefixare este mai eficienta deoarece nu este necesara o variabila auxiliara pentru copierea
valorii.

Codul urmator prezinta apelul acestui operator:


Contor c;
Contor c1 = c++;

5
165
Adonis Butufei

Operatorii de decrementare sunt similari cu operatorii de incrementare si implementarea lor este un


exercitiu propus la sfarsitul acestui capitol.

10.3.2 Operatorii de conversie


Pentru a putea converti tipul obiectului la un alt tip de obiect sau la tipuri elementare este necesara
implementarea unui operator de conversie.
Operatorii de conversie au urmatorul prototip:
operator tip_destinatie ();

Unde tip_destinatie reprezinta tipul la care se doreste conversia operatorului.

De exemplu, in cazul clasei Contor folosita anterior putem defini un operator de conversie la tipul intreg.
Pentru aceasta declaram urmatorul prototip:
operator int ();

Implementarea este prezentata in exemplul de mai jos:


Contor::operator int()
{
return _valoare;
}

10.4 Operatorii binari


Tipurile de operatori binari care pot fi supraincarcati sunt prezentati in urmatorul tabel:
Operator Nume Operator Nume
, Virgula % Modulo
!= Diferenta += Adunare si atribuire
== Egalitate -= Scadere si atribuire
> Mai mare *= Inmultire si atribuire
< Mai mic /= Impartire si atribuire
>= Mai mare sau egal %= Modulo si atribuire
<= Mai mic sau egal && Si logic
= Atribuire || Sau logic
+ Adunare & Si pe biti
- Scadere | Sau pe biti
* Inmultire ^ Sau exclusiv
/ Impartire << Deplasare la stanga

6
166
Adonis Butufei

Operator Nume Operator Nume


>> Deplasare la dreapta atribuire
&= Si pe biti si atribuire >>= Deplasare la dreapta si
atribuire
|= Sau pe biti si atribuire
[] Operatorul index
^= Sau exclusiv si atribuire
->* Selectia unui pointer la
<<= Deplasare la stanga si membru

Ca si in cazul operatorilor unari pentru declarare si implementare putem folosi functii friend sau metode
ale clasei. Vom folosi metodele clasei pentru implementare ori de cate ori este posibil din motivele
prezentate anterior.

10.4.1 Supraincarcarea operatorilor aritmetici


Deoarece principiile de implementare sunt similare, in acest capitol vom prezenta implementarea
operatorilor de adunare si exemplele vor folosi clasa complex.
Operatorul de adunare are urmatorul prototip:
Complex operator +(const Complex& src);

Acest operator se apeleaza in modul urmator:


Complex rezultat = c1 + c2;

Unde c1 si c2 sunt obiecte de tip complex definite anterior.


In expresia de mai sus este apelat operatorul pentru obiectul c1 care primeste o referinta la obiectul c2;
Rezultatul acestui calcul trebuie sa fie o alta variabila de tip complex care este returnata de operator.
Implementarea acestui operator este prezentata mai jos:

Complex Complex::operator + (const Complex& src)


{
return Complex (_re + src._re, _im + src._im);
}

In cazul in care vrem sa calculam suma dintre un numar real si un numar complex ca in exemplul de mai
jos operatorul de adunare nu se mai poate apela deoarece primul operand este de tip double.
double x = 3.0;
Complex c(1,2.5);
Complex result = x + c;

Pentru aceasta este necesara implementarea unui operator folosind o functie friend care are prototipul
urmator:
friend Complex operator + (double x, const Complex& y);

7
167
Adonis Butufei

Implementarea este prezentata in exemplul urmator:


Complex operator + (double x, const Complex& y)
{
return Complex(x + y._re, y._im);
}

10.4.2 Supraincarcarea operatorilor aritmetici cu atribuire


Ca si la variabilele simple folosirea operatorilor compusi +=, -= etc permit scrierea compacta a codului.
Spre deosebire de operatorii aritmetici, unde se crea un nou obiect care avea valoarea rezultatului, acesti
operatori modifica obiectul si returneaza o referinta la acest obiect.
Vom prezenta operatorul += pentru numere complexe. Prototipul acestui operator este:
Complex& operator += (const Complex& src);

Mai jos este prezentata implementarea acestui operator:


Complex& Complex::operator += (const Complex& src)
{
_re += src._re;
_im += src._im;
return *this;
}

10.4.3 Supraincarcarea operatorilor de comparare


Pentru comparare este necesara implementarea a doi operatori: == pentru testarea egalitatii si != pentru
testarea diferentei. Prototipurile acestor operatori sunt prezentate mai jos:
bool operator == (const Complex& src);
bool operator != (const Complex& src);

Implementarea compara valoarea atributelor si este prezentata mai jos:


bool Complex::operator == (const Complex& src)
{
const double DELTA = 10e-7;

bool bRe = abs(_re - src._re) < DELTA;


bool bIm = abs(_im - src._im) < DELTA;
return bRe && bIm;
}

bool Complex::operator != (const Complex& src)


{
return ! (*this == src);
}

8
168
Adonis Butufei

Pentru a evita duplicarea codului, operatorul != returneaza opusul lui ==. Asa cum am discutat la variabile
reale nu putem compara variabilele de tip double si pentru aceasta am definit precizia: o constanta a carei
valoare este abaterea maxima.

10.4.4 Supraincarcarea operatorilor >, >=, < si <=


Acesti operatori sunt folositi pentru a stabili relatii de ordine intre obiectele unei clase. Pentru exemplu
vom folosi clasa Contor prezentata anterior.
Operatorul > are urmatorul prototip:
bool operator > (const Contor& src);

Iar implementarea este prezentata mai jos:


bool Contor::operator > (const Contor& src)
{
return (_valoare > src._valoare);
}

Operatorul >= are urmatorul prototip:


bool operator >= (const Contor& src);

Iar implementarea este prezentata mai jos:


bool Contor::operator >= (const Contor& src)
{
return (_valoare >= src._valoare);
}

10.4.5 Supraincarcarea operatorilor de indexare operator []


Acesti operatori se folosesc in cazul claselor care au atribute de tip tablou atunci cand este necesara
accesarea elementelor din tablou. Pentru exemplu vom folosi clasa Vector a carei declarare este prezentata
mai jos:
class Vector
{
private:
int * _vector;
public:
Vector(int size = 10);

~Vector();

int & operator [] (int index);


};

Aceasta clasa are un atribut tablou. Operatorul [] returneaza referinte la elementele tabloului.

9
169
Adonis Butufei

Implementarea acestei clase este prezentata mai jos:


Vector::Vector(int size)
{
_vector = new int [size];
}

Vector::~Vector()
{
delete [] _vector;
}

int & Vector::operator [] (int index)


{
return _vector[index];
}

In cazul in care folosim obiecte constante este necesara adaugarea unui operator de indexare constant.
Exemplu:
int & operator [] (int index) const;

A carui implementare este:


int & Vector::operator [] (int index) const
{
return _vector[index];
}

10.5 Operatorii functie operator ()


Folosirea acestor operatori permite obiectelor sa aiba comportament similar functiilor. Ei sunt folositi ca si obiecte
functie in algoritmii STL despre care vom discuta intr-un capitol viitor.
Exemplu:
Urmatorul operator afiseaza un obiect de tip Complex:
class PrintComplex
{
public:
void operator() (const Complex& src);
};
Implementarea este prezentata mai jos:
void PrintComplex::operator() (const Complex& src)
{
cout << src.Re();

if(src.Im() > 0)

10
170
Adonis Butufei

{
cout << "+";
}
cout << src.Im() << "i\n";
}

Un exemplu de apel:
Complex c(3,2);
PrintComplex print;
print(c);

10.6 Operatorul de atribuire operator =


Pentru atribuirea valorii atributelor unui obiect se foloseste operatorul = care are urmatorul prototip:
Complex& operator = (const Complex& src);

Implementarea este prezentata mai jos:


7: Complex& Complex::operator = (const Complex& src)
8: {
9: if(this == &src)
10: {
11: return *this;
12: }
13:
14: Init(src._re, src._im);
15:
16: return *this;
17: }

Testul din linia 3 asigura functionarea corecta in cazul in care un obiect se atribuie lui insusi.
Exemplu:
Complex c1;
c1 = c1;

Aceasta verificare este importanta in cazul obiectelor care aloca dinamic memoria pentru a preveni
dealocarea eronata a pointerilor.

10.7 Operatori care nu pot fi supraincarcati


Exista operatori care nu pot fi supraincarcati in C++. Tabelul de mai jos prezinta acesti operatori.
Operator Nume

. Selectia membrilor

11
171
Adonis Butufei

.* Selectia pointerilor la membri

:: Domeniu de acces

?: Operatorul ternar

sizeof Returneaza dimensiunea in octeti a unui obiect

10.8 Metode si operatori definiti automat


Compilatorul genereaza automat urmatoarele metode in cazul in care nu sunt declarate de utilizator:
constructorul implicit, constructorul de copiere, operatorul de atribuire si destructorul in cazul in care nu
au fost declarate de utilizator.

Atunci cand clasele au atribute de tip pointer sau tablou este necesara implementarea de utilizator a
acestor metode si operatori. Constructorul de copiere si operatorul de asignare implicit realizeaza o copie
a valorilor atributelor bit cu bit care in aceste cazuri nu este intotdeauna corecta.

10.8.1 Copierea superficiala vs. copierea completa


Intelegerea diferentei intre cele doua este esentiala pentru implementarea corecta a claselor care lucreaza
dinamic cu memoria.
Sa presupunem ca avem o clasa care are un atribut de tip pointer. Acest pointer este alocat in constructor
si dealocat in destructor. Daca s-ar folosi metodele generate automat de compilator, in momentul in care
un obiect este copiat bit cu bit, atributul de tip pointer ar contine adresa zonei alocate in obiectul sursa si
aceasta zona de memorie ar fi dealocata in ambii destructori fapt ce ar avea efecte dezastruoase asupra
executiei programului.
Copierea bit cu bit a valorii atributelor se numeste copiere superficiala.

obiect1 obiect2 obiect1

ptr ptr ptr

“Hello”
“Hello”

In copierea completa se aloca memoria pentru variabilele pointeri si se copiaza valoarea acelor variabile
local.

12
172
Adonis Butufei

obiect1 obiect2

ptr ptr

“Hello” “Hello”
strcpy

Este important de identificat cazurile in care copierea superficiala este suficienta si cazurile in care
copierea completa este necesara.

Atunci cand este necesara folosirea copierii complete trebuiesc implementati constructorul de copiere si
operatorul de atribuire.
Pentru siguranta putem declara constructorul de copiere si operatorul de atribuire cu nivel de acces privat.
In acest mod, orice incercare de apel ar produce o eroare de compilare.

10.9 Sumar
• Operatorii permit implementarea unei functionalitati care este mai usor de utilizat si inteles.
• Exista doua categorii de operatori: unari si binari. Operatorii unari lucreaza cu un singur operand,
Operatorii binari au doi operanzi.
• Pentru operatorii de incrementare /decrementare de postfixare se foloseste un parametru de tip int care
permite compilatorului diferentierea de operatorul similar de prefixare.
• Operatorii se pot implementa folosind metode sau functii friend.
• Este preferabila folosirea metodelor in majoritatea cazurilor.
• In cazul in care obiectul nu se modifica putem folosi directiva const pentru operator.
• Exista operatori a caror suprascriere nu este permisa.
• Pentru orice clasa compilatorul adauga urmatoarele metode daca nu sunt definite de utilizator:
constructorul implicit, constructorul de copiere, operatorul de atribuire si destructorul.
• Implementarea implicita a operatorului de atribuire si a constructorului de copiere realizeaza o copiere
superficiala bit cu bit care in cazul claselor cu atribute pointer poate duce la functionarea
necorespunzatoare a programelor.
• Intelegerea diferentei intre copierea superficiala si copierea completa este esentiala.

10.10 Intrebari si exercitii


1. Implementati operatorii de decrementare pentru clasa Contor prezentata in acest capitol folosind
metode.
2. Implementati operatorii de incrementare folosind functiile friend.
3. Implementati operatorul de scadere pentru numere complexe folosind metode.
13
173
Adonis Butufei

4. Implementati operatorul -= pentru numere complexe.


5. Implementati operatorii < si <= pentru clasa Contor.
6. Implementati operatorul () care are un parametru de tip Contor si afiseaza valoarea atributului clasei.
7. Implementati operatorul de atribuire pentru clasa Contor.
8. Implementati operatorii == si != pentru clasa Contor.
9. Care este diferenta dintre copierea superficiala si copierea completa?
10. Scrieti declaratiile membrilor pentru a preveni copierea clasei urmatoare:
class A
{
public:
A() {}
};

10.11 Bibliografie
• Teach yourself C++ In an Hour A day, Sixth Edition, Sams, Jesse Liberty, Cap 13.
• Practical C++ Programming, Second Edition, O'Reilly, Steve Oualline, Cap 18.
• C++ Without Fear, Second Edition, Prentice Hall, Brian Overland, Cap 13.

14
174
Adonis Butufei

11. AGREGARE SI MOSTENIRE DE CLASE, POLIMORFISM

CUPRINS
11.Agregare si mostenire de clase, polimorfism...........................................................................................2
11.1 Agregarea claselor............................................................................................................................2
11.2 Mostenirea........................................................................................................................................2
11.2.1 Sintaxa derivarii........................................................................................................................3
11.2.2 Tipul de mostenire si accesul la membri din clasa de baza......................................................3
11.2.3 Constructori si destructori.........................................................................................................4
11.2.4 Transferul parametrilor la constructori.....................................................................................5
11.3 Polimorfism......................................................................................................................................6
11.3.1 Suprascrierea metodelor din clasa de baza...............................................................................7
11.3.2 Metode virtuale.........................................................................................................................8
11.3.3 Destructori virtuali....................................................................................................................9
11.3.4 Clonarea obiectelor si constructorii de copiere......................................................................10
11.4 Mosternire multipla........................................................................................................................10
11.4.1 Apelarea constructorilor claselor de baza...............................................................................12
11.4.2 Apelul explicit al metodelor virtuale din clasa de baza..........................................................13
11.4.3 Clase de baza virtuale.............................................................................................................14
11.5 Clase abstracte si interfete..............................................................................................................16
11.5.1 Functii virtuale pure................................................................................................................16
11.5.2 Clase abstracte........................................................................................................................16
11.5.3 Clase interfata.........................................................................................................................17
11.6 Sumar..............................................................................................................................................17
11.7 Intrebari si exercitii........................................................................................................................17
11.8 Bibliografie.....................................................................................................................................19

1
175
Adonis Butufei

11. AGREGARE SI MOSTENIRE DE CLASE, POLIMORFISM

In ultimele doua capitole au fost prezentate: organizarea claselor cu metode, atribute si operatori si modul
de realizare al incapsularii folosind directivele de acces. In programele reale clasele nu sunt insule izolate
ci exista relatii si interactiuni intre ele.
In acest capitol vom discuta despre:
• Relatiile de mostenire si agregare a claselor.
• Care sunt tipurile de mostenire.
• Implementarea polimorfismului cu ajutorul functiilor virtuale.
• Clase si metode abstracte.
• Clase interfata.

Nota:
In acest capitol pentru a reduce textul exemplelor si pentru simplitate toate clasele vor avea metodele
implementate inline.

11.1 Agregarea claselor


In lumea reala obiectele sunt alcatuite din alte parti componente mai mici. O casa, de exemplu, are usi,
ferestre, pereti etc. Acest proces de creare a obiectelor complexe din altele simple se numeste compozitie
sau agregare.
Agregarea implica o relatie de apartenenta a componentelor la intreg. In C++ aceasta relatie de
apartenenta este realizata cu ajutorul atributelor.
Exemplu:
class Casa
{
private:
Acoperis _acoperis;
};

11.2 Mostenirea
Folosind mostenirea sau derivarea putem crea o noua clasa care extinde functionalitatea unei clase
adaugand metode, atribute noi sau schimba comportamtentul unor metode deja existente in clasa de baza.
Clasa pe care o mostenim se numeste clasa parinte sau clasa de baza iar clasa nou creata se numeste
subclasa, clasa derivata1 sau clasa copil. Prin acest mecanism toate caracteristicile (atributele si metodele)
clasei de baza se regasesc in clasa derivata.

Intalnim conceptul de mostenire frecvent in activitatile curente atunci cand de la un concept general
trecem la unul particular. De exemplu tramvaiul si autobuzul sunt vehicule. Conceptul de tramvai are
automat toate caracteristicile unui vehicul.

1 Derivarea si mostenirea in contextul programarii obiectuale sunt echivalente.


2
176
Adonis Butufei

Mai multe clase intre care exista relatii de mostenire formeaza o ierarhie de clase.

Important
Folosim mostenirea atunci cand intre doua concepte, functionalitati exista o relatie de tipul „este un” sau
„este o”. Exemple: tramvaiul este un vehicul, ferastraul este o unealta.

11.2.1 Sintaxa derivarii


In C++ mostenirea este de trei tipuri: public, protected sau private. Tipul implicit este cel private.
Tipul mostenirii specifica nivelul de acces la membri clasei de baza.

Exemplu:
class Vehicul
{
int _vitezaMaxima;
public:
int VitezaMaxima() { return _vitezaMaxima; }
void VitezaMaxima(int vitezaMaxima)
{ _vitezaMaxima = vitezaMaxima; }
};

class Autobuz : public Vehicul


{
int _linie;
public:
int Linie () { return _linie; }
void Linie(int linie) { _linie = linie; }
}

In acest exemplu clasa Autobuz mosteneste public clasa Vehicul.

11.2.2 Tipul de mostenire si accesul la membri din clasa de baza


In cazul mostenirii publice membrii publici din clasa de baza raman publici in clasa derivata, membrii
protected raman protected iar membrii private sunt inaccesibili.
Exemplu:
Autobuz vtp;
vtp.VitezaMaxima(80);

In cazul mostenirii protected membrii publici din clasa de baza devin protected, membrii protected raman
protected iar membrii private sunt inaccesibili.

In cazul mostenirii private membrii publici din clasa de baza devin private in clasa derivata, membrii
3
177
Adonis Butufei

protected din clasa de baza devin private in clasa derivata iar membrii private din clasa de baza sunt
inaccesibili.

In cazul mostenirii publice obiectele claselor derivate au si tipul clasei de baza si pot fi folosite in locul
claselor de baza.
Exemplu:
1: Autobuz a;
2: Vehicul & referinta = a;
3: Vehicul *p = &a;

Definitiile din liniile 2 si 3 sunt corecte si ele permit folosirea polimorfismului despre care vom vorbi mai
tarziu in acest capitol.

Important
Este recomandata folosirea mostenirii publice. Celelalte tipuri de mostenire in majoritatea cazurilor pot fi
inlocuite cu agregarea.
Cand vrem ca o metoda sa fie accesibila in exterior folosim nivelul de acces public. In cazul in care dorim
ca o metoda sa fie accesibila claselor derivate folosim nivelul de acces protected.

11.2.3 Constructori si destructori


Cand cream un obiect de tip Autobuz constructorul clasei Vehicul este apelat primul apoi constructorul
clasei Autobuz. Atunci cand se termina domeniul de viata pentru acest obiect este apelat destructorul
clasei derivate Autobuz si apoi cel al clasei de baza Vehicul.
Exemplu:
#include <iostream>
using namespace std;
class Vehicul
{
public:
Vehicul() { cout << "Vehicul()\n" ; }
~Vehicul() { cout << "~Vehicul()\n"; }
// ...
};

class Autobuz : public Vehicul


{
public:
Autobuz() { cout << "Autobuz()\n"; }
~Autobuz(){ cout << "~Autobuz()\n"; }
// ...
};

4
178
Adonis Butufei

int main()
{
Autobuz v1;
v1.Linia(135);
}

La rularea acestui program pe ecran vor apare urmatoarele mesaje:


Vehicul()
Autobuz()
~Autobuz()
~Vehicul()

11.2.4 Transferul parametrilor la constructori


Frecvent se intalnesc situatii cand este necesar transferul parametrilor catre clasa de baza in constructor.
Cum am putea transmite de exempul viteza maxima clasei Vehicul in momentul construirii?
Initializarea clasei de baza se realizeaza prin scrierea numelui clasei urmata de parametri necesari.
Exemplu:
1: class Vehicul
2: {
3: public:
4: // codul anterior ...
5: Vehicul(int vitezaMaxima) :
6: _vitezaMaxima(vitezaMaxima)
7: {
8: cout << "Vehicul(int)\n" ;
9: }
10: };
11:
12: class Autobuz : public Vehicul
13: {
14: public:
15: // codul anterior ...
16:
17: Autobuz(int linie, int vitezaMaxima) :
18: Vehicul(vitezaMaxima),
19: _linie(linie)
20: {
21: cout << "Autobuz(int, int)\n";
22: }
23: };
24:

5
179
Adonis Butufei

25: int main()


26: {
27: Autobuz a(135,80);
28: }

In acest exemplu in linia 5 am definit un constructor care initializeaza viteza maxima pentru clasa
Vehicul. Acest constructor este apelat din constructorul clasei derivate in linia 18.
Ruland acest program pe ecran se afiseaza urmatoarele mesaje:
Vehicul(int)
Autobuz(int, int)
~Autobuz()
~Vehicul()

11.3 Polimorfism
Mostenirea este doar unul din avantajele programarii obiectuale. Adevarata putere este posibilitatea de a
trata obiectele claselor derivate ca si cum ar fi obiecte ale clasei de baza. Mecanismele care permit aceasta
sunt polimorfismul si legarea dinamica2.

Polimorfismul permite ca un obiect al unei clase derivate sa fie transmis unei functii care are ca parametru
un pointer sau referinta la clasa de baza.
Cand este apelata o metoda folosind un pointer sau o referinta la clasa de baza, mecanismul de legare
dinamica executa codul din clasa derivata. Codul care este executat depinde de tipul obiectului nu de cel
al pointerului sau referintei. In acest mod, obiectele claselor derivate pot fi substituite cu obiectele clasei
de baza fara schimbarea codului in functiile care folosesc obiectele.
Exemplu:
1: Autobuz a;
2: Vehicul & v = a;
3: Vehicul *p = &a;

In acest exemplu am folosit clasele declarate anterior. In linia 2 a fost declarata o referinta de tipul vehicul
iar in linia 3 un pointer la o clasa de tipul vehicul.

11.3.1 Suprascrierea metodelor din clasa de baza


O clasa derivata are acces la metodele si atributele clasei de baza si poate sa suprascrie o metoda a clasei
de baza. Cand o clasa derivata defineste o metoda cu acelasi nume si aceeasi semnatura insa cu o
implementare diferita fata de clasa de baza se spune ca suprascriem metoda din clasa de baza.
Exemplu:
class Vehicul
{
public:
// ... codul anterior
void Deplasare() { cout << "Deplasarea vehiculului.\n"; }

2 In limba englesza este folosit termenul Dynamic Binding.


6
180
Adonis Butufei

};
class Autobuz : public Vehicul
{
public:
// codul anterior ...
void Deplasare() { cout << "Deplasarea autobuzului.\n"; }
};

int main()
{
Autobuz a;
a.Deplasare();
}

In acest exemplu am suprascris metoda Deplasare in clasa Autobuz. La rularea se executa metoda din
clasa derivata si pe ecran apare mesajul:
Vehicul()
Autobuz()
Deplasarea autobuzului.
~Autobuz()
~Vehicul()

Pentru a apela metoda din clasa de baza folosim numele complet.


Exemplu:
void Autobuz::Deplasare()
{
Vehicul::Deplasare();
}

Sau in cazul instantei:


a.Vehicul::Deplasare();

Important
• Este recomandabil sa se evite schimbarea nivelului de acces in clasa derivata la metoda suprascrisa.
• Atunci cand ierarhia de mostenire se schimba trebuie analizat codul care apeleaza clasa de baza si daca
este cazul sa fie actualizat numele clasei3.
• Poate sa apara o confuzie intre suprascrierea si supraincarcarea. Supraincarcarea inseamna definirea
mai multor functii care au acelasi nume insa lista de parametri diferiti. Suprascrierea este definirea
unei functii cu aceeasi semnatura in clasa derivata.

3 De exemplu daca parintele clasei Autobuz devine MijlocTrasnportPublic care se deriveaza la randul lui din Vehicul trebuie
verificat daca apelul Vehicul::Deplasare() trebuie actualizat cu numele noii clase parinte.
7
181
Adonis Butufei

11.3.2 Metode virtuale


Pornind de la exemplul anterior atunci cand avem un pointer la clasa Vehicul ca in exemplul de mai jos
am dori ca apelul sa execute codul din clasa derivata.
Exemplu:
Vehicul *p = new Autobuz;
p->Deplasare();

Pentru acest lucru este necesara folosirea cuvantului cheie virtual în declararea metodei în clasa de
baza4.
Exemplu:
virtual void Deplasare();

Acum apelul p->Deplasare(); va afisa mesajul „Deplasarea autobuzului”.

Modul de selectie a metodelor


Daca folosim o clasa derivata compilatorul va cauta metoda în clasa derivata și apoi în clasa parinte.
In cazul în care folosim o varianila de tipul clasei de baza, chiar daca aceasta variabila este o instanta a
clasei derivate compilatorul va cauta metodele numai în clasa de baza. Singura exceptie este cand o
metoda este declarata folosind cuvantul cheie virtual pentru metoda. In acest caz compilatorul va cauta
metoda în clasa derivata, daca nu este suprascrisa în clasa derivata atunci se selecteaza metoda din clasa
parinte.
Aceasta este prezentata în tabelul urmator:
Tipul clasei Tipul metodei Ordinea de cautare

Derivata Normala Clasa derivata apoi clasa de baza

Baza Normala Clasa de baza

Baza Virtual Clasa derivata apoi clasa de baza

Note
Metodele virtuale folosesc mecanismul legarii dinamice descrise la inceputul paragrafului spre deosebire
de metodele obisnuite care folosesc mecanismul legarii statice.
Atunci cand folosim cuvantul cheie virtual în clasa de baza nu mai este necesara repetarea lui în clasele
derivate insa pentru o intelegere mai usoara putem continua.

11.3.3 Destructori virtuali


Pornind de la exemplul anterior care este mesajul afisat pe ecran la rularea urmatorului program?
1: Vehicul *p = new Autobuz;
2: delete p;

Datorita faptului ca p este un pointer la clasa de baza, atunci cand se executa linia 2 este apelat numai

4 Cuvantul cheie se foloseste numai la declarare, implemementarea metodei ramane neschimbata.


8
182
Adonis Butufei

destructorul clasei de baza. Pentru a functiona corect este necesara folosirea cuvantului cheie virtual
pentru destructorul clasei Vehicul.
Exemplu:
class Vehicul
{
public:
virtual ~Vehicul() { cout << "~Vehicul()\n"; }
};

Important
Pentru apelarea corecta a destructorilor claselor derivate este necesar ca destructorul clasei de baza sa fie
virtual.

11.3.4 Clonarea obiectelor si constructorii de copiere


Crearea de obiecte identice este frecvent intalnita în practica. Deoarece în C++ construcorii nu pot fi
virtuali este necesara folosirea constructorilor de copiere și a unei metode virtuale de clonare ca în
exemplul de mai jos:
1: class Vehicul
2: {
3: public:
4: // ...
5: Vehicul(const Vehicul & src)
{ _vitezaMaxima = src._vitezaMaxima; }
6: virtual Vehicul* Clone() { return new Vehicul(*this); }
7: };
8:
9: class Autobuz : public Vehicul
10: {
11: public:
12: // ...
13: Autobuz(const Autobuz & src) : Vehicul(src)
{ _linie = src._linie; }
14: virtual Vehicul* Clone() { return new Autobuz(*this); }
15: };

In acest caz apelul de mai jos creaza o copie a obiectului de tip autobuz.
Vehicul *p = new Autobuz;
Vehicul *p1 = p->Clone();

11.4 Mosternire multipla


Exista cazuri practice cand o clasa este derivata din mai multe clase. De exemplu un service auto este in
acelasi timp un garaj si un birou. Putem exprima aceste relatii folosind mostenirea multipla ca in
9
183
Adonis Butufei

exemplul de mai jos:


1: class Garaj
2: {
3: public:
4: Garaj() { cout << "Garaj()\n"; }
5: virtual ~Garaj() { cout << "~Garaj()\n"; };
6: };
7:
8: class Birou
9: {
10: public:
11: Birou() { cout << "Birou()\n"; }
12: virtual ~Birou() { cout << "~Birou()\n"; }
13: };
14:
15: class ServiceAuto : public Garaj, public Birou
16: {
17: public:
18: ServiceAuto() { cout << "ServiceAuto()\n"; }
19: ~ServiceAuto() { cout << "~ServiceAuto()\n"; };
20: };
21: int main()
22: {
23: ServiceAuto service;
24: Birou &b = service;
25: Garaj &g = service;
26:
27: return 0;
28: }

In linia 15 se declara mostenirea multipla, Sunt enumerate clasele de baza separate prin virgula. Fiecare
clasa de baza are specificat nivelul de acces.
Pentru a se elibera memoria in liniile 5 respectiv 12 au fost declarati destructorii virtuali.
In liniile 24 si 25 au fost atasate referinte de tipul claselor de baza la obiectul service. Datorita mostenirii
multiple se poate folosi polimorfismul pentru ambele clase de baza.
La instantiere constructorii claselor de baza sunt apelati in ordinea declararii apoi este apelat constructorul
clasei derivate. Atunci cand domeniul de viata al obiectului este depasit destructorii sunt apelati in ordinea
inversa: mai intai destructorul clasei derivate apoi destructorii claselor de baza in ordinea inversa
declararii.
Cand este creat un obiect de tipul ServiceAuto ambele clase de baza formeaza parti ale obiectului ca in
figura de mai jos:

10
184
Adonis Butufei

Garaj

Birou ServiceAuto

...

Cand este folosita mostenirea multipla trebuiesc adresate mai multe aspecte. De exemplu: ce se intampla
daca cele doua clase de baza folosesc acelasi nume pentru o metoda virtuala? Cum sunt apelati
constructorii claselor de baza? Ce se intampla atunci cand mai multe clase de baza sunt derivate din
aceeasi clasa? In sectiunile urmatoare vom analiza fiecare din aceste aspecte.

11.4.1 Apelarea constructorilor claselor de baza


In momentul crearii unei instante a clasei derivate, daca nu exista alta specificatie, compilatorul cauta
constructorul implicit. Transferul parametrilor catre unul sau mai multi constructori de baza se face
asemanator cu apelul constructorului de baza din mostenirea simpla.
In exemplul urmator vom adauga atributul _nrMasini de tip int pentru clasa Garaj care reprezinta numarul
de masini care pot fi reparate simultan in garaj si _nrEchipe de tip int pentru clasa Birou care reprezinta
numarul de echipe gestionate de birou. Pentru reducerea spatiului vom prezenta doar modificarile.
1: class Garaj
2: {
3: int _nrMasini;
4: public:
5: Garaj(int nrMasini) : _nrMasini (nrMasini)
6: { cout << "Garaj(int);\n"; }
7: };
8:
9: class Birou
10: {
11: int _nrEchipe;
12: public:
13: Birou(int nrEchipe) : _nrEchipe (nrEchipe)
14: { cout << "Birou(int);\n"; }
15: };
16:
17: class ServiceAuto : public Garaj, public Birou
18: {
19: public:
20: ServiceAuto(int nrMasini, int nrEchipe) :
21: Garaj(nrMasini), Birou(nrEchipe)
22: { cout << "ServiceAuto(int, int)\n"; }

11
185
Adonis Butufei

23: };
24:
25: int main()
26: {
27: ServiceAuto service(20,3);
28: return 0;
29: }

Apelul constructorilor de baza este realizat in linia 2: se specifica explicit care parametru este trimis
fiecarui constructor de baza.
In cazul in care implementarea constructorului din clasa derivata nu este inline codul se scrie in modul
urmator:
class ServiceAuto : public Garaj, public Birou
{
public:
ServiceAuto(int nrMasini, int nrEchipe);
};

ServiceAuto::ServiceAuto(int nrMasini, int nrEchipe) :


Garaj(nrMasini), Birou(nrEchipe)
{
cout << "ServiceAuto(int, int)\n";
}

11.4.2 Apelul explicit al metodelor virtuale din clasa de baza


Sa presupunem ca ambele clase de baza Garaj si Birou au o metoda virtuala care returneaza suprafata
spatiului de lucru. Ca in exemplul de mai jos.
class Garaj
{
public:
virtual double SuprafataActiva() { return _nrMasini * 10.5; }
};

class Birou
{
public:
virtual double SuprafataActiva() { return _nrEchipe * 7.2; }
};

Apelul din linia 2 este ambiguu pentru ca se poate apela fie metoda din clasa Garaj fie metoda din clasa
Birou. Din acest motiv compilatorul genereaza eroare de complilare.
1: ServiceAuto service(5,2);
2: double suprafata = service.SuprafataActiva();

12
186
Adonis Butufei

Utilizatorul trebuie sa specifice explicit care metoda doreste sa o apeleze.Apelul metodei din clasa Garaj
se realizeaza in modul urmator:
double suprafata = service.Garaj::SuprafataActiva();

11.4.3 Clase de baza virtuale


Sa extindem exemplul anterior adaugand clasa Cladire din care sunt derivate clasele Garaj si Birou.
In acest caz, o instanta a clasei ServiceAuto, datorita mostenirii multiple, va contine doua copii ale
instantei Cladire. Aceasta nu este corect deoarece ambele parti ale service-ului biroul si garajul sunt in
aceeasi cladire. Pentru a rezolva aceasta problema clasa Cladire devine clasa de baza virtuala pentru
clasele Birou si Garaj asa cum este prezentat in exemplul de mai jos.
1: class Cladire
2: {
3: int _nrEtaje;
4: public:
5: Cladire (int nrEtaje): _nrEtaje(nrEtaje)
6: {
7: cout << "Cladire(int) cu " <<
8: _nrEtaje << " etaje.\n";
9: }
10:
11: virtual ~Cladire() { cout << "~Cladire()\n"; };
12: };
13:
14: class Garaj : virtual public Cladire
15: {
16: int _nrMasini;
17:
18: public:
19: Garaj(int nrMasini) :
20: Cladire(2),_nrMasini (nrMasini)
21: { cout << "Garaj(int);\n"; }
22:
23: virtual ~Garaj() { cout << "~Garaj()\n"; };
24: virtual double SuprafataActiva()
25: { return _nrMasini * 10.5; }
26: };
27:
28: class Birou : virtual public Cladire
29: {
30: int _nrEchipe;
31: public:

13
187
Adonis Butufei

32: Birou(int nrEchipe) :


33: Cladire(1), _nrEchipe (nrEchipe)
34: { cout << "Birou(int);\n"; }
35:
36: virtual ~Birou() { cout << "~Birou()\n"; }
37:
38: virtual double SuprafataActiva() { return _nrEchipe * 7.2; }
39: };
40:
41: class ServiceAuto : public Garaj, public Birou
42: {
43: public:
44: ServiceAuto(int nrMasini, int nrEchipe, int nrEtaje) :
45: Garaj(nrMasini), Birou(nrEchipe) , Cladire(nrEtaje)
46: {
47: cout << "ServiceAuto(int, int, int)\n";
48: }
49:
50: ~ServiceAuto() { cout << "~ServiceAuto()\n"; };
51: };
52:
53: int main()
54: {
55: ServiceAuto service(20,4,3);
56: return 0;
57: }

In acest exemplu in liniile 14 si 28 clasa Cladire este clasa virtuala de baza pentru clasele Birou si Garaj.
Implicit clasa Garaj are 2 etaje si clasa Birou are un etaj. In linia 45 clasa ServiceAuto initializeaza
explicit clasa virtuala de baza. La rularea acestui exemplu se creeaza o singura instanta a clasei Cladire
care are 3 etaje.

Important
Clasele virtuale de baza se folosesc pentru a elimina instantele multiple care pot apare datorita mostenirii
multiple.
Initilaizarea clasei de baza virtuale este realizata de clasa cea mai derivata (in exemplul anterior
ServiceAuto).

11.5 Clase abstracte si interfete

11.5.1 Functii virtuale pure


Pana acum toate metodele claselor au avut o implementare. In C++ putem defini functii virtuale care nu
14
188
Adonis Butufei

au o implementare. Aceste metdode sunt declarare pentru a fi implementate de clasele derivate.


Exemplu:
class FiguraGeometrica
{
public:
virtual double Aria() = 0;
};

In acest exemplu clasa FiguraGeometrica are o metoda virtuala pura Aria. Clasele care au metode virtuale
pure nu pot fi instantiate urmatorul cod genereaza erori de compilare:
FiguraGeometrica f;

Ele sunt clasa de baza pentru o categorie de clase ca in exemplul urmator:

class Patrat : public FiguraGeometrica


{
double _l;
public:
Patrat(double l): _l(l) {}

double Aria () { return _l * _l; }


};

Deoarece clasa Patrat implementeaza metoda putem instantia clase de tipul Patrat.

11.5.2 Clase abstracte


In rezolvarea problemelor practice folosim concepte care grupeaza caracteristicile unor categorii concrete
de obiecte. De exemplu conceptul de cladire care are o adresa, suprafata, numar de etaje etc. Cladirile pot
fi locuinte, birouri, spatii comerciale etc. In acest context putem considera cladirea ca find un tip abstract
iar locuintele, birourile spatiile comerciale ca fiind tipuri concrete.
Pentru descrierea conceptelor folosim clasele abstracte. Aceste clase nu se instantiaza, ele sunt folosite ca
si clase de baza pentru subclasele care implementeaza aceste concepte.

In C++ o clasa care are o metoda virtuala pura este clasa abstracta. Clasa FiguraGeometrica din sectiunea
anterioara este o clasa abstracta.

In cazurile practice clasele abstracte se folosesc pentru implementarea functionalitatii comune unei familii
de clase. Metodele pentru care nu exista suficienta informatie pentru a fi implementare sunt declarate ca
virtual pure.

15
189
Adonis Butufei

11.5.3 Clase interfata


O clasa interfata are toate metodele virtual pure si nu are atribute. Clasele interfata contin doar declararea
metodelor si nici o implementare. Ele se folosesc atunci cand vrem sa definim o functionalitate pe care
clasele derivate trebuie sa o implementeze. Pentru identificare este recomandabil ca sa folosim prefixul I
pentru numele claselor interfata.
Exemplu:
class IVehicul
{
public:
virtual void Deplaseaza() = 0;
};

11.6 Sumar
Agregarea claselor se foloseste atunci cand intre clase exista o relatie de tipul parte – intreg.
Mostenirea de tipul public se foloseste atunci cand intre clase exista o relatie de tipul „este un” / „este o”.
Mostenirea de tipul protected sau private se foloseste in anumite cazuri particulare pentru implementarea
agregarii.
Metodele virtuale se folosesc pentru implementarea polimorfismului. Acest mecanism permite folosirea
instantelor claselor derivate acolo unde se folosec referinte sau pointeri de tipul clasei de baza.
Clasele de baza trebuie sa aiba destructorul virtual pentru a asigura dealocarea corecta.
Clasele de baza virtuale sunt folosite in cazul mostenrii multiple pentru a asigura o singura instanta a
clasei de baza.
Metodele virtuale pure sunt metodele virtuale care contin doar declararea metodei.
O clasa abstracta are cel putin o metoda virtuala pura.
Clasele interfata au toate metodele virtuale pure.

11.7 Intrebari si exercitii


1. Scrieti declaratia clasei abstracte Vehicul care are metoda Deplasare. Adaugati clasele Masina si
Autobuz care sunt derivate din Vehicul.
2. Scrieti declaratia clasei abstracte Motor, adaugati doua clase derivate MotorDiesel si
MotorCuBenzina.
3. Folosind relatia de agregare ataugati la clasa Vehicul un atribut privat de tipul pointer la motor.
In constructorul clasei Masina creati un MotorCuBenzina iar in constructorul clasei Autobuz creati
un MotorDiesel.
4. Scrieti declaratia unei clase interfata pentru telecomanda unei masini de jucarie care se poate deplasa
inainte, inapoi, la stanga si la dreapta.
5. Declarati clasa abstracta Animal care are un atribut varsta. Adaugati doua clase derivate Cal cu
metoda Alearga si Pasare cu metoda Zboara. Creati o clasa Pegas care este derivata din clasele Cal si
Pasare. Cum trebuie declarata mostenirea pentru a avea o singura instanta de tip animal in obiectele
de tip Pegas?
16
190
Adonis Butufei

6. Implementati exemplul cu clasele Cladire, Garaj, Birou si ServiceAuto.


7. Definiti o clasa care poate descrie orice forma simpla de tipul patrat, cerc sau triunghi echilateral.
Marimea acestor trei tipuri se poate reduce la o singura dimensiune. Definiti clasele derivate pentru
toate cele trei tipuri de clase. Creati o functie virtuala in clasa de baza care returneaza aria fiecarei
forme.
8. Scrieti o clasa de baza pentru animalele de casa. Definiti doua clase derivate Peste si Caine cu
caracteristicile fiecarui animal.Scrieti functii virtual pure in clasa de baza pentru operatiile care sunt
comune ambelor tipuri de animale care sunt realizate in mod diferit pentru fiecare din ele.
9. Urmatorul cod genereaza erori de compilare. Care este motivul?
class Imprimanta
{
public:
virtual void Tipareste() = 0;
};

int main()
{
Imprimanta i;
return 0;
}

10. Care este valoarea afisata pe ecran la rularea urmatorului exemplu?


class Baza
{
public:
int Valoare (){ return 5; }
};

class Derivata : public Baza


{
public:
int Valoare () { return 10; }
};

int main()
{
Baza *p = new Derivata;
cout << p->Valoare() << "\n";
delete p;
}

11.8 Bibliografie
• Teach yourself C++ In an Hour A day, Sixth Edition, Sams, Jesse Liberty, Cap 11.

17
191
Adonis Butufei

• Teach yourself C++ In an Hour A day, Sixth Edition, Sams, Jesse Liberty, Cap 12.
• Practical C++ Programming, Second Edition, O'Reilly, Steve Oualline, Cap 21.
• C++ Without Fear, Second Edition, Prentice Hall, Brian Overland, Cap 17.
• C++ Without Fear, Second Edition, Prentice Hall, Brian Overland, Cap 18.

18
192
Adonis Butufei

12. PROGRAMARE GENERICA

CUPRINS
12.Programare generica................................................................................................................................2
12.1 Functii template................................................................................................................................2
12.1.1 Lista de parametri generici.......................................................................................................3
12.1.2 Definirea functiilor template....................................................................................................3
12.1.3 Apelul functiilor template.........................................................................................................4
12.1.4 Specializarea completa a functiilor template............................................................................4
12.1.5 Supraincarcarea functiilor template..........................................................................................5
12.1.6 Apelul operatorilor si functiile template...................................................................................6
12.1.7 Parametri care nu sunt tipuri generice......................................................................................7
12.1.8 Selectia functiilor pentru apel.................................................................................................10
12.2 Clase template................................................................................................................................12
12.2.1 Declararea claselor template...................................................................................................12
12.2.2 Folosirea claselor template.....................................................................................................13
12.2.3 Folosirea parametrilor care nu sunt tipuri generice................................................................14
12.2.4 Valori implicite pentru parametri generici..............................................................................14
12.2.5 Specializarea completa a claselor template............................................................................16
12.2.6 Specializarea partiala a claselor template...............................................................................19
12.2.7 Rezolvarea ambiguitatii de specializare.................................................................................22
12.2.8 Selectarea specializarii pentru clasele template......................................................................22
12.2.9 Atribute statice pentru clase template.....................................................................................22
12.2.10 Functii friend pentru clase template.....................................................................................23
12.2.11 Clase friend template............................................................................................................24
12.3 Organizarea codului pentru functiile si clasele template................................................................25
12.4 Operatorii de cast generici .............................................................................................................25
12.4.1 Operatorul static_cast<> ........................................................................................................26
12.4.2 Operatorul dynamic_cast<> ..................................................................................................26
12.4.3 Operatorul reinterpret_cast<> ...............................................................................................27
12.5 Sumar.............................................................................................................................................28
12.6 Intrebari și exercitii........................................................................................................................29
12.7 Bibliografie.....................................................................................................................................30

1
193
Adonis Butufei

12. PROGRAMARE GENERICA


Unul dintre subiectele studiate in capitolul anterior a fost polimorfismul. Acest mecanism permite
implementarea unui comportament diferit pentru aceeasi functie in clase diferite.
In practica exista cazuri in care este necesara efectuarea aceleiasi procesari pe tipuri diferite de date.
De exemplu, comportamentul unei stive este acelasi in cazul unei stive de caramizi si al unei stive de
farfurii. Acelasi algoritm de sortare se poate folosi pentru a sorta valorile unui tablou de variabile intregi,
unui tablou de siruri de caractere etc.
Pentru rezolvarea acestui tip de probleme este folosita programarea generica care in C++ este
implementata cu ajutorul functiilor si claselor template.
Un template poate fi gandit ca o forma de tort: toate torturile au aceeasi forma de baza dar compozitia
poate fi diferita. Cei care au lucrat cu Open Office sau MS Office probabil ca au folosit documente
template sau sabloane. Folosind documentele template putem crea mai usor alte documente care au
caracteristici comune (stiluri de formatare, fonturi etc) dar continutul este diferit.
Programarea generica ofera un mecanism similar pentru scrierea codului separand functionalitatea de
tipul de date.
Clasele si functiile template inlocuiesc tipurile concrete de date cu tipuri generice. Aceste tipuri generice
declara un sablon care este folosit de compilator pentru generarea codului corespunzator prin inlocuirea
tipurilor de date generice cu cele de la apelul functiilor sau declararea instatelor.
In acest capitol vom discuta despre:
• Functii si clase template.
• Specializarea functiilor si claselor template.
• Operatori de cast generici.

12.1 Functii template


Functiile prezentate anterior permit dezvoltarea mult mai eficienta a programelor. Pentru functiile
obisnuite este necesara specificarea tipurilor parametrilor. Exista situatii practice cand trebuie sa
executam aceeasi prelucrare insa cu tipuri de date diferite. Pentru aceasta este necesara folosirea
supraincarcarii functiilor cu noile tipuri de parametri.
Sa presupunem ca vrem sa implementam o functie care primeste doua variabile de tip numeric si
returneaza maximul lor. Folosind functiile obisnuite vom avea cel putin doua implementari. Una pentru
variabile de tip int:
int Max(int x, int y)
{
return (x > y) ? x : y;
}

Si una pentru variabile de tip double:


double Max(double x, double y)
{
return (x > y) ? x : y;
}

2
194
Adonis Butufei

12.1.1 Lista de parametri generici


Folosind functiile template putem crea un tipar pentru o familie de functii. Ideea de baza este scrierea
functiei fara a specifica tipurile pentru unele sau pentru toate variabilele. In schimb, folosim un
substituent pentru tip numit tip generic care se declara folosind cuvantul cheie typename1 de un nume.
Exemplu:
typename TipGeneric

Aceasta spune compilatorului ca poate sa inlocuiasca TipGeneric cu orice tip.


La inceputul functiilor si claselor template avem lista de parametri generici. Aceasta incepe cu cuvantul
cheie template si cuprinde toate tipurile generice folosite in definirea functiei sau clasei separati prin
virgula.

Mai jos este prezentata lista cu un singur parametru generic:


template <typename T>

In urmatorul exemplu avem o lista cu doi parametrii generici:


template <typename T1, typename T2>

12.1.2 Definirea functiilor template


Functiile template sunt similare cu functiile obisnuite. Singura diferenta este folosirea tipurilor generice in
implementarea functionalitatii. In exemplul de mai jos este prezentata definirea functiei template Max:
1: template <typename T>
2: T Max(T x, T y)
3: {
4: return (x > y) ? x : y;
5: }

Linia 1 contine linia parametrilor template. Linia 2 contine semnatura funciei. Aici putem distinge
elementele: tipul de return (T in cazul nostru), numele functiei urmata de lista de parametri sau argumente
delimitata de paranteze rotuntde. Parametri sunt separati prin virtgula.

Intre liniile 3 – 5 este corpul functiei delimitat de acolade. In cazul de fata se foloseste operatorul ternar ?:
pentru selectia maximului care returneaza x daca x > y respectiv y in caz contrar.

Nota
Desi in cazul de fata atat tipul de return cat si parametrii functiei sunt generici, aceasta nu este obligatoriu
pentru functiile template. Putem scrie functii template care au tip de return void sau orice tip standard sau
definit de utilizator (non template). De asemenea parametri functiilor pot sa fie de orice tip non template.

1 In contextul functiilor si claselor template cuvintele cheie class si typename folosite pentru declararea
parametrilor generici sunt echivalente. Din punct de vedere istoric, la inceput a fost folosit cuvantul cheie
class pentru parametrii generici. Ulterior in standardul C++ a fost introdus cuvantul cheie typename.
3
195
Adonis Butufei

12.1.3 Apelul functiilor template


In exemplul de mai jos sunt prezentate modurile de apel pentru functiile template.
1: int n1 = 3, n2 = 5;
2: int max1 = Max(n1,n2);
3: int max2 = Max<int>(n1,n2);
4:
5: double dVal = Max(3.5, 7.3);
6: char cVal = Max('a', 'B');

In linia 1 se definesc doua variabile de tip int n1 si n2 cu valorile 3 respectiv 5.


In linia 2 este un apel implicit al functiei template pentru tipul int.
In linia 3 este un apel explicit al functiei template pentru tipul int.
Liniile 5 si 6 contin apeluri implicite pentru tipurile double respectiv char.

Nota:
Atunci cand se compileaza acest cod compilatorul foloseste definitia functiei template substituind tipurile
generice cu cele folosite pentru apel pentru a genera functiile concrete care se vor apela. O functie
template este folosita pentru generarea de catre compilator a unei intregi familii de functii cu tipuri
concrete.

12.1.4 Specializarea completa a functiilor template


Specializarea presupune inlocuirea parametrilor generici cu tipuri concrete de date2. In cazul functiilor
template este necesara inlocuirea tuturor parametrilor generici si din acest motiv specializarea se numeste
completa.

De ce este necesara specializarea functiilor template?


Exista cazuri cand pentru anumite tipuri de date concrete este necesara o prelucrare diferita de cea
generica.
De exemplu daca vrem sa folosim functia Max prezentata anterior pentru siruri de caractere este necesar
sa apelam functia strcmp pentru compararea sirurilor de caractere pentru a obtine rezultatul corect.
char *nume1 = "Ionescu";
char *nume2 = "Popescu";
cout << Max(nume1, nume2) << "\n";

Definim o versiune a functiei special pentru cazul in care parametrii sunt de tipul siruri de caractere. Cand
compilatorul intalneste un apel cu acest tip de parametrii cauta mai intai functiile specializate si daca
gaseste o functie specializata care prototipul identic cu cel care trebuie apelat, apeleaza acea functie in
locul functiei template.

2 Prin tipuri concrete de date se intelg toate tipurile predefinite in limbaj (char, int, double etc.) precum si tipurile definite de
utilizator (structuri, uniuni, clase).
4
196
Adonis Butufei

In exemplul nostru implementarea specializata este:


1: template <>
2: char* Max<char*>(char *x, char *y)
3: {
4: if(strcmp(x, y) >= 0)
5: {
6: return x;
7: }
8: return y;
9: }

Urmatoarele elemente caracterizeaza o functie template specializata complet:


1. Lista de parametri template nu are elemente (linia 1).
2. Semnatura functiei contine numai tipuri concrete (linia 2).
3. In dreapa numelui functiei se pun tipurile concrete pentru parameri generici intre delimitati de <
respectiv > (<char *>).

Apelul acestei functii se realizeaza in modul urmator:


char *x = "abc", *y = "def";
char *max1 = Max(x, y); // Apel implicit.
char *max2 = Max<char*>(x,y); // Apel explicit.

Important
Pentru a putea compila codul este necesar ca definitia functiei template sa preceada functia specializata
complet. Aceasta se realizeaza fie definind ambele functiin in aceleasi fisier fie incluzand headerul care
contine definitia functiei template in fisierul care defineste functia specializata complet.

12.1.5 Supraincarcarea functiilor template


In cazul specializarii complete parametri generici erau inlocuiti cu tipuri concrete de date. Exista cazuri in
care este necesar sa definim tot o functie template care sa implementeze un comportament similar pentru
o categorie diferita de parametri. In acest caz folosim mecanismul de supraincarcare. Supraincarcarea
functiilor template este similara cu supraincarcarea functiilor obisnuite3.
De exemplu functia template Max definita anterior, atunci cand este folosita pentru variabile de tip
pointer returneaza parametrul care are adresa mai mare deoarece functia compara pointerii nu valorile
obtinute prin indirectarea pointerilor. Pentru a corecta acest lucru este necesara supraincarcarea functiei
Max pentru parametri de tip pointer prezentata in exemplul de mai jos:
1: template<typename T>
2: T* Max(T *x, T *y)
3: {
4: return (*x > *y) ? x : y;
5: }

3 Prin functie obisnuita se intelege o functie care nu este template.


5
197
Adonis Butufei

In linia 4 se foloseste compararea valorilor si se returneaza pointerul care are valoarea mai mare.
Folosind aceasta functie se obtine rezultatul corect pentru secventa de cod de mai jos:
1: int *px = new int(10);
2: int *py = new int(9);
3:
4: int *pmax = Max(px,py);
5:
6: delete px;
7: px = 0;
8: delete py;
9: py = 0;

In liniile 1 si 2 se definesc doi pointeri de tipul int. Valorile locatiiloe de memorie sunt 10 respectiv 9.
Apelul din linia 4 returneaza pointerul pentru care locatia de memorie are valoarea cea mai mare (px in
cazul de fata).
In liniile 6 – 9 se dealoca memoria si se initializeaza valorile cu 0 corespunzatoare pointerului NULL.

Important
Compilatorul alege functia generica in functie de tipurile parametrilor de la apel. Daca parametri sunt de
tipul pointer se apeleaza functia definita in aceasta sectiune. Atunci cand parametri nu sunt pointeri se
apeleaza functia template care nu are parametri de tip pointer.

12.1.6 Apelul operatorilor si functiile template


Sa presupunem ca avem urmatoarea clasa:
class Fractie
{
int _numarator, _numitor;
public:
Fractie(int numarator, int numitor = 1) :
_numarator(numarator), _numitor(numitor) {}
};

Si dorim sa folosim functia Max pentru a returna maximul a doua fractii folosind urmatoarea secventa:
Fractie f1(5,4);
Fractie f2(2);
Fractie max = Max(f1,f2);

La compilare obtinem erori datorita faptului ca functia template compara doua instante de tipul Fractie si
clasa Fractie nu are definit operatorul >.

6
198
Adonis Butufei

Exista doua solutii pentru rezolvarea acestei probleme:


• scriem o functie template specializata pentru tipul Fractie
• implementam operatorul > in clasa Fractie

Pentru acest exemplu alegem varianta a doua si implementam operatorul > inline:
bool operator > (const Fractie& src) const
{
return (_numarator * src._numitor > _numitor * src._numarator);
}

Dupa adaugarea acestui operator apelul de mai sus se poate folosi functia template Max pentru tipul
Fractie.

12.1.7 Parametri care nu sunt tipuri4 generice


Parametri care nu sunt tipuri generice sunt parametri a caror declarare nu contine cuvantul cheie
typename.

Lista de parametri template poate contine parametri care nu sunt tipuri generice. Acestia pot sa aiba tipul
int, enumerat sau pot sa fie referinta sau pointer. In cazul in care sunt referinta sau pointer
variabilele respective trebuie sa fie variabile globale.
Acest mecanism este util pentru specificarea dimensiunii unui tablou ca in exemplul urmator:
1: template <typename T, int SIZE>
2: void PrintTablou(T tablou[])
3: {
4: for(int i = 0; i < SIZE; i++)
5: {
6: cout << tablou[i] << " ";
7: }
8: cout << "\n";
9: }

Apelul cestei functii este prezentat in exemplul de mai jos:


1: const int MAX = 3;
2: int test[MAX] = {1,2,3};
3: PrintTablou<int,MAX>(test);

Lista de parametri template ai functiei PrintTablou contine SIZE de tipul int. Aceasta valoare este folosita
pentru a transmite dimensiunea tabloului.
In linia 3 este se apeleaza functia pentru un tablou de elemente de tip int specificand dimensiunea curenta
a tabloului (MAX) in lista de parametri template.

4 In limba engleza se foloseste termenul Non-type template parameter.


7
199
Adonis Butufei

In exemplul urmator este prezentata folosirea unei functii ca parametru template.


1: template <bool F(int) >
2: bool Test(int n)
3: {
4: return F(n);
5: }
6:
7:
8: bool NumarPar(int n)
9: {
10: return (0 == n % 2);
11: }
12:
13:
14: int main()
15: {
16: int x = 5;
17:
18: bool ret = Test<NumarPar>(x);
19:
20: return 0;
21: }

In lista generica am folosit un tip de functie care primeste un parametru de tip int si returneaza rezultat
bool. In linia 4 se apeleaza funcia transmisa ca parametru generic si se returneaza rezultatul ei.
In liniile 8 – 11 este declarata o functie de verificare a numerelor pare.
Apelul din linia 18 prezinta modul de folosire.

Acest mecanism poate fi folosit, de exemplu, pentru schimbarea comportamentului unui algoritm de
sortare: daca schimbam functia de comparare putem sorta crescator sau descrescator un tablou fara a mai
schimba algoritmul.

In exemplul anterior am vazut cum se pot folosi parametri template pentru transferul functiilor. Exista
cazuri cand vrem sa transmitem functii template in loc de functii obisnuite. Exemplul de mai jos prezinta
o astfel de solutie:

8
200
Adonis Butufei

1: template <typename T, T Comparator(T, T), int SIZE>


2: T Limita(T tablou[])
3: {
4: T result = tablou[0];
5:
6: for(int i = 1; i < SIZE; ++i)
7: {
8: result = Comparator(result, tablou[i]);
9: }
10: return result;
11: }

Functia generica Limita, definita intre liniile 1 – 11 primeste ca parametru template o functie
Comparator care este folosita pentru selectia limitei maxime sau minime a tabloului.
Se incepe cu valoarea primului element in linia 4. Apoi incepand cu elementul 1 al tabloului se selecteaza
limita dintre elementul curent si valoarea limitei. La final se returneaza valoarea limitei.

Pentru calculul maximului se foloseste functia template Max definita anterior, iar pentru calculul
minimului se foloseste functia template Min definita mai jos:
1: template<typename T>
2: T Min(T x, T y)
3: {
4: return x < y ? x : y;
5: }

Mai jos este prezentat exemplul de folosire al celor trei functii:


1: const int DIM = 4;
2: int test [DIM] = {34, 19,21,-10};
3:
4: int max = Limita<int, Max<int>, DIM>(test);
5: int min = Limita<int, Min<int>, DIM>(test);

In linia 1 este definita constanta pentru dimensiunea tabloului. Tabloul este definit in linia 2. Liniile 4 si 5
folosesc cele 3 functii template pentru calculul valorilor maxime si minime ale tabloului.

9
201
Adonis Butufei

12.1.8 Selectia functiilor pentru apel


Se pot intalni cazuri practice cand avem o functie template, specializarea completa si o functie obisnuita5
cu aceeasi semnatura. Cum determina compilatorul ce functie trebuie sa apeleze in acest caz?
Compilatorul apeleaza functiile in urmatoarea ordine:
1. Daca exista o functie obisnuita compilatorul apeleaza aceasta functie.
2. Daca exista o specializare completa pentru acea functie compilatorul apeleaza functia specializata
complet.
3. Daca nu a fost selectata nicio functie la pasii anteriori se apeleaza functia template.
In exemplul de mai jos sunt prezentate toate aceste cazuri:
1: template<typename T>
2: void Print(const T &x)
3: {
4: cout << "Print<T>: " << x << "\n";
5: }
6:
7: template<>
8: void Print(const double &x)
9: {
10: cout << "Print<double>: " << x << "\n";
11: }
12:
13: template<>
14: void Print<string>(const string &x)
15: {
16: cout << "Print<string>: " << x << "\n";
17: }
18:
19: void Print(const string &x)
20: {
21: cout << "Print: " << x << "\n";
22: }

Functia template este definita in liniile 1 – 5. Specializarea completa pentru tipul double este definita in
liniile 7 – 11. Specializarea completa pentru tipul string este definita in liniile 13 – 17 iar functia non
template este definita in liniile 19 – 22.
Se observa doua variante pentru specializare. In cazul specializarii pentru double nu am mai specificat
tipul concret (linia 8 ) asa cum am procedat pentru string (linia 14). Compilatorul poate deduce tipul
concret din lista de parametri.

5 Prin functie obisnuita se intelege o functie care nu este template.


10
202
Adonis Butufei

Apelul acestor functii este prezentat mai jos:


1: string a = "abc";
2: cout << "Apel funcite non template.\n";
3: Print(a);
4:
5: cout << "\nApel explicit specializare functie template.\n";
6: Print<>(a);
7: Print<string>(a);
8:
9: double x = 1.5;
10: cout << "\nApel implicit specializare functie template.\n";
11: Print (x);
12:
13: cout << "\nApel explicit specializare functie template.\n";
14: Print<> (x);
15: Print<double> (x);
16:
17: int m = 3;
18: cout << "\nApel implicit functie template.\n";
19: Print(m);
20:
21: cout << "\nApel explicit functie template.\n";
22: Print<>(m);
23: Print<int>(m);

In linia 3 este apelata functia non template.


Daca dorim sa apelam specializarea completa pentru tipul string este necesar apelul explicit prezentat in
liniile 6 si 7: fie folosim doar simbolurile <> fie specificam si tipul pentru care dorim specializarea
<string>.

In linia 11 compilatorul apeleaza implicit specializarea pentru double a functiei template.


Apelurile din liniile 14 si 15 apeleaza explicit specializarea pentru double a functiei template.

Acelasi mod de apelare (implicit si explicit) se poate folosi si pentru functia template: liniile 19, 22 si 23.

11
203
Adonis Butufei

12.2 Clase template


Clasele template folosesc acelasi mecanism ca si functiile template si imbina programarea obiectuala cu
cea generica. Cel mai frecvent se folosesc ca si clase container (de tip tablou, lista etc) care contin
elemente de tip generic.

12.2.1 Declararea claselor template


Codul claselor generice este integral in fisierul header pentru a permite compilatorului generarea codului
in locul de apel. In exemplul de mai jos este declarat un tablou generic.
1: #ifndef TABLOU_H
2: #define TABLOU_H
3:
4: template<typename T>
5: class Tablou
6: {
7: int _size;
8: T *_data;
9:
10: public:
11: Tablou(int size = 10) :
12: _size(size)
13: {
14: _data = new T[size];
15: }
16:
17: ~Tablou()
18: {
19: delete [] _data;
20: }
21:
22:
23: T& operator [] (int index)
24: {
25: return _data[index];
26: }
27:
28:
29: int Size()
30: {
31: return _size;
32: }

12
204
Adonis Butufei

33: private:
34: Tablou(const Tablou& src);
35: Tablou& operator = (const Tablou& src);
36: };
37: #endif

In linia 4 avem lista de parametri generici care in cazul de fata prezinta tipul de elemente al tabloului.

Liniile 7 si 8 contin atributele clasei generice, in acest caz dimensiunea si pointerul pentru alocarea
tabloului.

Intre liniile 11 – 15 este definit constructorul cu ajutorul caruia se creaza un tablou de o anumita
dimensiune, valoarea implicita fiind 10 elemente.

Intre liniile 17 – 20 este definit destructorul care dealoca pointerul intern.

Operatorul de indexare este definit intre liniile 23 – 26. Acesta returneaza o referinta la elementul
specificat de index.

Metoda Size definita intre liniile 29 – 32 returneaza dimensiunea tabloului.

Pentru simplificarea exemplului constructorul de copiere si operatorul de atribuire au fost declarate


private si nu sunt implementate. Aceasta este o solutie foarte eficienta care previne copierea obiectelor de
tip tablou6.

12.2.2 Folosirea claselor template


Mai jos este prezentat un exemplu de folosire a clasei template definita anterior.
1: Tablou<int> ti(10);
2: Tablou<double> td(20);
3: ti[2] = 10;

In prima linie este instantiata clasa template. Prin declararea Tablou<int>, tipul int este folosit in locul lui
T. Astfel am creat un tablou pentru 10 elemente intregi. In a treia linie am setat valoarea 10 pentru
elementul cu indexul 2.
Folosind operatorul typedef putem defini un alias pentru tipuri frecvent utilizare ale clasei generice in
modul urmator:
typedef Tablou<int> IntTablou; // Tablouri de intregi
typedef Tablou<double> DoubleTablou; // Tablouri de double

6 Se genereaza eroare de compilare atunci cand se incearca apelarea operatorului de atribuire sau a constructorului de
copiere.
13
205
Adonis Butufei

Cu aceasta definitie putem inlocui linia 1 din exemplul anterior cu urmatoarea:


IntTablou ti(10);

Nota
Este recomandata folosirea operatorului typedef pentru simplificarea codului si reducerea erorilor de
scriere.

12.2.3 Folosirea parametrilor care nu sunt tipuri generice


Parametri care nu sunt tipuri generice se pot folosi in mod similar si pentru clasele template. Putem
specifica dimensiunea tabloului intern folosind parametri non generici. In acest mod nu mai este necesara
alocarea dinamica a tabloului si codul se poate modifica in modul urmator:
1: #ifndef TABLOU_H
2: #define TABLOU_H
3:
4: template <typename T, int SIZE>
5: class Tablou
6: {
7: T _data[SIZE];
8: public:
9: Tablou() {}
10: T& operator[] (int index) { return _data[index]; }
11: int Size() { return SIZE; }
12: private:
13: Tablou(const Tablou& src);
14: Tablou& operator = (const Tablou& src);
15: };
16: #endif

In acest caz SIZE este folosit ca o constanta pentru alocarea automata a tabloului si nu mai este necesara
implementarea constructorului sau a destructorului.
Instantierea acestui tablou se poate face in modul urmator:
Tablou<int, 5> tablou;

In acest caz se instantiaza un tablou de cinci elemente de tip intreg.

12.2.4 Valori implicite pentru parametri generici


Ca si in cazul functiilor si metodelor obisnuite clasele template pot avea valori implicite pentru parametri
generici. Aceasta ajuta la simplificarea declararii instantelor acestor clase.
Regulile sunt similare:
• Valorile implicite trebuie sa fie specificate incepand cu ultimul parametru de la dreapta.
• Valorile implicite trebuie sa fie specificate succesiv. Nu putem avea parametri generici fara valoare
implicita specificata intre alti doi parametri generici cu valoare implicita specificata.

14
206
Adonis Butufei

• Daca utilizatorul nu specifica o valoare pentru acel parametru, compilatorul alege valoarea implicita.
• Atunci cand utilizatorul specifica o valoare diferita pentru parametrul generic se alege valoarea
specificata de utilizator.
In exemplul de mai jos este prezentata varianta clasei tablou cu valoare implicita pentru dimensiune:
1: #ifndef TABLOU_H
2: #define TABLOU_H
3:
4: template <typename T, int SIZE = 10>
5: class Tablou
6: {
7: T _data[SIZE];
8: public:
9: Tablou() {}
10: T& operator[] (int index) { return _data[index]; }
11: int Size() { return SIZE; }
12: private:
13: Tablou(const Tablou& src);
14: Tablou& operator = (const Tablou& src);
15: };
16:
17: #endif

Instantierea acestui tablou pentru elemente de tip int cu dimensiunea implicita este prezentata mai jos:
Tablou<int> tablou;

In exemplul de mai jos este prezentata varianta clasei tablou cu valori implicite pentru tipul elementelor si
pentru dimensiune:
1: #ifndef TABLOU_H
2: #define TABLOU_H
3:
4: template <typename T = double, int SIZE = 10>
5: class Tablou
6: {
7: T _data[SIZE];
8:
9: public:
10: Tablou() {}
11: T& operator[] (int index) { return _data[index]; }
12: int Size() { return SIZE; }

15
207
Adonis Butufei

13: private:
14: Tablou(const Tablou& src);
15: Tablou& operator = (const Tablou& src);
16: };
17: #endif

Mai jos este prezentat un exemplu de instantiere folosind valorile implicite pentru toti parametri generici:
Tablou <> tablou;

Aceasta instanta este un tablou cu 10 elemente de tip int.

12.2.5 Specializarea completa a claselor template


Specializarea claselor template pentru anumite tipuri este similara cu specializarea functiilor template.
Scopul specializarii claselor template este pentru optimizarea codului sau pentru a corecta un
comportament eronat pentru diferite tipuri. In cazul clasei Tablou este necesara, de exemplu, specializarea
pentru tipul sirurilor de caractere.
In cazul claselor template putem specializa intreaga clasa sau numai o parte din metode. Atunci cand se
specializeaza intreaga clasa este necesara declararea si implementarea intregului set de metode al clasei
template.
In urmatorul exemplu este prezentata specializarea completa a clasei Tablou pentru tipul char*.
1: typedef char* PCHAR;
2:
3: template <>
4: class Tablou <PCHAR>
5: {
6: int _size;
7: PCHAR *_data;
8:
9: public:
10: Tablou(int size = 10) :
11: _size(size)
12: {
13: _data = new PCHAR[_size];
14:
15: for(int i = 0; i < _size; ++i)
16: {
17: _data[i] = 0;
18: }
19: }
20:

16
208
Adonis Butufei

21: ~Tablou()
22: {
23: for(int i = 0; i < _size; ++i)
24: {
25: if(_data[i] != 0)
26: {
27: delete [] _data[i];
28: }
29: }
30:
31: delete [] _data;
32: }
33:
34: PCHAR operator [] (int index) { return _data[index]; }
35:
36: void Set(int index, PCHAR elem)
37: {
38: if(_data[index] == elem)
39: {
40: return;
41: }
42:
43: delete [] _data[index];
44:
45: if(elem == 0)
46: {
47: _data[index] = elem;
48: return;
49: }
50:
51: _data[index] = new char[strlen(elem) + 1];
52: strcpy(_data[index], elem);
53: }
54:
55: int Size() { return _size; }
56:
57: private:
58: Tablou(const Tablou& src);
59: Tablou& operator = (const Tablou& src);
60: };

Pentru a simplifica definirea tipului in linia 1 a fost utilizata instructiunea typedef char* PCHAR.
17
209
Adonis Butufei

Observam ca lista parametrilor generici din linia 3 nu are niciun element, in schimb in linia 2 sunt
specificate toate tipurile pentru acesti parametri. Declaratia clasei specializate trebuie sa contina toti
membrii clasei generice iar parametrii generici sunt inlocuiti cu tipurile concrete folosite pentru
specializare.

In linia 7 este declarat tabloul intern de siruri de caractere.


Constructorul este declarat intre liniile 10 – 19. Pentru a asigura eliberarea corespunzatoare a memoriei
este necesara setarea valorii 0 pentru toti pointerii de tipul sir de caractere din liniile 15 – 18.

Destructorul este declarat in liniile 21 – 32. Observam ca mai intai se elibereaza memoria pentru fiecare
sir de caractere din tablou, apoi se elibereaza memoria alocata pentru tablou.

Operatorul de indexare este declarat la linia 34. In clasa template se returna o referinta la elementul de la
indexul specificat. In clasa specializata operatorul returneaza chiar elementul, care este un pointer.

Aceasta abordare simplifica operatiile de gestiune a memoriei si de copiere a datelor in interiorul


tabloului. Utilizatorul foloseste operatorul de indexare pentru citirea elementelor din tablou, fara a
dealoca acei pointeri. Pentru setare se foloseste metoda Set, care este adaugata in aceasta clasa. Alocarea
si dealocarea se face automat in specializarea completa a clasei Tablou.
Mai jos este prezentat un exemplu de instantiere si utilizare a specializarii complete pentru clasa Tablou:
1: Tablou<char*> test(5);
2: test.Set(0, "aa");
3: test.Set(1, "bb");
4: cout << test[1] << "\n";

In linia 1 se instantiaza un tablou cu 5 elemente de tip char*, compilatorul selecteaza specializarea


completa. In liniile 2 si 3 se seteaza valoarea elementelor in tablou. In linia 4 se afiseaza sirul de caractere
corespunzator celui de-al doilea element.
Important
In cazul specializarii complete nu se pot seta valori implicite pentru parametri generici si nu se pot folosi
parametri care nu sunt tipuri generice.

18
210
Adonis Butufei

12.2.6 Specializarea partiala a claselor template


In cazul specializarii partiale numai o parte din parametri template sunt inlocuiti.
In practica se pot intalni cateva categorii de specializare.
1. Specializarea de la tipul generic la pointer de tip generic.
In acest caz pentru unul sau mai multi parametri generici tipul este specializat in pointer la tipul generic
dupa cum este prezentat in exemplul de mai jos:
1: template <typename U, typename V>
2: class Demo
3: {
4: public:
5: Demo() { cout << "Clasa generica U V\n"; }
6: };
7:
8: template <typename U, typename V>
9: class Demo<U*,V*>
10: {
11: public:
12: Demo() { cout << "Clasa specializata partial U* V*\n"; }
13: };

In liniile 1 – 6 este declarata o clasa template cu doi parametri. In liniile 8 – 13 este declarata o clasa
template partial specializata pentru pointeri la U si V.
Urmatoarele obiecte sunt instante ale clasei generice:
Demo<int,double> demo1;
Demo<int*, double> demo2;
Demo<int, double*> demo3;

Pentru a instantia clasa partial specializata este necesar ca ambii parametri generici sa fie de tip pointer ca
in exemplul de mai jos:
Demo<int*, double*> demo4;

2. Specializarea in care doua tipuri generice devin identice.


Urmatorul exemplu este o specialziare partiala care intra in aceasta cadegorie pentru clasa Demo
prezentata anterior:
1: template <typename U>
2: class Demo<U,U>
3: {
4: public:
5: Demo() { cout << "Clasa specializata partial U U\n"; }
6:
7: };
19
211
Adonis Butufei

Urmatorul obiect este o instanta a acestei specializari:


Demo<int, int> demo5;

3. Specializarea cu parametri care nu sunt tipuri generice.


In cazul in care exista paramentri care nu sunt tipuri generice se poate implementa o specializare partiala
specificand tipul parametrului generic cum este prezentat in exempul de mai jos pentru clasa Tablou.
1: typedef char* PCHAR;
2: template <int SIZE>
3: class Tablou<PCHAR,SIZE>
4: {
5: PCHAR _data[SIZE];
6:
7: public:
8: Tablou()
9: {
10: for(int i = 0; i < SIZE; ++i)
11: {
12: _data[i] = 0;
13: }
14: }
15:
16: ~Tablou()
17: {
18: for(int i = 0; i < SIZE; ++i)
19: {
20: delete [] _data[i];
21: }
22: }
23:

20
212
Adonis Butufei

24: void Set(int index, PCHAR element)


25: {
26: if(_data[index] == element)
27: {
28: return;
29: }
30:
31: delete [] _data[index];
32:
33: if(element == 0)
34: {
35: _data[index] = element;
36: return;
37: }
38:
39: _data[index] = new char[strlen(element) + 1];
40: strcpy(_data[index], element);
41: }
42:
43: PCHAR operator [] (int index) { return _data[index]; }
44: int Size () { return SIZE; }
45: private:
46: Tablou(const Tablou& src);
47: Tablou& operator = (const Tablou& src);
48: };

Pentru acest exemplu s-a pastrat declararea tipului typedef char * PCHAR; prezentata in sectiunile
anterioare. Implementarea este simplificata fata de exemplul cu specializarea completa deoarece tabloul
nu este alocat dinamic.
Mai jos este prezentat un exemplu de instantiere si utilizare pentru aceasta specializare:
1: Tablou1<PCHAR, 3> test;
2: test.Set(0, "aa");
3: test.Set(1, "bb");
4: cout << test[1] << "\n";

In linia 1 se instantiaza un tablou de 3 elemente de tip sir de caractere. Se observa ca dimensiunea este
transmisa ca si parametru template nu ca parametru al constructorului.
In liniile 2 si 3 se seteaza valori pentru elementele tabloului iar in linia 4 se afiseaza continutul
elementului al doilea pe ecran.

Nota
Pentru acest caz exista si varianta de specializare cand se pastreaza parametrul de tip generic si se
specifica valoarea pentru cel care nu este tip generic.
21
213
Adonis Butufei

Important
• Folosisrea specializarii partiale reduce numarul de parametri din lista de parametri generici.
• Folosirea specializarii partiale este recomandata atunci cand functionalitatea pentru variabilele de tip
pointer necesita prelucrari suplimentare pentru alocare, eliberare sau initializare a memoriei.
• Specializarea claselor, completa sau partiala, se poate face doar daca este inclus fisierul clasei genreice
sau in acelasi fisier cu clasa generica.
• Nu se pot folosi valori implicite si specializarea partiala concomitent.

12.2.7 Rezolvarea ambiguitatii de specializare


Atunci cand pentru instantierea unei clase template se poate folosi mai mult de o specializare partiala
compilatorul genereaza o erorare de ambiguitate. Pentru eliminarea acestei erori este necesara adaugarea
unei specializari care sa poata fi folosita in acest caz.
De exemplu pentru urmatorul obiect:
Demo<int*, int*> demo5;

se pot folosi doua specializari partiale: cea prezentata in cazul 1 pentru pointeri si cea prezentata in cazul
2 si din acest motiv compilatorul genereaza eroare.

Pentru rezolvarea acestei erori este necesara adaugarea urmatoarei specializari:


template <typename U>
1: class Demo<U*,U*>
2: {
3: public:
4: Demo() { cout << "Clasa specializata partial U* U*\n"; }
5:
6: };

12.2.8 Selectarea specializarii pentru clasele template


Atunci cand se instantiaza o clasa template compilatorul cauta mai intai o specializare completa, daca
aceasta nu exista cauta o specializare partiala, in cazul in care nu exista o specializare partiala se
instantiaza clasa generica.

12.2.9 Atribute statice pentru clase template


Atunci cand clasele template au artibute statice este necesara definirea acestor membri. In exemplul de
mai jos este prezentata definirea unui atribut static in liniile 6 si 7.
1: template <typename T>
2: struct Test
3: {
4: static T _m; // Declararea atributului static _m.
5: };

22
214
Adonis Butufei

6: template<typename T> // Definirea atributului static _m.


7: T Test<T>::_m;

Pentru simplitate a fost folosita o structura care are tipul de acces implicit public.
Important
Toate instantele claselor pentru un anumit tip partajeaza acelasi atribut static. La rularea urmatorului
exemplu valoarea afisata pe ecran este 2.0.
1: Test<double> a;
2: a._m = 2.0;
3:
4: Test<double> b;
5: cout << b._m << "\n";

Instantele a si b de tipul double partajeaza atributul static _m. In linia 2 se seteaza valoarea atributului
folosind instanta a. In linia 5 se afiseaza pe ecran folosind instanta b.

12.2.10 Functii friend pentru clase template


Exista cazuri practice cand este necesara folosirea unor functii friend pentru clasele template.
Sa presupunem ca avem urmatoarea functie template care seteaza valoarea pentru atributul _t:
1: template<typename T, typename U>
2: void Set(T &instanta, const U &valoare) { instanta._t = valoare; }

Primul parametru generic al functiei este pentru tipul clasei, al doilea tip generic este pentru tipul
atributului. In linia 3 se setaza valoarea pentru atributul _t instantei instanta.
Dorim sa declaram aceasta functie friend pentru urmatoarea clasa:
1: template<typename T>
2: class DemoFriend
3: {
4: T _t;
5: public:
6: T Get() { return _t; }
7: };

Pentru a putea accesa atributul _t din clasa DemoFriend este necesara folosirea specializarii complete
functiei Set ca functie friend ca in exemplul de mai jos:
1: template<typename T>
2: class DemoFriend
3: {
4: T _t;
5: public:
6: T Get() { return _t; }
7: friend void Set<>(DemoFriend<T> &instanta, const T &valoare);
8: };

23
215
Adonis Butufei

Acum urmatorul cod se compileaza si ruleaza corect:


1: DemoFriend<int> x;
2: Set(x,2);
3: cout << x.Get() << "\n";

Important
Atunci cand se folosesc functii template friend este necesara declararea versiunii specializate ca in
exemplul prezentat in aceasta sectiune.

12.2.11 Clase friend template


Exista cazuri practice cand este necesar ca doua clase template sa fie declarate friend. Acest scenariu
apare de obicei atunci cand se folosesc clase de tip container lista, tablou etc pentru colectii de date.
Pentru accesarea elementului este necesara implementarea unui set de functii care se grupeaza in clase
accesor. Conceptual este necesara separarea operatiilor de accesla elementul curent de setul de operatii al
colectiei. Clasele accesor trebuie sa fie declarate friend pentru a putea accesa elementul curent din
container.

In exemplul de mai jos este prezentat modul de a folosi clasele friend template:
1: template <typename U> class Accesor;
2:
3: template <typename T>
4: class Container
5: {
6: template <typename U> friend class Accesor;
7: T _t; // Atribut privat.
8: public:
9: Accesor<T> Get() { return Accesor<T> (_t); }
10: };
11:
12: template <typename U>
13: class Accesor
14: {
15: U &_u;
16: public:
17: Accesor(U &u) : _u(u) {}
18: U& operator * () { return _u; }
19: };

In linia 1 se foloseste declararea anticipata pentru clasa Accesor. Aceasta informeaza compilatorul despre
clasa template Accesor.
Clasa Container este declarata in liniile 3 – 10. In linia 6 este scrisa declaratia friend pentru clase
Accesor.
Observam ca este folosit un alt parametru generic decat cel din lista de la linia 3, aceasta este necesar
deoarece parametri generici trebuie sa fie unici.
24
216
Adonis Butufei

Pentru simplitate clasa Container are un singur atribut de tip T.


Metoda publica Get returneaza o clasa Accesor.
Clasa Accesor este declarata in liniile 12 – 19. Are un atribut de tipul referinta la parametru generic U
care este initializata in constructor.
Operatorul * , definit in linia 18 este folosit pentru accesul la variabila _u.
Constructorul de copiere si operatorul = generati de compilator functioneaza corect in acest caz.
Mai jos este prezentat un exemplu de utilizare pentru clasele Container si Accesor.
1: Container<int> c;
2: Accesor<int> a = c.Get();
3: *a = 10;

12.3 Organizarea codului pentru functiile si clasele template


Codul generic este putin diferit de codul obisnuit. Pentru codul obisnuit prototipurile functiilor si
declararea claselor folosea fisierele header iar implementarea acestor functii sau clase se facea in fisierele
sursa.
In cazul functiilor si claselor template acest lucru nu mai este posibil deoarece codul acestor functii este
doar sablonul care este folosit de compilator pentru instantierea definitiilor template. Daca am folosi
aceeasi organizare a codului in fisiere header si fisiere sursa compilarorul nu ar avea informatii suficiente
pentru instantierea codului generic si ar genera erori. Din acest motiv este necesar sa plasam codul
template integral in fisierele header.

12.4 Operatorii de cast generici


In capitoul 4 am intalnit operatorii de cast preluati din limbajul C. Ei lucreaza foarte bine pentru tipurile
de date standard definite in limbajele C/C++ insa aplicati claselor si pointerilor catre clase poate rezulta
un cod care se poate compila cu succes si care genereaza erori de rulare.
Exemplu:
1: #include <iostream>
2: using namespace std;
3: class A
4: {
5: float _i, _j;
6: };
7:
8: class B
9: {
10: int _x, _y;
11: public:
12: B (int a, int b) { _x=a; _y=b; }
13: int Result() { return _x + _y;}
14: };

25
217
Adonis Butufei

15: int main ()


16: {
17: A d;
18: B * padd = (B *) &d;
19: cout << padd->Result();
20: return 0;
21: }

In acest exemplu a fost creata o instanta a clasei A in linia 18 si apoi, in linia 19 convertim o variabila de
tipul pointer la clasa B la adresa instantei d. In linia 20 este apelata metoda Result a clasei B.
Acest cod se compileaza cu succes insa functionarea este incorecta.

12.4.1 Operatorul static_cast<>


Acest operator poate realiza conversii de la o clasa derivata la clasa de baza si invers. Aceasta asigura compatibilitatea
tipurilor.
Exemplu:
1: class A {};
2: class B : public A {};
3: class C {};
4: A *a = new B;
5: B * b = static_cast<B*>(a); // este permisa
6: C * c = static_cast<C*>(a); // eroare de compilare
In acest exemplu operatia de cast din linia 5 este permisa insa operatia din linia 6 genereaza eroare de compilare.
Se poate folosi operatorul static_cast si pentru tipurile de baza ca in urmatorul exemplu:
double x = 3.56;
int i = static_cast<int>(x);

12.4.2 Operatorul dynamic_cast<>


Exista cazuri cand avem nevoie de verificari detaliate ale pointerului, care se pot realiza doar in timpul
rularii. Folosind operatorul anterior urmatorul cod se poate compila:
1: class A {};
2: class B : public A {};
3: A *a = new A;
4: B * b = static_cast<B*>(a);

Codul din linia 4 se compileaza, insa este incorect. La final avem un pointer la o clasa partial initializata.
Folosirea acestui pointer este periculoasa si poate genera erori greu de identificat.
In cazurile in care nu putem verifica instanta este necesara folosirea operatorului dynamic_cast<> care
foloseste informatia despre tipul obiectelor in timpul rularii7 pentru verificari suplimentare. Cu acest
operator putem realiza conversii intre clasele polimorfice. O clasa este polimorfica daca are cel putin o
metoda virtuala.

7 In engleza se foloseste termenul Run -Time Type Information RTTI.


26
218
Adonis Butufei

Exemplu:
1: class A {};
2: class B : public A {};
3: A *a = new B;
4: B * b = static_cast<B*>(a);

Acest cod genereaza erori de compilare deoarece clasa A nu are metode virtuale. Adaugand destructorul
virtual ca in exemplul de mai jos erorile de compilare sunt eliminate.
1: class A
2: {
3: public:
4: virtual ~A() {};
5: };
6: class B : public A {};

Operatorul dynamic_cast verifica instantele obiectelor si daca nu sunt indeplinite conditiile, rezultatul
operatiei de cast este pointerul NULL, ca in exemplul de mai jos:
1: A *a = new A;
2: B *b = dynamic_cast<B*>(a); // b va fi NULL
3: A *c = new B;
4: B *d = dynamic_cast<B*>(c); // d va fi diferit de NULL.

Deoarece valoarea lui a este o instanta a clasei de baza rezultatul operatiei de cast din linia 2 este
pointerul NULL. In cazul operatiei de cast din linia 4 se obtine adresa corecta a obiectului.
Nota
Executia operatorului dynamic_cast<> necesita mai mult timp si este recomandata folosirea lui doar in
cazurile unde nu se poate verifica tipul obiectelor in momentul compilarii.

12.4.3 Operatorul reinterpret_cast<>


Acest operator converteste orice tip, rezultatul este o copie binara de la un pointer la altul. Toate
conversiile sunt permise. Nu este facuta nicio verificare a pointerilor sau locatiilor de memorie catre care
pointeaza. Operatorul reinterpret_cast<> se foloseste atunci cand ceilalti operatori prezentati anterior nu
se pot folosi.
1: class A {};
2: class B : public A {};
3: class C {};
4: A a;
5: C *p = reinterpret_cast<A*>(&a);

27
219
Adonis Butufei

12.5 Sumar
• Functiile si clasele template folosesc o lista de parametri generici a caror declarare incepe cu cuvantul
cheie template.
• Parametrii generici se declara folosind unul dintre cuvintele cheie echivalente typename sau class.
• In cazul in care sunt mai multi parametri generici, acestia sunt separati prin virgula.
• Specializarea functiilor template presupune scrierea functiei inlocuind parametrii generici cu tipuri
concrete si se foloseste in cazurile in care compilatorul nu poate genera cod care sa execute corect
operatiile generice (cum este cazul copierii sirurilor de caractere C).
• Pentru clase exista doua tipuri de specializare: partiala si totala. In cazul specializarii partiale doar o
parte din tipurile generice este inlocuita iar specializarea totala se obtine atunci cand toti parametrii
generici sunt inlocuiti.
• Functiile template permit doar specializarea completa.
• Este recomandabila scrierea integrala a codului generic in fisierele header.
• Operatorul static_cast<> permite verificarea tipului claselor si genereaza erori cand se realizeaza
conversia intre clase care nu apartin aceleiasi ierarhii.
• Operatorul dynamic_cast<> se foloseste pentru verificari suplimentare in timpul rularii. In cazul in
care tipul este valid insa instanta nu este corecta rezultatul operatiei de cast este pointerul NULL.
• Operatorul reinterpret_cast<> se foloseste in cazurile in care static_cast<> si dynamic_cast<> nu se
pot folosi. Dezvoltatorul este responsabil pentru verificari atunci cand foloseste acest operator.

28
220
Adonis Butufei

12.6 Intrebari și exercitii


4. Care este diferenta dintre parmetrii unei functii template si cei ai unei functii normale.
5. Implementati funcia template Min.
6. Implementati specializarea functiei Min pentru siruri de caractere.
7. Implementati clasa generica Tablou.
8. Folosind clasa generica Tablou implementati clasa Coada in care primul element introdus este
elementul returnat.
9. Implementati o clasa generica Set care este un container de 10 elemente si are urmatoarele metode:
Add – adauga un element la set.
Reset – elimina toate elementele din set.
Test – verifica daca elementele sunt in set.
10. Care este valoarea pointerului x in exemplul de mai jos:
class A
{
public:
virtual ~A()
};

class B : public A {};


class C : public B {};

int main()
{
A *a = new A;
C *x = dynamic_cast<C*>(a);
return 0;
}

11. Implementati operatorul () intr-o clasa template care primeste un parametru T si nu returneaza
valori. Acest operator scrie / afiseaza parametrul pe ecran.
12. Adaugati specializare pentru numerele complexe definite in capitolele anterioare.
13. Se poate compila urmatorul cod fara erori?
class A {};
class B : protected A {};
int main()
{
A *a = new B;
B * b = static_cast<B*>(a);
return 0;
}

29
221
Adonis Butufei

12.7 Bibliografie
• Teach yourself C++ In an Hour A day, Sixth Edition, Sams, Jesse Liberty, Cap 15.
• Teach yourself C++ In an Hour A day, Sixth Edition, Sams, Jesse Liberty, Cap 11.
• Practical C++ Programming, Second Edition, O'Reilly, Steve Oualline, Cap 24.
• C++ Templates: The Complete Guide, Addison Wesley, David Vandevoorde; Nicolai M. Josuttis, Cap
2, 3, 4, 6.

30
222
Adonis Butufei

13. INTRARE IESIRE

CUPRINS
13.Intrare / iesire...........................................................................................................................................2
13.1 Fluxuri de date..................................................................................................................................2
13.1.1 Folosirea bufferelor pentru citire si scriere...............................................................................2
13.2 Citirea cu istream.............................................................................................................................2
13.2.1 Citirea sirurilor de caractere.....................................................................................................2
13.3 Scrierea cu ostream..........................................................................................................................3
13.3.1 Formatarea scrierii....................................................................................................................3
13.3.2 Mutarea cursorului pe o linie noua...........................................................................................3
13.3.3 Reprezentarea numerelor in baze diferite.................................................................................4
13.3.4 Setarea preciziei si formatului numerelor reale........................................................................5
13.3.5 Setarea latimii pentru urmatoarea scriere.................................................................................6
13.3.6 Alinierea mesajelor...................................................................................................................7
13.4 Supraincarcarea operatorilor de intrare iesire pentru clase .............................................................7
13.5 Scrierea / citirea fisierelor pe disc....................................................................................................8
13.5.1 Scrierea fisierelor......................................................................................................................9
13.5.2 Citirea fisierelor........................................................................................................................9
13.5.3 Fisiere de intrare si iesire........................................................................................................11
13.5.4 Optiunile pentru deschiderea fisierelor la scriere...................................................................12
13.5.5 Fisiere binare si fisiere text.....................................................................................................12
13.6 Fluxuri pentru siruri de caractere...................................................................................................12
13.7 Sumar.............................................................................................................................................13
13.8 Intrebari si exercitii........................................................................................................................14
13.9 Bibliografie.....................................................................................................................................14

1
223
Adonis Butufei

13. INTRARE / IESIRE


In capitolele anterioare am citit valorile variabilelor simple de la tastatura si am afisat rezultatele pe ecran.
In acest capitol vom discuta despre:
• Fluxuri de date.
• Clasele folosite pentru citirea si scrierea din / in fisiere.
• Citirea si scrirerea din siruri de caractere.
• Formatarea scrierii.
• Supraincarcarea operatorilor de intrare / iesire pentru clase.

13.1 Fluxuri de date


Un fisier este o colectie de date. C++ considera un fisier ca o serie de octeti sau flux de date. Majoritatea
fisierelor sunt stocate fizic pe disc. Imprimantele, liniile de comunicare, tastatura etc. sunt si ele
considerate fisiere.
In C++ interactiunea cu fluxurile de date se realizeaza prin intermediul a trei clase de baza: istream
pentru citire, ostream pentru scriere si iostream pentru citire si scriere.

13.1.1 Folosirea bufferelor pentru citire si scriere


Operatiile de citire si scriere pe disc sunt consumatoare de timp. Pentru optimizarea acestor activitati clasele prezentate
anterior folosesc bufferele. Acestea sunt zone de memorie tampon. De exemplu datele scrise intr-un flux se colecteaza in
buffer. In momentul cand bufferul este plin, continutul acestui buffer se salveaza in fisier. O problema a acestui sistem
este ca atunci cand programul se blocheaza, datele nesalvate din buffer se pierd.

13.2 Citirea cu istream


Pentru exemplele din aceasta sectiune vom folosi clasa implicita care citeste valorile de la tastatura cin.

13.2.1 Citirea sirurilor de caractere


Sa presupunem ca vrem sa citim de la tastatura valoarea numelui ca in exemplul de mai jos:
char nume [15];
cin >> nume;

Ce se intampla atunci cand de la tastatura au fost introduse mai mult de 15 caractere? In acest caz se
depaseste dimensiunea alocata pentru variabila nume si comportamentul programului este nedeterminat.
O solutie pentru aceasta problema este sa folosim manipulatorii.
Un manipulator este un obiect folosit pentru modificarea fluxului de intrare (sau iesire) atunci cand
efectuam operatii de citire (scriere). Pentru a folosi manipulatorii este necesara includerea fisierului
<iomanip>.
Exemplu:
char nume [15];
cin >> setw(15) >> nume;

2
224
Adonis Butufei

In acest caz, datorita manipulatorului setw se vor citi numai 14 caractere de la tastatura1.

Metodele get
Operatorul >> citeste valorile pana la urmatorul spatiu sau enter. Exista cazuri cand dorim sa citim si
aceste valori. In acest caz folosim metodele get. In urmatorul exemplu se citeste cate un caracter:
for(char ch = cin.get(); ch != EOF; ch = cin.get())
{
cout << "ch: " << ch << "\n";
}

Aceasta bucla citeste toate caracterele de la tastatura si le afiseaza pe ecran pana in momentul cand de la
tastatura se introduce Ctrl + z care corespunde sfarsitului de fisier EOF.

A doua metoda get primeste un sir de caractere ca parametru si este prezentata in exemplul urmator:
char nume[15];
cin.get(nume,15);

In acest exemplu se citesc maxim 14 caractere de la tastatura in variabila nume. Citirea se opreste daca
utilizatorul a apasat Enter sau daca s-a atins numarul limita de caractere.

Metoda getline
Aceasta metoda functioneaza similar cu get, primeste bufferul si dimensiunea2 acestuia ca parametri, dar
in momentul cand intalneste caracterul de linie noua muta cursorul dupa el (il descarca din flux), spre
deosebire de get la care pozitia curenta ramane la caracterul de linie noua.
Exemplu:
char nume[15];
cin.getline(nume,15);

13.3 Scrierea cu ostream

13.3.1 Formatarea scrierii


De multe ori este necesara formatarea informatiei, pentru a putea fi citita mai usor de pe ecran, pentru a
putea fi transmisa unui alt program ca date de intrare etc. Pentru aceasta in C++ se folosesc manipulatorii.

13.3.2 Mutarea cursorului pe o linie noua


Am vazut anterior ca pentru a muta cursorul pe o linie noua se foloseste secventa escape '\n'. Pentru a

1 Lasand un spatiu pentru terminatorul de string '\0'.


2 Aceste metode au 3 parametri, ultimul parametru este delimitatorul a carui valoare implicita este caracterul de linie noua
'\n'.
3
225
Adonis Butufei

usura scrierea, in C++ s-a definit manipulatorul endl care poate fi folosit ca in exemplul urmator:
#include <iostream>
#include <string>
using namespace std;

int main()
{
string mesaj = "hello";
cout << mesaj << endl;
return 0;
}

Folosirea lui endl ofera avantajul separarii mesajului de formatare. In acest fel putem schimba
formatarea3 fara a modifica mesajul.

13.3.3 Reprezentarea numerelor in baze diferite


Afisarea numerelor intregi se poate face in cele trei baze prezentate in capitolele anterioare.
Exemplu:
1: #include <iostream>
2: using namespace std;
3: int main()
4: {
5: int i = 25;
6: cout << "Reprezentarea numarului " << i;
7: cout << " fara afisarea bazei" << endl;
8: cout << "in baza 16: " << hex << i << endl;
9: cout << "in baza 10: " << dec << i << endl;
10: cout << "in baza 8: " << oct << i << endl;
11: cout << endl;
12:
13: cout.setf(ios_base::dec); // Reset flags.
14:
15: cout.setf(ios::showbase); // Setarea flagului pentru afisarea bazei.
16: cout << "Reprezentarea numarului " << i;
17: cout << " cu afisarea bazei" << endl;
18: cout << "in baza 10: " << dec << i << endl;
19: cout << "in baza 16: " << hex << i << endl;
20: cout << "in baza 8: " << oct << i << endl;
21:
22: cout.unsetf(ios::showbase);
23: cout.setf(ios_base::dec);

3 De exemplu daca am vrea sa afisam mesajul diferit pe ecran si intr-un fisier de log.
4
226
Adonis Butufei

24: return 0;
25: }

In liniile 8 - 10 se afiseaza valoarea 25 in bazele 16, 10 si 8. In liniile 18 – 20 se afiseaza si baza impreuna


cu valoarea. In linia 22 am resetat valorile pentru afisarea bazei iar in linia 23 am setat baza 10 pentru
afisare. Acestea sunt necesare doarece revine la starea implicita a lui cin.

Pentru afisarea bazei am folosit manipulatorii dec, hex, oct iar pentru afisarea bazei am folosit
ios::showbase.

13.3.4 Setarea preciziei si formatului numerelor reale


Numerele pot fi afisate folosind doua formate: stiintific, care afiseaza puterile lui 10 si cu numar de
zecimale fix.
Exemplu:
1: #include <iostream>
2: using namespace std;
3:
4: int main()
5: {
6: double x = 2.0 / 7.0;
7:
8: cout << "Precizie implicita" << endl;
9: cout << "Variabila double format fixed: " << endl;
10: cout << fixed << x << endl << endl;
11:
12: cout << "Variabila double format scientific: " << endl;
13: cout << scientific << x << endl << endl;
14:
15: int newPrec = 7;
16: int oldPrec = cout.precision(newPrec);
17:
18: cout << "Precizie: " << newPrec << endl;
19: cout << "Variabila double format fixed: " << endl;
20: cout << fixed << x << endl << endl;
21:
22: cout << "Variabila double format scientific: " << endl;
23: cout << scientific << x << endl << endl;
24:
25: cout.precision(oldPrec);
26:
27: return 0;
5
227
Adonis Butufei

28: }

In acest exemplu in linia 10 este afisata variabila x cu precizia implicita in formatul fixed. Acest format
presupune ca am un numar fix de zecimale. In linia 13 se foloseste formatul stiintific pentru afisare apoi
se repeta procesul pentru o precizie de 7 zecimale in liniile 20, 23. In linia 25 se seteaza precizia
implicita.

13.3.5 Setarea latimii4 pentru urmatoarea scriere


Sunt cazuri cand dorim sa specificam latimea urmatoarei scrieri. Aceasta se realizeaza cu manipulatorul
setw ca in urmatorul exemplu:
1: #include <iostream>
2: #include <string>
3: #include <iomanip>
4:
5: using namespace std;
6:
7: int main()
8: {
9: string mesaj = "hello";
10: cout << mesaj << endl;
11: cout << setw(10) << mesaj;
12:
13: return 0;
14: }

Pentru folosirea acestui manipulator este necesara includerea fisierului iomanip (linia 3).
In linia 10 scriem mesajul normal apoi in linia 11 specificam o latime de 10 caractere pentru scriere.
Deoarece alinierea implicita este la dreapta, cuvantul hello are 5 litere si latimea este de 10 caractere, pe
ecran apare un spatiu de 5 caractere intre cele doua mesaje.

Daca mesajul are mai multe caractere decat latimea specificata cu setw, se ignora valoarea specificata
de setw si se scrie mesajul complet incepand cu marginea stanga ca in exemplul de mai jos:
#include <iostream>
#include <string>
#include <iomanip>

using namespace std;

int main()
{
string mesaj = "hello";

4 Latimea este masurata in caractere.


6
228
Adonis Butufei

cout << mesaj << endl;


cout << setw(4) << mesaj << endl;

return 0;
}

Deoarece variabila mesaj are 5 caractere si am setat o latime de 4 caractere pe ecran va apare al doilea
mesaj complet, scrierea incepe in acest caz din marginea stanga.

13.3.6 Alinierea mesajelor


Exista cazuri cand vrem ca mesajele sa fie aliniate intr-un anumit fel. Aceasta se realizeaza cu ajutorul
manipulatorilor left, right ca in exemplul urmator:
#include <iostream>
#include <string>
#include <iomanip>

using namespace std;

int main()
{
string x = "hello";
cout << setw(10) << left << x << endl;
cout << setw(10) << right << x << endl;

return 0;
}

Observam ca acesti manipulatori se folosesc impreuna cu setw. Este necesar ca latimea specificata sa fie
mai mare decat numarul de caractere al mesajelor pentru a obtine rezultatul dorit.

13.4 Supraincarcarea operatorilor de intrare iesire pentru clase


In C++ putem supraincarca operatorii de intrare si iesire pentru clase. Deoarece primul parametru este de
tip stream este necesara implementarea acestor operatori ca functii friend. In urmatorul exemplu vom
prezenta operatorii pentru clasa Complex definita in capitolele precedente. In fisierul header adaugam
urmatoarele declaratii:

1: #ifndef COMPLEX_H
2: #define COMPLEX_H
3: #include <iostream>
4: using namespace std;
5:
6: class Complex

7
229
Adonis Butufei

7: {
8: // ...
9: public:
10: // ...
11: friend ostream& operator << (ostream& out, const Complex &c);
12: friend istream& operator >> (istream& in, Complex &c);
13: };
14: #endif

Pentru a putea declara operatorii in linia 3 a fost inclus fisierul <iostream> apoi in linia 4 este adaugata
directiva using. In linia 11 a fost declarat operatorul de scriere iar in linia 12 operatorul de citire. Restul
implementarii ramane neschimbat.
Implementarea acestor operatori este prezentata mai jos:
1: ostream& operator << (ostream& out, const Complex &c)
2: {
3: out << c._re << " " << c._im;
4: return out;
5: }
6:
7: istream& operator >> (istream& in, Complex &c)
8: {
9: in >> c._re;
10: in >> c._im;
11: return in;
12: }

In linia 3 scriem valorile campurilor real si imaginar in fluxul de iesire. Observam ca este plasat un spatiu
intre cele doua valori. Acesta este necesar pentru a putea delimita cele doua campuri la citire.
Citirea valorilor din fluxul de intrare se realizeaza in liniile 9 si 10.

13.5 Scrierea / citirea fisierelor pe disc


Cand se lucreaza cu fisierele pe disc este necesara folosirea variantei pentru fisiere a acestor clase
ifstream, ofstream respectiv fstream care sunt declarate in fisierul <fstream>.

13.5.1 Scrierea fisierelor


Mai jos este prezentat un exemplu simplu de scriere intr-un fisier:
1: #include <iostream>
2: #include <iomanip>
3: #include <fstream>
4: using namespace std;
5:

8
230
Adonis Butufei

6: int main()
7: {
8: ofstream out("Exemplu.dat");
9:
10: if(! out)
11: {
12: cout << "Deschiderea fisierului a esuat!\n";
13: return 1;
14: }
15:
16: out << "Prima linie" << endl;
17: out << "A doua linie" << endl;
18: out.close();
19:
20: out.open("Exemplu.dat",ios::app);
21: out << "A treia linie" << endl;
22: out.close();
23: return 0;
24: }

In linia 8 este creat un obiect de tipul stream de iesire. In liniiile 10 – 14 se verifica daca deschiderea
fisierului este cu succes. Apoi se scrie informatia in fisier in liniile 16 – 17. Inchidem fisierul in linia 18.
In linia 20 deschidem din nou fisierul pentru a scrie la sfarsitul lui urmatoarea linie.

Nota
In linia 8 am folosit doar numele curent pentru fisier. In acest caz fisierul este creat daca nu exista in
directorul curent.

13.5.2 Citirea fisierelor


Mai jos este prezentat un exemplu de citire dintr-un fisier:
1: #include <iostream>
2: #include <iomanip>
3: #include <fstream>
4: using namespace std;
5:
6: int main()
7: {
8: ifstream in("Exemplu.dat");
9:
10: if(! in)
11: {
12: cout << "Deschiderea fisierului a esuat!\n";
9
231
Adonis Butufei

13: return 1;
14: }
15:
16: const int MAX = 256;
17: char linie[MAX];
18:
19: while(in)
20: {
21: in.getline(linie,MAX);
22: cout << linie << endl;
23: }
24:
25: in.close();
26: return 0;
27: }

In linia 8 este creat un obiect de tipul stream de intrare. In liniiile 10 – 14 se verifica daca deschiderea
fisierului este cu succes. Apoi in liniile 19 – 23 se citeste informatia din fisier si se afiseaza pe ecran.

Nota
Putem folosi clasa string in locul sirurilor de caractere C, incluzand fisierul string. In acest caz
partea de citire a codului devine:
1: while(in)
2: {
3: string linie;
4: getline( in , linie);
5: cout << linie << endl;
6: }

Functia getline din linia 4 este declarata in fisierul string.

13.5.3 Fisiere de intrare si iesire


Fluxurile din ultimele doua sectiuni erau specializate pentru scriere sau citire. In C++ este posibila
folosirea aceluiasi flux si pentru intrare si pentru iesire folosind clasa ifstream. Operatiile de scriere si
de citire se pot face in orice pozitie in fisier. Aceasta clasa are o pozitie curenta pentru scriere si o pozitie
curenta pentru citire. Deplasarea in acest fisier cu un numar de octeti se face in trei moduri: fata de
inceput, fata de sfarsit sau fata de pozitia curenta prin apelul functiei seekg pentru citire si seekp pentru
scriere.
Exemple de mutare a cursorului de citire:
1: ifs.seekg(20, ios::cur);
2: ifs.seekg(-50, ios::end);
3: ifs.seekg(15, ios::beg);

In linia 1 se muta cursorul cu 20 de octeti inainte fata de pozitia curenta. In linia 2 se muta cursorul cu 50

10
232
Adonis Butufei

de octeti inapoi fata de sfarsitul fisierului. In linia 3 se muta cursorul cu 15 octeti fata de inceputul
fisierului. Variabila ifs este o instanta de tip ifstream.
Pentru a muta cursorul de la inceputul fisierului este necesar urmatorul apel:
ifs.seekg(0, ios::beg);

Iar pentru a muta cursorul la sfarsitul fisierului este necesar urmatorul apel:
ifs.seekg(0, ios::end);

Mai jos este prezentat un exemplu de citire si scriere intr-un fisier:


1: #include <iostream>
2: #include <iomanip>
3: #include <fstream>
4: using namespace std;
5:
6: int main()
7: {
8: fstream iofile("ExempluRW.dat",
9: ios::in | ios::out | ios::trunc);
10:
11: if(! iofile)
12: {
13: cout << "Deschiderea fisierului a esuat!\n";
14: return 1;
15: }
16:
17: iofile << "Prima linie" << endl;
18: iofile << "A doua linie" << endl;
19:
20: iofile.seekp(-14,ios::end);
21: iofile << "Linia finala" << endl;
22:
23: iofile.seekg(0,ios::beg);
24:
25: while(iofile)
26: {
27: string linie;
28: getline(iofile, linie);
29: cout << linie << endl;
30: }
31:
32: iofile.close();
33: return 0;

11
233
Adonis Butufei

34: }

In linia 9 sunt setate valorile pentru deschiderea fisierului. Cand se foloseste optiunea ios::in pentru
deschiderea unui fisier nou acesta nu este creat. Adaugarea optiunii ios::trunc creaza fisierul atunci cand
nu exista si sterge continutul fisierului existent atunci cand exista. In liniile 17 si 18 s-au scris doua
mesaje in fisier. In linia 20 se muta cursorul de scriere la sfarsitul primului mesaj si se scrie alt mesaj in
linia 21. In linia 23 se muta cursorul de citire la inceputul fisierului si se afisaza continutul fisierului pe
ecran in liniile 25 – 30. In linia 32 se inchide fisierul. La rularea acestui exemplu numai primul si ultimul
mesaj sunt afisate deoarece al doilea a fost sters prin mutarea cursorului de scriere.

13.5.4 Optiunile pentru deschiderea fisierelor la scriere


In tabelul de mai jos sunt prezentate optiunile pentru deschiderea fisierelor la scriere:
Optiune Explicatie

ios::app adauga la sfarsitul fisierului.

ios::ate muta cursorul la sfarsitul fisierului dar se poate scrie oriunde

ios::trunc sterge vechiul continut al fisierelor existente, acesta este valoarea implicita

ios::nocreate daca fisierul nu exista operatia de deschidere esueaza

ios::noreplace daca fisierul exista operatia de deschidere esueaza

13.5.5 Fisiere binare si fisiere text


Toate datele fisierelor text sunt reprezentate ca siruri de caractere. Acest format are avantajul ca poate fi
editat foarte usor. Fisierele binare ofera o eficienta sporita insa aceste fisiere nu mai pot fi editate cu
ajutorul editoarelor de text. Pentru a deschide un fisier in modul binar este necesara folosirea optiunii
ios::binary.

13.6 Fluxuri pentru siruri de caractere


Exista un set de clase de flux pentru siruri de caractere care permit scrierea si citirea informatiei din siruri
de caractere. Spre deosebire de cin si cout aceste clase nu sunt conectate la un dispozitiv de intrare /
iesire. Aceste clase pot fi folosite pentru gruparea informatiei si afisarea ei ulterioare. In fisierul sstream
sunt definite 3 clase principale: istringsream derivata din istream, ostringstream derivata din
ostream si stringstream derivata din iostream.
Mai jos este prezentat un exemplu de folosire pentru stringstream.
1: #include <string>
2: #include <sstream>
3:
4: int main()
5: {
6: stringstream stream;
7: int x, y = 10;
8:
9: stream << y;

12
234
Adonis Butufei

10:
11: cout << stream.str() << endl;
12: stream >> x;
13:
14: stream.str("");
15: stream.clear();
16:
17: return 0;
18: }

In linia 6 se creaza fluxul pentru siruri de caractere. In linia 9 se scrie in flux valoarea variabilei y. Prin
apelul metodei str in lina 11 se afiseaza continutul fluxului pe ecran. In linia 12 se citeste valoarea din
flux in variabila x. Liniile 14 si 15 sterg continutul fluxului.

13.7 Sumar
• Fisierele sunt organizate ca un flux de date. Pentru citirea continutului acestor fisiere se folosesc
clasele de flux.
• Fisierele pot fi stocate pe hard disc sau pot fi reprezentate prin dispozitive de intrare sau iesire precum
tastatura sau ecranul.
• Exista trei clase de flux de baza definite in fisierul iostream: istream pentru fluxuri de intrare,
ostream pentru fluxuri de iesire si iostream pentru fluxuri de intrare si iesire.

• Pentru modificarea fluxurilor de intrare si iesire se folosesc clase speciale numite manipulatori.
• Operatiile de scriere si citire pe disc sunt consumatoare de timp si din acest motiv se folosesc buffere.
Acestea sunt variabile in memorie care colecteaza datele ce trebuiesc scrise in fisier sau in care se
citesc date din fisier.

13.8 Intrebari si exercitii


1. Ce sunt fluxurile de date?
2. Care sunt cele trei clase de baza folosite pentru lucrul cu fluxurile de date?
3. Ce sunt manipulatorii?
4. Ce manipulator trebuie folosit pentru a limita numarul de caractere citit de la tastatura?
5. Care este diferenta dintre metodele get si getline ale clasei cin?
6. Scrieti un program care citeste de la tastatura un numar si afiseaza acel numar cu precizie de 3
zecimale in format fixed.
7. Scrieti un program care citeste de la tastatura un numar si il afiseaza in bazele 8, 10 si 16.
8. Modificati programul de la exercitiul anterior astfel incat sa afiseze si baza.
9. Scrieti un program care citeste dintr-un fisier urmatoarele date: numarul de elemente apoi fiecare
valoare numerica si initializeaza un tablou cu aceste valori.

13
235
Adonis Butufei

10. Implementati supraincarcarea operatorilor de intrare iesire pentru clasa complex si salvati un numar
complex intr-un fisier si apoi cititi si initializati alta variabila complex. La final verificati valoarea
variabilelor.

13.9 Bibliografie
• Teach yourself C++ In an Hour A day, Sixth Edition, Sams, Jesse Liberty, Cap 27.
• Practical C++ Programming, Second Edition, O'Reilly, Steve Oualline, Cap 16.

14
236
Adonis Butufei

14. TRATAREA ERORILOR SI EXCEPTII

CUPRINS
14.Tratarea erorilor si exceptii......................................................................................................................2
14.1 Moduri de tratare a erorilor..............................................................................................................2
14.1.1 Returnarea unei valori de succes..............................................................................................2
14.1.2 Terminarea programului in cazul erorilor ................................................................................3
14.1.3 Testarea conditiilor cu assert....................................................................................................3
14.1.4 Mecanismul exceptiilor ...........................................................................................................4
14.2 Tratarea exceptiilor...........................................................................................................................4
14.2.1 Ridicarea exceptiilor.................................................................................................................5
14.2.2 Tratarea exceptiilor blocurile try / catch...................................................................................5
14.2.3 Utilizarea mai multor blocuri catch pentru tratarea exceptiilor................................................6
14.2.4 Tratarea oricarei exceptii, blocul catch(...)...............................................................................7
14.3 Ridicarea exceptiilor in blocurile catch............................................................................................8
14.4 Exceptii, domeniu de viata si alocarea resurselor............................................................................8
14.5 Exceptii si metodele claselor..........................................................................................................12
14.5.1 Ridicarea exceptiilor in constructori.......................................................................................12
14.5.2 Tratarea exceptiilor in lista de initializare a constructorilor...................................................13
14.5.3 Exceptiile si destructorii.........................................................................................................14
14.6 Crearea claselor si ierarhiilor de clase pentru exceptii...................................................................15
14.6.1 Moduri de transfer al exceptiilor catre blocul catch...............................................................16
14.6.2 Ordinea blocurilor catch in cazul ierarhiilor de exceptii........................................................17
14.7 Sumar.............................................................................................................................................17
14.8 Intrebari si exercitii........................................................................................................................18
14.9 Bibliografie.....................................................................................................................................18

1
237
Adonis Butufei

14. TRATAREA ERORILOR SI EXCEPTII


Atunci cand se ruleaza programele pot apare erori: utilizatorul a introdus o valoare incorecta, un
dispozitiv nu a trimis raspunsul la o comanda, un fisier nu se poate deschide din cauza drepturilor de
acces etc.
Tratarea erorilor depinde de etapa in care se afla programul si necesita o buna planificare pentru obtinerea
unui cod de calitate. Adaugarea mecanismului de tratare a erorilor dupa implementarea programului de
cele mai multe ori genereaza un cod care este greu de mentinut: fixarea unui defect adauga alte defecte.
Atunci cand se intampla acest lucru problema este la nivelul conceptiei nu la nivelul implementarii si
rezolvarea ei presupune rescrierea unor parti ale programului.
In timpul dezvoltarii programului sunt utile anumite strategii de verificare a parametrilor, a rezultatelor
intermediare de calcul etc. care pot semnala erori interne greu de identificat in etapa de testare.
Alte erori apar datorita interactiunii programului cu sistemul de operare sau cu utilizatorul.
Pentru tratarea acestor conditii de eroare in C++, ca in majoritatea limbajelor de programare obiectuale, se
foloseste mecanismul exceptiilor.

In acest capitol vom discuta despre:


• Moduri de tratare a erorilor.
• Utilizarea exceptiilor pentru tratarea erorilor.
• Crearea propriilor clase si ierarhii de clase de exceptie.

14.1 Moduri de tratare a erorilor

14.1.1 Returnarea unei valori de succes


Acest mecanism este folosit pentru limbajele de programare care nu suporta exceptii si in situatiile in care
folosirea mecanismului exceptiilor nu este adecvata.
In exemplele de pana acum in functia main in ultima linie se returna 0 indicand terminarea programului
cu succes.
Sa presupunem ca avem functia Factorial. In cazul in care variabila de intrare are valoare negativa trebuie
semnalata eroarea. Putem in acest caz returna, de exemplu, -1 ca in exemplul de mai jos:
int Factorial(int n)
{
if (n < 0)
{
return -1;
}
//...
}

Urmatorul apel trebuie sa trateze intr-un fel eroarea:


int f = factorial (x); // x este o variabila definita anterior
if(f < 0)

2
238
Adonis Butufei

{
//...
}
else
{
}

Nu intotdeauna este posibila tratarea erorii in locul in care a aparut. In aceste cazuri este necesar
transferul unor parametri si mesaje care sa semnaleze eroarea. Asta presupune aparitia unor blocuri if /
else in multe locuri in cod si acestea sunt dificil de intretinut si inteles.

Datorita faptului ca tratarea erorilor presupune doar verificarea valorilor returnate de functii este posibila
ignorarea acestor verificari si continuarea executiei in conditii eronate.

14.1.2 Terminarea programului in cazul erorilor


In cazul aparitiei erorilor programul informeaza utilizatorul despre eroare si executia se opreste.
Exemplu:
1: int Factorial(int n)
2: {
3: if(n < 0)
4: {
5: cerr << "Parametrul de intrare are valoare negativa\n";
6: exit(1);
7: }
8: // ...
9: }

In acest exemplu dupa afisarea mesajului de eroare executia programului se termina prin apelul functiei
exit.
Acesta este un mod radical de a trata erorile. Nu intotdeauna exista suficienta informatie pentru a trata
eroarea in locul in care a aparut.

14.1.3 Testarea conditiilor cu assert


In dezvoltarea programelor este necesara testarea conditiilor si functionarii corecte a codului. Pentru
aceasta mediile de dezvoltare integrata folosesc doua configuratii principale Debug si Release.
In configuratia Debug compilatorul adauga cod suplimentar care ajuta dezvoltatorul sa verifice valorile
parametrilor functiilor pentru a identifica apeluri gresite, rezultatul unor calcule pentru a identifica
depasirea intervalelor de valori etc.
Configuratia Release este folosita pentru compilarea codului de productie care va fi livrat clientului.
Pentru aceasta configuratie compilatorul elimina acest cod suplimentar si adauga optimizari pentru
performanta.

3
239
Adonis Butufei

Functia assert se foloseste pentru verificarea conditiilor in configuratia Debug. Pentru utilizarea acestei
functii este necesara includerea fisierului cassert. Atunci cand conditia nu este verificata, assert
opreste executia programului si afiseaza numele fisierului si linia in care conditia nu a fost verificata.
Mai jos este prezentat un exemplu simplu de folosire a functiei assert:
1: #include <iostream>
2: #include <cassert>
3: using namespace std;
4:
5: void Print(int *x)
6: {
7: assert( x != NULL);
8: cout << *x << endl;
9: }
10:
11: int main()
12: {
13: int *x = NULL;
14: Print(x);
15: return 0;
16: }

Functia print, inainte de afisarea valorii catre care pointeaza parametrul x, apeleaza assert pentru a
verifica daca acesta este un pointer valid. Deoarece apelul este explicit cu valoarea NULL, la rularea
acestui exemplu, dupa linia 7, executia programului se opreste si utilizatorul obtine informatii despre
conditia care a esuat, fisierul si linia in care se afla conditia care a esuat.
Important
assert se foloseste pentru conditiile care nu ar trebui sa apara. Programul se opreste atunci cand aceste
conditii apar. Mentenanta este usurata.

14.1.4 Mecanismul exceptiilor


Exceptiile se folosesc pentru problemele care nu apar frecvent dar care pot apare. Aceste probleme
ocazionale, atunci cand apar, se pot aborda intr-un mod flexibil si progresiv:
• Executarea unei actiuni de corectie fara perturbarea utilizatorului.
• Informarea utilizatorului si oferirea alternativelor de corectie a problemei pentru continuare.
• Informarea utilizatorului si oprirea eleganta a programului.
• Oprirea brusca a programului.

14.2 Tratarea exceptiilor


Partile tratarii exceptiilor:
• Calculatorul incearca sa execute o secventa de instructiuni. Acest cod poate sa aloce resurse, sa
acceseze o baza de date, sa autentifice un utilizator etc.
4
240
Adonis Butufei

• Exista un cod scris pentru a trata situatiile cand acea secventa esueaza din anumite motive.
• In cazul in care secventa de instructiuni care se executa este in alta functie este necesar un mecanism
de transfer al informatiilor intre punctul in care a aparut problema si codul care trateaza problema.
Mecanismul tratarii exceptiilor conecteaza cele trei elemente prezentate mai sus si permite separarea
codului care trateaza eroarea de codul obisnuit, asigurand o intelegere mai usoara a fiecarei parti si
reducand eforturile de mentenanta.

14.2.1 Ridicarea exceptiilor


In momentul cand apare o eroare in executia unui program si continuarea procesarii nu mai este posibila
se pot ridica exceptii folosind cuvantul cheie throw urmat de exceptia pe care vrem sa o ridicam.
Exceptia poate fi o variabila numerica, un sir de caractere sau o clasa.
Exemplu:
1: int Factorial(int n)
2: {
3: if(n < 0)
4: {
5: throw "Parametru negativ.";
6: }
7:
8: // ...
9: }

In acest exemplu, in linia 5 se ridica o exceptie de tipul sir de caractere atunci cand parametrul de intrare
este negativ. Dupa ridicarea unei exceptii, executia programului se opreste daca aceasta nu este tratata
intr-un bloc try / catch prezentat in sectiunea urmatoare. Spre deosebire de valorile returnate
exceptiile nu pot fi ignorate!

14.2.2 Tratarea exceptiilor blocurile try / catch


In blocul try sunt introduse instructiunile, apelurile etc. care pot ridica exceptii. Aceasta este ramura
normala de executie a programului. In blocurile catch (se introduce cate un bloc catch pentru fiecare
exceptie care este tratata) se introduce codul pentru tratarea exceptiilor. Un bloc catch se numeste
handler pentru ca el trateaza eroarea.
Mai jos este prezentat un exemplu de tratare a exceptiei pentru functia Factorial.
1: #include <iostream>
2: #include "Factorial.h"
3: using namespace std;
4: int main()
5: {
6: try
7: {
8: int result = Factorial(-1);

5
241
Adonis Butufei

9: }
10: catch(char* msg)
11: {
12: cout << msg << endl;
13: }
14: return 0;
15: }

In blocul try este introdus apelul functiei Factorial (linia 8) apoi in blocul catch liniile 10 – 13 sunt
tratate exceptiile de tip sir de caractere. Dupa ridicarea exceptiei in functia Factorial, executia
programului se muta in linia 12 care trateaza exceptia: in cazul de fata afiseaza mesajul pe ecran.

14.2.3 Utilizarea mai multor blocuri catch pentru tratarea exceptiilor


In exemplul anterior, pentru functia Factorial am tratat doar o conditie: cea in care valoarea parametrului
era negativa. Din capitolul 3 stim ca variabilele au un interval de valori si limita maxima pentru tipul int
este INT_MAX care are valoarea 2147483647. Este necesar sa verificam si valoarea maxima a lui n
pentru care rezultatul este corect. In cazul functiei Factorial, pentru n = 12, valoarea returnata de functie
este 479001600. Pentru valori mai mari ale lui n rezultatul este eronat datorita depasirii intervalului de
valori.
Pentru a preveni utilizarea unor rezultate incorecte este necesara ridicarea unei exceptii in cazul in care n
> 12. Putem folosi tot un sir de caractere sau putem folosi un alt tip de variabila pentru exceptie. In
exemplul de mai jos vom folosi o exceptie de tipul int.
1: int Factorial(int n)
2: {
3: const int MAX = 12; // Valoarea maxima pentru care n! este calculat
4: // corect.
5:
6: if(n < 0)
7: {
8: throw "Parametru negativ.";
9: }
10:
11: if (n > MAX )
12: {
13: throw MAX;
14: }
15: // ...
16: }

Apelul functiei se face in modul urmator:


1: #include <iostream>
2: #include "Factorial.h"
3: using namespace std;
4: int main()

6
242
Adonis Butufei

5: {
6: try
7: {
8: int result = Factorial(13);
9: }
10: catch(char* msg)
11: {
12: cout << msg << endl;
13: }
14: catch(int x)
15: {
16: cout << "Valoarea maxima: "<< x <<" a parametrului este depasita.\n";
17: }
18: return 0;
19: }

In acest exemplu avem doua blocuri de tratare a exceptiilor: unul pentru siruri de caractere si altul pentru
variabile intregi.
Nota
In practica, pentru fiecare tip de exceptie tratata se foloseste cate un bloc catch.

14.2.4 Tratarea oricarei exceptii, blocul catch(...)


Exista situatii practice in care indiferent de tipul exceptiei tratate este necesara o singura prelucrare. In
aceste cazuri se foloseste blocul catch (...) care trateaza toate exceptiile ridicate de apelulul functiilor
din blocul try.
Exemplu:
1: try
2: {
3: FunctieCareRidicaExceptii();
4: }
5: catch(FileException )
6: {
7: // ...
8: }
9: catch(MemoryException )
10: {
11: // ...
12: }
13: catch (...)
14: {
15: // trateaza toate exceptiile pentru care nu exista un bloc catch.
16: // ...
7
243
Adonis Butufei

17: }

Acest exemplu prezinta un aspect important: blocul catch(...) trebuie plasat ultimul.
In momentul cand o exceptie este ridicata in blocul try, compilatorul selecteaza blocul catch
corespunzator acelui tip. Daca nu exista un bloc catch specializat, compilatorul selecteaza blocul
catch(...). In cazul in care nici acesta nu exista, exceptia este transferata un nivel mai sus in stiva de
apel a functiilor1. In cazul in care nu exista nici un bloc care sa trateze aceasta exceptie, executia
programului se opreste.

Important
Inainte de a utiliza blocul catch(...) este utila analiza codului. Folosirea unui bloc catch specializat
este de preferat la nivelul unde exista suficienta informatie pentru a putea trata acea exceptie in mod
adecvat.

14.3 Ridicarea exceptiilor in blocurile catch


Nu intotdeauna este posibila tratarea completa a exceptiilor la un anumit nivel. Uneori doar se colecteaza
informatia despre exceptie si este necesara ridicarea aceleiasi exceptii. Alteori se ridica un alt tip de
exceptie.

Ridicarea aceleiasi exceptii se realizeaza apeland throw fara parametri ca in exemplul de mai jos:
catch(int )
{
throw;
}

Ridicarea altor tipuri de exceptii se face folosind throw urmat de tipul exceptiei.

14.4 Exceptii, domeniu de viata si alocarea resurselor

Variabilele alocate pe stiva in interiorul blocului try sunt dealocate automat in momentul ridicarii
exceptiilor.
Exemplu:
1: #include <iostream>
2: using namespace std;
3: class Data
4: {
5: public:
6: Data() { cout << "Data()" << endl; }
7: ~Data() { cout << "~Data()" << endl; }

1 Executia programelor foloseste o stiva pentru apelul functiilor. In momentul cand se termina executia unei functii aceasta
este eliminata de pe stiva si executia este continuata cu functia apelanta.
8
244
Adonis Butufei

8: };
9:
10: int main()
11: {
12: try
13: {
14: cout << "Inceputul blocului try" << endl;
15: Data d;
16: cout << "Ridicarea exceptiei" << endl;
17: throw 1;
18: cout << "Sfarsitul blocului try" << endl;
19: }
20: catch(int)
21: {
22: cout << "Tratarea exceptiei" << endl;
23: }
24: return 0;
25: }

In acest exemplu a fost utilizata o clasa simpla Data pentru afisarea mesajelor la alocarea si eliberarea
memoriei. In blocul try in linia 15 este creata o instanta a acestei clase apoi este ridicata o exceptie. La
rularea acestui program pe ecran apar urmatoarele mesaje:
Inceputul blocului try
Data()
Ridicarea exceptiei
~Data()
Tratarea exceptiei

Se observa ca dupa ridicarea exceptiei memoria pentru variabila d este eliberata si executia se continua cu
tratarea exceptiei.

Ce se intampla daca folosim alocarea dinamica a memoriei ca in exemplul de mai jos?


1: int main()
2: {
3: try
4: {
5: cout << "Inceputul blocului try" << endl;
6: Data * d = new Data;
7: cout << "Ridicarea exceptiei" << endl;
8: throw 1;
9: delete d;

9
245
Adonis Butufei

10: cout << "Sfarsitul blocului try" << endl;


11: }
12: catch(int)
13: {
14: cout << "Tratarea exceptiei" << endl;
15: }
16:
17: return 0;
18: }

Deoarece este ridicata inainte de eliberarea memoriei si domeniul de viata al pointerului se termina dupa
ridicarea exceptiei aceasta zona de memorie nu se mai elibereaza. La rularea acestui program pe ecran
apar urmatoarele mesaje:
Inceputul blocului try
Data()
Ridicarea exceptiei
Tratarea exceptiei

Important
Atunci cand se folosesc exceptiile este necesara eliberarea resurselor care au fost folosite in interiorul
blocului try. Prin eliberarea resurselor se intelege eliberarea memoriei alocate in acest bloc, inchiderea
fisierelor, inchiderea conexiunilor la bazele de date etc.

O solutie posibila de eliberare a resurselor ar fi declararea variabilelor inaintea blocului try si eliberarea
lor dupa executia blocurilol catch ca in exemplul de mai jos:
1: int main()
2: {
3: Data * d = NULL;
4: try
5: {
6: cout << "Inceputul blocului try" << endl;
7: d = new Data;
8: cout << "Ridicarea exceptiei" << endl;
9: throw 1;
10: cout << "Sfarsitul blocului try" << endl;
11: }
12: catch(int)
13: {
14: cout << "Tratarea exceptiei" << endl;
15: }
16: if (d != NULL)
17: {
18: delete d;

10
246
Adonis Butufei

19: }
20: return 0;
21: }

Acelasi rezultat se poate obtine folosind o clasa auxiliara care in constructor aloca toate resursele iar in
destructor elibereaza aceste resurse.
Exemplu:
1: class Helper
2: {
3: Data *_d;
4: public:
5: Helper (): _d(NULL)
6: {
7: _d = new Data();
8: }
9:
10: ~Helper()
11: {
12: if(_d != NULL)
13: {
14: delete _d;
15: }
16: }
17:
18: Data* D() { return _d; }
19: };

Aceasta clasa are o implementare foarte simpla: alocarea si dealocarea sunt realizate in constructor
respectiv destructor si mai exista o metoda de acces la date.
Acum programul anterior poate fi modificat sa foloseasca aceasta clasa in interiorul blocului try.
1: int main()
2: {
3: try
4: {
5: cout << "Inceputul blocului try" << endl;
6:
7: Helper h;
8:
9: Data *d = h.D();
10:
11: cout << "Ridicarea exceptiei" << endl;
12:
11
247
Adonis Butufei

13: throw 1;
14: cout << "Sfarsitul blocului try" << endl;
15: }
16: catch(int)
17: {
18: cout << "Tratarea exceptiei" << endl;
19: }
20:
21: return 0;
22: }

Folosind aceasta strategie putem grupa toate resursele folosite in interiorul blocului try in clasa Helper.
Deoarece instanta clasei Helper este alocata pe stiva destructorul acestei clase este apelat atunci cand se
termina blocul try sau cand o exceptie este ridicata in interior.

14.5 Exceptii si metodele claselor


Exceptiile pot fi folosite in mod similar in metodele si operatorii claselor. Exista cateva particularitati
pentru constructori si destructori pe care le vom discuta in aceasta sectiune.

14.5.1 Ridicarea exceptiilor in constructori


Folosirea exceptiilor este singura modalitate de a semnala erorile in constructor. Atunci cand se ridica
exceptii in constructor instanta obiectului nu mai este creata si destructorul nu mai este apelat.
Este necesara eliberarea tuturor resurselor alocate in constructor inainte de ridicarea exceptiei.
Exemplu:
1: #include <iostream>
2: using namespace std;
3:
4: class Data
5: {
6:
7: public:
8: Data() { cout << "Data()" << endl; }
9:
10: Data(int n)
11: {
12: cout << "Data(int )" << endl;
13: if (0 == (n % 2) )
14: {
15: throw n;
16: }
17: }
18:

12
248
Adonis Butufei

19: ~Data() { cout << "~Data()" << endl; }


20: };
21:
22: int main()
23: {
24: try
25: {
26: Data d(2);
27: }
28: catch(int x)
29: {
30: cout << "Tratarea exceptiei " << x << endl;
31: }
32:
33: return 0;
34: }

In acest exemplu, clasei Data i-a fost adaugat un constructor care ridica exceptii in cazul in care
parametrul este un numar par.
Ruland acest program pe ecran se afiseaza urmatoarele mesaje:
Data(int )
Tratarea exceptiei 2

Se observa ca destructorul clasei nu a mai fost apelat.

14.5.2 Tratarea exceptiilor in lista de initializare a constructorilor


Acesta este un caz special de tratare a exceptiilor si se foloseste atunci cand un atribut de tip clasa sau o
clasa de baza poate ridica exceptii in constructor.
Spre deosebire de exemplele anterioare cand tratarea exceptiei insemna disparitia (absorbtia) ei in cazul
exceptiilor tratate in constructor acest lucru nu este posibil deoarece constructorul clasei nu se mai
executa. Compilatorul ridica aceeasi exceptie dupa ce se executa codul de tratare sau utilizatorul poate
ridica un tip diferit de exceptie. Ne putem intreba care este utilitatea tratarii exceptiilor in acest caz?
Tratarea exceptiilor in lista de initializare a constructorului este utila in urmatoarele cazuri:
• Pentru logarea erorilor.
• Pentru ridicarea altei exceptii decat cea tratata.

In exemplul de mai jos este prezentata combinatia celor doua cazuri. Pentru a reduce dimensiunea, clasa
Data nu a mai fost introdusa in exemplu:
1: #include "Data.h"
2: #include <iostream>
3: using namespace std;
4: class Container

13
249
Adonis Butufei

5: {
6: Data _d;
7: public:
8: Container(int n)
9: try :
10: _d(n)
11: {
12: cout << "Container ()" << endl;
13: }
14: catch(int x)
15: {
16: cout << "Exceptia " << x << " a fost tratata in constructor" << endl;
17: throw "Containerul nu a putut fi creat";
18: }
19:
20: ~Container()
21: {
22: cout << "~Container()" << endl;
23: }
24: };
25:
26: int main()
27: {
28: try
29: {
30: Container c(2);
31: }
32: catch (char* mesaj)
33: {
34: cout << mesaj << endl;
35: }
36:
37: return 0;
38: }

In linia 9 incepe blocul try care in acest caz contine toata lista de initializare a obiectului. Blocul de
tratare a exceptiei este plasat dupa corpul constructorului. Acest bloc afiseaza pe ecran un mesaj si ridica
o alta exceptie care este tratata in linia 34.

14.5.3 Exceptiile si destructorii


Atunci cand destructorul unui obiect este apelat, domeniul de viata al acelui obiect s-a terminat. Pentru a
asigura eliberarea corespunzatoare a resurselor este necesara folosirea blocurilor try / catch in
14
250
Adonis Butufei

implementarea destructorilor.
Este important ca in interiorul destructorilor sa nu se ridice exceptii, mai ales in destructorii claselor de
baza deoarece acesta previne apelul destructorilor claselor derivate si resursele nu se pot elibera
corespunzator.

14.6 Crearea claselor si ierarhiilor de clase pentru exceptii


In cazurile practice este necesara utilizarea claselor specializate pentru exceptii care pot furniza informatii
suplimentare blocurilor catch si asigura o intelegere mai buna a codului.
De exemplu pentru functia Factorial putem folosi urmatoarele clase de exceptie:
class Exceptie
{
string _mesaj;
public:
Exceptie(string mesaj) : _mesaj(mesaj) {}
virtual ~Exceptie() {}
virtual string Mesaj() { return _mesaj; }
};

class ParametruNegativ : public Exceptie


{
public:
ParametruNegativ() :
Exceptie ("Parametru este negativ.")
{
}
};

class DepasireMax : public Exceptie


{
public:
DepasireMax() :
Exceptie("Parametrul a depasit valoarea maxima.")
{
}
};

In acest caz functia Factorial are urmatoarea implementare:


1: int Factorial(int n)
2: {
3: const int MAX = 12;

15
251
Adonis Butufei

4:
5: if(n < 0)
6: {
7: throw ParametruNegativ();
8: }
9:
10: if (n > MAX )
11: {
12: throw DepasireMax();
13: }
14:
15: // ....
16: }

Iar apelul poate avea urmatoarea implementare:


1: int main()
2: {
3: try
4: {
5: Factorial(-1);
6: Factorial(13);
7: }
8: catch(Exceptie e)
9: {
10: cout << e.Mesaj() << endl;
11: }
12: return 0;
13: }

In blocul catch am putut trata ambele exceptii folosind polimorfismul. Dupa executia liniei 5 se ridica o
exceptie de tipul ParametruNegativ. Comentand aceasta linie se va apela functia Factorial cu valoarea 13
care ridica exceptia DepasireMax.

14.6.1 Moduri de transfer al exceptiilor catre blocul catch


Transferul exceptiilor catre blocurile catch se realizeaza in trei moduri ca si transferul parametrilor
functiei: prin valoare, prin referinta sau prin adresa.
Exemplul anterior a folosit modul de ridicare prin valoare. In acest caz exceptia ridicata in functia
Factorial este copiata in variabila corespunzatoare parametrului.
Daca blocul catch foloseste referinta la exceptie atunci se elimina copierea si transferul se face prin
referinta.
Exemplu:
catch(Exceptie& e)

16
252
Adonis Butufei

{
cout << e.Mesaj() << endl;
}

Pentru transferul prin adresa se folosesc pointerii. In locul in care se ridica exceptia se aloca memoria
pentru instanta exceptiei iar blocul catch are parametru de tip pointer. In acest caz este necesara
eliberarea memoriei in blocul catch.

Nota
Folosirea transferului exceptiei prin referinta este recomandata pentru ca ofera urmatoarele avantaje:
elimina copierea obiectelor si destructorul exceptiei este apelat automat.

14.6.2 Ordinea blocurilor catch in cazul ierarhiilor de exceptii


Pentru tratarea corespunzatoare a exceptiilor este necesar ca blocurile catch cu clasele specializate sa fie
plasate inaintea celor care contin clase de baza.
Pentru clasele de exceptie prezentate in aceasta sectiune blocurile trebuie sa contina mai intai clasele
ParametruNegativ sau DepasireMax si apoi Exceptie ca in exemplul de mai jos:
try
{
// .....
}
catch( ParametruNegativ& e)
{
// ...
}
catch ( Exceptie& e)
{
// ...
}

14.7 Sumar
• In acest capitol am discutat despre modul de tratare a erorilor: returnarea valorilor de eroare,
terminarea programului, folosirea functiei assert si exceptiile.
• Folosirea valorilor de eroare aglomereaza codul normal cu teste care sunt greu de mentinut.
• Terminarea programului este utila in cazul erorilor grave.
• Folosirea functiei assert este utila in timpul dezvoltarii programului pentru idendificarea unor
conditii care nu trebuie sa apara in executie.
• Exceptiile sunt folosite pentru tratarea erorilor si ofera avantajul separarii codului care trateaza erorile
de executia obisnuita.

17
253
Adonis Butufei

• Exceptiile au trei elemente importante: blocul try in care se executa cod care poate ridica exceptii,
blocurile catch folosite pentru tratarea exceptiilor si ridicarea exceptiilor cu ajutorul apelului throw.
• Atunci cand se folosesc exceptiile trebuiesc eliberate resursele alocate in blocul try.
• Se pot defini clase si ierarhii de clase pentru exceptii care ajuta la intelegerea mai usoara a codului si la
transferul de informatii intre locul unde s-a ridicat exceptia si blocul catch care a tratat exceptia.
• Transferul exceptiilor se poate face prin valoare, referinta sau adresa. Pentru simplitate este preferat
transferul prin referinta deoarece elimina copierea si apeleaza automat destructorul exceptiilor.

14.8 Intrebari si exercitii


1. Definiti o clasa fractie care ridica o exceptie in momentul in care numitorul este 0.
2. Creati o clasa exceptie pentru exercitiul de la punctul anterior.
3. Unde se plaseaza blocul catch (...)?
4. Cum se ridica exceptii?
5. Urmatorul cod genereaza erori. Care este motivul?
Factorial(3);
catch(int x)
{
cout << x << endl;
}

6. Implementati si rulati functia Factorial si clasele pentru exceptii prezentate apoi rulati exemplul.
7. Care este scopul tratarii exceptiilor in lista de initializare a constructorului?
8. Ce se intampla daca se ridica o exceptie in destructorul unei clase de baza si aceasta nu este tratata?
9. Care este ordinea blocurilor catch pentru urmatoarele clase de exceptie considerand ca se poate
ridica oricare din cele trei tipuri?
class EA {};
class EB : public EA {};
class EC : public EB {};

10. Cu se poate ridica aceeasi exceptie dintr-un bloc catch?

14.9 Bibliografie
• Teach yourself C++ In an Hour A day, Sixth Edition, Sams, Jesse Liberty, Cap 28.
• Practical C++ Programming, Second Edition, O'Reilly, Steve Oualline, Cap 22.

18
254
Adonis Butufei

15. PREZENTARE STL - STANDARD TEMPLATE LIBRARY

CUPRINS
15.Prezentare STL – Standard Template Library..........................................................................................2
15.1 Containere........................................................................................................................................2
15.1.1 Clasa vector..............................................................................................................................2
15.1.1.1 Instantierea........................................................................................................................3
15.1.1.2 Adaugarea elementelor.....................................................................................................3
15.1.1.3 Accesare valori..................................................................................................................4
15.1.1.4 Stergere elemente de la sfarsit..........................................................................................5
15.1.1.5 Dimensiune si capacitate..................................................................................................6
15.1.2 Clasa deque...............................................................................................................................6
15.1.3 Clasa list...................................................................................................................................7
15.1.3.1 Adaugarea elementelor la sfarsitul listei...........................................................................8
15.1.3.2 Adaugarea elementelor la inceputul listei.........................................................................9
15.1.3.3 Stergerea elementelor din lista........................................................................................10
15.1.4 Clasele set si multiset.............................................................................................................10
15.1.4.1 Adaugarea elementelor in containerele set si multiset....................................................11
15.1.4.2 Cautarea elementelor......................................................................................................12
15.1.4.3 Stergerea elementelor......................................................................................................13
15.1.5 Clasele map si multimap.........................................................................................................14
15.1.5.1 Adaugarea elementelor in containerele map si multimap...............................................14
15.1.5.2 Accesarea elementelor din map si multimap..................................................................15
15.1.5.3 Cautarea elementelor......................................................................................................16
15.1.5.4 Stergerea elementelor......................................................................................................17
15.2 Iteratori...........................................................................................................................................19
15.3 Obiecte functie si predicate............................................................................................................20
15.3.1 Obiecte functie unare..............................................................................................................20
15.3.2 Obiecte functie binare.............................................................................................................21
15.3.3 Predicate unare.......................................................................................................................21
15.3.4 Predicate binare......................................................................................................................22
15.4 Algoritmi........................................................................................................................................23
15.4.1 Numararea si cautarea elementelor.........................................................................................23
15.4.2 Initializarea containerelor ......................................................................................................26
15.4.3 Procesarea elementelor intr-un interval..................................................................................26
15.4.4 Transformari si obiecte functie binare....................................................................................27
15.4.5 Operatii de copiere si stergere................................................................................................28
15.4.6 Inlocuirea elementelor............................................................................................................29
15.4.7 Sortarea, cautarea intr-un container sortat, stergerea elementelor duplicate..........................29
15.5 Sumar.............................................................................................................................................30
15.6 Intrebari si exercitii........................................................................................................................31
15.7 Bibliografie.....................................................................................................................................31

1
255
Adonis Butufei

15. PREZENTARE STL – STANDARD TEMPLATE LIBRARY


In majoritatea programelor apare lucrul cu liste, tablouri, stive etc. Modul de lucru cu aceste tipuri de date
este similar, insa tipurile de date folosite si detaliile de implementare difera. Pentru refolosirea codului
designerii limbajului C++ au grupat containerele frecvent utilizate (tablouri, liste, stive, cozi etc) intr-o
biblioteca a limbajului. Numele acestei biblioteci este Standard Template Library sau STL.
Aceste containere sunt template pentru a putea lucra cu orice tip de date. Biblioteca ofera pe langa
containere iteratori care permit accesul la elementele containerelor intr-un mod uniform si usor.
De asemenea in STL exista algoritmi care executa operatii asupra containerelor ca sortare, cautare,
selectarea unui interval de elemente etc.
In acest capitol vom discuta despre urmatoarele componente principale si clase reprezentative ale STL:
• Containere: string, vector, list, set si map.
• Iteratori: forward, reverse, bidirectional si random access
• Predicate si obiecte functie
• Algoritmi: find, count, for_each, sort

15.1 Containere
Containerele sunt clase folosite pentru stocarea datelor. STL ofera doua tipuri de containere: secventiale si
asociative.
Containerele secventiale organizeaza datele in mod secvential. Exemple reprezentative sunt tablourile si
listele. Introducerea datelor in aceste containere este rapida insa operatiile de cautare sunt lente. STL are
urmatoarele containere secventiale:
• vector – un tablou dinamic la care elementele pot fi adaugate la sfarsit.
• deque – o coada, similara cu vectorul dar la care elementele pot fi inserate la inceput sau la sfarsit.
• list – functioneaza ca o lista de elemente.

Containerele asociative organizeaza datele similar unui dictionar. Introducerea datelor este mai lenta insa
operatiile de cautare sunt mai rapide. STL are urmatoarele containere asociative:
• set – o lista sortata de elemente unice.
• map – stocheaza perechi cheie – valoare, sortate dupa chei unice.
• multiset – un tip de set care permite mai multe elemente care au aceeasi valoare.
• multimap – un tip de map in care cheile nu sunt unice.

15.1.1 Clasa vector


Clasa template vector implementeaza functionalitatea unui tablou dinamic cu urmatoarele caracteristici:
• Adaugarea elementelor la sfarsitul tabloului se executa in acelasi timp indiferent de dimensiunea vectorului.
• Timpul necesar inserarii sau stergerii elementelor din interiorul tabloului este direct proportional cu numarul
elementelor dinaintea elementului care este inlocuit.
• Numarul elementelor este dinamic. Clasa vector isi gestioneaza memoria necesara pentru elemente.
2
256
Adonis Butufei

15.1.1.1 Instantierea
Modalitatile de instantiere a containerelor vector sunt prezentate in exemplul de mai jos:
1: #include <iostream>
2: #include <vector>
3: using namespace std;
4: int main()
5: {
6: vector<int> v1;
7:
8: vector<int> v2(10);
9:
10: vector<int> v3(5, 24);
11:
12: vector<int> v4(v3);
13:
14: vector<int> v5(v4.begin(), v4.begin() + 2);
15: return 0;
16: }

In linia 2 este adaugata directiva de includere pentru fisierul vector. In linia 6 este instantiat un vector
pentru elemente de tip int folosind constructorul implicit. In linia 8 este instantiat un vector care are
rezervate 10 elemente de tip int. In linia 10 este instantiat un vector cu 5 elemente de tip int care sunt
initializate cu valoarea 24. In linia 12 este instantiat un vector care contine o copie a elementelor
vectorului v3. In linia 14 este instantiat un vector care contine o copie a primelor 3 elemente ale
vectorului v4.

15.1.1.2 Adaugarea elementelor


Adaugarea elementelor este prezentata in exemplul de mai jos:

1: #include <iostream>
2: #include <vector>
3: using namespace std;
4:
5: int main()
6: {
7: vector<int> v;
8: v.push_back(1);
9: v.push_back(5);
10: v.push_back(10);
11:
12: cout << "Vectorul contine " << v.size() << " elemente." << endl;

3
257
Adonis Butufei

13:
14: return 0;
15: }

In linia 7 este instantiat un vector, apoi in liniile 8 – 10 sunt adaugate elemente la sfarsitul vectorului.
Numarul final de elemente este afisat pe ecran in linia 12.

15.1.1.3 Accesare valori


Atribuirea valorilor pentru elemente este prezentata in exemplul de mai jos:
1: #include <iostream>
2: #include <vector>
3: using namespace std;
4:
5: int main()
6: {
7: vector<int> v(2);
8:
9: v[0] = 10;
10: v[1] = 100;
11:
12: cout << "Vectorul contine " << v.size() << " elemente." << endl;
13:
14: return 0;
15: }

In linia 7 este instantiat un vector cu 2 elemente. Valorile elementelor sunt atribuite in liniile 9 si 10.

Citirea valorilor unui vector este prezentata in exemplul de mai jos:

1: #include <iostream>
2: #include <vector>
3: using namespace std;
4:
5: int main()
6: {
7: vector<int> v(3);
8:
9: v[0] = 125;
10: v[1] = 476;
11: v[2] = 953;
12:

4
258
Adonis Butufei

13: int max = v.size();


14:
15: for(int i = 0; i < max; i++)
16: {
17: cout << "v[" << i << "]= " << v[i] << endl;
18: }
19:
20: return 0;
21: }

Vectorul este instantiat si elementele sunt initializate in liniile 7 - 11. Continutul vectorului este afisat pe
ecran cu ajutorul buclei for din liniile 15 – 18.

15.1.1.4 Stergere elemente de la sfarsit


Stergerea elementelor de la sfarsitul vectorului este prezentata in exemplul urmator:
1: #include <iostream>
2: #include <vector>
3: using namespace std;
4:
5: int main()
6: {
7: vector<int> v;
8:
9: v.push_back(23);
10: v.push_back(45);
11: v.push_back(89);
12: v.push_back(32);
13:
14: cout << "Initial vectorul contine " << v.size() << " elemente.\n";
15:
16: v.pop_back();
17:
18: cout << "Dupa apelul pop_back() vectorul contine " << v.size()
19: cout << " elemente." << endl;
20:
21: int max = v.size();
22:
23: for(int i = 0; i < max; i++)
24: {
25: cout << "v[" << i << "]= " << v[i] << endl;

5
259
Adonis Butufei

26: }
27:
28: return 0;
29: }

In acest exemplu este creat un vector cu patru elemente de tip int. In linia 4 este sters ultimul element apoi
sunt afisate pe ecran elementele ramase cu ajutorul buclei din liniile 23 – 26.

15.1.1.5 Dimensiune si capacitate


Este important de inteles diferenta dintre dimensiune si capacitate. Dimensiunea reprezinta numarul de
elemente prezente in vector. Capacitatea reprezinta numarul total al elementelor care pot sa fie stocate in
vector fara realocare de memorie.
Exemplu:
1: #include <iostream>
2: #include <vector>
3: using namespace std;
4:
5: int main()
6: {
7: vector<int> v(4);
8:
9: cout << "Vectorul a fost creat cu " << endl;
10: cout << "dimensiunea: " << v.size();
11: cout << " capacitate: " << v.capacity() << endl;
12:
13: v.push_back(10);
14:
15: cout << "Dupa inserarea unui element" << endl;
16: cout << "dimensiunea: " << v.size();
17: cout << " capacitatea: " << v.capacity() << endl;
18:
19: return 0;
20: }

In linia 7 este instantiat un vector pentru 4 elemente de tip int. In acest caz dimensiunea si capacitatea
sunt egale cu 4. In linia 13 este adaugat un element la sfarsit. Dupa executia acestei linii dimensiunea este
5 si capacitatea 6. Afisarea se face in liniile 16 – 17.

15.1.2 Clasa deque


Este un tablou dinamic cu proprietati similare clasei vector cu deosebirea ca permite adaugarea
elementelor la ambele capete ale tabloului. Pentru folosirea acestei clase este necesara includerea
fisierului deque.
Exemplu:
1: #include <iostream>
6
260
Adonis Butufei

2: #include <deque>
3: using namespace std;
4:
5: int main()
6: {
7: deque<int> d;
8:
9: const int SIZE = 10;
10: for(int i = 0; i < SIZE; i++)
11: {
12: if( 0 == (i % 2))
13: {
14: d.push_back(i);
15: }
16: else
17: {
18: d.push_front(i);
19: }
20: }
21:
22: for(int i = 0; i < SIZE; ++i)
23: {
24: cout << d[i] << endl;
25: }
26:
27: return 0;
28: }

In linia 7 este instantiat un container deque pentru elemente de tip int. Cu ajutorul buclei din liniile 10 –
20 se adauga numerele pare la sfarsit iar numerele impare la inceput. Continutul containerului este afisat
cu bucla din liniile 22 – 25.

15.1.3 Clasa list


Organizarea elementelor in clasa list sunt optimizate pentru a permite adaugarea si stergerea elementelor
din orice pozitie. In acest container elementele nu mai sunt situate in locatii succesive de memorie.
Conceptual exista doua tipuri de liste: simplu inlantuite si dublu inlantuite. In cazul listelor simplu
inlantuite fiecare element are o referinta catre urmatorul element permitand navigarea intr-un singur sens.
Pentru listele dublu inlantuite fiecare element are o referinta catre urmatorul cat si spre elementul anterior.
Aceasta permite navigarea in ambele sensuri.
Deoarece elementele nu mai sunt la adrese succesive de memorie ele nu mai pot fi accesate folosind
operatorul de indexare ca in cazul claselor vector si deque ci se folosesc iteratorii.
Iteratorii joaca rolul unui cursor care poate fi mutat de la un element la altul folosind operatorii de
incrementare (si decrementare), care pot accesa valoarea elementului curent (folosind operatorul de
7
261
Adonis Butufei

indirectare). De asemenea ei pot fi folositi pentru cautarea sau stergerea unui element.

Clasa list contine doua tipuri de iteratori: pentru acces de citire / scriere sau constanti pentru acces doar
de citire.

15.1.3.1 Adaugarea elementelor la sfarsitul listei


Adaugarea elementelor la sfarsitul listei este prezentata in exemplul de mai jos:
1: #include "PrintContainer.h"
2: #include <iostream>
3: #include <list>
4: using namespace std;
5:
6: int main()
7: {
8: list<int> lst;
9:
10: lst.push_back(10);
11: lst.push_back(15);
12: lst.push_back(20);
13:
14: PrintContainer(lst);
15:
16: return 0;
17: }

In linia 3 este inclus fisierul list pentru a putea lucra cu acest container. In linia 3 a fost creata o instanta
pentru o lista de elemente de tipul int. Urmeaza apoi adaugarea a trei elemente la sfarsitul listei: liniile 10
– 12.

Apoi este afisata lista cu ajutorul functiei template PrintContainer care este definita in fisierul
PrintContainer.h prezentata mai jos:
1: #ifndef PRINTCONTAINER_H
2: #define PRINTCONTAINER_H
3: template <typename Container>
4: void PrintContainer(const Container &src)
5: {
6: cout << "{ " ;
7:
8: Container::const_iterator end = src.end();
9:
10: for(Container::const_iterator crt = src.begin(); crt != end; ++crt)
11: {

8
262
Adonis Butufei

12: cout << (*crt) << " ";


13: }
14:
15: cout << "}" << endl;
16: }
17: #endif

Pentru a putea afisa continutul mai multor containere a fost folosita o functie template. Parametrul de
intrare este referinta constanta la container deoarece functia afiseaza continutul pe ecran are nevoie doar
de acces de citire.
In linia 8 este extras iteratorul de sfarsit. Aceasta pozitie a cursorului este dupa ultimul element introdus
in lista.
In blocul for din linia 8 se incepe iteratia de la primul element care poate fi accesat apeland metoda
begin a containerului. Valoarea elementului curent este afisata pe ecran in linia 12 folosind operatorul de
indirectare pentru pozitia curenta a iteratorului.
Dupa afisare, pozitia iteratorului este incrementata in blocul for si comparata cu pozitia de sfarsit.
Continutul containerului este afisat pe o linie delimitata de acolade care sunt afisate in liniile 6 si 15.

15.1.3.2 Adaugarea elementelor la inceputul listei


Folosind metoda push_front in loc de push_back, ca in exemplul de mai jos, elementele sunt adaugate la
inceputul listei.
1: #include "PrintContainer.h"
2: #include <iostream>
3: #include <list>
4: using namespace std;
5:
6: int main()
7: {
8: list<int> lst;
9:
10: lst.push_front(10);
11: lst.push_front(15);
12: lst.push_front(20);
13:
14: PrintContainer(lst);
15:
16: return 0;
17: }

In acest exemplu instantierea este idendica, elementele sunt adaugate in lista in liniile 10 - 12 iar pentru
afisarea continutului s-a folosit aceeasi functie template.
La rularea acestui program pe ecran apare urmatorul continut:
9
263
Adonis Butufei

{ 20 15 10 }

15.1.3.3 Stergerea elementelor din lista


In exemplul de mai jos este prezentata stergerea elementelor din lista.
1: #include "PrintContainer.h"
2: #include <iostream>
3: #include <list>
4: using namespace std;
5:
6: int main()
7: {
8: list<int> lst;
9: lst.push_front(6);
10: lst.push_front(7);
11:
12: cout << "Continutul listei inainte de stergere" << endl;
13: PrintContainer(lst);
14:
15: lst.erase(lst.begin(), lst.end());
16:
17: cout << "Continutul listei dupa stergere" << endl;
18:
19: PrintContainer(lst);
20:
21: return 0;
22: }

In linia 13 sunt afisate elementele listei inainte de stergere. In linia 15 se sterg elementele cuprinse intre
pozitia de inceput si cea de sfarsit. Se pot alege orice alte valori intermediare iar pentru stergerea unui
element se apeleaza metoda erase cu iteratorul la pozitia care trebuie stearsa.

15.1.4 Clasele set si multiset


Clasele set si multiset sunt containere care ofera functionalitatea cautarii unor chei in containerul care
le stocheaza. Cheile sunt valorile elementelor stocate in aceste containere. Clasa multiset permite
valori duplicate pentru chei iar clasa set foloseste valori unice. Pentru folosirea acestor clase este
necesara includerea fisierului set.
Pentru optimizarea cautarii elementelor, acestea sunt sortate intern folosind un arbore binar1.Adaugarea
unui element in aceste tipuri de containere este mai lenta decat in containerele prezentate anterior pentru
ca necesita calcule pentru determinarea pozitiei in arbore insa cautarea este mult mai rapida.

1 Arborele binar este o structura abstracta de date in care fiecare element poate avea cel mult doi descendenti.
10
264
Adonis Butufei

15.1.4.1 Adaugarea elementelor in containerele set si multiset


Adaugarea elementelor in containerele set si multiset este prezentata in exemplul de mai jos:
1: #include "PrintContainer.h"
2: #include <iostream>
3: #include <set>
4: using namespace std;
5:
6: int main()
7: {
8: set<int> sint;
9: sint.insert(15);
10: sint.insert(12);
11: sint.insert(17);
12:
13: sint.insert(12);
14:
15: cout << "Continutul containerului set:" << endl;
16: PrintContainer(sint);
17:
18: multiset<int> msint;
19: msint.insert(15);
20: msint.insert(12);
21: msint.insert(17);
22:
23: msint.insert(12);
24:
25: cout << "Continutul containerului multiset:" << endl;
26: PrintContainer(msint);
27:
28: cout << "Containerul multiset contine " << msint.count(12);
29: cout << " elemente cu valoarea 12." << endl;
30: return 0
31: }

Containerul set este instantiat in linia 8, apoi este populat cu elemente in liniile 9 – 11. In linia 13 se
incearca introducerea unui element duplicat. Continutul acestui container este afisat pe ecran in linia 16
folosind aceeasi functie template.

In continuare se executa aceleasi operatii pentru containerul multiset. In linia 28 este afisat numarul de
elemente care au valoarea 12. La rularea acestui exemplu continutul containerului set este: {12 15 17}
iar continutul containerului multiset este {12 12 15 17 }.

11
265
Adonis Butufei

15.1.4.2 Cautarea elementelor


Cautarea elementelor este similara pentru ambele tipuri de container. In exemplul de mai jos este
prezentata cautarea intr-un container de tip set.

1: #include <iostream>
2: #include <set>
3: using namespace std;
4:
5: void Find(int element, const set<int> &src)
6: {
7: set<int>::iterator itElement = src.find(element);
8:
9: if(itElement != src.end())
10: {
11: cout << "Elementul " << (*itElement)
12: cout << " a fost gasit in set" << endl;
13: }
14: else
15: {
16: cout << "Elementul " << element << " nu exista in set" << endl;
17: }
18: }
19:
20: int main()
21: {
22: set<int> sint;
23:
24: sint.insert(15);
25: sint.insert(32);
26: sint.insert(11);
27:
28: Find(11, sint);
29:
30: Find(-1, sint);
31:
32: return 0;
33: }

Pentru cautare a fost folosita functia Find definita intre liniile 5 – 18. Aceasta are doi parametri: valoarea
cautata si referinta la container. In linia 7 se foloseste metoda find care returneaza un iterator
corespunzator pozitiei elementului cautat atunci cand elementul se afla in container sau pozitia de sfarsit
in caz contrar. In linia 9 se compara pozitia iteratorului returnat de metoda find si pozitia de sfarsit. In
cazul in care a fost gasit elementul dorit se afiseaza mesajele din liniile 11 si 12. Altfel se afiseaza mesajul

12
266
Adonis Butufei

din linia 16.

Codul de initializare si populare a containerului este similar exemplelor anterioare. In linia 30 se face o
cautare pentru un element inexistent.

15.1.4.3 Stergerea elementelor


Stergerea elementelor este similara pentru ambele tipuri de containere. In exemplul de mai jos este
prezentata stergerea elementelor pentru containerul multiset.
1: #include "PrintContainer.h"
2: #include <iostream>
3: #include <set>
4: using namespace std;
5:
6: int main()
7: {
8: multiset<int> mset;
9:
10: mset.insert(11);
11: mset.insert(20);
12: mset.insert(13);
13: mset.insert(11);
14:
15: cout << "Continutul containerului:" << endl;
16: PrintContainer(mset);
17:
18: cout << "Introduceti valoarea elementului pentru stergere: " ;
19: int elem;
20: cin >> elem;
21:
22: mset.erase(elem);
23:
24: cout << "Continutul containerului dupa stergere:" << endl;
25: PrintContainer(mset);
26:
27: return 0;
28: }

In liniile 9 – 13 este instantiat si populat un container de tipul multiset. Apoi se afiseaza continutul acestui
container pe ecran (liniile 15,16). In liniile 18 – 20 utilizatorul este rugat sa introduca valoarea care va fi
stearsa din container. In linia 22 se sterge elementul din container, apoi in linia 25 este afisat continutul
containerului. Daca se introduce valoarea 11 vor fi sterse ambele elemente din container.

13
267
Adonis Butufei

15.1.5 Clasele map si multimap


Containerele map si multimap sunt folosite pentru stocarea perechilor de date cheie – valoare si care
permit cautarea elementelor dupa cheie. Diferenta dintre map si multimap este ca primul este folosit
numai cu chei unice iar al doilea permite mai multe chei cu aceeasi valoare. Pentru utilizarea acestor clase
este necesara includerea fisierului map. Elementele sunt sortate la adaugare pentru optimizarea cautarii.

15.1.5.1 Adaugarea elementelor in containerele map si multimap


In exemplul de mai jos sunt prezentate toate metodele de adaugare a elementelor in aceste containere.
1: #include "PrintPairs.h"
2: #include <map>
3: #include <string>
4: #include <iostream>
5:
6: using namespace std;
7:
8: typedef map<string, string> Map;
9: typedef multimap<string, string> MultiMap;
10:
11: int main()
12: {
13: Map map;
14:
15: map.insert(Map::value_type("unu","1"));
16: map.insert(make_pair("doi","2"));
17: map.insert(pair<string,string>("trei","3"));
18:
19: map["patru"] = "4";
20:
21: cout << "Map-ul contine " << map.size() << " elemente." << endl;
22: cout << "Continutul map-ului este:" << endl;
23: PrintPairs(map);
24:
25: MultiMap mmap;
26: mmap.insert(MultiMap::value_type("unu","1"));
27: mmap.insert(MultiMap::value_type("unu","I"));
28:
29: mmap.insert(make_pair("doi","2"));
30: mmap.insert(make_pair("doi","II"));
31:
32: mmap.insert(pair<string,string>("trei","3"));
33: mmap.insert(pair<string,string>("trei","III"));
34:

14
268
Adonis Butufei

35: cout << "Multimap-ul contine " << mmap.size() << " elemente" << endl;
36: cout << "Continutul multimap-ului este:" << endl;
37: PrintPairs(mmap);
38:
39: return 0;
40: }

In linia 1 este inclus fisierul care contine functia template care afiseaza perechile unui map sau multimap
pe ecran. Pentru simplificarea scrierii in liniile 8 si 9 au fost definite aliasuri pentru tipurile de map
respectiv multimap.
In linia 13 este creata o instanta pentru containerul map. In liniile 15 – 17 este creat cate un element de
tipul pair folosind metode ajutatoare sau instantiind explicit acest tip de clasa. In linia 19 este folosit
operatorul de indexare. In cazul in care nu exista nici un element pereche care are cheia specificata ca
index se creaza acel element si se insereaza in map. Daca pentru acea cheie exista un element pereche
atunci valoarea lui este schimbata. In linia 23 se afiseaza continutul mapului.

Liniile 25 – 37 executa operatii similare pentru o clasa de tip multimap. Pentru multimap se adauga cate
doua elemente pereche cu acceasi cheie si valori diferite. Pentru prima valoare se folosesc cifrele arabe
pentru cealalta se folosesc cifrele romane. In cazul multimapului sunt doar 3 metode de adaugare a
elementelor deoarece multimapul nu mai are operator de indexare.

15.1.5.2 Accesarea elementelor din map si multimap


Spre deosebire de containerele prezentate anterior pentru map si multimap elementele au doua
componente: cheia si valoarea. Din acest motiv iteratorii acestui tip de container au doua campuri: first si
second care contin valoarile acestor componente.
In exemplul de mai jos este prezentata functia PrintPairs care acceseaza elementele containerelor de tip
map sau multimap.
1: #ifndef PRINTPAIRS_H
2: #define PRINTPAIRS_H
3:
4: template <typename Container>
5: void PrintPairs(const Container& src)
6: {
7: Container::const_iterator end = src.end();
8:
9: for(Container::const_iterator crt = src.begin(); crt != end; ++crt)
10: {
11: cout << "(" << crt->first << ": ";
12: cout << crt->second << " ) " << endl;
13: }
14: }
15: #endif

Implementarea este asemanatoare cu cea a functiei template PrintContainer, prezentata anterior, in acest
15
269
Adonis Butufei

caz se folosesc membrii first si second in liniile 11 si 12.

15.1.5.3 Cautarea elementelor


Pentru containerele map si multimap cautarea dupa cheie in cazul in care exista o pereche cu acea cheie
se returneaza un iterator care are pozitia acelei perechi altfel iteratorul are pozitia de sfarsit. Pentru
multimap iteratorul returnat permite navigarea prin toate perechile care au aceeasi cheie.
Exemplu:
1: #include "PrintPairs.h"
2: #include <map>
3: #include <string>
4: #include <iostream>
5:
6: template<typename Container>
7: void Find(const string key, const Container& src)
8: {
9: Container::const_iterator it = src.find(key);
10: if(it == src.end())
11: {
12: cout << "Nu exista nici o pereche cu cheia " << key << ".\n";
13: return;
14: }
15:
16: int count = src.count(key) ;
17:
18: if ( 1 == count )
19: {
20: cout << "A fost gasita o pereche care are cheia ";
21: }
22: else
23: {
24: cout << "Au fost gasite " << count << " perechi care au cheia ";
25: }
26:
27: cout << key << "." << endl;
28:
29: for(int i = 0; i < count; ++i, ++it)
30: {
31: cout << "(" << it->first << ": " << it->second << ")" << endl;
32: }
33: }
34:
35: int main()

16
270
Adonis Butufei

36: {
37: Map map;
38:
39: map.insert(Map::value_type("unu","1"));
40: map.insert(make_pair("doi","2"));
41: map.insert(pair<string,string>("trei","3"));
42:
43: cout << "Continutul map-ului este:" << endl;
44: PrintPairs(map);
45: Find("trei", map);
46:
47: MultiMap mmap;
48: mmap.insert(MultiMap::value_type("unu","1"));
49: mmap.insert(MultiMap::value_type("unu","I"));
50:
51: mmap.insert(make_pair("doi","2"));
52: mmap.insert(make_pair("doi","II"));
53:
54: mmap.insert(pair<string,string>("trei","3"));
55: mmap.insert(pair<string,string>("trei","III"));
56:
57: cout << "Continutul multimap-ului este:" << endl;
58: PrintPairs(mmap);
59:
60: Find("trei",mmap);
61:
62: return 0;
63: }

Cautarea este implementata in functia template Find. In linia 9 se apeleaza metoda find a containerului.
Daca nu exista perechi care au cheia respectiva, iteratorul are pozitia de sfarsit si se executa liniile 12 si
13. Altfel se apeleaza metoda count (linia 16) pentru a determina numarul de perechi gasite si se afiseaza
informatia pe ecran liniile 18 – 27.
Pentru test au fost folosite doua containere: un map intre liniile 37 – 45 si un multimap intre liniile 47 –
60.

15.1.5.4 Stergerea elementelor


Stergerea elementelor este similara pentru ambele tipuri de containere. In exemplul de mai jos sunt
prezentate metodele de stergere a elementelor din multimap.

1: #include "PrintPairs.h"
2: #include <map>

17
271
Adonis Butufei

3: #include <string>
4: #include <iostream>
5:
6: int main()
7: {
8: MultiMap mmap;
9: mmap.insert(MultiMap::value_type("unu","1"));
10: mmap.insert(MultiMap::value_type("unu","I"));
11:
12: mmap.insert(make_pair("doi","2"));
13: mmap.insert(make_pair("doi","II"));
14:
15: mmap.insert(pair<string,string>("trei","3"));
16: mmap.insert(pair<string,string>("trei","III"));
17:
18: cout << "Continutul multimap-ului este:" << endl;
19: PrintPairs(mmap);
20:
21: mmap.erase("unu");
22: cout << "Continutul multimap-ului dupa stergerea unei cheii unu:\n";
23: PrintPairs(mmap);
24:
25: MultiMap::iterator it = mmap.find("doi");
26: if(it != mmap.end())
27: {
28: mmap.erase(it);
29:
30: cout << "Continutul multimap-ului dupa stergerea iteratorului:\n”;
31: PrintPairs(mmap);
32: }
33:
34: mmap.erase(mmap.lower_bound("trei"), mmap.upper_bound("trei"));
35: cout << "Continutul multimap-ului dupa stergerea unui interval:\n";
36: PrintPairs(mmap);
37:
38: return 0;
39: }

Elementele sunt adaugate in container intre liniile 8 – 18. In linia 19 este apelata metoda erase pentru
elementele cu cheia "unu". Dupa apelul acestei metode toate elementele cu aceasta cheie sunt sterse din
container. In linia 28 se foloseste un iterator pentru stergerea elementului. In cazul acestui apel numai
elementul care are pozitia corespunzatoare iteratorului este sters. Ultima metoda de stergere este
prezentata in linia 34. Prin folosirea unui interval se pot sterge mai multe elemente ale caror chei se afla in
acel interval.
18
272
Adonis Butufei

15.2 Iteratori
Iteratorii sunt clase template care functioneaza in mod similar cu pointerii. Ei permit utilizatorului sa
execute operatii asupra containerelor (cautare, inserare, sortare etc). Operatiile executate asupra
containerelor pot fi algoritmi care sunt implementati ca functii template. Iteratorii sunt puntea care
permite algoritmilor sa lucreze cu containerele in mod uniform. Iteratorii sunt definiti in fisierul iterator.
Dupa directia in care se executa operatiile de acces al elementelor iteratorii se impart in:
• Iteratori de intrare – care permit operatiile de citire a elementelor din containere.
• Iteratori de iesire – care permit operatii de modificare a containerelor.
Acesti iteratori sunt folositi in operatiile de intrare iesire.

Dupa modul de :
• Iteratori care se deplaseaza in fata – Specializeaza operatiile iteratorilor de intrare si iesire asigurand
deplasarea de la un element al containerului intr-o singura directie implementand operatiile de
incrementare si este folosit in anumite tipuri de liste.
• Iteratori care se deplaseaza in ambele directii – Fata de tipul precedent de iterator permite deplasarea
in ambele sensuri catre elementele containerului, implementand operatiile de incrementare si
decrementare si este folosit de urmatoarele contaiere: list, set, multiset, map multimap.
• Iteratori cu acces aleator – Fata de iteratorii anteriori permite deplasarea in ambele sensuri cu mai
mult de un element si este folosit de urmatoarele containere: vector, deque, string.
In tabelul de mai jos sunt prezentate operatiile suportate de categoriile de iteratori:
Operatie Explicatie Tip iterator

*it acceseaza elementul intrare, iesire, deplasare in fata,


deplasare in ambele directii,
it-> acces de citire la element acces aleator
++it se muta in fata si returneaza noua pozitie

it++ se muta in fata si returneaza vechea pozitie

it1 == it2 verifica egalitatea iteratorilor

it1 != it2 verifica diferenta iteratorilor

--it se muta in spate si returneaza noua pozitie deplasare in ambele directii,


acces aleator
it-- se muta in spate si returneaza vechea pozitie

it[n] returneaza elementul de la indexul n acces aleator

it+=n muta n pozitii in fata (sau in spate daca n < 0)

it-=n muta n pozitii in spate (sau in fata daca n < 0)

it + n returneaza iteratorul pentru elementul cu n pozitii in fata

19
273
Adonis Butufei

Operatie Explicatie Tip iterator

n + it returneaza iteratorul pentru elementul cu n pozitii in fata

it – n returneaza iteratorul pentru elementul cu n pozitii in spate

it1 – it2 returneaza distanta intre doi iteratori

15.3 Obiecte functie si predicate


Obiectele functie2 sunt clase template care implementeaza operatorul functie (operator () ) prezentat
in capitolul 10. Predicatele sunt obiecte functie pentru care tipul de return al operatorului () este bool.

15.3.1 Obiecte functie unare


Obiectele functie pentru care operatorul () are un singur parametru se numesc obiecte functie unare.
Exemplu:
1: template <typename T>
2: class Sum
3: {
4: T _t;
5: public:
6: Sum(T t = 0): _t(t) {}
7:
8: void operator () (const T& element) { _t += element; }
9:
10: T Valoare() { return _t; }
11: };

In acest exemplu este prezentat un obiect functie unar folosit pentru calcularea sumei elementelor unui
container. Un exemplu de utilizare este prezentat mai jos:
1: vector<int> vint;
2: vint.push_back(19);
3: vint.push_back(12);
4: vint.push_back(21);
5:
6: Sum<int> s;
7: int size = vint.size();
8: for(int i =0; i < size; i++)
9: {
10: s(vint[i]);
11: }
12:

2 In engleza se foloseste termenul functor.


20
274
Adonis Butufei

13: cout << "Suma elementelor: " << s.Valoare() << endl;

In liniile 1 – 4 se initializeaza un vector cu elemente de tip int. In linia 6 se instantiaza un obiect functie
care este folosit in interiorul buclei pentru calculul sumei elementelor. In linia 13 se afiseaza suma pe
ecran.

15.3.2 Obiecte functie binare


Sunt obiecte functie pentru care operatorul () are doi parametrii.
Exemplu:
1: template<typename T>
2: class AdunaElemente
3: {
4: public:
5: T operator () (const T& t1, const T& t2) { return t1 + t2; }
6: };

In acest exemplu este prezentat un obiect functie binar, el poate fi folosit pentru calculul sumei
componentelor a doi vectori cum este prezentat mai jos.
1: const int SIZE = 10;
2: vector<int> a(SIZE), b(SIZE), c(SIZE);
3:
4: for(int i = 0; i < SIZE; i++)
5: {
6: a[i] = 2 * i;
7: b[i] = a[i] + 1;
8: }
9:
10: AdunaElemente<int> add ;
11:
12: for(int i = 0; i < SIZE; i++)
13: {
14: c[i] = add(a[i], b[i]);
15: }

In linia 2 sunt instantiate trei vectori cu elemente de tip int. Primii doi vectori sunt initializati in liniile 4 –
8. In linia 10 este creat un obiect functie care este folosit in linia 14 pentru calculul componentelor
vectorului c.

15.3.3 Predicate unare


Predicatele unare sunt predicate pentru care operatorul () are un singur parametru.
Exemplu:
1: class TestMultiplu
2: {

21
275
Adonis Butufei

3: int _divizor;
4: public:
5: TestMultiplu(int divizor = 1): _divizor(divizor) {}
6:
7: bool operator() (const int& src) { return (0 == src % _divizor ); }
8: };

In acest exemplu este prezenat un predicat unar care testeaza daca parametrul operatorului () este
multiplul valorii specificate in constructor. Mai jos este prezentat un exemplu de utilizare:
1: const int SIZE =3;
2: vector<int> vint(SIZE);
3: vint [0] = 19;
4: vint [1] = 12;
5: vint [2] = 21;
6:
7: vector<int> m;
8:
9: TestMultiplu multiplu(3);
10:
11: for(int i =0; i < SIZE; i++)
12: {
13: if(multiplu(vint[i]) )
14: {
15: m.push_back(vint[i]);
16: }
17: }

In liniile 1 – 5 este initializat un vector cu elemente de tip int. In linia 7 este creat un vector care va
colecta multiplii. Predicatul unar este instantiat in linia 9. Vectorul m este populat cu multiplii de 3
selectati cu bucla din liniile 11 – 17.

15.3.4 Predicate binare


Sunt predicate pentru care operatorul () are doi parametrii. In exempul urmator este prezentat un predicat
care verifica egalitatea a doua clase de tip complex3:
1: class CompareComplex
2: {
3: public:
4:
5: bool operator() (const Complex& c1, const Complex& c2)
6: {
7: const double DELTA = 1e-5;
8:
9: bool b1 = abs(c1.Re() - c2.Re()) < DELTA;

3 Clasa Complex a fost definita in capitolul 9.


22
276
Adonis Butufei

10: bool b2 = abs(c1.Im() - c2.Im()) < DELTA;


11:
12: return b1 && b2;
13: }
14: };

Deoarece atributele clasei Complex sunt de tipul double este necesara folosirii valorii DELTA pentru
compararea egalitatii. Mai jos este prezentat un exemplu de apel:
1: vector<Complex> vcplx;
2: vcplx.push_back(Complex(3.2,4.5));
3: vcplx.push_back(Complex(1.3, 2.6));
4:
5: CompareComplex compare;
6: Complex t(3.2, 4.5);
7:
8: int count = vcplx.size();
9: bool gasit = false;
10:
11: for(int i = 0; i < count; i++)
12: {
13: if(compare(t,vcplx[i]))
14: {
15: gasit = true;
16: break;
17: }
18: }

In liniile 1 – 3 este instaintiat si initializat un vector cu doua elemente de tip Complex. In linia 5 este
creata o instanta a predicatului binar care este folosita pentru cautarea in vector in liniile 11 – 18.

15.4 Algoritmi
Operatiile de cautare, sortare etc sunt cerinte standard care ar trebui implementare o singura data.
Algoritmii permit refolosirea acestei implementari aduce cresteri importante ale productivitatii. Pentru
folosirea algoritmilor este necesara includerea fisierului algorithm.

15.4.1 Numararea si cautarea elementelor


Pentru numararea elementelor care au o valoare se foloseste algorimul count. Cu ajutorul algoritmului
count_if pot numara elementele care inteplinesc o anumita conditie. Pentru cautarea elementelor care au
anumita valoare se foloseste algoritmul find iar pentru cautarea elementelor care indeplinesc o anumita
conditie se foloseste algoritmul find_if. In cazul algoritmilor count si find se transmit ca parametrii
limitele intervalului in care dorim sa se execute operatia si valoarea elementului. In cazul algoritmilor
count_if si find_if in locul valorii elementului este transmisa o functie sau un predicat unar pentru
verificarea conditiei.

23
277
Adonis Butufei

In exemplul de mai jos sunt prezentate scenariile de numarare si cautare a unui element in containere.
1: #include "PrintPairs.h"
2: #include <algorithm>
3: #include <iostream>
4: #include <vector>
5: using namespace std;
6:
7: bool NumarImpar(int i)
8: {
9: return (0 != (i % 2));
10: }
11:
12: int main()
13: {
14: vector<int> v;
15:
16: for(int i = -3; i < 5; i++)
17: {
18: v.push_back(i);
19: }
20: v.push_back(3);
21:
22: cout << "Elementele vectorului" << endl;
23: PrintContainer(v);
24:
25: int nrImpare = count_if(v.begin(),v.end(), NumarImpar);
26: cout << "Vectorul are " << nrImpare << " numere impare" << endl;
27:
28: int nr3 = count(v.begin(), v.end(), 3);
29: cout << "Vectorul contine de " << nr3 << " ori numarul 3" << endl;
30:
31: vector<int>::iterator it = find(v.begin(), v.end(), 2);
32: if(it != v.end())
33: {
34: cout << "Numarul 2 a fost gasit in vector" << endl;
35: }
36:
37: vector<int>::iterator itImpar = find_if(v.begin(),v.end(),NumarImpar);
38:
39: if(itImpar != v.end())
40: {
41: cout << "Numarul " << (*itImpar ) << " se afla pe pozitia [";

24
278
Adonis Butufei

42: cout << distance(v.begin(), itImpar) << "]" << endl;


43: }
44:
45: return 0;
46: }

In liniile 14 – 20 este initializat un vector cu elemente de tip int. In linia 25 se calculeaza numarul de
elemente impare folosind algoritmul count_if. In apelul acestui algoritm sunt specificate pozitia de
inceput, pozitia de sfarsit si functia NumarImpar definita in liniile 7 – 9.
In linia 28 se calculeaza cate elemente au valoarea 3 folosind algoritmul count. In linia 31 se cauta
elementul cu valoarea 2 folosind algoritmul find. Acest algoritm returneaza un iterator cu pozitia
elementului in cazul in care a fost gasit sau cu valoarea de sfarsit in caz contrar. In linia 37 se cauta
primul mumar folosind algoritmul find_if si functia NumarImpar. In linia 42 se calculeaza pozitia
elementului gasit folosind functia distance definita in STL.

Exista cazuri cand este necesara cautarea unui intreg set de elemente sau a unei secvente de elemente de
valoare egala intr-un container. Exemplul de mai jos prezinta aceste scenarii de cautare.
1: vector<int> v;
2: for(int i = -5; i < 5; i++)
3: {
4: v.push_back(i);
5: }
6: v.push_back(5);
7: v.push_back(5);
8:
9: cout << "Continutul vectorului" << endl;
10: PrintContainer(v);
11:
12: list<int> interval;
13: for(int i = -2; i < 3; i++)
14: {
15: interval.push_back(i);
16: }
17: cout << "Continutul listei" << endl;
18: PrintContainer(interval);
19:
20: cout << "Cautarea elementelor listei in vector" << endl;
21: vector<int>::iterator itSearch = search(v.begin(),v.end(),
22: interval.begin(), interval.end());
23: if(itSearch != v.end())
24: {
25: cout << "Elementele listei au fost gasite la pozitia " ;

25
279
Adonis Butufei

26: cout << distance(v.begin(), itSearch) << endl << endl;


27: }
28:
29: cout << "Cautarea secventei {5 5}" << endl;
30: vector<int>::iterator itSearchN = search_n(v.begin(), v.end(), 2, 5);
31: if(itSearchN != v.end())
32: {
33: cout << "secventa {5 5} a fost gasita pe pozitia " ;
34: cout << distance(v.begin(), itSearchN) << endl;
35: }

In liniile 1 – 7 este initializat un vector de elemente de tip int. Apoi in liniile 12 – 16 este intializata o
lista cu setul de elemente care va fi cautat in vector. In linia 21 se utilizeaza algoritmul search pentru
cautarea acestui interval. In acest apel au fost specificate limitele containerului in care se face cautarea
precum si limitele intervalului cautat. Algoritmul returneaza iteratorul cu pozitia de inceput unde a fost
gasit intervalul sau cu pozitia de sfarsit a containerului.

In linia 30 se cauta secventa de elemente care are valorile 5 5 folosind algoritmul search_n. In acest apel
sunt specificate limitele containerului in care se face cautarea, numarul de elemente din secventa si
valoarea elementului. Algoritmul returneaza iteratorul cu pozitia de inceput unde a fost gasit intervalul
sau cu pozitia de sfarsit a containerului.

15.4.2 Initializarea containerelor


De multe ori este necesara intializarea unui interval al containerelor cu o anumita valoare aceasta se poate
realiza cu algoritmii fill si fill_n prezentati in exemplul de mai jos.
1: vector<int> v(3);
2: fill(v.begin(), v.end(), 5);
3:
4: v.resize(5);
5: fill_n(v.begin() + 3, 2,-4);

In linia 1 este instantiat un vector de 3 elemente de tip int. Elementele acestui vector sunt initializate cu
valoarea 5 folosind algoritmul fill. Apelul din linia 2 specifica limitele containerului si valoarea de
initializare.
In linia 4 vectorul este redimensionat sa contina 5 elemente. Apoi in linia 5 ultimele doua elemente sunt
initializate cu valoarea -4 folosint algoritmul fill_n. In acest apel se specifica pozitia de inceput,
numarul de elemente si valoarea de initializare.

15.4.3 Procesarea elementelor intr-un interval


De multe ori apare necesitatea executarii unei operatii pentru toate elementele dintr-un interval. Aceasta
se poate realiza cu ajutorul algoritmului for_each prezentat in exemplul de mai jos:
1: vector<int> v;
2:
3: for(int i = 0; i < 6; i++)

26
280
Adonis Butufei

4: {
5: v.push_back(i);
6: }
7:
8: cout << "Continutul vectorului" << endl;
9: PrintContainer(v);
10:
11: Sum<int> sum = for_each(v.begin(), v.end(), Sum<int> ());
12: cout << "Suma elementelor este ";
13: cout << sum.Valoare() << endl;

In linia 11se calculeaza suma pentru valorile elementelor din vector folosind obiectul functie unara Sum
prezentat anterior. Apelul specifica limitele intervalului si functia care implementeaza prelucrarea.
Important
Valoarea returnata este o copie a obiectului functie. In cazul de fata contstructorul de copiere implicit
functioneaza corect si nu a fost necesara definirea lui explicita. In cazurile practice daca functia unara are
pointeri care sunt alocati dinamic este necesara implementarea constructorului de copiere, operatorului de
atribuire si destructorului.

15.4.4 Transformari si obiecte functie binare


Rezultatul prelucrarii anterioare era o singura valoare. Atunci cand prelucrarea genereaza un interval de
valori (cum se intampla de exemplu atunci cand se calculeaza suma componentelor a doi vectori) este
necesara folosirea algoritmului transform acest algoritm foloseste doua tipuri de obiecte functie unare si
binare si rezultatul este salvat intr-un container. In exemplul de mai jos este prezentat algoritmul
transform care calculeaza suma componentelor a doi vectori.
1: const int SIZE = 10;
2: vector<int> x(SIZE), y(SIZE), z(SIZE);
3:
4: for(int i = 0; i < SIZE; i++)
5: {
6: x[i] = i;
7: y[i] = SIZE - i;
8: }
9:
10: cout << "Vectorul x" << endl;
11: PrintContainer(x);
12:
13: cout << "Vectorul y" << endl;
14: PrintContainer(y);
15:
16: transform(x.begin(), x.end(), y.begin(), z.begin(),
17: AdunaElemente<int>());
18:
27
281
Adonis Butufei

19: cout << "Vectorul care contine suma elementelor" << endl;
20: PrintContainer(z);

In linia 2 sunt instantiati doi vectori cu zece elemente de tip int. Primii doi vectori sunt initializati in
liniile 4 – 8. In linia 16 este folosit algoritmul transform pentru a calcula suma componentelor celor doi
vectori. Rezultatul este salvat in cel de-al treilea vector. Suma se calculeaza cu ajutorul obiectului
AdunaElemente prezentat anterior. Apelul specifica pozitiile de inceput si de sfarsit al primului container,
pozitia de inceput pentru al doilea container si obiectul functie care executa transformarea.

15.4.5 Operatii de copiere si stergere


In STL sunt doi algoritmi de copiere copy care copiaza elementele dintr-un interval sursa intr-un interval
destinatie de la inceput catre sfarsit si copy_backward care copiaza elementele de la sfarsit spre inceput.
Algoritmul remove sterge elementele care au valoarea specificata dintr-un container iar algorimtul
remove_if sterge elementele din container folosind un predicat unar. In exemplul de mai jos este
prezentat modul de folosire al acestor algoritmi.
1: list<int> lst;
2: for(int i = 0; i < 10; i++)
3: {
4: lst.push_back(i);
5: }
6:
7: cout << "Continutul listei" << endl;
8: PrintContainer(lst);
9:
10: vector<int> v(lst.size() * 2);
11: copy(lst.begin(), lst.end(), v.begin());
12:
13: copy_backward(lst.begin(),lst.end(), v.end());
14: cout << "Continutul vectorului dupa copiere" << endl;
15: PrintContainer(v);
16:
17: cout << "Stergerea elementelor 0 din vector" << endl;
18: vector<int>::iterator endAfterRemove = remove(v.begin(), v.end(),0);
19: v.erase(endAfterRemove,v.end());
20: PrintContainer(v);
21:
22: cout << "Stergerea elementelor impare din vector" << endl;
23: endAfterRemove = remove_if(v.begin(),v.end(),NumarImpar);
24: v.erase(endAfterRemove,v.end());
25: PrintContainer(v);

In liniile 1 – 5 este initializata o lista cu elemente folosite pentru copiere. In linia 10 este instantiat un
vector care poate contine de doua ori mai multe elemente. In linia 11 se foloseste algoritmul copy pentru
a copia elementele din lista in vector de la inceputul listei catre sfarsit.
In linia 13 se foloseste algoritmul copy_backward pentru a copia elementele din lista incepand de la
28
282
Adonis Butufei

sfarsit catre inceput. Dupa aceste doua operatii vectorul contine de doua ori elementele din lista.
In linia 18 se sterg elementele care au valoarea 0 folosind algoritmul remove. Acest algoritm returneaza
iteratorul corespunzator sfarsitului dupa executarea stergerii. Executia algoritmului nu elimina elemente
din container ci le muta la sfarsit. Pentru eliminarea elementelor din container este necesar apelul metodei
erase a containerului folosind iteratorul returnat de algoritm (linia 19).

In linia 23 se foloseste algoritmul remove_if pentru stergerea elementelor impare din vector. Si acest
algoritm returneaza un iterator corespunzator sfarsitului dupa executarea stergerii. Pentru eliminarea
elementelor din container este apelata metoda erase din linia 24.

15.4.6 Inlocuirea elementelor


Pentru inlocuirea elementelor se folosesc algoritmii replace si replace_if. Primul algoritm foloseste
valoarea returnata de operatorul == pentru compararea elementelor iar al doilea foloseste un predicat unar
care returneaza true pentru elementele care trebuiesc returnate. In exempul de mai jos este prezentat
modul de folosire al acestor algoritmi.
1: vector<int> v(5);
2: fill(v.begin(),v.begin() + 2,3);
3: fill_n(v.begin() + 2, 3,4);
4: random_shuffle(v.begin(), v.end());
5:
6: cout << "Continutul initial al containerului" << endl;
7: PrintContainer(v);
8:
9: cout << "Valoarea 4 a fost inlocuita cu 7 folosind replace" << endl;
10: replace(v.begin(), v.end(), 4, 7);
11: PrintContainer(v);
12:
13: cout << "Numerele impare au fost inlocuite cu -2";
14: cout << " folosind replace_if" << endl;
15: replace_if(v.begin(), v.end(), NumarImpar, -2);
16: PrintContainer(v);

In liniile 1 – 4 este initializat vectorul pentru acest exemplu. Apelul din linia 4 repozitioneaza elementele
vectorului in mod aleator.
In linia 10 este apelat algoritmul replace pentru inlocuirea valorii 4 cu valoarea 7. Apoi in linia 15 este
apelat algoritmul replace_if pentru inlocuirea numerelor impare cu -2.

15.4.7 Sortarea, cautarea intr-un container sortat, stergerea elementelor


duplicate
Operatiile de sortare apar frecvent in practica. De multe ori sortarea este necesara pentru optimizarea
cautarii. De asemenea de multe ori este necesara pastrarea elementelor cu valori unice si stergerea din
container a elementelor duplicate. Sortarea se realizeaza cu ajutorul algoritmului sort. Pentru cautarea
intr-un interval sortat se foloseste algoritmul binary_search iar pentru stergerea elementelor duplicate
29
283
Adonis Butufei

se foloseste algoritmul unique. Folosirea acestor algoritmi este prezentata in exemplul urmator.
1: vector<int> v;
2: for(int i = 0; i < 10; i++)
3: {
4: v.push_back(i % 3);
5: }
6:
7: cout << "Continutul initial al vectorului" << endl;
8: PrintContainer(v);
9:
10: cout << "Sortarea vectorului" << endl;
11: sort(v.begin(), v.end());
12: PrintContainer(v);
13:
14: cout << "Cautarea elementului 2 in vector folosind binary_search\n";
15: bool found = binary_search(v.begin(), v.end(), 2);
16:
17: if(found)
18: {
19: cout << "Elementul 2 a fost gasit" << endl;
20: }
21: else
22: {
23: cout << "Elementul 2 nu a fost gasit" << endl;
24: }
25:
26: cout << "Eliminarea valorilor duplicate" << endl;
27: vector<int>::iterator endAfterRemove = unique(v.begin(), v.end());
28: v.erase(endAfterRemove, v.end());
29: PrintContainer(v);

Initializarea vectorului este realizata in liniile 1 – 5. In linia 11 este apelat algoritmul de sortare. In linia
15 se foloseste algoritmul binary_search pentru cautarea elementului cu valoarea 2. Acest algoritm
returneaza true in cazul in care elementul a fost gasit.
In linia 27 este folosit algorimul unique pentru stergerea elementelor duplicate. Acest algoritm returneaza
un iterator corespunzator pozitiei de sfarsit dupa stergerea elementelor duplicate. Functionarea
algoritmului unique este similara cu remove si necesita apelul erase pentru eliminarea elementelor din
vector (linia 28).

15.5 Sumar
• Folosirea componentelor STL reduce efortul de implementare si imbunatateste calitatea codului.
• Utilizarea clasei string simplifica lucrul cu sirurile de caractere incapsuland operatiile de alocare de
memorie si copiere.
30
284
Adonis Butufei

• Containerele sunt clase template. Ele organizeaza elementele fie in mod secvential, fie similar unui
dictionar.
• Iteratorii sunt similari pointerilor: prin indirectare putem accesa elementele containerelor.
• Algoritmii sunt functii template care folosesc iteratorii pentru a executa operatii asupra containerelor.
• Pentru specificarea conditiilor se folosesc predicatele care se transmit algoritmilor.

15.6 Intrebari si exercitii


1. Care sunt categoriile de containere din STL?
2. Enumerati cate 2 tipuri de containere din fiecare categorie.
3. Ce este un predicat?
4. Ce este un predicat binar?
5. Pentru ce tipuri de prelucrari sunt folositi algoritmii count si count_if?
6. Pentru ce tipuri de prelucrari sunt folositi algoritmii find si find_if?
7. Care este diferenta dintre algoritmii search si search_n?
8. Care este diferenta dintre algoritmii fill si fill_n?
9. Care este diferenta dintre algoritmii for_each si transform?
10. Care este diferenta dintre algoritmii copy si copy_backward?
11. Pentru ce tip de prelucrare sunt folositi algoritmii remove si remove_if?
12. Pentru ce tip de prelucrare sunt folositi algoritmii replace si replace_if?
13. Ce trebuie apelat dupa folosirea unuia din algoritmii remove, remove_if si unique pentru a elmina
elementele din container?
14. Care este algoritmul folosit pentru cautare in containerele sortate?

15.7 Bibliografie
• Practical C++ Programming, Second Edition, O'Reilly, Steve Oualline, Cap 25.
• Teach yourself C++ In an Hour A day, Sixth Edition, Sams, Jesse Liberty, Cap 16 - 24.

31
285

S-ar putea să vă placă și