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.1. Alegerea limbajului de programare. 1.2. Alegerea unui mediu de dezvoltare. 1.3. Alegerea unui tutorial.

1. Materialele necesare:

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 1.4.1

Limbaje de programare: tipuri si caracteristici 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 1.6.1

Ciclul de viata al programelor software 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

1. Determinare obiective

Progres

2. identificare si adresare riscuri

3. Evaluare alternative

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/visualcpp-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 1.8.1

Scrierea primului program 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: 7: 8: } cout << "Hello World!\n"; return 0;

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 uor 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 2.2.1

Variabile si constante - introducere 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 Numere intregi Numere reale Valoare de adevar O singura litera Siruri de caractere
int double bool char string

Detalii

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; "Numele complet este: " << nume << " ";

cout << 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= " cout << "VERDE= " return 0; } << ROSU << VERDE << "\n"; << "\n"; cout << "GALBEN= " << GALBEN << "\n";

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= " return 0; } << VERDE << "\n";

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 2.3.1

Instructiuni, operatori si expresii introducere 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 adunare scadere inmultire impartire modulo (restul impartirii intregi)

Exemplu
result = 4 + 5; // result = 9 5 result = 20 15; // result = result = 8 *

4; // result = 32 9; // result = 4; // result = 4 2

result = 36 / result = 10 %

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
x == y x != y x > y x < y x >= y x <= y

Explicatii Se testeaza egalitatea valorilor lui x si y Se testeaza daca x si y au valori diferite Se testeaza daca valoarea lui x este mai mare decat valoarea lui y Se testeaza daca valoarea lui x este mai mica decat valoarea lui y Se testeaza daca valoarea lui x este mai mare sau egala cu valoarea lui 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: 4: 5: 6: 7: 8: 9: } } return false; int rest = val % 2; if(0 == rest) { return true;

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 { case 'r': cout << "Stop!\n"; break; case 'v': cout << "Stop!\n"; break; default: cout << "Culoare invalida\n"; break; } return 0; } // se evalueaza trebuie sa fie char, int sau enum. // constante cu care se compara valoarea variabilei. // cod care se executa cand variabila are valoarea 'r' // transfera executia dupa blocul switch.

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; default: cout << "caracterul este consoana\n"; break; } return 0; } // are una din valorile specificate de // case (a, e, i, o sau u)

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: 7: 8: 9: 10: 11: 12: int factorial = 1; while(n > 1) int n; cin >> n; cout << "Introduceti un numar intreg\n";

17 33

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

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
do { // secventa instructiuni } while (conditie);

Bucle do while

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

Urmatorul program calculeaza factorialul unui numar folosind bucla do while.


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

18 34

Adonis Butufei
20: 21: 22: } cout << "n! = " << factorial << "\n"; return 0;

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: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: } cout << "n! = " << factorial << "\n"; return 0; } for(int i = n; i > 1; --i) { factorial = factorial * i; int factorial = 1; int n; cin >> n; cout << "Introduceti un numar intreg\n";

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: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: } } return 0; } if(i == n) { cout << "Numarul este prim\n"; } int i=2; for(;i < n; i++) { if(0 == n % i ) { cout << "Numarul nu este prim\n"; break; cout << "Introduceti un numar intreg\n"; int n; cin >> n;

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: 4: 5: 6: 7: 8: } } cout << i << "\n"; if(0 == i % 2) { continue;

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: 4: } // instructiuni

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: 4: } 5: 6: int main() 7: { 8: 9: 10: } int s = Suma(3, 5); return 0; return x + y;

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: 6: 7: } 8: 9: int Suma(int x, int y) 10: { 11: 12: } return x + y; int s = Suma(3, 5); return 0;

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: 8: } 9: 10: int main() 11: { 12: 13: 14: } AfisareMesaj("Hello"); return 0; cout << mesaj << "\n";

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
asm catch

Anexa: cuvintele cheie standard pentru C++


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

continue dynamic_cast extern goto mutable protected short struct typeid virtual

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:
abcd 10 =a10 + b10 + c10 + d10
3 2 1 0

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 n2 n+ b n12n1+ + b121+ b020 =( bn b n1b1 b 0)2 Unde b n , b n1 , 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=02 1+ 02 0=(00)2 (1)10=021+ 120 =( 01)2 (2)10=121+ 020=(10)2 (3)10=121+ 120=(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 4b 3 b 2 b1 b0h 1 h 0 Codurile numerelor pentru reprezentarea zecimala, binara si hexazecimala sunt prezentate in tabelul de mai jos. Zecimal 0 1 Binar 0000 0001 Hexazecimal 0 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Exemple: Zecimal 255 62 74 Binar 11111111 00111110 01001010 Hexazecimal FF = 15161 + 15160 3E 4A Binar 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 Hexazecimal 2 3 4 5 6 7 8 9 A B C D E F

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: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: } cout << total << "\n"; // aceasta variabila are valoarea 0 return 0; } cout << total << "\n"; // avem rezultatul asteptat } for(int i = 0; i < 10; i++) { total = total + i; { int total = 0; // mascheaza variabila total // declarata in blocul parinte. int total = 0;

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: 6: } 7: 8: void badsum(int a, int b) 9: { 10: 11: } 12: 13: int main() 14: { 15: 16: 17: } int result = sum(2,3); badsum(4,5); total = a + b; return a + b;

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: 6: 7: } // instructiuni. return 0;

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

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
3.3.1.1

Variabile intregi
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 n1)] Exemplu: In cazul unui octet putem stoca numere pozitive cuprinse intre 0 si 2 81 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 71 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
short int long long long

Dimensiune in octeti Exemplu


2 4 4 8 short intreg16b = 16; int intreg32b = 32; long intreg32b = 32; 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
char

Limta SCHAR_MIN SCHAR_MAX UCHAR_MAX CHAR_MIN CHAR_MAX

Valoare (-128) 127 0xff SCHAR_MIN sau 0 SCHAR_MAX sau UCHAR_MAX (-32768) 32767 0xffff (-2147483647 - 1) 2147483647 0xffffffff (-2147483647L - 1) 2147483647L 0xffffffffUL

Explicatii valoare minima a tipului char valoare maxima a tipului caracter cu semn valoare maxima a tipului caracter fara semn valoare minima a tipului caracter valoare maxima a tipului caracter (cu semn sau fara valoare minima a tipului intreg (cu semn) short valoare maxima a tipului intreg (cu semn) short valoare maxima a tipului intreg (fara semn) short valoare minima a tipului intreg (cu semn) valoare maxima a tipului intreg (cu semn) valoare maxima a tipului intreg (fara semn) valoare minima a tipului long (cu semn) valoare maxima a tipului long (cu semn) valoare maxima a tipului long (fara semn)

char

char

char

char

short

SHRT_MIN SHRT_MAX USHRT_MAX INT_MIN INT_MAX UINT_MAX LONG_MIN LONG_MAX ULONG_MAX

short

unsigned short

int

int

unsigned

long

long

unsigned long

12 57

Adonis Butufei
long long

LLONG_MAX LLONG_MIN

9223372036854775807i64

valoare maxima a tipului long long (cu semn)

long long

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

unsigned long long

ULLONG_MAX

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: Unde
3.3.2.1 y= f.f 1 f 2 f n x 10 este fractia zecimala.
exponent

f.f 1 f 2 f n

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
float double long double9

Explicatie numar real in precizie simpla numar real in precizie dubla similar cu double

Numar de octeti 4 8 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 abEcd care este echivalenta cu ab x 10cd . In aceasta notatie putem folosi ambele litere (E) sau (e). Exemple: 3e+2 = 3x102 = 300 3.24e-3= 3.24x103 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 DBL_DIG
DBL_EPSILON DBL_MAX DBL_MAX_10_EXP DBL_MIN DBL_MIN_10_EXP

Valoare 15
2.2204460492503131e-016 1.7976931348623158e+308 308 2.2250738585072014e-308 (-307)

Explicatie Numar de cifre semnificative


Cea mai mica valoare pentru care 1.0+DBL_EPSILON != 1.0 Valoarea maxima Exponentul maxim in vaza 10 Valoarea pozitiva minima Exponentul minim in baza 10

Constante pentru float


FLT_DIG FLT_EPSILON FLT_MAX FLT_MAX_10_EXP FLT_MIN FLT_MIN_10_EXP 6 1.192092896e-07F 3.402823466e+38F 38 1.175494351e-38F (-37)

Numar de cifre semnificative


Cea mai mica valoare pentru care 1.0F+FLT_EPSILON != 1.0 Valoarea maxima Exponentul maxim in vaza 10 Valoarea pozitiva minima 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; char y = '5'; // x are valoarea 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: 7: 8: 9: 10: 11: } } return 0; for(int i =0; i < 256; i++) { cout << (char) i << " " << i << "\n";

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 " return 0; } << c << "\n";

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' '\a' '\b' '\t' '\n' '\v' '\f' '\r'

marcheaza sfarsitul unui sir de carcatere produce un sunet muta cursorul un spatiu inapoi insereaza un tab orizontal muta cursorul pe linia urmatoare insereaza un tab vertical muta cursorul pe pagina urmatoare 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
isalpha(c) isupper(c) islower(c) isdigit(c) isxdigit(c) isspace(c) ispunct(c) isalnum(c) isprint(c) isgraph(c) iscntrl(c)

Explicatie returneaza 1 daca c este litera mare sau mica A-Z, a-z returneaza 1 daca c este litera mare A-Z returneaza 1 daca c este litera mica a-z returneaza 1 daca c este cifra 0-9 returneaza 1 daca c este cifra hexa 0-9,a-f,A-F returneaza 1 daca c este spatiu ' ','\t','\n','\r','\f' sau '\v' returneaza 1 daca c este character de punctuatie returneaza 1 daca c este litera sau cifra returneaza 1 daca c este afisabil cu spatiu returneaza 1 daca c este afisabil fara spatiu 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: 8: 9: for(int c = 0; c < 256; c++) { if(ispunct(c))

20 65

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

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: 24: 25: 26: 27: } return 0; } { cout << c << " " << (int)c << "\n";

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


Exemplu:

Acesti operatori ii intalnim in special in expresiile de atribuire.

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 x += y; x -= y; x *= y; x /= y; x %= y; Echivalenta x = x + y; x = x - y; x = x * y; x = 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 true true true false true true false false y true false true 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 true false false false true true false false y true false true 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 I 0 0 1 1 Exemplu:
1: #include <iostream> 2: using namespace std; 3: 4: int main() 5: { 6: 7: 8: 9: 10: 11: // 1110 & 0011 -> 0010 (0x2). // 1110 | 0011 -> 1111 (0xf). cout << "0xe | 0x3 = " << (0xe | 0x3 ) << "\n"; cout << hex << showbase;

Bit rezultat
| & ^

II 0 1 0 1 0 1 1 1

0 0 0 1

0 1 1 0

12 81

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

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 x |= y x &= y x ^= y x >>= n x <<= n Echivalenta x=x|y x=x&y x=x^y x = 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; { cout << x << " " << y << "\n"; if ( conditie) { continue; // in acest caz nu se mai actualzeaza y! } y *= 4.2; } x < 10 && y < 1e6; x++)

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 2
++ -()

Descriere incrementare, decrementare cu postfixare apel de functie incrementare, decrementare cu prefixare negare logica si negare pe biti operatori de semm adunare scadere

Asociativitate de la stanga la dreapta

++ -! ~ + -

de la dreapta la stanga

+ -

de la stanga la dreapta 15 84

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.

Adonis Butufei Precedenta Operator Descriere Asociativitate

7 8 9 10 11 12 13 14 15 16

<< < <= > >=

>>

deplasare la stanga, dreapta comparare egalitate, diferenta si pe biti sau exclusiv pe biti sau pe biti si logic sau logic operatorul conditional de la dreapta la stanga

== != & ^ | && || ?:

= 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( ! { } else { x = 24; } (n %4 == 0) ) x = 35;

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 } else if( n%6 == 0) { = 1;

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


int

tablou[1] tablou[2] tablou[3] tablou[4] 2 3 4 5

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: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: for(int i= 0; i < SIZE; i++) { cout << i <<": " << tablou [i] << "\n"; } for(int i = 0; i < SIZE; i++) { cout << "tablou[" << i << "]="; cin >> tablou [i]; const int SIZE = 3; int tablou[SIZE];

3 99

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

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

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: 8: 9: char mesaj1 [] = "Hello World!"; char mesaj2 [6];

7 103

Adonis Butufei
10: 11: 12: 13: 14: 15: } cout << mesaj1 << "\n" << mesaj2 << "\nCopiere cu success.\n"; return 0; strncpy(mesaj2, mesaj1, 5); mesaj2[5] = '\0';

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: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: } cout << nume << "\n"; cout << prenume << "\n"; cout << id return 0; << "\n"; strcpy(id,nume); strcat(id,"."); strcat(id,prenume); char nume [] = "Popescu"; char prenume [] = "Vasile"; char id [60] = {'\0'};

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: 7: 8: }; 9: 10: int main() 11: { 12: 13: 14: 15: 16: 17: 18: return 0; cout << "c1.re = " << c1.re << "\n"; cout << "c1.im = " << c1.im << "\n"; Complex c1 = {2.0,3.5}; double re; double im;

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: 7: 8: }; 9: int i; double d;

11 107

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

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: 7: 8: 9: 10: 11: 12: 13: 14: } return 0; test = 25; cout << "Valoare finala: " << valoare << "\n"; int valoare = 10; int &test = valoare; cout << "Valoare initiala: " << valoare << "\n";

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: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: } return 0; cout << "v1 " << v1 << "\n"; cout << "r1 " << r1 << "\n"; r2 = v2; int v2 = 20; int &r2 = v1; cout << "v1 " << v1 << "\n"; cout << "r1 " << r1 << "\n"; int v1 = 10; int &r1 = v1;

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: 7: 8: 9: } 10: 11: int main() 12: { 13: 14: int x = 10; int y = 20; int temp = x; x = y; y = temp;

3 113

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

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

Determinarea adresei variabilelor, operatorul &

Pentru determinarea adresei variabilelor se foloseste operatorul &.

#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 10 x Important

0x3045 0x1000 p

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 << return 0; "p " << p << "\n";

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: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: } return 0; } for (int i = 0; i < MAX; { cout << i << ": " << *(p + i) << "\n"; i++) const int MAX = 3; int v [] = {1,2,3}; int * p = v;

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 v[0] 0x5001 v[1] 0x5002 v[2] 0x5001 p+1 0x5000 0x5001 0x5002 v[0] v[1] v[2]

0x5000 p

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: 7: 8: 9: } 10: 11: int main() 12: { 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: } return 0; cout << "Valori finale\n"; cout << "x " << x << "\n"; cout << "y " << y << "\n"; Swap(&x , &y); cout << "Valori initiale\n"; cout << "x " << x << "\n"; cout << "y " << y << "\n"; int x = 10; int y = 20; int temp = *x; *x = *y; *y = temp;

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: 7: 8: }; 9: 10: int main() 11: { 12: 13: Complex c1 = {2.0, 3.5}; Complex * pC1 = &c1; double re; double im;

11 121

Adonis Butufei
14: 15: 16: 17: } return 0; (*pC1).im = 10;

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 = m [2] = delete m; 4; new int [3];

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 subprobleme 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 } int main() { int a = 2; cout << "valoare inainte apel" << a << "\n"; Test(a); cout << "valoare dupa apel" << a << "\n"; } << "n modificat in functia Test" << n << "\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 } int main() { int a = 2; cout << "valoare inainte apel" << a << "\n"; Test(&a); cout << "valoare dupa apel" << a << "\n"; } << "n modificat in functia Test" << n << "\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: 7: 8: }; 9: 10: 11: double& Im(Complex& c) double re; double im;

9 134

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

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 = result.im = } void AfiseazaComplex(const Complex& c) { cout << c.re; if(c.im > 0) { c1.re + c2.re; c1.im + c2.im ;

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( n1)+ 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 2: { 3: 4: 5: 6: public: 7: 8: 9: 10: 11: double Im(); void Im(double val); double Re(); void Re(double val); double _re; double _im; 12: };

1 Clasele derivate si mostenirea vor fi tratate intr-un capitol viitor.

4 149

Adonis Butufei
13: double Complex::Re() 14: { 15: 16: } 17: 18: double Complex::Im() 19: { 20: 21: } 22: void Complex::Re(double val) 23: { 24: 25: } _re = val; return _im; return _re; 26: void Complex::Im(double val) 27: { 28: 29: } 30: 31: int main() 32: { 33: 34: 35: 36: 37: 38: } return 0; Complex c1; c1.Re(2.0); c1.Im(3.5); _im = val;

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 { public: Complex(); }; } Complex::Complex() { _re = 0.0; _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 { public: Complex(double re, double im); }; } Complex::Complex(double re, double im) { _re = re; _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: 4: 5: } Complex c(2.5, 3.4); return 0;

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 { public: ~Complex(); }; Complex::~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 { return _re; } } double Complex::Im()const { 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 { private: int _valoare; public: Contor(int valoare = 0) ; Contor(const Contor& src); }; } Contor::Contor(int valoare) { _valoare = valoare;

Contor::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: 4: 5: 6: } Contor result = *this; _valoare++; return result;

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 Virgula Diferenta Egalitate Mai mare Mai mic Mai mare sau egal Mai mic sau egal Atribuire Adunare Scadere Inmultire Impartire

Operator
% += -= *= /= %= && || & | ^ <<

Nume Modulo Adunare si atribuire Scadere si atribuire Inmultire si atribuire Impartire si atribuire Modulo si atribuire Si logic Sau logic Si pe biti Sau pe biti Sau exclusiv Deplasare la stanga

6 166

Adonis Butufei Operator


>> &= |= ^= <<=

Nume Deplasare la dreapta Si pe biti si atribuire Sau pe biti si atribuire

Operator

Nume atribuire

>>=

Deplasare la dreapta si atribuire Operatorul index Selectia unui pointer la membru

[]

Sau exclusiv si atribuire Deplasare la stanga si

->*

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


Complex& operator = (const Complex& src);

Pentru atribuirea valorii atributelor unui obiect se foloseste operatorul = care are urmatorul prototip: Implementarea este prezentata mai jos:
7: Complex& Complex::operator = (const Complex& src) 8: { 9: 10: 11: 12: 13: 14: 15: 16: 17: } return *this; Init(src._re, src._im); } if(this == &src) { return *this;

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 .* :: ?: sizeof Selectia pointerilor la membri Domeniu de acces Operatorul ternar 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 ptr obiect2 ptr obiect1 ptr

Hello

Hello

In copierea completa se aloca memoria pentru variabilele pointeri si se copiaza valoarea acelor variabile local.

12 172

Adonis Butufei obiect1 ptr obiect2 ptr

Hello strcpy

Hello

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 << // ... }; class Autobuz : public Vehicul { public: Autobuz() { cout << "Autobuz()\n"; } ~Autobuz(){ cout << "~Autobuz()\n"; } // ... }; "Vehicul()\n" ; } ~Vehicul() { cout << "~Vehicul()\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: 6: 7: 8: 9: 10: }; 11: 12: class Autobuz : public Vehicul 13: { 14: public: 15: 16: 17: 18: 19: 20: 21: 22: 23: }; 24: } { cout << "Autobuz(int, int)\n"; Autobuz(int linie, int vitezaMaxima) : Vehicul(vitezaMaxima), _linie(linie) // codul anterior ... } { cout << "Vehicul(int)\n" ; Vehicul(int vitezaMaxima) : _vitezaMaxima(vitezaMaxima)

5 179

Adonis Butufei
25: int main() 26: { 27: 28: } Autobuz a(135,80);

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 Derivata Baza 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. Tipul metodei Normala Normala Virtual Ordinea de cautare Clasa derivata apoi clasa de baza Clasa de baza Clasa derivata apoi clasa de baza

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: 6: 7: }; 8: 9: class Autobuz : public Vehicul 10: { 11: public: 12: 13: 14: 15: }; // ... Autobuz(const Autobuz & src) : Vehicul(src) { _linie = src._linie; } virtual Vehicul* Clone() { return new Autobuz(*this); } // ... Vehicul(const Vehicul & src) { _vitezaMaxima = src._vitezaMaxima; } virtual Vehicul* Clone() { return new Vehicul(*this); }

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: 5: 6: }; 7: 8: class Birou 9: { 10: public: 11: 12: 13: }; 14: 15: class ServiceAuto : public Garaj, public Birou 16: { 17: public: 18: 19: 20: }; 21: int main() 22: { 23: 24: 25: 26: 27: 28: } return 0; ServiceAuto service; Birou &b = service; Garaj &g = service; ServiceAuto() { cout << "ServiceAuto()\n"; } ~ServiceAuto() { cout << "~ServiceAuto()\n"; }; Birou() { cout << "Birou()\n"; } virtual ~Birou() { cout << "~Birou()\n"; } Garaj() { cout << "Garaj()\n"; } virtual ~Garaj() { cout << "~Garaj()\n"; };

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: 4: public: 5: 6: 7: }; 8: 9: class Birou 10: { 11: 13: 14: 15: }; 16: 17: class ServiceAuto : public Garaj, public Birou 18: { 19: public: 20: 21: 22: ServiceAuto(int nrMasini, int nrEchipe) : Garaj(nrMasini), Birou(nrEchipe) { cout << "ServiceAuto(int, int)\n"; } int _nrEchipe; Birou(int nrEchipe) : _nrEchipe (nrEchipe) 12: public: { cout << "Birou(int);\n"; } Garaj(int nrMasini) : _nrMasini (nrMasini) { cout << "Garaj(int);\n"; } int _nrMasini;

11 185

Adonis Butufei
23: }; 24: 25: int main() 26: { 27: 28: 29: } ServiceAuto service(20,3); return 0;

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: 4: public: 5: 6: 7: 8: 9: 10: 11: 12: }; 13: 14: class Garaj : virtual public Cladire 15: { 16: 17: 18: public: 19: 20: 21: 22: 23: 24: 25: 26: }; 27: 28: class Birou : 29: { 30: 31: public: int _nrEchipe; virtual public Cladire virtual ~Garaj() { cout << "~Garaj()\n"; }; virtual double SuprafataActiva() { return _nrMasini * 10.5; } Garaj(int nrMasini) : Cladire(2),_nrMasini (nrMasini) { cout << "Garaj(int);\n"; } int _nrMasini; virtual ~Cladire() { cout << "~Cladire()\n"; }; } Cladire (int nrEtaje): _nrEtaje(nrEtaje) { cout << "Cladire(int) cu " << _nrEtaje << " etaje.\n"; int _nrEtaje;

13 187

Adonis Butufei
32: 33: 34: 35: 36: 37: 38: 39: }; 40: 41: class ServiceAuto : public Garaj, public Birou 42: { 43: public: 44: 45: 46: 47: 48: 49: 50: 51: }; 52: 53: int main() 54: { 55: 56: 57: } ServiceAuto service(20,4,3); return 0; ~ServiceAuto() { cout << "~ServiceAuto()\n"; }; } { cout << "ServiceAuto(int, int, int)\n"; ServiceAuto(int nrMasini, int nrEchipe, int nrEtaje) : Garaj(nrMasini), Birou(nrEchipe) , Cladire(nrEtaje) virtual double SuprafataActiva() { return _nrEchipe * 7.2; } virtual ~Birou() { cout << "~Birou()\n"; } Birou(int nrEchipe) : Cladire(1), _nrEchipe (nrEchipe) { cout << "Birou(int);\n"; }

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, { return (x > y) } ? x : y; double 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 3: { 4: 5: } return (x > y) ? x : y; Max(T x, T y)

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: 2: 3: 4: 5: 6: double dVal = Max(3.5, 7.3); char cVal = Max('a', 'B'); int n1 = 3, n2 = 5; int max1 = Max(n1,n2); int max2 = Max<int>(n1,n2);

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: 5: 6: 7: 8: 9: } } return y; if(strcmp(x, y) >= 0) { return x;

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: 5: } return (*x > *y) ? x : y;

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: 5: 6: 7: 8: 9: } } cout << "\n"; for(int i = 0; i < SIZE; i++) { cout << tablou[i] << " ";

Apelul cestei functii este prezentat in exemplul de mai jos:


1: 2: 3: const int MAX = 3; int test[MAX] = {1,2,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: 5: } 6: 7: 8: bool 9: { 10: 11: } 12: 13: 14: int main() 15: { 16: 17: 18: 19: 20: 21: } return 0; bool ret = Test<NumarPar>(x); int x = 5; return (0 == n % 2); NumarPar(int n) return F(n);

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: 5: 6: 7: 8: 9: 10: 11: } } return result; for(int i = 1; i < SIZE; ++i) { result = Comparator(result, tablou[i]); T result = tablou[0];

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: 5: } return x < y ? x : y;

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: 5: } 6: 7: template<> 8: void Print(const double &x) 9: { 10: 11: } 12: 13: template<> 14: void Print<string>(const string &x) 15: { 16: 17: } 18: 19: void Print(const string &x) 20: { 21: 22: } cout << "Print: " << x << "\n"; cout << "Print<string>: " << x << "\n"; cout << "Print<double>: " << x << "\n"; cout << "Print<T>: " << x << "\n";

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: 8: 9: 10: public: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: } int Size() { return _size; } T& operator [] (int index) { return _data[index]; } ~Tablou() { delete [] _data; } { _data = new T[size]; Tablou(int size = 10) : _size(size) int _size; T *_data;

12 204

Adonis Butufei
33: private: 34: 35: 36: }; 37: #endif Tablou(const Tablou& src); Tablou& operator = (const Tablou& src);

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: 9: 10: 11: 13: 14: 15: }; 16: #endif T _data[SIZE]; Tablou() {} T& operator[] (int index) { return _data[index]; } int Size() { return SIZE; } Tablou(const Tablou& src); Tablou& operator = (const Tablou& src); 8: public:

12: private:

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: 9: 10: 11: 13: 14: 15: }; 16: 17: #endif T _data[SIZE]; Tablou() {} T& operator[] (int index) { return _data[index]; } int Size() { return SIZE; } Tablou(const Tablou& src); Tablou& operator = (const Tablou& src); 8: public:

12: private:

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: 8: 9: public: 10: 11: 12: Tablou() {} T& operator[] (int index) { return _data[index]; } int Size() { return SIZE; } T _data[SIZE];

15 207

Adonis Butufei
13: private: 14: 15: 16: }; 17: #endif Tablou(const Tablou& src); Tablou& operator = (const Tablou& src);

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: 7: 8: 9: public: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: } } for(int i = 0; i < _size; ++i) { _data[i] = 0; { _data = new PCHAR[_size]; Tablou(int size = 10) : _size(size) int _size; PCHAR *_data;

16 208

Adonis Butufei
21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: private: 58: 59: 60: }; Tablou(const Tablou& src); Tablou& operator = (const Tablou& src); int Size() { return _size; } } _data[index] = new char[strlen(elem) + 1]; strcpy(_data[index], elem); } if(elem == 0) { _data[index] = elem; return; delete [] _data[index]; } void Set(int index, PCHAR elem) { if(_data[index] == elem) { return; PCHAR operator [] (int index) { return _data[index]; } } delete [] _data; } } ~Tablou() { for(int i = 0; i < _size; ++i) { if(_data[i] != 0) { delete [] _data[i];

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: 6: }; 7: 8: template <typename U, typename V> 9: class Demo<U*,V*> 10: { 11: public: 12: 13: }; Demo() { cout << "Clasa specializata partial U* V*\n"; } Demo() { cout << "Clasa generica U V\n"; }

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: 6: 7: }; Demo() { cout << "Clasa specializata partial U U\n"; }

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: 6: 7: public: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: } } ~Tablou() { for(int i = 0; i < SIZE; ++i) { delete [] _data[i]; } } Tablou() { for(int i = 0; i < SIZE; ++i) { _data[i] = 0; PCHAR _data[SIZE];

20 212

Adonis Butufei
24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 46: 47: 48: }; PCHAR operator [] (int index) { return _data[index]; } int Size () { return SIZE; } Tablou(const Tablou& src); Tablou& operator = (const Tablou& src); } _data[index] = new char[strlen(element) + 1]; strcpy(_data[index], element); } if(element == 0) { _data[index] = element; return; delete [] _data[index]; } void Set(int index, PCHAR element) { if(_data[index] == element) { return;

45: private:

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: 5: 6: }; Demo() { cout << "Clasa specializata partial U* U*\n"; }

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: 5: }; static T _m; // Declararea atributului static _m.

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: 6: 7: }; T _t; T Get() { return _t; } 5: public:

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: 6: 7: 8: }; T _t; T Get() { return _t; } friend void Set<>(DemoFriend<T> &instanta, const T &valoare); 5: public:

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: 7: 9: 10: }; 11: 12: template <typename U> 13: class Accesor 14: { 15: 17: 18: 19: }; U &_u; Accesor(U &u) : _u(u) {} 16: public: U& operator * () { return _u; } template <typename U> friend class Accesor; T _t; // Atribut privat. Accesor<T> Get() { return Accesor<T> (_t); }

8: public:

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 = 3: *a = 10; c.Get();

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: 6: }; 7: 8: class B 9: { 10: 12: 13: 14: }; int _x, _y; B (int a, int b) { _x=a; _y=b; } int Result() { return _x + _y;} 11: public: float _i, _j;

25 217

Adonis Butufei
15: int main () 16: { 17: 18: 19: 20: 21: } A d; B * padd = (B *) &d; cout << padd->Result(); return 0;

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: 5: }; 6: class B : public A {}; virtual ~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: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: cout.unsetf(ios::showbase); cout.setf(ios_base::dec); cout.setf(ios::showbase); // Setarea flagului pentru afisarea bazei. cout << "Reprezentarea numarului " << i; cout << " cu afisarea bazei" << endl; cout << "in baza 10: " << dec << i << endl; cout << "in baza 16: " << hex << i << endl; cout << "in baza 8: " << oct << i << endl; cout.setf(ios_base::dec); // Reset flags. int i = 25; cout << "Reprezentarea numarului " << i; cout << " fara afisarea bazei" << endl; cout << "in baza 16: " << hex << i << endl; cout << "in baza 10: " << dec << i << endl; cout << "in baza 8: cout << endl; " << oct << i << endl;

3 De exemplu daca am vrea sa afisam mesajul diferit pe ecran si intr-un fisier de log.

4 226

Adonis Butufei
24: 25: } return 0;

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: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: return 0; cout.precision(oldPrec); cout << "Variabila double format scientific: " << endl; cout << scientific << x << endl << endl; cout << "Precizie: " << newPrec << endl; cout << "Variabila double format fixed: " << endl; cout << fixed << x << endl << endl; int newPrec = 7; int oldPrec = cout.precision(newPrec); cout << "Variabila double format scientific: " << endl; cout << scientific << x << endl << endl; cout << "Precizie implicita" << endl; cout << "Variabila double format fixed: " << endl; cout << fixed << x << endl << endl; double x = 2.0 / 7.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: 10: 11: 12: 13: 14: } return 0; string mesaj = "hello"; cout << mesaj << endl; cout << setw(10) << mesaj;

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 5: 6: class Complex <iostream> 4: using namespace std;

7 229

Adonis Butufei
7: { 8: 10: 11: 12: 13: }; 14: #endif // ... // ... friend ostream& operator << (ostream& out, const Complex &c); friend istream& operator >> (istream& in, Complex &c); 9: public:

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: 4: 5: } 6: 7: istream& operator >> (istream& in, Complex &c) 8: { 9: 10: 11: 12: } in >> c._re; in >> c._im; return in; out << c._re << " " << c._im; return out;

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: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: } out.open("Exemplu.dat",ios::app); out << "A treia linie" << endl; out.close(); return 0; out << "Prima linie" << endl; out << "A doua linie" << endl; out.close(); } if(! out) { cout << "Deschiderea fisierului a esuat!\n"; return 1; ofstream out("Exemplu.dat");

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: 9: 10: 11: 12: if(! in) { cout << "Deschiderea fisierului a esuat!\n"; ifstream in("Exemplu.dat");

9 231

Adonis Butufei
13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: } in.close(); return 0; } while(in) { in.getline(linie,MAX); cout << linie << endl; const int MAX = 256; char linie[MAX]; } return 1;

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: 4: 5: 6: } string linie; getline( in , linie); cout << linie << endl;

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: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: iofile.close(); return 0; } while(iofile) { string linie; getline(iofile, linie); cout << linie << endl; iofile.seekg(0,ios::beg); iofile.seekp(-14,ios::end); iofile << "Linia finala" << endl; iofile << "Prima linie" << endl; iofile << "A doua linie" << endl; } if(! iofile) { cout << "Deschiderea fisierului a esuat!\n"; return 1; fstream iofile("ExempluRW.dat", ios::in | ios::out | ios::trunc);

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 ios::app ios::ate ios::trunc ios::nocreate ios::noreplace Explicatie adauga la sfarsitul fisierului. muta cursorul la sfarsitul fisierului dar se poate scrie oriunde sterge vechiul continut al fisierelor existente, acesta este valoarea implicita daca fisierul nu exista operatia de deschidere esueaza 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: 7: 8: 9: stream << y; stringstream stream; int x, y = 10;

12 234

Adonis Butufei
10: 11: 12: 13: 14: 15: 16: 17: 18: } return 0; stream.str(""); stream.clear(); cout << stream.str() << endl; stream >> x;

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: 4: 5: 6: 7: 8: 9: } } // ... if(n < 0) { cerr << "Parametrul de intrare are valoare negativa\n"; exit(1);

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: 8: 9: } 10: 11: int main() 12: { 13: 14: 15: 16: } int *x = NULL; Print(x); return 0; assert( x != NULL); cout << *x << endl;

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: 4: 5: 6: 7: 8: 9: } // ... } if(n < 0) { throw "Parametru negativ.";

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: 7: 8: try { int result = Factorial(-1);

5 241

Adonis Butufei
9: 10: 11: 12: 13: 14: 15: } } return 0; } catch(char* msg) { cout << msg << endl;

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: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: } } // ... if (n > MAX ) { throw MAX; } if(n < 0) { throw "Parametru negativ."; const int MAX = 12; // Valoarea maxima pentru care n! este calculat // corect.

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: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: } } return 0; } catch(int x) { cout << "Valoarea maxima: "<< x <<" a parametrului este depasita.\n"; } catch(char* msg) { cout << msg << endl; try { int result = Factorial(13);

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: 4: } 5: catch(FileException ) 6: { 7: 8: } 9: catch(MemoryException ) 10: { 11: 12: } 13: catch (...) 14: { 15: 16: // trateaza toate exceptiile pentru care nu exista un bloc catch. // ... // ... // ... FunctieCareRidicaExceptii();

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

Data() { cout << "Data()" << endl; } ~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: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: }

try { cout << "Inceputul blocului try" << endl; Data d; cout << "Ridicarea exceptiei" << endl; throw 1; cout << "Sfarsitul blocului try" << endl; } catch(int) { cout << "Tratarea exceptiei" << endl; } return 0;

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: 4: 5: 6: 7: 8: 9: try { cout << "Inceputul blocului try" << endl; Data * d = new Data; cout << "Ridicarea exceptiei" << endl; throw 1; delete d;

9 245

Adonis Butufei
10: 11: 12: 13: 14: 15: 16: 17: 18: } return 0; } } catch(int) { cout << "Tratarea exceptiei" << endl; cout << "Sfarsitul blocului try" << endl;

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: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: } if (d != NULL) { delete d; } catch(int) { cout << "Tratarea exceptiei" << endl; Data * d = NULL; try { cout << "Inceputul blocului try" << endl; d = new Data; cout << "Ridicarea exceptiei" << endl; throw 1; cout << "Sfarsitul blocului try" << endl;

10 246

Adonis Butufei
19: 20: 21: } } return 0;

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: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: }; Data* D() { return _d; } } } ~Helper() { if(_d != NULL) { delete _d; } Data *_d; Helper (): _d(NULL) { _d = new Data(); 4: public:

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: 4: 5: 6: 7: 8: 9: 10: 11: 12:

try { cout << "Inceputul blocului try" << endl; Helper h; Data *d = h.D(); cout << "Ridicarea exceptiei" << endl;

11 247

Adonis Butufei
13: 14: 15: 16: 17: 18: 19: 20: 21: 22: }

throw 1; cout << "Sfarsitul blocului try" << endl; } catch(int) { cout << "Tratarea exceptiei" << endl; } return 0;

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: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: } } Data(int n) { cout << "Data(int )" << endl; if (0 == (n % 2) ) { throw n; Data() { cout << "Data()" << endl; }

12 248

Adonis Butufei
19: 20: }; 21: 22: int main() 23: { 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: } return 0; } } catch(int x) { cout << "Tratarea exceptiei " << x << endl; try { Data d(2); ~Data() { cout << "~Data()" << endl; }

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: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: }; 25: 26: int main() 27: { 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: } return 0; } } catch (char* mesaj) { cout << mesaj << endl; try { Container c(2); } ~Container() { cout << "~Container()" << endl; } } catch(int x) { cout << "Exceptia " << x << " a fost tratata in constructor" << endl; throw "Containerul nu a putut fi creat"; { cout << "Container ()" << endl; Data _d; Container(int n) try : _d(n) 7: public:

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: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: } // .... } if (n > MAX ) { throw DepasireMax(); } if(n < 0) { throw ParametruNegativ();

Iar apelul poate avea urmatoarea implementare:


1: int main() 2: { 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: } } return 0; } catch(Exceptie e) { cout << e.Mesaj() << endl; try { Factorial(-1); Factorial(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: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: }

vector<int> v1; vector<int> v2(10); vector<int> v3(5, 24); vector<int> v4(v3); vector<int> v5(v4.begin(), v4.begin() + 2); return 0;

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: 8: 9: 10: 11: 12: cout << "Vectorul contine " << v.size() << " elemente." << endl; vector<int> v; v.push_back(1); v.push_back(5); v.push_back(10);

3 257

Adonis Butufei
13: 14: 15: } return 0;

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: 8: 9: 10: 11: 12: 13: 14: 15: } return 0; cout << "Vectorul contine " << v.size() << " elemente." << endl; v[0] = 10; v[1] = 100; vector<int> v(2);

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: 8: 9: 10: 11: 12: v[0] = 125; v[1] = 476; v[2] = 953; vector<int> v(3);

4 258

Adonis Butufei
13: 14: 15: 16: 17: 18: 19: 20: 21: } return 0; } for(int i = 0; i < max; i++) { cout << "v[" << i << "]= " << v[i] << endl; int max = v.size();

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: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: for(int i = 0; i < max; i++) { cout << "v[" << i << "]= " << v[i] << endl; int max = v.size(); cout << "Dupa apelul pop_back() vectorul contine " << v.size() cout << " elemente." << endl; v.pop_back(); cout << "Initial vectorul contine " << v.size() << " elemente.\n"; v.push_back(23); v.push_back(45); v.push_back(89); v.push_back(32); vector<int> v;

5 259

Adonis Butufei
26: 27: 28: 29: } return 0; }

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: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: } return 0; cout << "Dupa inserarea unui element" << endl; cout << "dimensiunea: " << v.size(); << endl; cout << " capacitatea: " << v.capacity() v.push_back(10); cout << "Vectorul a fost creat cu " << endl; cout << "dimensiunea: " << v.size(); cout << " capacitate: " << v.capacity() << endl; vector<int> v(4);

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: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: } return 0; } for(int i = 0; i < SIZE; ++i) { cout << d[i] << endl; } } } else { d.push_front(i); const int SIZE = 10; for(int i = 0; i < SIZE; i++) { if( 0 == (i % 2)) { d.push_back(i); deque<int> d;

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: 9: 10: 11: 12: 13: 14: 15: 16: 17: } return 0; PrintContainer(lst); lst.push_back(10); lst.push_back(15); lst.push_back(20); list<int> lst;

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: 7: 8: 9: 10: 11: for(Container::const_iterator crt = src.begin(); crt { != end; ++crt) Container::const_iterator end = src.end(); cout << "{ " ;

8 262

Adonis Butufei
12: 13: 14: 15: 16: } 17: #endif cout << "}" << endl; } cout << (*crt) << " ";

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: 9: 10: 11: 12: 13: 14: 15: 16: 17: } return 0; PrintContainer(lst); lst.push_front(10); lst.push_front(15); lst.push_front(20); list<int> lst;

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: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: } return 0; PrintContainer(lst); cout << "Continutul listei dupa stergere" << endl; lst.erase(lst.begin(), lst.end()); cout << "Continutul listei inainte de stergere" << endl; PrintContainer(lst); list<int> lst; lst.push_front(6); lst.push_front(7);

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: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: } cout << "Containerul multiset contine " << cout << " elemente cu valoarea 12." << endl; return 0 msint.count(12); cout << "Continutul containerului multiset:" << endl; PrintContainer(msint); msint.insert(12); multiset<int> msint; msint.insert(15); msint.insert(12); msint.insert(17); cout << "Continutul containerului set:" << endl; PrintContainer(sint); sint.insert(12); set<int> sint; sint.insert(15); sint.insert(12); sint.insert(17);

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: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: } 19: 20: int main() 21: { 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: } return 0; Find(-1, sint); Find(11, sint); sint.insert(15); sint.insert(32); sint.insert(11); set<int> sint; } } else { cout << "Elementul " << element << " nu exista in set" << endl; if(itElement != src.end()) { cout << "Elementul " << (*itElement) cout << " a fost gasit in set" << endl; set<int>::iterator itElement = src.find(element);

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: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: } return 0; cout << "Continutul containerului dupa stergere:" << endl; PrintContainer(mset); mset.erase(elem); cout << "Introduceti valoarea elementului pentru stergere: " ; int elem; cin >> elem; cout << "Continutul containerului:" << endl; PrintContainer(mset); mset.insert(11); mset.insert(20); mset.insert(13); mset.insert(11); multiset<int> mset;

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: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: mmap.insert(pair<string,string>("trei","3")); mmap.insert(pair<string,string>("trei","III")); mmap.insert(make_pair("doi","2")); mmap.insert(make_pair("doi","II")); MultiMap mmap; mmap.insert(MultiMap::value_type("unu","1")); mmap.insert(MultiMap::value_type("unu","I")); cout << "Map-ul contine " << map.size() << " cout << "Continutul map-ului este:" << endl; PrintPairs(map); elemente." << endl; map["patru"] = "4"; map.insert(Map::value_type("unu","1")); map.insert(make_pair("doi","2")); map.insert(pair<string,string>("trei","3")); Map map;

14 268

Adonis Butufei
35: 36: 37: 38: 39: 40: } return 0; cout << "Multimap-ul contine " << mmap.size() << " elemente" << endl; cout << "Continutul multimap-ului este:" << endl; PrintPairs(mmap);

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 3: 4: template <typename Container> 5: void PrintPairs(const Container& src) 6: { 7: 8: 9: 10: 11: 12: 13: 14: } 15: #endif } for(Container::const_iterator crt = src.begin(); crt != end; ++crt) { cout << "(" << crt->first << ": "; cout << crt->second << " ) " << endl; Container::const_iterator end = src.end(); PRINTPAIRS_H 2: #define PRINTPAIRS_H

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: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: } 34: 35: int main() } for(int i = 0; i < { cout << "(" << it->first << ": " << it->second << ")" << endl; count; ++i, ++it) cout << key << "." << endl; } } else { cout << "Au fost gasite " << count << " perechi care au cheia "; if ( 1 == count ) { cout << "A fost gasita o pereche care are cheia "; int count = src.count(key) ; } Container::const_iterator it = src.find(key); if(it == src.end()) { cout << "Nu exista nici o pereche cu cheia " << key << ".\n"; return;

16 270

Adonis Butufei
36: { 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: } return 0; Find("trei",mmap); cout << "Continutul multimap-ului este:" << endl; PrintPairs(mmap); mmap.insert(pair<string,string>("trei","3")); mmap.insert(pair<string,string>("trei","III")); mmap.insert(make_pair("doi","2")); mmap.insert(make_pair("doi","II")); MultiMap mmap; mmap.insert(MultiMap::value_type("unu","1")); mmap.insert(MultiMap::value_type("unu","I")); cout << "Continutul map-ului este:" << endl; PrintPairs(map); Find("trei", map); map.insert(Map::value_type("unu","1")); map.insert(make_pair("doi","2")); map.insert(pair<string,string>("trei","3")); Map map;

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: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: } return 0; mmap.erase(mmap.lower_bound("trei"), mmap.upper_bound("trei")); cout << "Continutul multimap-ului dupa stergerea unui interval:\n"; PrintPairs(mmap); } cout << "Continutul multimap-ului dupa stergerea iteratorului:\n; PrintPairs(mmap); MultiMap::iterator it = mmap.find("doi"); if(it != mmap.end()) { mmap.erase(it); mmap.erase("unu"); cout << "Continutul multimap-ului dupa stergerea unei cheii unu:\n"; PrintPairs(mmap); cout << "Continutul multimap-ului este:" << endl; PrintPairs(mmap); mmap.insert(pair<string,string>("trei","3")); mmap.insert(pair<string,string>("trei","III")); mmap.insert(make_pair("doi","2")); mmap.insert(make_pair("doi","II")); MultiMap mmap; mmap.insert(MultiMap::value_type("unu","1")); mmap.insert(MultiMap::value_type("unu","I"));

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 *it it-> ++it it++ acceseaza elementul acces de citire la element se muta in fata si returneaza noua pozitie se muta in fata si returneaza vechea pozitie Tip iterator intrare, iesire, deplasare in fata, deplasare in ambele directii, acces aleator

it1 == it2 verifica egalitatea iteratorilor it1 != it2 --it it-it[n] it+=n it-=n it + n verifica diferenta iteratorilor se muta in spate si returneaza noua pozitie se muta in spate si returneaza vechea pozitie returneaza elementul de la indexul n muta n pozitii in fata (sau in spate daca n < 0) muta n pozitii in spate (sau in fata daca n < 0) returneaza iteratorul pentru elementul cu n pozitii in fata 19 273 deplasare in ambele directii, acces aleator acces aleator

Adonis Butufei Operatie Explicatie n + it it n it1 it2 returneaza iteratorul pentru elementul cu n pozitii in fata returneaza iteratorul pentru elementul cu n pozitii in spate returneaza distanta intre doi iteratori Tip iterator

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: 6: 7: 8: 9: 10: 11: }; T Valoare() { return _t; } void operator () (const T& element) { _t += element; } T _t; Sum(T t = 0): _t(t) {} 5: public:

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: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: } Sum<int> s; int size = vint.size(); for(int i =0; i < size; i++) { s(vint[i]); vector<int> vint; vint.push_back(19); vint.push_back(12); vint.push_back(21);

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: 6: }; T operator () (const T& t1, const T& t2) { return t1 + t2; }

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: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: } for(int i = 0; i < SIZE; i++) { c[i] = add(a[i], b[i]); AdunaElemente<int> add ; } for(int i = 0; i < SIZE; i++) { a[i] = 2 * i; b[i] = a[i] + 1; const int SIZE = 10; vector<int> a(SIZE), b(SIZE), c(SIZE);

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: 5: 6: 7: 8: }; bool operator() (const int& src) { return (0 == src % _divizor ); } int _divizor; TestMultiplu(int divizor = 1): _divizor(divizor) {}

4: public:

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: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: } } for(int i =0; i < SIZE; i++) { if(multiplu(vint[i]) ) { m.push_back(vint[i]); TestMultiplu multiplu(3); vector<int> m; const int SIZE vint [0] = 19; vint [1] = 12; vint [2] = 21; =3; vector<int> vint(SIZE);

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: 6: 7: 8: 9: bool b1 = abs(c1.Re() - c2.Re()) < DELTA; bool operator() (const Complex& c1, const Complex& c2) { const double DELTA = 1e-5;

3 Clasa Complex a fost definita in capitolul 9.

22 276

Adonis Butufei
10: 11: 12: 13: 14: }; } return b1 && b2; bool b2 = abs(c1.Im() - c2.Im()) < DELTA;

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: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: } } for(int i = 0; i < count; i++) { if(compare(t,vcplx[i])) { gasit = true; break; int count = vcplx.size(); bool gasit = false; CompareComplex compare; Complex t(3.2, 4.5); vector<Complex> vcplx; vcplx.push_back(Complex(3.2,4.5)); vcplx.push_back(Complex(1.3, 2.6));

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: 10: } 11: 12: int main() 13: { 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: if(itImpar != v.end()) { cout << "Numarul " << (*itImpar ) << " se afla pe pozitia ["; vector<int>::iterator itImpar = find_if(v.begin(),v.end(),NumarImpar); } vector<int>::iterator it = find(v.begin(), v.end(), 2); if(it != v.end()) { cout << "Numarul 2 a fost gasit in vector" << endl; int nr3 = count(v.begin(), v.end(), 3); cout << "Vectorul contine de " << nr3 << " ori numarul 3" << endl; int nrImpare = count_if(v.begin(),v.end(), NumarImpar); cout << "Vectorul are " << nrImpare << " numere impare" << endl; cout << "Elementele vectorului" << endl; PrintContainer(v); } v.push_back(3); for(int i = -3; i < 5; i++) { v.push_back(i); vector<int> v; return (0 != (i % 2));

24 278

Adonis Butufei
42: 43: 44: 45: 46: } return 0; } cout << distance(v.begin(), itImpar) << "]" << endl;

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: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: { cout << "Elementele listei au fost gasite la pozitia " ; cout << "Cautarea elementelor listei in vector" << endl; vector<int>::iterator itSearch = search(v.begin(),v.end(), interval.begin(), interval.end()); if(itSearch != v.end()) } cout << "Continutul listei" << endl; PrintContainer(interval); list<int> interval; for(int i = -2; i < 3; i++) { interval.push_back(i); cout << "Continutul vectorului" << endl; PrintContainer(v); } v.push_back(5); v.push_back(5); vector<int> v; for(int i = -5; i < 5; i++) { v.push_back(i);

25 279

Adonis Butufei
26: 27: 28: 29: 30: 31: 32: 33: 34: 35: } cout << "Cautarea secventei {5 5}" << endl; vector<int>::iterator itSearchN = search_n(v.begin(), v.end(), 2, 5); if(itSearchN != v.end()) { cout << "secventa {5 5} a fost gasita pe pozitia " ; cout << distance(v.begin(), itSearchN) << endl; } cout << distance(v.begin(), itSearch) << endl << endl;

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: 2: 3: 4: 5: v.resize(5); fill_n(v.begin() + 3, 2,-4); vector<int> v(3); fill(v.begin(), v.end(), 5);

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: 2: 3: for(int i = 0; i < 6; i++) vector<int> v;

26 280

Adonis Butufei
4: 5: 6: 7: 8: 9: 10: 11: 12: 13: Sum<int> sum = for_each(v.begin(), v.end(), Sum<int> ()); cout << "Suma elementelor este "; cout << sum.Valoare() << endl; cout << "Continutul vectorului" << endl; PrintContainer(v); } { v.push_back(i);

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: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: transform(x.begin(), x.end(), y.begin(), z.begin(), AdunaElemente<int>()); cout << "Vectorul y" << endl; PrintContainer(y); cout << "Vectorul x" << endl; PrintContainer(x); } for(int i = 0; i < SIZE; i++) { x[i] = i; y[i] = SIZE - i; const int SIZE = 10; vector<int> x(SIZE), y(SIZE), z(SIZE);

27 281

Adonis Butufei
19: 20: cout << "Vectorul care contine suma elementelor" << endl; 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: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: cout << "Stergerea elementelor impare din vector" << endl; endAfterRemove = remove_if(v.begin(),v.end(),NumarImpar); v.erase(endAfterRemove,v.end()); PrintContainer(v); cout << "Stergerea elementelor 0 din vector" << endl; vector<int>::iterator endAfterRemove = remove(v.begin(), v.end(),0); v.erase(endAfterRemove,v.end()); PrintContainer(v); copy_backward(lst.begin(),lst.end(), v.end()); cout << "Continutul vectorului dupa copiere" << endl; PrintContainer(v); vector<int> v(lst.size() * 2); copy(lst.begin(), lst.end(), v.begin()); cout << "Continutul listei" << endl; PrintContainer(lst); } list<int> lst; for(int i = 0; i < 10; i++) { lst.push_back(i);

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: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: cout << "Numerele impare au fost inlocuite cu -2"; cout << " folosind replace_if" << endl; replace_if(v.begin(), v.end(), NumarImpar, -2); PrintContainer(v); cout << "Valoarea 4 a fost inlocuita cu 7 folosind replace" << endl; replace(v.begin(), v.end(), 4, 7); PrintContainer(v); cout << "Continutul initial al containerului" << endl; PrintContainer(v); vector<int> v(5); fill(v.begin(),v.begin() + 2,3); fill_n(v.begin() + 2, 3,4); random_shuffle(v.begin(), v.end());

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: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: cout << "Eliminarea valorilor duplicate" << endl; vector<int>::iterator endAfterRemove = unique(v.begin(), v.end()); v.erase(endAfterRemove, v.end()); PrintContainer(v); } } else { cout << "Elementul 2 nu a fost gasit" << endl; if(found) { cout << "Elementul 2 a fost gasit" << endl; cout << "Cautarea elementului 2 in vector folosind binary_search\n"; bool found = binary_search(v.begin(), v.end(), 2); cout << "Sortarea vectorului" << endl; cout << "Continutul initial al vectorului" << endl; PrintContainer(v); } vector<int> v; for(int i = 0; i < 10; i++) { v.push_back(i % 3);

sort(v.begin(), v.end());
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