Documente Academic
Documente Profesional
Documente Cultură
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
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.
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
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
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
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
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
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
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
3. Evaluare alternative
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
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
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
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.
14 14
Adonis Butufei
1.8.2
Scrierea codului
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
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.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
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
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
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
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
string prenume; cout << "Introduceti prenumele: \n"; cin >> prenume; // variabila prenume este initializata // de la consola; "Numele complet este: " << nume << " ";
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.
2.2.7
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
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";
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";
2.2.9
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
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
Exemplu
result = 4 + 5; // result = 9 5 result = 20 15; // result = result = 8 *
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
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
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
if(conditie); // ; aici este gresit daca este indeplinita conditia { } // instructiunea nula, adica (;) pus din greseala // aceste instructiuni se executa mereu!
2.
{
if( x = y) //... }
3.
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.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:
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
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
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
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;
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
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
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
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; }
27 43
Adonis Butufei
cin >> anNastere; cout << Introduceti anul curent\n; cin >> anCurent; varsta = anCurent anNastere; return 0; }
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
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
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
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
3.1.4.2
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
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
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
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
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
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
11 56
Adonis Butufei
3.3.1.5
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
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
short
unsigned short
int
int
unsigned
long
long
unsigned long
12 57
Adonis Butufei
long long
LLONG_MAX LLONG_MIN
9223372036854775807i64
long long
valoare minima a tipului (9223372036854775807i64 - long long (cu semn) 1) 0xffffffffffffffffui64 valoare maxima a tipului long long (fara semn)
ULLONG_MAX
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
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
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
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
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; }
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)
17 62
Adonis Butufei
3.4
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
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
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
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
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--;
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.
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.
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.
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.
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.
-= *= /= %=
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;
4 73
Adonis Butufei Acesti operatori au acelasi nivel de precedenta cu operatorul = si asociativitatea este de la dreapta spre stanga.
++ --
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;
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
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
|| && !
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
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(); }
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
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;
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
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.
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);
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
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; }
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; }
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++)
Descriere incrementare, decrementare cu postfixare apel de functie incrementare, decrementare cu prefixare negare logica si negare pe biti operatori de semm adunare scadere
++ -! ~ + -
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.
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
== != & ^ | && || ?:
18
virgula
de la stanga la dreapta
Adonis Butufei
int y = 0; int x = ++y;
17 86
Adonis Butufei
x = 2; } else { x = 3; }
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
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
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.
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
#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 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.
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.
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.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
1 97
Adonis Butufei
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};
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.
Initializarea tuturor elementelor cu 0 se realizeaza folosind o singura valoare in dreapta operatorului =. Exemplu:
double matrice [2][2] = {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.
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; }
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.
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.
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.
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.
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.
In acest caz varloarea variabilei re este 1.2 iar valoarea variabilei im este 3.4.
10 106
Adonis Butufei
19: }
In acest exemplu in liniile 15 si 16 se afiseaza valorile partii reale si imaginare ale variabilei complex 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.
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:
#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 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;
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
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;
7.2.4
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
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!
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
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
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;
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
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
13 123
Adonis Butufei
int & ref1 = x; int & ref2 = ref1; ref2 = 35;
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;
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
1 126
Adonis Butufei
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.
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();
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
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.
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";
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.
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
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.
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;
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.
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.
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.
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.
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.
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.
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.
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.
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
Pentru a folosi entitatile din spatiile de nume Contabilitate respectiv Management vom folosi 16 141
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.
17 142
Adonis Butufei
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; }
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"; }
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.
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.
Adonis Butufei
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
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); }
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() { }
8 153
Adonis Butufei Modificarile valorilor atributelor in metodele declarate constante genereaza erori de compilare.
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. //.... };
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
};
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.
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";
11 156
Adonis Butufei
}; #endif
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.
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); }
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); };
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
1 161
Adonis Butufei
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);
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
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.
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); }
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.
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
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; }
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.
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 ();
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
Operator
Nume atribuire
>>=
[]
->*
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.
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
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.
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);
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
In cazul in care folosim obiecte constante este necesara adaugarea unui operator de indexare constant. Exemplu:
int & operator [] (int index) const;
()
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);
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.
Adonis Butufei .* :: ?: sizeof Selectia pointerilor la membri Domeniu de acces Operatorul ternar Returneaza dimensiunea in octeti a unui obiect
Hello
Hello
In copierea completa se aloca memoria pentru variabilele pointeri si se copiaza valoarea acelor variabile local.
12 172
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.
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
1 175
Adonis Butufei
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.
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.
4 178
Adonis Butufei
int main() { Autobuz v1; v1.Linia(135); }
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.
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(); }
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
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
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.
In acest caz apelul de mai jos creaza o copie a obiectului de tip autobuz.
Vehicul *p = new Autobuz; Vehicul *p1 = p->Clone();
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
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 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"; }
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();
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).
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;
Deoarece clasa Patrat implementeaza metoda putem instantia clase de tipul Patrat.
15 189
Adonis Butufei
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.
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; }
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
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;
2 194
Adonis Butufei
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>
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
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.
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
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.
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.
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.
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;
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
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.
10 202
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 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.
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: 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;
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;
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
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
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.
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"; }
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.
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
Important Atunci cand se folosesc functii template friend este necesara declararea versiunii specializate ca in exemplul prezentat in aceasta sectiune.
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
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
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.
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);
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
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.
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
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
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.
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
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.
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.
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.
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";
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.
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
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
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>.
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.
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;
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);
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.6
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
1 237
Adonis Butufei
14.1
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.
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.
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
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.
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!
/ 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.
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.
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
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
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
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.
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
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.
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
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.") { } };
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();
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.
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.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 {};
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
1 255
Adonis Butufei
15.
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.
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
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
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
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
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.
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.
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
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
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
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.
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
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
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
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.
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);
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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