0% au considerat acest document util (0 voturi)
3 vizualizări192 pagini

Complemente CPP

Documentul oferă o prezentare detaliată a limbajului C și C++, incluzând istoria, standardizarea și dezvoltarea acestora. De asemenea, se discută despre proiectul GNU și fundația Free Software, care au contribuit la crearea unui sistem de operare gratuit, GNU/Linux. Informațiile sunt structurate în lecții, acoperind concepte fundamentale de programare, programare orientată pe obiecte și programare generică.

Încărcat de

Cosmin Florescu
Drepturi de autor
© All Rights Reserved
Respectăm cu strictețe drepturile privind conținutul. Dacă suspectați că acesta este conținutul dumneavoastră, reclamați-l aici.
Formate disponibile
Descărcați ca PDF, TXT sau citiți online pe Scribd
0% au considerat acest document util (0 voturi)
3 vizualizări192 pagini

Complemente CPP

Documentul oferă o prezentare detaliată a limbajului C și C++, incluzând istoria, standardizarea și dezvoltarea acestora. De asemenea, se discută despre proiectul GNU și fundația Free Software, care au contribuit la crearea unui sistem de operare gratuit, GNU/Linux. Informațiile sunt structurate în lecții, acoperind concepte fundamentale de programare, programare orientată pe obiecte și programare generică.

Încărcat de

Cosmin Florescu
Drepturi de autor
© All Rights Reserved
Respectăm cu strictețe drepturile privind conținutul. Dacă suspectați că acesta este conținutul dumneavoastră, reclamați-l aici.
Formate disponibile
Descărcați ca PDF, TXT sau citiți online pe Scribd

VLAD TUDOR TUDOR SORIN

Complemente de

C++
Elemente de Limbaj
Programare Orientată pe Obiecte
Programare Generică
Standard Template Library

Limbajul C++ Standard

L&S Info-mat
Copyright 2018  L&S INFO-MAT
Toate drepturile asupra acestei lucrări aparţin editurii L&S INFO-MAT.
Reproducerea integrală sau parţială a textului din această carte este
posibilă doar cu acordul în scris al editurii L&S INFO-MAT.

După confirmarea plății, fiecare carte poate fi descărcată de maximum 5 ori


şi este disponibilă 30 de zile.

Drepturi de autor / Copyright. Fiecare PDF este securizat în 28 de zone


cu watermark invizibil (id comandă, e-mail) pentru a nu putea fi distribuit
pe alte căi virtuale. Sistemul este unul complex şi se detectează automat
persoana care a redistribuit cartea ilegal.
CUPRINS

Partea I. Noţiuni introductive


Lecţia 1. Istoric C/C++ 5
Lecţia 2. GNU Project şi Free Software Foundation 6
Lecţia 3. Un mediu de dezvoltare: MinGW Developer Studio 8
Lecţia 4. Colecţia DJGPP 17

Partea a II-a. Elemente de limbaj


Lecţia 1. Pointeri 24
Lecţia 2. Referinţe 27
Lecţia 3. Modificatorul “const” 30
Lecţia 4. Legatura între masive şi pointeri 36
Lecţia 5. Operaţii cu pointeri 39
Lecţia 6. Expresii indexate 40
Lecţia 7. Alocarea dinamică a memoriei 42
Lecţia 8. Parametri pentru program 45
Lecţia 9. Supraîncarcarea functiilor 46
Lecţia 10. Funcţii cu un număr variabil de parametri 47
Lecţia 11. Pointeri catre funcţii 49
Lecţia 12. Spaţii de nume 51
Lecţia 13. Instrucţiunile “break” şi “continue” 53
Lecţia 14. Excepţii 54

Partea a III-a. Iniţiere în programarea orientată pe obiecte


Lecţia 1. Ce este programarea orientată pe obiecte ? 58
Lecţia 2. Clase 59
Lecţia 3. Constructori 64
Lecţia 4. Destructor 66
Lecţia 5. Copierea obiectelor 69
Lecţia 6. Funcţii prietene 71
Lecţia 7. Supraîncarcarea operatorilor cu metode ale clasei. Generalităţi 72
Lecţia 8. Supraîncarcarea operatorilor prin funcţii prietene 74
Lecţia 9. Cuvântul cheie “this” 76
Lecţia 10. Despre supraîncarcarea unor operatori unari 77
Lecţia 11. Supraîncarcarea operatorilor “++”, “--" 78
Lecţia 12. Supraîncarcarea operatorului “=” 80
Lecţia 13. Constructori de copiere 82
Lecţia 14. Supraîncarcarea operatorului “[]” 84
Lecţia 15. Supraîncarcarea operatorului “()” 85
Lecţia 16. Membri statici ai unei clase 86
Lecţia 17. Tratarea excepţiilor prin utilizarea claselor 88

3
CUPRINS

Lecţia 18. Clase derivate 89


Lecţia 19. Accesul la membrii unei clase 94
Lecţia 20. Comportamentul constructorilor şi desctructorilor în cazul
derivării claselor 97
Lecţia 21. Pointeri către obiecte 102
Lecţia 22. Metode virtuale 104
Lecţia 23. Metode virtuale pure 106
Lecţia 24. Clasa “string” (din STL, pentru a lucra uşor cu şiruri…) 109

Partea a IV-a. Iniţiere în programarea generică


Lecţia 1. Ce este programarea generică ? 115
Lecţia 2. Funcţii template 116
Lecţia 3. Clase template 118
Lecţia 4. Derivarea în cazul claselor template 123

Partea a V-a. Iniţiere în STL (Standard Template Library)


Lecţia 1. Ce este STL ? 127
Lecţia 2. Mai mult despre containere 129
Lecţia 3. Mai mult despre iteratori 130
Lecţia 4. Un exemplu mult simplificat pentru înţelegerea noţiunilor de
container, iterator şi algoritm 132
Lecţia 5. Clasa vector<T> 137
Lecţia 6. Clasa list<T> 144
Lecţia 7. Clasa set<T, Comp> 149
Lecţia 8. Clasa multiset<T,Comp> 154
Lecţia 9. Clasa map<Cheie, Data, Compar> 157
Lecţia 10. Clasa stack<T> (stiva) 161
Lecţia 11. Clasa queue<T> (coada) 162
Lecţia 12. Clasa priority_queue<T,Sequence,Compar> 163
Lecţia 13. Cum se scrie un algoritm ? 164
Lecţia 14. Algoritmii şi predicatele 169
Lecţia 15. Algoritmii şi elementele de asociere 172
Lecţia 16. Algoritmul for_each() 174
Lecţia 17. Algoritmi de tip find 175
Lecţia 18. Algoritmi de tip count 177
Lecţia 19. Algoritmi de tip search 178
Lecţia 20. Algoritmi de tip mismatch si equal 180
Lecţia 21. Algoritmi de tip copy 182
Lecţia 22. Algoritmi de tip replace 183
Lecţia 23. Algoritmi de tip remove 184
Lecţia 24. Algoritmi de tip transform 185
Lecţia 25. Algoritmi de tip sort 186
Lecţia 26. Căutarea binară 187
Lecţia 27. Ordinea lexicografică 189
Lecţia 28. Partiţionare 190
Lecţia 29. Algoritmi de tip fill 191
Lecţia 30. Algoritmi de tip unique 192

4
PARTEA I. Noţiuni introductive

LECŢIA 1. Istoric C/C++

C este unul dintre cele mai răspândite limbaje de programare la ora actuală,
acest fapt datorându-se eficienţei cu care se poate scrie cod pentru dezvoltarea
aplicaţiilor software. În cele ce urmează va fi prezentat un scurt istoric al acestuia.
Dezvoltarea limbajului C a început între anii 1969-1973, în cadrul unui
laborator de la AT&T Bell Labs. Pornind de la lipsa de funcţionalitate a limbajului
1
B (realizat de către Ken Thompson – o variantă minimizată a limbajului BCPL ,
pentru a putea fi utilizat pe minicalculatoarele
de la acea vreme) în implementarea unui
sistem de operare pentru o maşină de calcul de
2
tip PDP-11 , Dennis Ritchie (foto-dreapta) şi
Ken Thompson (foto-stânga) au pus baza
primei versiuni a limbajului C.
3
În anul 1973, partea centrală (kernel ) a
sistemului de operare Unix a fost rescrisă cu
ajutorul C-ului, fiind astfel prima dată când un sistem de operare renunţă la limbajul
de asamblare. De asemenea, datorită portabilităţii sporite a acestuia, au fost
implementate o serie de compilatoare ce permiteau astfel compilarea unui cod pe
diverse platforme şi sisteme de operare.
Dennis Ritchie şi Brian Kernighan au publicat în anul 1978 prima ediţie a
unei cărţi ce conţinea specificaţiile tehnice ale limbajului C, numită “The C
Programming Language”. Această variantă a limbajului este cunoscută sub numele
de K&R C şi este considerată baza pe care trebuie să o suporte orice compilator.
În anii ‘80, popularitatea limbajului C a crescut prin utilizarea sa împreună cu
platforma IBM-PC. De asemenea, Bjarne Stroustrup şi alţi programatori de la
AT&T Bell Labs au introdus programarea orientată pe obiecte, definind astfel
limbajul C++. Sistemul de operare Microsoft Windows îl utilizează ca principal limbaj
de programare de atunci, varianta C rămânând populară pentru utilizatorii Unix.
ANSI C şi ISO C. În anul 1983, institutul ANSI (American National Standards
Institute) a început definirea standardului limbajului de programare C. După
îndelungi dezbateri, pe data de 14 decembrie 1989 acest standard a fost definitivat

1
Basic Combined Programming Language, realizat în anul 1966 de către Martin Richards de la
Universitatea din Cambridge, cu scopul de a facilita scrierea compilatoarelor pentru alte limbaje.
2
Minicomputer pe 16 biţi, produs de Digital Equipment Corp.
3
Componentă esenţială a sistemului de operare care se ocupă cu managementul resurselor şi
comunicaţia dintre hardware şi software.

5
PARTEA I

şi numit ANSI X3.159-1989. În anul 1990, standardul a fost adoptat de către ISO
(International Organization for Standardization), sub numele de ISO/IEC
9899:1990. Necesitatea acestor standarde este evidentă, având în vedere dorinţa de
a globaliza utilizarea acestui limbaj de programare şi de a-l face independent de
platformă şi/sau sistem de operare. Astfel, marea majoritate a compilatoarelor de
C de astăzi respectă aceste standarde.

Ca orice standard ISO, acesta face subiectul unor constante revizuiri şi


adăugiri, prin publicarea unor documente. Spre exemplu, în anul 1994 au fost
efectuate 40 de modificări prin corecţii sau clarificări. Recent, au fost adăugate în
jur de 50 de pagini în care sunt introduse şi explicate noi librării de funcţii.
Momentan, ultima variantă a standardului ISO pentru C este ISO/IEC 9899:TC2.
Pe pagina de Internet a grupului ce se ocupă de acest standard se regăsesc toate
documentele care pot fi descărcate gratuit:

[Link]

Dezvoltarea limbajului C++ îşi are originile în


cadrul aceluiaşi laborator de la Bell Labs, unde în anul
1979 Bjarne Stroustrup a început să lucreze la ceea
ce urma să devină “programarea orientată pe obiecte”,
introducând iniţial noţiunea de “clasă“, care generic
reprezintă o colecţie de variabile şi funcţii (metode) ce
descriu un anumit comportament. Apoi, au fost
introduse noi capabilităţi, dintre care menţionăm
funcţiile virtuale, supraîncărcarea operatorilor sau
manipularea excepţiilor. Prima denumire a noului
limbaj a fost “C cu clase”, pentru ca în anul 1983 să fie schimbat în C++, ideea
venind chiar de la operatorul de incrementare.

În anii ’80, autorul limbajului a publicat două ediţii ale cărţii “The C++
Programming Language”, iar în anul 1990 a apărut primul manual de referinţă al
limbajului C++, care reprezenta baza standardului care urma să fie acceptat în anul
1998 de către cele două organizaţii, ISO şi ANSI. Totodată, au fost introduse noi
elemente (spre exemplu, contextele de tip namespace). Pentru a accesa ultima
variantă a acestui standard, cât şi rapoartele tehnice publicate, vizitaţi pagina:
[Link]

NOTĂ. În această carte este respectat standardul ISO al limbajului C++.

LECŢIA 2. GNU Project şi Free Software Foundation


În anul 1984, Richard Stallman a lansat proiectul “GNU Project“ pentru a
dezvolta un sistem de operare complet gratuit, asemănător cu Unix, sub numele de
sistemul GNU (“GNU’s Not Unix”). Din nevoia de fonduri pentru susţinerea acestui
proiect, acesta a înfiinţat în anul 1985 fundaţia “Free Software Foundation“.
Deoarece un sistem de operare reprezintă un ansamblu complex de componente
software, finalizarea acestuia a avut loc în anul 1990, singura lipsă fiind kernel-ul.

6
NOŢIUNI INTRODUCTIVE

Linus Torvalds a implementat această parte independent de proiect în anul 1991,


similar cu cel al Unix-ului şi l-a numit “Linux“. În anul 1992, Linux devenind un
software gratuit, a fost combinat cu GNU, care era aproape terminat, formând un
sistem complet de operare, numit “GNU/Linux“ (sau mai simplu, Linux – deşi
pentru a nu provoca ambiguitate, este preferată prima variantă). La ora actuală,
sunt estimaţi zeci de milioane de utilizatori ai acestui tip de sistem de operare, în
diferite variante: Slackware, Debian, RedHat sau altele.

Fundaţia FSF nu s-a limitat doar la implementarea sistemelor de operare,


aceasta extinzându-şi activitatea spre programe utilitare, unele dintre ele fiind
realizate chiar de către angajaţii acesteia, iar altele, de către programatorii
voluntari, sub licenţa ”GNU General Public Licence” (GNU GPL). Această licenţă
poartă numele de ”Copyleft” şi permite ca orice persoană să copieze şi să
redistribuie un program GNU, cu sau fără modificări, dar acesta la rândul său
trebuie să ofere şi libertatea unei noi redistribuiri sau modificări ulterioare (codul
sursă). Astfel, fiecare programator poate să beneficieze de un software gratuit, pe
care să-l poată studia, modifica sau copia după bunul plac şi apoi să-l ofere la
rândul său, în acelaşi mod.

În cadrul proiectului GNU, Richard Stallman a scris complet un nou


compilator de C, pe care l-a numit ”GCC” (GNU C Compiler) şi l-a utilizat la
dezvoltarea sistemului de operare GNU/Linux. Deoarece la ora actuală acesta
suportă mai multe limbaje de programare (C, C++, Objective-C, Java, Fortran şi
Ada), a fost redenumit GNU Compiler Collection, păstrând aceleaşi iniţiale.
Compilatorul GCC are cea mai mare portabilitate, fiind adaptat pentru o gamă
foarte mare de procesoare şi sisteme de operare.

GCC pentru Windows. Deşi compilatorul GCC nu a fost implementat pentru a


putea fi utilizat pe un sistem de operare Microsoft, au fost realizate câteva
variante care să permită această posibilitate. Dintre cele mai renumite, menţionăm:

 Colecţia DJGPP, realizată de DJ Delorie începând cu anul 1989, reprezintă


un mediu de dezvoltare C/C++ pe 32 de biţi pentru gama de procesoare
Intel x386 (cel puţin) care rulează un sistem de operare MS-DOS sau
Windows, într-o fereastră de comandă DOS. Împreună cu compilatorul GCC,
această colecţie cuprinde o serie de utilitare GNU care au fost adaptate
pentru a rula sub DOS şi este distribuită gratuit sub licenţa GNU GPL.
Site web: “[Link]
4
 Colecţia Cygwin, dezvoltată de către Cygnus Solutions , permite accesul
la funcţionalităţile GNU/Linux-ului sub sistemul de operare Windows, oferind
totodată portabilitatea aplicaţiilor scrise sub GNU/Linux, prin recompilarea

4
Compania Cygnus Solutions, iniţial numită Cygwin Support, a fost fondată în anul 1989 şi avea scopul
de a oferi suport tehnic pentru software-ul gratuit. Un rol important în cadrul evoluţiei aplicaţiilor GNU
l-au avut programatorii de la Cygnus, care au participat esenţial la dezvoltarea continuă a debugger-ului,
assembler-ului şi a linkerului-ul GNU. Totodată, aceştia au ajutat la asigurarea portabilităţii
compilatorului GCC pentru o mare varietate de arhitecturi de calcul. În anul 1999, compania Cygnus
Solutions a fuzionat cu Red Hat, Inc şi a fost închisă în anul 2000.

7
PARTEA I

codului sursă. Această colecţie este formată dintr-o librărie care se comportă
5
similar cu interfaţa API a GNU/Linux-ului şi un set de unelte specifice,
adaptate corespunzător pentru a putea fi rulate sub Windows.

Site web: “[Link]


 MinGW (“Minimalistic GNU for Windows”), realizat de Colin Peters, este
un set de unelte capabile să compileze cod pentru a fi apoi rulat pe platforma
Win32 (Windows pe 32 de biţi). Limbajele pe care le recunoaşte sunt
C, C++, Objective-C şi Fortran. Spre deosebire de alte programe de acest
gen, MinGW conţine un pachet format din fişiere header şi librării pentru a
putea fi suportată interfaţa Win32 Application Programming Interface
a firmei Microsoft, esenţială în dezvoltarea aplicaţiilor Windows. Acest
pachet se numeşte “win32api”.
6
Colecţia de unelte MinGW nu conţine un mediu IDE , dar se poate utiliza
pentru scrierea fişierelor orice editor de text, spre exemplu, Notepad.

Site web: ”[Link]


Parinya Software a realizat un program numit MinGW Developer Studio
pentru C/C++, foarte util în cazul în care un mediu IDE este preferat de
către utilizator. Acesta este oferit gratuit spre descărcare de pe Internet.

NOTĂ. Orice compilator pe care îl folosiţi la rularea programelor dvs. trebuie să


respecte standardele ANSI ISO, prezentate pe parcursul acestei scurte introduceri,
asigurând astfel codului dvs. o portabilitate sporită. După cum a mai fost menţionat,
standardele sunt revizuite periodic, deci trebuie vizitate site-urile web asociate
fiecăruia şi citite noile modificări aduse respectivelor documente.

LECŢIA 3. Un mediu de dezvoltare pe care îl putem folosi


pentru scrierea programelor: MinGW Developer Studio

Până în prezent, pentru a scrie un program, am utilizat mediul Borland. Este


momentul să încercăm şi altceva. De ce?

 MinGW Developer Studio ne obligă să lucrăm în C++ standard. Dacă


utilizăm, de exemplu, Borland C++ 3.1, nu putem lucra în C++ standard. În
perioada când a fost realizat Borland C++ 3.1, nu existau standardele
actuale de limbaj. Dacă vom încerca să rulăm un program realizat pentru
Borland într-un mediu ca acesta, la compilare, vom primi mesaje de
5
Application Programming Interface reprezintă un set de rutine, protocoale şi unelte necesare realizării
aplicaţiilor software. Spre exemplu, Microsoft oferă gratuit informaţii despre Windows API pentru a
asigura programatorilor consistenţă în dezvoltarea programelor.
6
Integrated Development Environment este un tip de aplicaţie care asistă programatorul la dezvoltarea
de software, oferindu-i acestuia un editor de text performant, un compilator, un debugger şi multe
alte componente suplimentare, cum ar fi: diagramă cu ierarhia claselor, inspector de obiecte, etc.
Exemple de astfel de programe: Borland Delphi, Borland C++ Builder sau Microsoft Visual Studio.

8
NOŢIUNI INTRODUCTIVE

avertizare. De asemenea, actualul limbaj C++ standard conţine multe noutăţi


7
care facilitează scrierea programelor, cum ar fi, de exemplu STL (Standard
Template Library). Dacă am lucra sub Borland, STL-ul nu ar putea fi folosit…
 Cu MinGW Developer Studio se lucrează foarte uşor sub Windows, iar
programele pe care le realizăm vor afişa rezultatele într-o fereastră
MS-DOS, deschisă automat din acest mediu.

1. Acestea fiind zise, să trecem “la treabă”! Download-aţi şi instalaţi


mediul de la adresa: ”[Link]

2. După instalare, lansaţi programul utilizând icon-ul alăturat.

 Prin utilizarea acestui mediu, un program va fi conţinut într-un proiect.


Practic, proiectul este alcătuit din ansamblul de fişiere sursă care alcătuiesc
programul + 1 fişier cu extensia mdsp (fişierul proiect propriu-zis). De cele
mai multe ori, mai ales în perioada de început, pe lângă fişierul proiect (cu
extensia mdsp), vom avea un singur fişier sursă, cu extensia cpp.

 Pentru realizarea primului program, vom parcurge următorii paşi:

1) Din meniu, apelaţi Project şi New Project. Va apărea cutia de dialog de


mai jos, unde selectăm Win32 Console Application, scriem în edit-ul
Project name numele proiectului (de exemplu Nou) şi, în edit-ul Location,
selectăm folder-ul unde dorim ca acesta să se reţină:

Figura 1.1.
Deschiderea unui
nou proiect

2) În continuare vom ataşa un fişier cu extensia .cpp (deocamdată singurul), care


va conţine programul nostru. Pentru aceasta, din meniu, File şi New. Va apărea
cutia de dialog de mai jos, de unde selectăm C/C++ file şi scriem numele fişierului:

Figura 1.2.
Ataşarea unui fişier sursă
la proiect

7
Vedeţi ultima parte a acestei cărţi.

9
PARTEA I

3) De acum putem scrie programul, unul uşor, care afişează un mesaj:

#include<iostream>
using namespace std;
int main()
{ cout<<"Salut";
return 0;
}

OBSERVAŢII
a) În C++ standard, când includem un fişier antet nu mai trecem extensia ”.h”:
#include<iostream>.
b) În C++ standard, se folosesc spaţiile de nume. Acestea sunt prezentate în
partea a II-a a acestei cărţi. Deocamdată, scrieţi linia using namespace std;.
Prin aceasta, îi “spunem” compilatorului că se utilizează spaţiul de nume standard,
din STL.
c) În C++ standard, se trece tipul funcţiei ”main()”: int.
d) Funcţia ”main()”, având un anumit tip, are şi o valoare de retur: 0. Totuşi, din
motive care ţin de editare, în programele prezentate în această carte nu am scris
valoarea de retur pentru această funcţie.
f) După ce aţi scris programul, fişierul text care îl conţine trebuie să aibă o ultimă
linie vidă. Prin urmare, tastaţi Enter. Altfel, la compilare, veţi primi un avertisment.

Mai jos, puteţi observa cum arată fereastra în care am scris programul:

Figura 1.3. Fereastra mediului MinGW Developer Studio

4) Rulaţi programul, “apăsând” acel buton care conţine semnul ”!”. Dacă
programul nu conţine erori, atunci va apărea o fereastră MS-DOS care conţine şirul
de caractere afişat de program. Dacă programul conţine erori, acestea vor fi afişate
în partea de jos a ferestrei anterior
prezentate. Corectaţi-le şi reîncercaţi
să rulaţi programul!

Figura 1.4.
Fereastra MS-DOS

10
NOŢIUNI INTRODUCTIVE

 Presupunem că am închis mediul. Îl redeschidem şi dorim să reîncărcăm


aplicaţia anterior creată. Din meniu, apelaţi Project şi Open Project. Va
apărea o cutie de dialog care permite regăsirea proiectului (extensia .mdsp).
El se va găsi într-un folder cu acelaşi nume ca al proiectului (în exemplu, Nou).

 Să observăm că fişierele care alcătuiesc proiectul sunt afişate de mediu ca


făcând parte din mai multe folder-e. În fapt, acestea se găsesc într-un singur
folder care are numele proiectului.

Există patru categorii de fişiere ce pot fi introduse într-un proiect:

a) Source Files - conţine fişierele sursă. De exemplu, pentru programul


anterior, ”[Link]” se găseşte în acest folder.

b) Header Files - conţine, dacă există, fişierele antet pe care le scrie


creatorul programului (nu cele standard). Ele vor fi incluse de către
programator în sursa principală prin: #include ”nume.h”.

c) Resource Files - anumite fişiere care nu prezintă interes pentru ce


se tratează în această carte.
d) Other Files - fişiere text necesare programului.

OBSERVAŢIE. Pentru a include în proiect un nou fişier, utilizaţi din meniu comanda
File şi New.

Vom prezenta câteva exemple în care vom arăta utilitatea existenţei a trei
din cele patru folder-e.

Exemplul 1. Programul apelează o funcţie, iar funcţia se găseşte într-un fişier


antet numit ”functii.h”:

a) ”[Link]” se găseşte în Header Files:

#include <iostream>
using namespace std;
#include "functii.h"
int main()
{ cout<<"Exemplul 1"<<endl;
f();
}

b) ”functii.h” se găseşte în Source Files:

#include<iostream>
using namespace std;
void f()
{ cout<<"Eu sunt f()";
}

11
PARTEA I

Exemplul 2. Programul apelează o funcţie. Antetul funcţiei se găseşte în fişierul


”functii.h”. Definiţia funcţiei se găseşte în fişierul ”[Link]”.
a) ”[Link]” se găseşte în Source Files:
#include <iostream>
using namespace std;
#include "functii.h"
int main()
{ cout<<"Exemplul 1"<<endl;
f();
}

b) ”functii.h” se găseşte în Header Files:


void f();

c) ”[Link]”, definiţia funcţiei se găseşte în Source Files:


#include <iostream>
using namespace std;
void f()
{ cout<<"Eu sunt functia f"; }

Exemplul 3. Programul apelează o funcţie. La rândul ei, aceasta apelează un


fişier text pentru a afişa mesajul conţinut de acesta pe prima linie. Antetul funcţiei
se găseşte în fişierul ”functii.h”. Definiţia funcţiei este reţinută în fişierul
”[Link]”, iar fişierul text se găseşte în Other Files.
a) ”[Link]” se află în Source Files:
#include <iostream>
using namespace std;
#include "functii.h"
int main()
{ cout<<"Exemplul 1"<<endl;
f();
}

b) ”functii.h” se găseşte în Header Files:


void f();

c) ”[Link]”, definiţia funcţiei se găseşte în Source Files:


#include <iostream>
#include <fstream>
using namespace std;
void f()
{ fstream fis("[Link]",ios::in);
char s[100];
[Link](s,99); cout<<s;
}

d) ”[Link]”, fişierul de test, aflat în Source Files, conţine un şir de caractere:


Acest text se gaseste in fisierul text pe o singura linie.

12
NOŢIUNI INTRODUCTIVE

Depanarea programelor

Adesea, programele implementate nu se comportă aşa cum ne-am dori şi


deşi la prima vedere programul “arată” corect, rezultatul nu este cel aşteptat. În
aceste cazuri, putem apela anumite comenzi integrate în mediul de programare,
care ne ajută la identificarea erorilor din codul sursă.

Principiul de depanare al unui program este foarte simplu: prin comenzi


specifice, putem rula “asistat” programul implementat, în sensul că putem interveni
în timpul executării acestuia pentru a verifica corectitudinea sa (de exemplu, oprim
programul pentru a vizualiza valoarea unei variabile şi apoi îi continuăm rularea).

Vom da un exemplu de program pentru a explica mai uşor comenzile ce pot fi


utilizate la depanare:

1 #include <iostream>
2
3 using namespace std;
4
5 void afiseaza()
6 { cout<<"Mesaj din functia afiseaza()"<<endl;
7 }
8
9 int main()
10 { cout<<"Mesaj din functia main()"<<endl;
11 afiseaza();
12 cout<<"Gata!";
13 }
14

Mediul de programare ne pune la dispoziţie meniul “Debug” care conţine o


serie de comenzi, prezentate în continuare.

 Go – Se rulează programul în mod normal, executându-se toate instrucţiunile


(alternativ, se poate apăsa tasta F5).

 Toggle Breakpoint – Pe linia curentă (în


figura alăturată, 11) se introduce un punct de
oprire (apare un cerculeţ roşu în dreptul liniei).
La rularea programului prin comanda Go, se va opri executarea acestuia pe
linia care conţine un astfel de punct de oprire, fără a executa instrucţiunea.
Pentru a şterge un punct de oprire, se apelează din nou această comandă,
pe linia respectivă (rapid, se apasă tasta F9).
Rularea programului se continuă prin comanda Go!

 QuickWatch – “supraveghere” – Această comandă deschide o fereastră


suplimentară în care se poate introduce numele unei (mai multor) variabile
pentru ca apoi, în timpul rulării programului, să avem acces rapid la valoarea
acesteia (acestora). Astfel, spunem că “supraveghem” o variabilă.

13
PARTEA I

Pentru rularea “pas cu pas” a programului, puteţi utiliza:

 Step Into – Execută o instrucţiune (cea curentă, indicată de o săgeată de


culoare galbenă) şi opreşte rularea programului pe linia următoare.
Dacă instrucţiunea curentă apelează o funcţie, prin această comandă se
“sare” pe prima linie a funcţiei, fără a o
executa. Spre exemplu, putem porni
programul prin intermediul acestei comenzi,
oprindu-ne pe prima linie din corpul funcţiei
“main()” (vezi fig. alăturată). Linia 10
conţine o instrucţiune care trebuie să
afişeze un mesaj. Dacă vizualizaţi fereastra
programului, mesajul nu a fost încă afişat!

Exemplu. Dacă instrucţiunea curentă este


11 şi apăsăm această comandă, mediul va
opri rularea programului pe linia 6, intrând
în corpul funcţiei “afiseaza()”, fără a
executa însă instrucţiunea.

 Step Out of Function – Dacă am utilizat instrucţiunea precedentă


(Step Into) pentru a intra în corpul unei funcţii, se rulează funcţia în
totalitate şi se iese din ea, revenindu-se în cadrul celei care a apelat-o, se
execută următoarea linie de cod şi apoi se opreşte rularea programului.

Exemplu. Pentru cazul de mai sus, se revine


în cadrul funcţiei “main()” pe linia 12, se
execută instrucţiunea (se afişează mesajul
“Gata”) şi se opreşte rularea programului pe
linia 13. Pentru a continua, se apasă Go.

 Step Over – Execută instrucţiunea curentă şi opreşte rularea programului.


Spre deosebire de Step Into, această comandă nu efectuează saltul în
corpul unei funcţii, ci doar o execută.

Exemplu. Dacă instrucţiunea curentă este


11 şi apăsăm această comandă, mediul nu
va intra în corpul funcţiei “afiseaza()”, ci
doar o va executa. Programul se opreşte din
executare pe linia 12.

 Run to Cursor – Execută toate instrucţiunile pănă la cea pe care se află


cursorul mouse-ului.
Atenţie: codul de pe linia unde este poziţionat cursorul nu va fi executat!

 Stop Debugging – în cazul în care este dorită ieşirea forţată din modul de
depanare, se alege această comandă.

14
NOŢIUNI INTRODUCTIVE

Exemplu de depanare: “ciclu infinit”. Presupunem următorul cod sursă:


1 #include <iostream>
2 using namespace std;
3
4 void diviz(int n)
5 { int i=n;
6 while (i>0)
7 { if (n%i == 0) cout<<i<<" ";
8 //...
9 }
10 }
11
12 int main()
13 { int n;
14 cout<<"n=";
15 cin>>n;
16 cout<<"Divizorii lui n sunt: ";
17 diviz(n);
18 return 0;
19 }
20

unde se citeşte un număr întreg (n) şi se afişează descrescător toţi divizorii săi,
determinaţi în cadrul funcţiei “diviz”. Pe linia 8 am omis intenţionat decremen-
tarea valorii lui i, astfel încât instrucţiunea repetitivă while să cicleze la infinit.

Înainte de a continua, creaţi un proiect,


introduceţi codul de mai sus, rulaţi acest
program şi introduceţi pentru n o valoare.
Pentru a opri executarea programului (se va
afişa la nesfârşit valoarea lui n – adică cel mai
mare divizor), apăsaţi combinaţia de taste Ctrl+Break (fereastra MS-DOS a
programului se va închide).

Varianta 1

a) Pentru că doar în funcţia ”diviz” avem o instrucţiune repetitivă, înainte de a


rula programul, vom introduce un punct de oprire pe linia 7 (poziţionaţi cursorul
mouse-ului pe linie şi apăsaţi tasta F9).

b) Pornim programul prin comanda Go, iar acesta


se va opri înainte de a executa instrucţiunea de
pe linia 7, aşa cum am impus apriori.

c) Totodată, a fost deschisă şi fereastra


programului. Dvs. introduceţi o valoare pentru n şi
apăsaţi tasta Enter. Observaţi faptul că modul de
lucru este în paralel: dăm comenzi de depanare în
cadrul mediului de programare şi apoi vizualizăm comportamentul programului.

15
PARTEA I

d) Apăsăm tasta F5 pentru a continua. Executarea programului se va opri din nou


la linia 7, iar în fereastra programului a fost afişată valoarea primului divizor (chiar
valoarea lui n).

Până aici totul este corect … însă dacă


repetăm acest pas, valoarea va fi afişată din
nou (pentru cazul prezentat, corect ar fi “3”).

e) Dacă în editorul de text poziţionăm cursorul mouse-ului deasupra


variabilei i, în timpul rulării, va fi afişată valoarea ei curentă.
Observăm faptul că ea are valoarea “9”, deci nu a fost decre-
mentată. Astfel, am identificat eroarea.

f) Oprim programul (rapid, prin combinaţia de taste


Shift+F9) şi modificăm codul sursă, adăugând
instrucţiunea “i--;” pe linia 8. De asemenea, ştergem
punctul de oprire de pe linia 7.

g) Rulăm programul şi totul funcţionează acum corect!

Varianta 2

a) Rulăm programul “pas cu pas”: dăm comanda Step


Into pentru a porni programul. Acesta se opreşte din
executare pe linia 13, prima din “main()”.

b) Deoarece ne interesează ce se petrece în interiorul funcţiei ”diviz”,


poziţionăm cursorul mouse-ului pe linia 17 şi apelăm comanda Run to Cursor.
În fereastra programului, alegeţi o valoare pentru n şi apăsaţi tasta Enter (de
exemplu “12”). Apoi, programul îşi va opri rularea
înainte de a executa instrucţiunea “diviz(n);”.

c) Intrăm în corpul funcţiei ”diviz” prin


comanda “Step Into”. Apoi, apăsăm tasta
F10 (comanda Step Over) pentru a fi rulată
instrucţiunea de iniţializare a variabilei i.

d) Apelăm comanda QuickWatch şi se va deschide o fereastră


unde introducem în câmpul “Expression” numele variabilei (“i”)
şi apăsăm butonul “Add Watch”. În partea de jos (“Current
value”) va fi afişată valoarea curentă a lui i.

e) Fără a închide această fereastră, executăm câteva cicluri


while cu ajutorul comenzii Step Over, aplicată succesiv. Vom
observa că valoarea lui i nu se schimbă, deci am identificat din
nou eroarea: trebuie să-i decrementăm valoarea.

f) Oprim programul, introducem instrucţiunea lipsă “i--;” şi recompilăm!

16
NOŢIUNI INTRODUCTIVE

LECŢIA 4. Colecţia DJGPP


Colecţia de unelte DJGPP, realizată de DJ Delorie, este un mediu de
programare gratuit, care poate fi descărcat de pe Internet de la adresa:
“[Link]

Pentru detalii suplimentare, deschideţi pagina web:


[Link] (ghidul utilizatorului).

De asemenea, consultaţi fişierul “readme.1st” şi lista de întrebări şi


răspunsuri din directorul “FAQ” (Frequent Asked Questions) care se regăsesc în
folder-ul “DJGPP“, după instalarea mediului de programare.

1. Instalare

A. Accesaţi site-ul menţionat mai sus şi din secţiunea ”Introduction”, link-ul “Zip
Picker”. Se va deschide o nouă fereastră în care trebuie să alegeţi tipul sistemului
de operare şi limbajele de programare suportate (bifaţi C++). Apăsaţi butonul “Tell
me which files I need” şi salvaţi pe disk toate fişierele recomandate.
B. Creaţi un director pe disk, de exemplu “D:\DJGPP” şi dezarhivaţi fişierele în el.
C. Pentru că utilitarele colecţiei lucrează prin comenzi date de la tastatură (ca în
MS-DOS, un sistem de operare mai vechi, creat de Microsoft), va trebui să
folosiţi un program, numit “[Link]”, care simulează sistemul de operare amintit.
Programul se regăseşte în folder-ul sistemului (ex: “WINNT\system32\” pentru
Windows 2000). Identificaţi-l cu Search! Programul creează o fereastră în care se
pot da comenzi MS-DOS, aceasta mai numindu-se şi “consolă”.
D. Pentru a utiliza cu uşurinţă acest program, veţi crea un shortcut
către el pe Desktop (vezi figura alăturată). Va trebui ca fereastra
“CMD” să fie deschisă în subfolderul “bin” al directorului care reţine
soft-ul DJGPP instalat. Executaţi click cu butonul drept al mouse-ului
şi selectaţi Properties. La apariţia cutiei de dialog de mai jos,
introduceţi calea folder-ului amintit anterior în câmpul “Start in”:

Figura 1.5.
Cutia de dialog
Properties

17
PARTEA I

Dacă shortcut-ul a fost creat corect, atunci când executaţi dublu click pe el,
va apărea fereastra de mai jos, de unde veţi da comenzi de la tastatură:

Figura 1.6.
Fereastra de comenzi
(consola) “[Link]”

E. Întrucât în perioada de iniţiere programele dvs. se vor găsi în


subfolder-ul “bin”, pentru a lucra mai uşor, este recomandabil să creaţi
pe Desktop un shortcut către acesta.

F. Setarea variabilelor de mediu. Se efectuează click dreapta pe iconiţa My


Computer, se alege din meniul flotant Properties, Advanced şi apoi,
Environment Variables (vezi fig. 1.7):

Figura 1.7.
Fereastra de dialog
System Properties

Se va deschide următoarea fereastră de dialog, în care veţi adăuga calea


către subfolder-ul “bin”. Alegeţi “Path” din lista “System variables” şi apăsaţi
butonul Edit:

Figura 1.8.
Fereastra de dialog
Environment
Variables

18
NOŢIUNI INTRODUCTIVE

În cutia de dialog de mai jos, adăugaţi calea la cele deja prezente (în cazul meu,
“D:\DJGPP\bin;“ ). Căile se scriu separate cu semnul “;”. Apăsaţi butonul OK.

Figura 1.9.
Cutia de dialog
Edit System Variable

Apăsaţi butonul New din fereastra de dialog Environment Variables. Se va


deschide cutia de dialog din fig. 1.10, în care veţi introduce numele “DJGPP” şi ca
valoare, calea către fişierul menţionat (ex: “D:\DJGPP\[Link]”):

Figura 1.10.
Cutia de dialog
New System Variable

Apăsaţi butonul OK!

IMPORTANT! Căile utilizate în acest paragraf sunt propuse ca exemplu. Dvs. le


veţi introduce corespunzător, în funcţie de locaţia aleasă pentru instalare.

G. În cazul sistemelor de operare Windows NT, 2000 sau XP, nu este necesară
resetarea calculatorului deoarece noile setări sunt reţinute imediat. Totuşi, trebuie
închisă şi redeschisă fereastra “CMD“.

Dacă toţi paşii au fost urmaţi corect, operaţia de instalare a mediului de


programare este acum terminată.

2. Parametrii de apel ai compilatorului GPP

La apelul compilatorului din paragraful anterior, am utilizat un parametru


(“-o”) pentru a-i indica acestuia numele dorit pentru executabil, însă există mai
mulţi parametri care pot fi folosiţi. Sintaxa compilatorului GPP este următoarea:
”gpp [parametri] fisier_cod_sursa”.

 “-o nume_executabil” – specifică numele executabilului care va fi creat


de către compilator. Nu este obligatoriu ca numele fişierului sursă să fie
identic cu cel executabil!
Exemplu. Testaţi în consolă ”gpp –o [Link] [Link]”.

19
PARTEA I

 Compilarea unui program se poate face fără nici un parametru:

”gpp fisier_cod_sursa”.

În acest caz, compilatorul va crea automat în directorul “bin” un fişier


executabil cu numele de “[Link]”.

Exemplu. Testaţi în consolă ”gpp [Link]” şi apoi rulaţi programul “a”.

 “-Wall” - implicit, compilatorul nu afişează anumite avertizări (în engleză,


warning), ci doar erorile depistate. Pentru a beneficia de aceste informaţii
suplimentare, se foloseşte acest parametru.

Exemplu. Considerăm programul următor, în care am declarat o variabilă a, de tip


întreg, care însă nu este utilizată. Salvaţi fişierul cu numele “[Link]”:

#include <iostream>
using namespace std;

int main()
{ int a;
return 0;
}

Compilând programul de mai sus prin comanda:

”gpp –o [Link] -Wall [Link]”,

vom obţine în fereastra CMD următorul mesaj:

Figura 1.11.
Apelarea compilatorului
cu mai mulţi parametri

OBSERVAŢII
 Dacă sunt introduşi mai mulţi parametri, aceştia se separă cu un spaţiu şi,
atenţie, să nu se intercaleze!

Incorect: ”gpp –o -Wall [Link] [Link]”;


Corect: ”gpp -Wall –o [Link] [Link]”.

20
NOŢIUNI INTRODUCTIVE

 Întotdeauna numele fişierului sursă trebuie scris după toţi parametri.

Incorect: ”gpp –o [Link] [Link] -Wall”;


Corect: ”gpp –o [Link] -Wall [Link]”.

 Pentru a vizualiza toţi parametrii compilatorului gpp, se scrie următoarea


comandă în consolă:
”gpp –-help”.

3. Depanarea programelor

Pentru această parte, găsiţi documentaţie la:

”[Link]

De multe ori, din cauza anumitor greşeli conceptuale, programele implemen-


tate nu se comportă aşa cum ne-am dori. În cazul în care sursa are o dimensiune
redusă, eroarea poate fi identificată relativ uşor, însă lucrurile încep să se complice
în cadrul unui proiect complex, cu foarte multe linii de cod...

Există soft-uri specializate care vin în ajutorul programatorului, în scopul de


a oferi suport pentru depanarea programelor. Un astfel de utilitar se numeşte
generic “debugger“ şi mediul de programare DJGPP conţine unul, numit “gdb”.

Utilitarul “gdb” permite rularea “controlată” a unui program, în sensul că:

 se poate opri programul în timpul rulării pentru a-l analiza, prin setarea
în prealabil a unuia sau a mai multor puncte de oprire (în engleză,
“breakpoint“); astfel, se pot vizualiza şi chiar modifica valorile unor
variabile;
 se poate impune un punct de supraveghere asociat unei variabile (în
engleză, “watchpoint“), care are rolul de a sista rularea programului
de fiecare dată când se modifică valoarea acesteia;
 permite rularea “pas cu pas” - după executarea fiecărei instrucţiuni,
programul se opreşte.

IMPORTANT! Pentru a putea rula un program sub debugger, sunt necesare


anumite informaţii suplimentare pe care compilatorul le ataşează fişierului
executabil. Pentru aceasta, la compilare se foloseşte parametrul “-g”:

“gpp –o [Link] -g cod_sursa.cpp“.

1. Pentru a porni debugger-ul, se apelează acesta având ca parametru numele


fişierului executabil. Pentru exemplul considerat, vom da comanda:
“gdb nume_program“.

21
PARTEA I

2. Pentru a lansa programul în executare, se dă comanda:

“run“.

Dacă această comandă a fost dată fără a seta în prealabil un punct de oprire, se
rulează programul în întregime, fără a-l putea depana.

3. Pentru a ieşi din debugger, se scrie “quit“.

4. Pentru a putea depana programul, este obligatoriu să-i daţi un punct de oprire:

“break numar_linie_program“.

Apoi, în acel punct, puteţi da şi alte comenzi (vedeţi mai jos).

5. Pentru a reporni programul, după ce a fost oprit (de exemplu, într-un punct de
oprire), se dă comanda:

“cont“.

6. Pentru a afişa conţinutul unei variabile, într-un punct în care a fost oprit
programul, se dă comanda:

“print nume_variabila“

7. Un punct de supraveghere se introduce prin comanda “watch”, urmată de


numele variabilei:

“watch nume_variabila“.

Comanda se dă atunci când programul este oprit într-un punct de oprire.

La terminarea rulării programului, acestea sunt şterse automat.

8. Modificarea unei variabile în timpul rulării programului se efectuează prin


comanda “set”, care are sintaxa de mai jos:

“set var nume_variabila = valoare_noua“.

Comanda se dă atunci când programul este oprit într-un punct de oprire.

9. Punctele de oprire rămân memorate până la ieşirea din debugger. Pentru a


şterge un breakpoint, se apelează comanda

“clear linie“.

10. Dacă doriţi ca, atunci când programul este oprit, să se treacă la linia
următoare, introduceţi comanda:
“next“.

22
NOŢIUNI INTRODUCTIVE

Exemplu: “împărţirea la zero”. Considerăm următorul program, “[Link]”, care


determină toţi divizorii unui număr natural şi-i afişează în ordine descrescătoare:
#include <iostream>
using namespace std;
int main()
{ int a,i;
cin>>a;
i=a;
while (i>=0)
{ if (a%i==0) cout<<i<<" ";
i--; }
return 0;
}

Compilând şi rulând programul, vom obţine o eroare care ne indică faptul că


este efectuată o împărţire la zero:

Figura 1.12.
Eroarea afişată la
rularea programului

Eroarea este evidentă şi a fost introdusă intenţionat: ciclul while permite


variabilei i să ia valoarea 0. Să vedem în continuare cum putem analiza acest
program cu ajutorul utilitarului “gdb”. Nu uitaţi: utilizaţi parametrul “-g” la compilare!
a) Pentru a porni debugger-ul, se apelează acesta având ca parametru numele
fişierului executabil. Pentru exemplul considerat, vom da comanda: ”gdb zero”.
b) Pentru a rula programul sub controlul depanatorului, se dă comanda “run”, în
urma căreia vom obţine rularea propriu-zisă. Introducem o valoare pentru variabila
a (în exemplul de mai jos, “12”) şi vom obţine un mesaj care ne indică o eroare
(excepţie) aritmetică, pe linia 9 a codului sursă:

Figura 1.13. Executarea programului sub debugger

c) Pentru a ne convinge de faptul că este efectuată


împărţirea la zero, afişăm valoarea reţinută de variabila i
prin comanda ”print i”:
Figura [Link] variabilei

d) Programul a fost oprit temporar de către debugger, iar pentru a continua


rularea, tastăm comanda “continue” şi apăsăm tasta Enter. Programul îşi va
încheia execuţia cu o serie de mesaje suplimentare.

23
PARTEA A II-A. Elemente de limbaj

LECŢIA 1. Pointeri

După cum ştim, memoria internă este alcătuită din octeţi. Să presupunem că
aceştia sunt numerotaţi de la 0 la n-1. O variabilă oarecare ocupă unul sau mai
mulţi octeţi consecutivi.
 Prin adresa unei variabile vom înţelege numărul de ordine al primului octet
1
al ei . Prin pointer către o variabilă de un anumit tip, T, vom înţelege o
valoare de tipul T* care indică adresa la care se găseşte respectiva
variabilă. Evident, o variabilă care poate reţine un pointer de tip T* trebuie
să aibă acest tip (T*).
 Fiind dată o variabilă cu numele num, de tipul T, cu ajutorul operatorului “&“,
prefixat, se poate obţine un pointer de tip T* către variabilă cu ajutorul
expresiei &num.

Exemple:

 Variabila a este de tip int. Variabila adr, va reţine adresa lui a. Tipul
variabilei adr este int* (aici, T este de tipul int).
int a=3;
int* adr=&a;

 Declararea se mai putea face şi aşa:


int a=3, * adr=&a;

 Variabila b este de tipul float. Variabila adrfloat va reţine adresa varia-


bilei b. Tipul ei este float*.
float b=2, * adrfloat=&b;

 Pornind de la un pointer către o anumită variabilă, se poate accesa


conţinutul variabilei respective, în vederea citirii sau modificării acelei
variabile. Pentru aceasta, se foloseşte operatorul “*“ prefixat.

Exemplu. În secvenţa de mai jos se adresează conţinutul variabilei a, pornind de la


adresa ei. Iniţial, conţinutul se incrementează, apoi acesta este afişat. Se va tipări 4.
int a=3, * adra=&a;
(*adra)++;
cout<<*adra;

1
În realitate, mecanismul de adresare a memoriei este cu mult mai complicat. Dar, pentru a
înţelege noţiunea de adresă, este suficient acest model, simplificat.

24
ELEMENTE DE LIMBAJ

 Pornind de la un pointer adr către o variabilă X, definită cu struct, există


două modalităţi de a accesa un câmp al lui X:

a) (*adr).nume_camp - este prima formă care ne vine în minte, unde prin


*adr adresăm pe X, apoi se adresează câmpul dorit cu ajutorul operatorului “.“,
urmat de numele câmpului.

b) adr->nume_camp; - a doua formă utilizează operatorul “->“, creat special


pentru a adresa uşor un câmp al unei variabile, pornind de la un pointer către
variabilă. Evident, este de preferat a doua formă.

Exemplu. La executarea secvenţei de mai jos, se afişează de două ori 3:


struct S
{ int x;
float y;
}X;

X.x=3;
X.y=4;
S* adrX=&X;
cout<<(*adrX).x<<endl;
cout<<adrX->x;

OBSERVAŢII
1. Dacă aveţi de declarat mai multe variabile de tip pointer, adăugaţi pentru
fiecare dintre ele caracterul “*“.

Exemplu. Declarăm 3 variabile care pot reţine adrese către varibile de tip double:

double *a, *b, *c;

Dacă declararea este double* a, b, c;, atunci a poate reţine un pointer


(este de tip double*), dar b şi c sunt de tip double.

2. Nu sunt permise atribuirile între două variabile de tip pointer care sunt către
tipuri diferite.

Exemplu. În secvenţa următoare, linia 3 este eronată. Se încearcă atribuirea unei


variabile de tip int* a conţinutului alteia, de tip float *:
int a=3, * adra=&a;
float b=4, *adrb=&b;
adra=adrb; // Eroare

3. Dacă doriţi să efectuaţi o conversie de pointeri, puteţi utiliza tipul void*. Unei
variabile de tip void* i se poate atribui direct un pointer către o variabilă de orice
tip, iar unei variabile care reţine un pointer către tip i se poate atribui prin
conversie explicită valoarea reţinută de o variabilă de tip void*. Oricum, rareori
este nevoie de astfel de conversii.

25
PARTEA A II-A

Reţineţi: conversiile explicite între două tipuri oarecare de pointeri, fără utilizarea
unei variabile de tip void*, nu sunt recomandate, fiind adesea neportabile (cum ar
fi, pentru exemplul de mai sus, adra=(int*)adrb).

Exemplu. În secvenţa de mai sus, atribuirea eronată din linia 3 se înlocuieşte cu:
void* man=adrb;
adra=(int*)man;

4. Un pointer către o variabilă de tipul T* este de tipul T**. Un pointer către o


variabilă de tipul T** are tipul T***..., ş.a.m.d.

Exemplu. Priviţi secvenţa următoare, unde variabila x este de tip double şi reţine
3. Variabila adrx este de tipul double* şi reţine adresa lui x. Variabila adradrx
este de tipul double** şi reţine un pointer către adrx. Programul afişează 3,
adică valoarea lui x:
double x=3;
double* adrx=&x; // pointer catre x
double** adradrx=&adrx; //pointer catre adrx;
cout<<**adradrx; // afisez x

EXERCIŢII PROPUSE
1. Ce se afişează în urma executării secvenţei de mai jos ?
double a=5, *b=&a;
*b*=3;
cout<<a+2;

a) 5; b) 7; c) 17; d) 15.

2. Ce afişează secvenţa următoare pe ultima linie?


int a=5, *b=&a,**c=&b,***d=&c;
cout<<++a<<endl;
cout<<(*b)++<<endl;
cout<<++(**c)<<endl;
cout<<(***d)++<<endl;

a) 5; b) 6; c) 7; d) 8.

3. Ce se afişează în urma executării secvenţei următoare?


int a=3, b=4,*c=&a,*d=&b;
cout<<a*(*c)*b*(*d);

a) eroare; b) 144; c) 12; d) 7.

4. Scrieţi o funcţie care are ca parametri de intrare pointeri către două variabile de
tip int. Funcţia interschimbă conţinutul celor două variabile.

26
ELEMENTE DE LIMBAJ

5. Fiind dată secvenţa de mai jos, care dintre expresii nu afişează caracterul ‘d’?
char a='c';
char *b=&a;
char* &c=b;

a) cout<<a+1; b) cout<<(char)(a+1);
c) cout<<(char)(*b+1); d) cout<<(char)(*c+1);

RĂSPUNSURI
1.c). b este pointer către a, iar variabila a reţine 5. Lui a (*b) i se atribuie
vechiul conţinut, înmulţit cu 5 (15). Apoi, se afişează a+2 (17).
2.d). 3.b).
4. Funcţia este următoarea:
void swap(int* x, int* y)
{ int man;
man=*x; *x=*y; *y=man; }
5.a.

LECŢIA 2. Referinţe

Atunci când am studiat transmiterea paramerilor pentru funcţii, am arătat că


parametrii pot fi transmişi în două feluri: prin valoare şi prin referinţă. De
asemenea, am arătat faptul că atunci când transmitem parametrii prin referinţă,
valorile reţinute de variabilele transmise ca parametri pot fi modificate în funcţie.
Totuşi, acesta este doar un caz particular de utilizare a referinţelor. În realitate,
în C++ mecanismul referinţelor este cu mult mai complex.

 Printr-o referinţă la o variabilă vom înţelege un alt nume pentru această


variabilă.

Exemple:

 În exemplul următor, y este un alt nume pentru variabila x. Prin urmare, la


executare se afişează 3, adică valoarea lui x:
int x=3;
int& y=x;
cout<<y;

 Mai jos, x, y şi z sunt 3 nume pentru aceeaşi variabilă. La executare, se


afişează de trei ori 3:
int x, &y=x,&z=y;
z=3;
cout<<x<<" "<<y<<" "<<z;

27
PARTEA A II-A

OBSERVAŢII
1. Nu se poate defini o referinţă în sine, care nu porneşte de la o anumită
variabilă.

Exemple:

 int& t; // incorect, tentativă de creare a unei referinţe fără a preciza


variabila către care este referinţa.
 int& t=1; // incorect, tentativă de creare a unei referinţe către o
constantă.

2. Având în vedere semnificaţia unei referinţe, putem interpreta un parametru


formal de tip referinţă, ca un alt nume pentru parametrul efectiv. Prin urmare, în
funcţie se operează asupra aceleeaşi variabile, de unde rezultă efectul de
modificare în subprogram a conţinutului variabilei.

3. Să nu se facă confuzie între pointer şi referinţă. Pointer-ul este o adresă către


o anumită variabilă de un anumit tip. Pornind de la această adresă, se poate
accesa variabila respectivă. Referinţa este un alt nume pentru o variabilă. Evident,
pornind de la referinţă se poate accesa conţinutul variabilei.

4. Uneori, veţi întâlni funcţii care returnează o referinţă. În acest caz, înseamnă
că funcţia returnează variabila în întregime, nu numai valoarea ei.

Exemplu. Priviţi programul următor: funcţia f() are ca parametru transmis prin
referinţă pe x. Dacă ţinem cont de apelul f(y), înseamnă că x este un alt nume
pentru y. Cum, înainte de apel, y reţine 1 şi cum acesta a fost incrementat în
funcţie, rezultă că variabila cu două nume, x şi y, va reţine 2. Deoarece funcţia
returnează variabila în întregul ei, atunci prin int& z=f(y), am creat un al treilea
nume pentru aceeaşi variabilă, z. În acest moment, avem o variabilă cu două
nume y, z (x nu mai există după ieşirea din funcţie), care este de tipul int şi reţine
2. Această variabilă este incrementată, pornind de la unul din numele ei, y, apoi
afişată, pornind de la celălalt nume al ei, z. Prin urmare, se afişează 3.
#include <iostream>
using namespace std;

int& f(int& x)
{ x++;
return x;
}

int main ()
{ int y=1;
int& z=f(y);
y++;
cout<<z;
}

28
ELEMENTE DE LIMBAJ

5. Nu se pot declara pointeri către referinţe.

Exemplu:
int a=1;
int& b=a;
int& *c=&b; // pointer catre referinta - se semnaleaza eroare

6. Există posibilitatea declarării referinţelor către variabile de tip pointer.

Exemplu. În urma executării secvenţei următoare, se afişează 3.


int a=1;
int* b=&a;
int* &c=b; // referinta catre pointer
cout<<*c;

 În fapt, o referinţă este simulată cu ajutorul unui pointer. Acest lucru este
transparent pentru programator, dar, în lecţia următoare, vom vedea că are
o consecinţă importantă.

EXERCIŢII PROPUSE
1. Pentru secvenţa de mai jos, răspundeţi la următoarele întrebări:
int& f(int& x)
{ x++;
return x;
}
...
int y=1;
int& z=f(y);
int t=f(y);

1.1. Ce valoare reţin, după executarea secvenţei, variabilele z şi t?

1.2. Care dintre perechile de nume de mai jos reprezintă una şi aceeaşi variabilă?

a) (z,t); b) (y,z); c) (y,t).

2. Fiind date declaraţiile de mai jos, care dintre expresii nu afişează 3?


int a=2; int& b=a; int* c=&a;

a) cout<<++b; b) cout<<++c; c) cout<<++(*c); d) cout<<++a;

RĂSPUNSURI
1. 1.1. 3 şi 3.
1.2. b).
2. b).

29
PARTEA A II-A

LECŢIA 3. Modificatorul “const”

În linii mari, putem spune că modificatorul const are rolul de a permite


programatorului să declare în program anumite constante. Rolul constantelor este
de a face un program lizibil, aceasta dacă numele ales pentru ele este semnificativ,
dar, în logica din C++ şi acela de a proteja anumite date. În cele ce urmează, ne
propunem să prezentăm modul în care acţionează acest modificator în funcţie de
locul în care acesta este aşezat.

A) Tipul constantei nu este pointer către un anumit tip. În aceste condiţii, este
indiferent locul în care acesta este plasat în cadrul definiţiei constantei.

Exemplu. Declarăm o constantă număr întreg. Observaţi faptul că modificatorul


const a fost plasat, prima dată înaintea tipului (int), iar a doua oară, imediat
după acesta. În ambele situaţii efectul este acelaşi, N este o constantă sau, altfel
spus, o variabilă “read only”.
const int N=20;
int const N=20;

Următoarele tentative de atribuire de valori pentru N sunt sortite eşecului, fiind


sancţionate de compilator:
1) N=12; // N este constanta
2) const int * adr=&N; *adr=7; // N este constanta

B) Tipul constantei este pointer către un anumit tip. Se pune problema cine
trebuie să fie constantă? Variabila care reţine pointer-ul sau variabila a cărei
adresă este indicată de către pointer?

B1) Dacă modificatorul const este declarat înaintea tipului, atunci este protejată
variabila indicată de pointer. Practic, aceasta nu poate fi modificată prin intermediul
pointerului. De asemenea, tentativa de a atribui valoarea reţinută de o astfel de
variabilă, unei alte variabile, de tip pointer către acelaşi tip, este sancţionată de
compilator. Motivul? Astfel, se protejează conţinutul variabilei indicată de pointer,
pentru că se evită “trucul” cu atribuirea.

Exemple:

 Priviţi secvenţele următoare: în timp ce secvenţa din dreapta este corectă şi


în urma executării ei se afişează 2, cea din stânga dă eroare la compilare.
Motivul? Am încercat să modificăm conţinutul variabilei a cărei adresă este
reţinută de adr.

int a=7; int a=7;


const int* adr=&a; int* adr=&a;
*adr=2; // eroare *adr=2;
cout<<*adr; cout<<*adr;

30
ELEMENTE DE LIMBAJ

 Secvenţa de mai jos este corectă şi, la executare, se afişează 3. Modul în care
am aşezat modificatorul const ne permite să modificăm conţinutul variabilei
care reţine pointer-ul, adr, dar nu a variabilei indicată de către pointer.
int a=7,b=3;
const int* adr=&a;
adr=&b;
cout<<*adr;

 Secvenţa de mai jos este corectă! Variabila a este protejată doar dacă este
adresată prin intermediul lui adr.
int a=2;
const int* adr=&a;
a=7;

 Atribuirea de mai jos este incorectă şi este sancţionată de compilator. Dacă


o astfel de atribuire ar fi corectă, s-ar putea modifica valoarea reţinută de
valoarea a.
const int* adr=&a;
int* b=adr; // eroare

 Aşa cum s-a precizat, protecţia are efect doar dacă variabila este accesată
prin intermediul pointer-ului. O astfel de protecţie devine utilă dacă pointer-ul
respectiv este parametru de intrare pentru o funcţie. Deoarece din interiorul
funcţiei, în mod normal, nu putem accesa conţinutul acestei variabile decât
prin intermediul parametrului, protecţia devine reală.
int f(const int* adr)
{ ... }

 Uneori veţi întâlni funcţii care returnează un pointer. Înaintea valorii de retur
din antet, se regăseşte modificatorul const. Ce rost are? Dacă este trecut
const, conţinutul variabilei indicată de pointer nu poate fi modificat prin
intermediul valorii returnată de funcţie, iar valoarea returnată de funcţie nu
poate fi atribuită unei variabile de acelaşi tip cu tipul funcţiei.

Exemplu. Priviţi programul următor. Fie x care reţine un şir de caractere. Numele
x este un pointer constant către tipul char. Funcţia ex() primeşte ca parametru
pe x şi returnează aceeaşi valoare. În stânga, şirul se modifică prin valoarea
returnată de funcţie. În dreapta, acest lucru nu este posibil întrucât antetul funcţiei
este precedat de const.

#include <iostream> #include <iostream>


using namespace std; using namespace std;
char* ex (char * c) const char* ex (char * c)
{ return c; } { return c; }
int main() int main()
{ char x[10]="un sir"; { char x[10]="un sir";
ex(x)[0]='a'; ex(x)[0]='a'; // eroare
cout<<x; } cout<<x; }

31
PARTEA A II-A

De multe ori, când veţi citi documentaţii pentru diverse clase, veţi găsi astfel de
antete de funcţii. Trebuie să înţelegeţi care este semnificaţia lor!

B2) Dacă modificatorul const este declarat imediat după *, atunci se consideră
constantă variabila care reţine pointer-ul (nu i se poate asocia o altă valoare). În
schimb, variabila către care pointează nu mai are conţinutul protejat.

Exemplu. Fie declaraţiile:


int a=7,b=3;
int *const adr=&a;

În aceste condiţii:
- atribuirea adr=&b; este incorectă;
- atribuirea *adr=7; este corectă.

 Şi referinţele pot fi prefixate de modificatorul const. În acest caz, nu putem


efectua o atribuire utilizând noul nume al variabilei. Cu alte cuvinte, se
protejează conţinutul variabilei.

Exemple:

 Fie declaraţiile:

int a=7;
const int& b=a;

În aceste condiţii:

- atribuirea b=3; este incorectă;


- atribuirea a=3; este corectă.

 Funcţia din stânga este eronată, dar cea din dreapta este corectă:

void ex (const int& x) void ex (int& x)


{ x++; } //eroare { x++; } //corect

 Uneori, veţi întâlni declarări precum cea de mai jos, care este corectă. Aici x
este o referinţă către o variabilă temporară care reţine numărul 3 (în astfel
de cazuri, compilatorul generează cod pentru crearea unei variabile
temporare). Conţinutul variabilei temporare nu poate fi modificat, prin urmare
x este o constantă. Acesta este un alt mod de definire a constantelor.

const int& x=3;

 Modul de definire a unei constante prezentat mai sus se dovedeşte a fi util


atunci când este folosit ca parametru al unei funcţii.

32
ELEMENTE DE LIMBAJ

Exemplu. Priviţi programul de mai jos, care este corect. Deşi x este o referinţă,
faptul că x este precedat de const, permite ca parametrul formal să fie o valoare.
Pentru primul apel, compilatorul generează o variabilă temporară care reţine
valoarea 7, iar x va reţine o referinţă către ea. Pornind de la x, în funcţie putem
numai să accesăm acea valoare. Pentru al doilea apel, x va reţine o referinţă către
z, dar din funcţie nu avem voie să modificăm valoarea lui x.

#include <iostream>
using namespace std;

void ex (const int& x)


{ cout<<x; }

int main()
{ int z=3;
ex(7);
ex(z);
}

OBSERVAŢIE
Dacă o variabilă este transmisă prin referinţă utilizând modificatorul const
şi dacă nu putem modifica valoarea ei, atunci care este rostul unei astfel de
transmisii? Puteam să utilizăm pur şi simplu transmiterea prin valoare. O referinţă
este realizată, aşa cum s-a arătat, printr-un pointer ascuns (netransparent pentru
programator). Acesta ocupă foarte puţin spaţiu, comparativ cu unele variabile
transmise prin valoare, care ocupă mult spaţiu (de exemplu, una neomogenă,
obţinută cu struct).

 În concluzie, dacă T este un tip oarecare şi avem o funcţie f(const T& v)


{…} şi o altă funcţie, g(T v) {…} şi varb este o variabilă de tip T, iar
valr este o valoare de tip T, atunci:

a) Apelul g(varb) - este un apel prin valoare. În acest caz, v din g va


reţine valoarea reţinută de varb. Dacă varb ocupă mulţi octeţi, atunci v
ocupă de asemenea mulţi octeţi.
b) Apelul f(varb) este un apel prin referinţă. Prin urmare, v din f va
reţine o referinţă către varb. O referinţă ocupă cu mult mai puţin (am arătat
că este simulată printr-un pointer) decât o valoare cu mulţi octeţi. În acest
caz, metoda b) este mai eficientă decât metoda a). Oricum, în f() nu este
permis să modificăm pe varb.
c) Apelul g(valr) este un apel prin valoare. În acest caz, v din g va
reţine valoarea valr.
d) Apelul f(valr) este un apel prin referinţă. Se creează o variabilă
temporară care reţine valr, iar v din f() va reţine referinţa către variabila
temporară. În astfel de cazuri, vă daţi seama, parametrul nu ocupă mulţi
octeţi. Prin urmare, apelul d) nu este cu mult mai ineficient decât c).

33
PARTEA A II-A

 După cum ştim, funcţiile pot întoarce referinţe. Şi acestea pot fi precedate de
const cu acelaşi efect. Nu se poate utiliza referinţa pentru a modifica
valoarea obiectului referit.

Exemplu. Mai jos, funcţia ex() returnează o referinţă către variabila primită ca
parametru tot prin referinţă. În primul caz, variabila nu poate fi modificată pornind
de la returul funcţiei (vezi stânga), dar în al doilea caz, aceasta poate fi modificată
(vezi dreapta):

const int& ex (int& x) int& ex (int& x)


{ return x; } { return x; }
int main() int main()
{ int x=4; { int x=4;
++ex(x); //eroare ++ex(x);
cout<<x; cout<<x;
} }

 Modificatorul const nu poate fi scris după &. În fapt, orice referinţă este
prin definiţie constantă (adică dacă am definit o referinţă către o anumită
variabilă, atunci acea referinţă va fi numai către acea variabilă).

Exemplu. A doua linie din secvenţa următoare este sancţionată de compilator:

int a=7;
int &const c=a; // eroare

EXERCIŢII PROPUSE
1. Pentru fiecare dintre funcţiile de mai jos, precizaţi dacă este corectă sau nu:

1.1.
void f(const int V[10])
{ V[1]=2; }

1.2.
void f(const int* a)
{ int b=3;
a=&b; }

1.3.
void f(int* const a)
{ int b=3;
a=&b; }

1.4.
void f(int* const a)
{ *a=4; }

1.5.
void f(const int* const a)
{ cout<<*a; }

34
ELEMENTE DE LIMBAJ

1.6.
void f(int& const x)
{ cout<<x; }

1.7.
void f(const int& x)
{ cout<<x; }

1.8.
void f(const int& x)
{ cout<<++x; }

2. Pentru fiecare dintre programele de mai jos, precizaţi dacă este corect sau nu:

2.1.
int& f(int& x)
{ return x; }
int main()
{ int z=1;
++f(z);
cout<<z; }

2.2.
const int& f(int& x)
{ return x; }
int main()
{ int z=1;
++f(z);
cout<<z; }

2.3.
int* f(int* x)
{ return x; }
int main()
{ int z=1;
cout<<1+*f(&z);
}

2.4.
const int* f(int* x)
{ return x; }
int main()
{ int z=1;
cout<<1+*f(&z);
}

2.5.
const int* f(int* x)
{ return x; }
int main()
{ int z=1;
*f(&z)=2; }

35
PARTEA A II-A

RĂSPUNSURI
1.
1.1. Nu. Componentele lui V sunt protejate la scriere.
1.2. Da. Este protejată la scriere variabila către care pointează a, nu variabila
locală a.
1.3. Nu. Variabila locală a este protejată la scriere.
1.4. Da. Este protejată la scriere variabila a, nu variabila către care pointează a.
1.5. Da. Este protejată la scriere variabila a şi variabila către care pointează a,
dar nu efectuăm atribuiri, ci numai o afişare.
1.6. Nu. Referinţele sunt în mod implicit constante. Sintaxa nu permite o astfel de
scriere.
1.7. Da. Este protejată la scriere variabila x, dar funcţia nu-i modifică valoarea.
1.8. Nu. Este protejată la scriere variabila x.

2.
2.1. Da. Se afişează 2.
2.2. Nu. Nu se poate modifica variabila pornind de la referinţa returnată de funcţie.
2.3. Da. Se afişează 2.
2.4. Da. Se afişează 2.
2.5. Nu. Tentativă de modificare a valorii unei variabile printr-un pointer care nu
permite acest lucru.

LECŢIA 4. Legătura între masive şi pointeri

Un masiv n dimensional poate fi privit ca un vector cu componente masive


n-1 dimensionale. De exemplu, dacă masivul este int M[3][4][5], atunci el
poate fi privit ca un vector cu 3 componente. Fiecare astfel de componentă este o
matrice cu 4 linii şi 5 coloane cu tipul de bază int.

 Numele unui masiv n dimensional este un pointer constant către un masiv


n-1 dimensional.

 Pentru a declara o variabilă care poate reţine un pointer către un masiv n>1
dimensional cu elemente de tip T, se foloseşte declaraţia:

T (* nume)[][][]...[
 ]
de n ori
Exemple:

 V este un vector cu 10 componente de tipul int. Prin urmare, V are


dimensiunea 1. O componentă are dimensiunea 0, adică este de tipul int.
Dacă dorim să avem un pointer care reţine numele masivului, acesta trebuie
să fie pointer către int. Analizaţi secvenţa următoare, în care lui V[1] i se
atribuie o valoare, valoare care este apoi afişată cu ajutorul pointer-ului.

36
ELEMENTE DE LIMBAJ

int V[10];
V[1]=2;
int* p=V;
cout<<p[1];

Atribuirea V=p este sortită eşecului pentru că numele V este pointer


constant.

 A este un masiv cu 3 dimensiuni. O componentă are dimensiunea 2, adică


este de tipul double [8][9]. Dacă dorim să avem un pointer care reţine
numele masivului, acesta trebuie să fie pointer către double [8][9].
Analizaţi secvenţa următoare, în care lui A[1][2][4] i se atribuie o
valoare, valoare care este apoi afişată cu ajutorul pointer-ului p:
double A[3][8][9];
A[1][2][4]=12;
double (*p)[8][9]=A;
cout<<p[1][2][4];

Atribuirea A=p este sortită eşecului pentru că numele A este pointer


constant.

OBSERVAŢIE. Dacă p ar fi declarat ca double *p[8][9], atunci ar însemna că


avem un masiv cu 8 linii şi 9 coloane cu elemente de tip double* (pointeri către
double). Aceasta pentru că operatorul * are prioritate mai mică decât operatorul [].
Ar însemna că, mai întâi, se consideră p[8][9] care este masiv, iar tipul de bază
este double*. Dar dacă declarăm aşa cum am arătat, avem mai întâi (*p), adică
pointer, apoi [8][9] către matricea cu 8 linii şi 9 coloane cu tipul de bază double.

 Fie M un masiv n dimensional cu componente de tipul T, declarat astfel:


T M[m1][m2]...[mn]. Fie 0a1<m1, 0a2<m2, ... 0an<mn numere naturale.

Atunci:
 atunci M[a1] este un masiv n-1 dimensional cu tipul de bază T;
 atunci M[a1][a2] este un masiv n-2 dimensional cu tipul de bază T;
 ...
 atunci M[a1][a1]...[an] este un element al masivului cu tipul T.

Exemplu. Fie declaraţia: int M[10][12][8];. Atunci, următoarele declaraţii


sunt corecte:
a) int (*q)[8]=M[3];
b) int *r=M[3][5];
c) int s=M[3][5][2];

Explicaţii:
a) q este pointer către masive unidimensionale cu 8 elemente de tipul int. M[3]
este masiv bidimensional cu 12 linii şi 8 coloane. Prin urmare, numele lui este

37
PARTEA A II-A

pointer constant către vectori cu 8 elemente de tipul int. În aceste condiţii,


atribuirea este corectă.
b) r este pointer către întreg. M[3][5] este vector, cu 8 elemente de tipul int.
Numele lui este pointer către întreg. Prin urmare, atribuirea este corectă.
c) s este un întreg. M[3][5][2] este întreg, deci atribuirea se efectuează fără
probleme. De fapt, acest caz a fost întâlnit de multe ori.

EXERCIŢII PROPUSE
1. Analizaţi programul următor. Ce afişează?

#include <iostream>
using namespace std;

void afis( int (*p)[4])


{ for (int i=0;i<4;i++)
{ for (int j=0;j<4;j++)
cout<<p[i][j]<<" ";
cout<<endl;
}
}

int main()
{ int A[4][4][4],v=0;
for (int i=0;i<4;i++)
for (int j=0;j<4;j++)
for (int k=0;k<4;k++)
A[i][j][k]=++v;
afis(A[2]);
}

2. Găsiţi un alt antet pentru funcţia afis() din programul anterior, astfel încât
parametrul de intrare să fie un masiv şi dacă acesta înlocuieşte vechiul antet,
funcţia să realizeze acelaşi lucru.

RĂSPUNSURI
1. Masivul A poate fi privit ca un vector cu 4 elemente.  33 34 35 36 
Fiecare element este o matrice cu 4 linii şi 4 coloane, cu  
elemente de tip int. Funcţia afis() afişează o astfel de  37 38 39 40 
matrice, cea transmisă prin parametru. În cazul de faţă  41 42 43 44 
trebuie să afişeze matricea de indice 2 (adică a treia  
 45 46 47 48 
matrice). Primele două matrice au fiecare câte 16 
elemente.

2. void afis(int p[4][4]).

38
ELEMENTE DE LIMBAJ

LECŢIA 5. Operaţii cu pointeri


 Fiind dat un pointer p către tipul T şi un număr natural k, prin p+k vom
înţelege un pointer către tipul T care indică adresa care se obţine pornind de
la adresa iniţială, la care se adaugă produsul dintre k şi numărul de octeţi
ocupaţi de o variabilă de tipul T.

 Fiind dat un pointer p, către tipul T şi un număr natural k, prin p-k vom
înţelege un pointer către tipul T care indică adresa care se obţine pornind de
la adresa iniţială, la care se scade produsul dintre k şi numărul de octeţi
ocupaţi de o variabilă de tipul T.

Exemple:

 Programul următor iniţializează şi afişează componentele unui vector. Să


observăm că afişarea se face prin utilizarea operaţiilor cu pointeri. Practic,
numele vectorului este un pointer constant către componente de tip int (are
tipul int*). Pentru a adresa componenta de indice i se face adunarea a+i.
Valoarea a+i este un pointer. Dacă i=0, se adresează prima componentă a
vectorului (cea de indice 0), dacă i este 1, se adresează a doua
componentă a vectorului (cea de indice 1), ş.a.m.d. Evident, pentru a afişa
conţinutul componentei de indice i, pornind de la un pointer către ea, se
utilizează operatorul ‘*’.

#include <iostream>

using namespace std;

int main()
{
int A[5],v=0,i;
for (i=0;i<5;i++)
A[i]=++v;
for (i=0;i<5;i++)
cout<<*(A+i);
}

 La fel ca mai sus, numai că se iniţializează şi afişează elementele unei


matrice cu 4 linii şi 5 coloane. Să nu uităm că o matrice precum următoarea
poate fi privită ca un vector cu 4 componente. Fiecare componentă este un
vector cu 5 componente de tip int. Prin urmare, prin A+i obţinem un
pointer către componenta de indice i, a vectorului iniţial. Prin *(A+i)
adresăm componenta propriu-zisă. Ea este un vector cu 5 componente de
tip int. Atunci, prin *(A+i)+j obţinem un pointer către componenta de
indice j a acestui vector. De aici, rezultă că prin *(*(A+i)+j) adresăm
valoarea reţinută de componenta de indice j a acestui vector.

39
PARTEA A II-A

#include <iostream>
using namespace std;
int main()
{ int A[4][5],v=0,i,j;
for (i=0;i<4;i++)
for (j=0;j<5;j++)
A[i][j]=++v;
for (i=0;i<4;i++)
{ for (j=0;j<5;j++)
cout<<*(*(A+i)+j)<<" ";
cout<<endl;
}
}

EXERCIŢII PROPUSE
1. Fie declaraţia: char sir[30]="acesta este un sir";

1.1. Ce se afişează prin instrucţiunea: cout<<*(sir+3); ?


1.2. Ce se afişează prin instrucţiunea: cout<<(char)(*sir+2); ?

1 2
2. Fie A    . Ce afişează secvenţa de mai jos?
 3 4
cout<< **A+*(*(A+1)+1);

RĂSPUNSURI
1. 1.1. Caracterul ”s”.
1.2. *s reprezintă caracterul ”a”, *s+2 se face suma între codul caracterului
”a” şi 2 şi se obţine codul caracterului ”c”, iar întregul astfel obţinut este
convertit către char. Se afişează ”c”.
2. 5.

LECŢIA 6. Expresii indexate


Astfel de expresii am întâlnit de câte ori am accesat un element al unui
masiv. Ele se bazează pe operatorul cu prioritate maximă, care se asociază de la
stânga la dreapta: []. Fie declaraţia: int A[5];. Aşa cum am văzut, pentru a
adresa componenta de indice 3, utilizăm fie expresia A[3], fie expresia *(A+3).

 De aici, rezultă că [] este operator care efectuează calculul A[i]=*(A+i).


Se poate observa că *(A+i)=*(i+A). De asemenea, în mod surprinzător,
*(i+A)=i{A]. În concluzie,

A[i]=*(A+i)=*(i+A)=i[A];.

40
ELEMENTE DE LIMBAJ

De exemplu, dacă avem A[2][3][1]=7;, atunci pentru a adresa acest


element se pot folosi expresiile:
A[2][3][1];
(*(A+2))[3][1];
(*(*(A+2)+3))[1];
*(*(*(A+2)+3)+1);

OBSERVAŢIE. Se folosesc parantezele rotunde pentru că, aşa cum s-a mai arătat,
prioritatea operatorului [] este mai mare decât prioritatea operatorului *.

Exemple:

 La executare, secvenţa de mai jos afişează de două ori valoarea 3.


int x=3, *adrx=&x;
cout<<adrx[0]<<0[adrx];

Variabila x este de tip int şi reţine 3. Variabila adrx, este de tip int* şi
reţine adresa lui x. Prin expresia adrx[0] se calculează *(adrx+0)=*adrx. Prin
expresia 0[adrx] se calculează *(0+adrx)=*adrx.

 Fie declaraţiile:
float A[3][7][5];
A[1][2][3]=4;

În aceste condiţii, secvenţa următoare afişează 4:


float (*p)[7][5]=A;
cout<<p[1][2][3];

Variabila p reţine un pointer către o matrice cu 7 linii şi 5 coloane de tip


float. Prin urmare, lui p i se poate atribui A, care este pointer constant către o
matrice cu 7 linii şi 5 coloane. Pentru afişare se fac, în ordine, operaţiile:
p[1]=*(A+1); // matrice cu 7 linii si 5 coloane
p[1][2]=*(p[1]+2)=*(*(A+1)+2); // vector cu 5 coloane
p[1][2][3]=*(p[1][2]+3)=*( *(*(A+1)+2)+3);

EXERCIŢII PROPUSE
1. Este corectă secvenţa de mai jos? Justificaţi răspunsul:
double v=10, *v1=&v,**v2=&v1;
cout<<v2[0][0];

2. Fie declaraţia: int A[3][4];. Să se precizeze pentru fiecare dintre


secvenţele de mai jos dacă afişează Da sau Nu.
2.1.
if ( *(*(A+1)+2)==A[1][2] ) cout<<"Da";
else cout<<"Nu";

41
PARTEA A II-A

2.2.
if ( *(A+1)+1==&A[1][1] ) cout<<"Da";
else cout<<"Nu";

2.3.
if (*A==A[0]) cout<<"Da";
else cout<<"Nu";
2.4.
if ((*(A+1))[1]==A[1][1]) cout<<"Da";
else cout<<"Nu";

2.5.
if (*(A+1)[1]==A[1][1]) cout<<"Da";
else cout<<"Nu";

RĂSPUNSURI
1. Da, se calculează **v2, adică se adresează variabila care reţine valoarea 10.

2. 2.1. Da! Amândouă expresiile adresează A[1][2];.


2.2. Da! Amândouă reprezintă adresa lui A[1][1];:
*(A+1)+1=A[1]+1=&*(A[1]+1)=&A[1][1].
2.3. Da! Amândouă reprezintă adresa primei componente a lui A, adică
adresa primului vector de 4 componente.
2.4. Da! Amândouă expresiile reprezintă A[1][1]:
A[1][1]=(*A+1)[1];.
2.5. Nu! *(A+1)[1]=**(A+1+1)=**(A+2)=*A[2]=A[2][0];

LECŢIA 7. Alocarea dinamică a memoriei


Uneori este necesar să alocăm anumite variabile în timpul executării
programelor. O astfel de alocare a variabilelor se numeşte alocare dinamică.

 Pentru a aloca dinamic o variabilă se utilizează operatorul new. Dacă tipul


variabilei alocate este T, vom defini o variabilă de tip pointer către T pe care
o iniţializăm cu pointer-ul către variabilă. Practic, operatorul new alocă spaţiu
în memorie pentru variabilă şi returnează pointer-ul către ea. Instrucţiunea
de alocare va arăta astfel:
T* nume_variabila_pointer=new T;
Câteodată, avem nevoie să eliberăm spaţiul ocupat de variabilă, pentru ca
acesta să poată fi utilizat pentru alocarea altei variabile. Dacă un program nu
eliberează memoria şi face numai alocări, există posibilitatea ca, la un moment dat,
toată memoria disponibilă să fie ocupată şi, la o nouă tentativă de alocare a unei
variabile, programul să se termine cu eroare.

42
ELEMENTE DE LIMBAJ

 Pentru a elibera spaţiul alocat de variabilă se utilizează operatorul delete.


Instrucţiunea de dezalocare va arăta astfel:
delete nume_variabila_pointer;

Exemple:
 Se alocă dinamic o variabilă de tip int, apoi aceasta se iniţializează, se
afişează şi se şterge:

int * adr=new int;


*adr=7;
cout<<*adr;
delete adr;

 Se alocă dinamic o variabilă neomogenă (structură), aceasta se iniţializează,


se afişează şi se şterge:
struct valori
{ int x;
float y;
};

valori* v=new valori;


v->x=1;v->y=2;
cout<<v->x<<" "<<v->y;
delete v;

 Există posibilitatea să alocăm şi vectori. Alocarea vectorului se efectuează


cu operatorul new[]. Pentru ştegerea unui vector se utilizează operatorul
delete[].

IMPORTANT. Nu este necesar ca numărul de componente al vectorului alocat să


fie cunoscut de la început (înaintea compilării).

Exemplu. Se alocă dinamic un vector cu n componente numere întregi. Valoarea


lui n se introduce de la tastatură. Cele n componente ale vectorului se iniţializează,
vectorul se afişează şi, la sfârşit, se şterge.
int n,i;
cout<<"n="; cin>>n;
int* v=new int [n];
for (i=0;i<n;i++) v[i]=i+1;
for (i=0;i<n;i++) cout<<v[i];
delete[] v;

 Se pot aloca dinamic şi masive n dimensionale. Să nu uităm ce am învăţat


deja: un masiv n dimensional este tratat ca un vector de masive, n-1
dimensionale. Cum numărul de elemente ale vectorului nu trebuie cunoscut
înaintea compilării, această valoare se poate stabili în timpul executării
programului. În schimb, celelalte n-1 dimensiuni trebuie cunoscute de
la început.

43
PARTEA A II-A

Exemplu. Se alocă o matrice cu n linii şi 3 coloane. Elementele matricei se


iniţializează, apoi se afişează. În final, spaţiul ocupat de matrice este eliberat.
int n,i,j,s=0;
cout<<"n="; cin>>n;
int (*mat)[3]=new int[n][3];
for (i=0;i<n;i++)
for (j=0;j<3;j++) mat[i][j]=++s;
for (i=0;i<n;i++)
{ for (j=0;j<3;j++) cout<<mat[i][j]<<" ";
cout<<endl;
}
delete[] mat;

O problemă foarte importantă care apare atunci când se lucrează cu


programe mari, în care se alocă multe variabile, este dată de faptul că, la un moment
dat, s-ar putea să nu mai existe memorie disponibilă. Din acest motiv, există o
modalitate prin care programatorul poate decide ce este de făcut într-o astfel de
situaţie, modalitate bazată pe mecanismul excepţiilor. În linii mari, excepţiile
tratează anumite evenimente nedorite care apar în timpul executării programului.

 Pentru a realiza “ca la carte” o alocare pentru o variabilă se procedează ca


mai jos, unde, dacă alocarea s-a putut efectua, nu se afişează nimic,
contrar, se afişează un mesaj lămuritor. Desigur, în locul acelui mesaj, se
2
poate scrie orice secvenţă decideţi.
int* adr;
try
{ adr=new int; }
catch (bad_alloc)
{ cout<<" Lipsa spatiu"; }

EXERCIŢII PROPUSE
1. Care dintre următoarele declarări sunt incorecte?
a) char *s=new char[20];
b) long v=100; char* s=new char[v];
c) long (*a)[10]=new long[3][10];
d) long (*a)[3]=new long[3][10];
e) int n=4; long (*a)[10]=new long[n][10];
f) int n=4; long (*a)[n]=new long[n][10];

2. Ce se afişează în urma executării secvenţei următoare?


int* a=new int, *b=a;
*a=4; *b=++(*a); cout<<*b;

RĂSPUNSURI
1.d), f), g). 2. Valoarea 5.

2
Mecanismul excepţiilor va fi tratat separat.

44
ELEMENTE DE LIMBAJ

LECŢIA 8. Parametri pentru program


Uneori este util să putem apela un program cu parametrii scrişi pe linia de
comandă. În C++ există această posibilitate. De exemplu, dacă executabilul se
numeşte test, am putea introduce parametri la apel. În acest caz, apelul ar fi sub
forma: test p1 p2 ... pn (unde p1, p2, ..., pn sunt parametri). Problema este
ca în program să putem “extrage” parametrii pentru a-i utiliza. În C++ s-a preluat
convenţia din C şi anume: dacă se doreşte preluarea parametrilor, antetul funcţiei
main() va conţine doi parametri:

1. Primul parametru este de tip int şi reprezintă numărul de parametri ai


programului. De exemplu, int nrarg.

2. Al doilea parametru este un vector de şiruri de caractere (de exemplu, acesta


se poate declara char* siruri[]). Primul şir reţinut de acest vector este chiar
numele programului executabil, iar următoarele şiruri sunt date de valorile
parametrilor.

Exemplu. Programul următor afişează toate şirurile transmise ca parametri de apel:

#include <iostream>
using namespace std;
int main (int nrarg, char* siruri[])
{ for (int i=1;i<nrarg;i++) cout<<siruri[i]<<endl;
}

EXERCIŢII PROPUSE
1. Scrieţi un program care afişează un fişier text. Numele fişierului se găseşte pe
linia de apel a programului. Testaţi programul listând chiar sursa lui.
2. Scrieţi un program care creează un fişier text. Liniile se citesc de la tastatură,
iar numele fişierului se găseşte pe linia de apel a programului.
3. Scrieţi un program care copiază într-un fişier text, numele celor două fişiere,
sursă şi final (în această ordine), care se găsesc pe linia de apel a programului.

INDICAŢIE
1. Fiind un singur parametru, ştim din start că acesta are, în vectorul şirurilor de
caractere, indicele 1:
#include <iostream>
#include <fstream>
using namespace std;
int main (int nrarg, char* siruri[])
{ fstream f(siruri[1],ios::in);
char linie[200];
while([Link](linie,200)) cout<<linie<<endl;
}

45
PARTEA A II-A

LECŢIA 9. Supraîncărcarea funcţiilor


 În C++ există posibilitatea ca mai multe funcţii să aibă acelaşi nume, dar să
difere prin numărul parametrilor formali şi/sau tipul parametrilor formali. La
apel, se selectează una sau alta în funcţie de parametrii efectivi. Exemplul
următor este concludent.

Exemplu. Toate funcţiile au acelaşi nume: f(). Programul face distincţie între:
- funcţiile cu număr diferit de parametri, de exemplu f(int) şi f(int,int);
- funcţiile cu acelaşi număr de parametri, dar de tip diferit, de exemplu f(int,int)
şi f(double,double):

#include <iostream>

using namespace std;

int f(int x, int y)


{ cout<<"int x, int y"<<endl;
return x+y;
}

int f(int x)
{ cout<<"int x"<<endl;
return x+1;
}

double f(double x, double y)


{ cout<<"double x, double y"<<endl;
return x+y;
}

double f(double x)
{ cout<<"double x"<<endl;
return x+1;
}

int main()
{ cout<<f(3)<<endl;
cout<<f(3.2, 7.5)<<endl;
cout<<f(1.7)<<endl;
cout<<f(8,9)<<endl;
}

 La ce este utilă supraîncărcarea funcţiilor? Mai multe funcţii pot face din
punct de vedere logic acelaşi lucru. Pentru a le reţine mai uşor, toate vor
avea acelaşi nume.

EXERCIŢIU. Scrieţi mai multe funcţii numite adun(). Una dintre ele adună două
numere naturale, alta adună două numere reale, alta două numere complexe, alta
două matrice pătratice şi ultima două numere raţionale.

46
ELEMENTE DE LIMBAJ

LECŢIA 10. Funcţii cu un număr variabil de parametri

Uneori, în practica programării este necesar să creăm funcţii pentru care nu


cunoaştem de la început numărul de parametri. Un astfel de exemplu, este funcţia
printf, despre care se ştie că primul parametru este de tipul char*. În funcţie de
ce cuprinde şirul transmis, ceilalţi parametri pot fi de tip int, double, etc.
Întrebarea este: cum se pot construi astfel de funcţii?

 Pentru a construi astfel de funcţii trebuie să avem în vedere că primii k1


parametri trebuie să fie ficşi, adică să se cunoască numele şi tipul lor şi, la
apel, aceştia să se găsească întotdeauna printre parametrii efectivi, exact în
ordinea în care aceştia figurează în antet. După parametrii ficşi, antetul
funcţiei va conţine caracterul virgulă, urmat de “...”.

 Imaginăm parametrii efectivi ca pe o listă unde primul element este primul


parametru efectiv, al doilea element este al doilea parametru efectiv, ş.a.m.d.
O astfel de listă are un tip special numit va_list. Se va declara o variabilă de
tip va_list şi asupra ei se va lucra cu 3 funcţii, care sunt prezentate în
continuare. Pentru a le putea utiliza, trebuie inclus fişierul antet cstdarg:

#include <cstdarg>

 void va_start(va_list variabila, numele_ultimului


parametru fix); - fixează un pointer intern asupra elementului din listă
care conţine ultimul parametru fix;
 tip va_arg(va_list varibila, tip) – pointer-ul intern avansează
indicând următorul element al listei şi funcţia returnează valoarea
parametrului efectiv, care are tipul tip şi este indicat de pointer-ul intern;
 void va_end() - închide lista. Se utilizează pentru că în timpul
prelucrărilor de acest tip lista parametrilor efectivi (evident, se găseşte pe
stivă) poate fi modificată.

OBSERVAŢIE FOARTE IMPORTANTĂ. Implicit, un parametru efectiv de tip char


este convertit către int, iar un număr real este convertit către double. Vedeţi al
doilea exemplu!

Exemple:
 Programul următor conţine funcţia t(). Parametrul fix este k şi acesta va
primi ca valoare numărul celorlalţi parametri efectivi, toţi aceştia fiind de tipul
int. Se cere să se afişeze toţi parametrii efectivi.

#include <iostream>
#include <cstdarg>

using namespace std;

47
PARTEA A II-A

void t(int k,...)


{ va_list lista;
int nr;
va_start(lista,k);
for (int i=0;i<k;i++)
{ nr=va_arg(lista,int);
cout<<nr<<endl;
}
va_end(lista);
}
int main()
{ t(3,1,2,7);
t(4,2,1,7,6);
}

 Programul următor conţine o funcţie t(), care are ca prim parametru un


pointer către un şir. Următorii parametri pot fi de unul din tipurile: int, char
sau float. Şirul transmis ca parametru este o succesiune oarecare de
caractere ”i”, ”c” şi ”r”. Astfel, dacă şirul este ”iicrr”, înţelegem că
parametrii efectivi care urmează şirului sunt, în ordine, de tipurile int, int,
char şi float. Se cere să se afişeze toţi parametrii efectivi.

Rezolvare. Variabila ps este de tip char*. Ea va reţine, pe rând, un pointer către


primulul caracter din şir, apoi un pointer către al doilea caracter din şir, ş.a.m.d. Nu
uitaţi, ultimul caracter dintr-un şir este ”0”! Se stabileşte pentru fiecare caracter din
şir dacă este ”i”, ”c” şi ”r” şi, în fiecare caz, se efectuează citirea lui, apoi acesta
este afişat. Pentru că un parametru efectiv de tip char este convertit automat către
int, acesta este preluat ca int şi convertit explicit către char. Pentru că un
parametru efectiv de tip float este convertit automat către double, acesta este
preluat ca double şi convertit explicit către float.
#include <iostream>
#include <cstdarg>
using namespace std;

void t(char* s,...)


{ va_list lista;
int v1; char v2; float v3;
va_start(lista,s);
char* ps=s;
while (*ps) // cat timp nu sau fost tratate toate caracterele
{ switch (*ps)
{ case 'i': v1=va_arg(lista,int); cout<<v1<<endl; break;
case 'c': v2=(char)va_arg(lista,int); cout<<v2<<endl; break;
case 'r': v3=(float)va_arg(lista,double);cout<<v3<<endl;
break;
}
ps++;
}
va_end(lista);
}

48
ELEMENTE DE LIMBAJ

int main()
{ t("ciiirc",'m',2,3,4,7.2,'a');
t ("iiic",12,11,22,”a”);
}

EXERCIŢIU. Scrieţi o funcţie care calculează impozitul de 16% datorat statului de


către mai multe persoane fizice. Pentru o persoană, funcţia primeşte ca parametri
următoarele:
- nume; // sir
- numar surse de venit (n)
- sursa 1 //sir
- venit 1 // real
...
- sursa n // sir
- venit n; // real

Funcţia va afişa situaţia sub forma:


"Nume" XXXX
sursa 1 venit 1
...
sursa n venit n;
"Total venit" [Link]
"Impozit" [Link]

LECŢIA 11. Pointeri către funcţii

În C++ se poate lucra cu pointeri către funcţii.

 O variabilă care poate reţine un pointer către o funcţie se declară ca mai jos:
tip_functie (*nume_variabila) (lista tipurilor de
parametri formali)

 Un pointer către o funcţie se obţine fie utilizând direct numele funcţiei, fie
prefixându-l pe acesta cu operatorul ”&”.

Exemplu. Funcţia suma este de tipul int şi returnează suma a două numere
întregi. Variabila f, poate reţine un pointer către funcţii de tip int, pentru care lista
parametrilor formali este (int,int). Iniţial, variabila f reţine un pointer către
funcţia suma, pointer-ul fiind dat de numele funcţiei: suma. Pornind de la conţinutul
lui f, se adresează funcţia.

#include <iostream>
using namespace std;

int suma(int a, int b)


{ return a+b;
}

49
PARTEA A II-A

int main()
{ int (*f)(int,int)=suma;
cout<<f(2,3)<<endl;
f=&suma;
cout<<f(4,5);
}

OBSERVAŢIE: o variabilă de tip pointer către funcţie, definită aşa cum s-a arătat,
poate reţine pointeri către toate funcţiile care au acel tip şi aceeaşi listă a
parametrilor formali.

Exemplu. Fie int (*f)(int,int) o variabilă care poate reţine pointeri către
funcţii de tip int, cu doi parametri formali de tip int. Atunci, dacă avem funcţiile
de antet:
a) void s1(int a, int b);
b) int s2(int a);
atribuirile următoare sunt eronate: f=&s1;, f=s1;, f=&s2;, f=s2.

 De vreme ce există un tip de variabilă care reţine pointeri către funcţii,


înseamnă că, utilizând variabile de acest tip, funcţiile pot primi ca parametri
pointeri către funcţii. Aceasta înseamnă, prin abuz de limbaj, că funcţiile pot
avea ca parametri funcţii. De altfel, aceasta este şi raţiunea existenţei
pointer-ilor către funcţii.

Exemplu. Se cere să se scrie o funcţie, numită afis(), care afişează primii n


termeni ai unui şir de numere reale. Aici, folosim definiţia din matematică a şirului,
şi anume o funcţie definită pe mulţimea numerelor naturale. În exemplul dat, avem
două şiruri an şi bn, date sub forma unor funcţii de genul float F(int).
Observaţi fapul că funcţia afis() primeşte ca parametru o funcţie şi îi listează
valorile. Cu alte cuvinte, o aceeaşi funcţie, poate lista valorile tuturor funcţiilor de tip
float, cu un parametru formal de tip int.
#include <iostream>
using namespace std;

float an (int n)
{ return 3*n+1; }

float bn(int n)
{ return n*n-n; }

void afis(int n,float(*sir)(int))


{ for (int i=1;i<=n;i++) cout<<sir(i)<<endl; }

int main()
{ cout<<"primii 5 termeni ai sirului an"<<endl;
afis(5,an);
cout<<"primii 5 termeni ai sirului bn"<<endl;
afis(5,bn);
}

50
ELEMENTE DE LIMBAJ

EXERCIŢIU. Se dau funcţiile:


double f (double x) { return x*x-1; }
double g(double x) { return x+1; }

Se cere să se scrie o funcţie double afis(int n, int x,...) care să


calculeze sume cu n termeni în care un termen poate fi f(x) sau g(x).
Exemplu:
afis(4,2,f,g,f,f); returnează: f(2)+g(2)+f(2)+f(2)=3+3+3+3=12.
Rezolvare:
#include <iostream>
#include <cstdarg>
using namespace std;
double f (double x)
{ return x*x-1; }
double g(double x)
{ return x+1; }
double afis(int n, int x,...)
{ va_list lista;
va_start(lista,x);
double suma=0;
double (*p)(double);
for(int i=1;i<=n;i++)
{ p=va_arg(lista, double (*)(double) );
suma+=p(x);
}
return suma;
}
int main()
{ cout<<afis(4,2,f,g,f,f); }

LECŢIA 12. Spaţii de nume

Spaţiile de nume au fost introduse în C++ ca o modalitate de a marca prin


nume o grupare logică care are în structura ei variabile, funcţii, etc.

 Pentru a introduce un spaţiu de nume se utilizează directiva namespace, iar


variabilele sau funcţiile care fac parte din grupare sunt incluse între ”{}”.

Exemplu:
namespace calcule
{ int a=1;
int f (int x)
{ return x*x-1; }
}

51
PARTEA A II-A

 O modalitate de a accesa variabilele şi metodele dintr-un spaţiu de nume


este folosirea operatorul de rezoluţie “::”. Practic, se scrie numele spaţiului
de nume, urmat de operator şi de numele variabilei sau funcţiei.
Exemplu. Pentru spaţiul de nume definit anterior, se afişează variabila a şi f(2):
cout<<calcule::a<<endl;
cout<<calcule::f(2);

 Pentru a utiliza mai uşor variabilele şi funcţiile dintr-un spaţiu de nume, se


utilizează directiva using, sub forma:
using namespace nume_spatiu.

Exemplu. Pentru spaţiul de nume definit anterior se afişează variabila a şi f(2).


using namespace calcule;
cout<<a<<endl;
cout<<f(2);

Desigur, spaţiile de nume ridică anumite întrebări la care ne propunem să


răspundem în continuare.

1. Dacă în cadrul blocului de unde se face apelul, definim o variabilă cu un nume


care se regăseşte în spaţiul de nume, care variabilă este accesată, cea din spaţiul
de nume sau cea astfel definită? Răspuns: cea definită în spaţiul de unde o
accesăm.
Exemplu. Pentru spaţiul de nume definit anterior, secvenţa de mai jos afişează 2:
using namespace calcule;
int a=2;
cout<<a<<endl;

2. În condiţiile de la 1. mai putem accesa variabila din spaţiul de nume?


Răspuns: da, dacă se utilizează operatorul de rezoluţie.
Exemplu. Pentru spaţiul de nume definit anterior, secvenţa de mai jos afişează 1.
int a=2;
cout<<calcule::a<<endl;

3. În continuare, este prezentată o ambiguitate. Care ar trebui să fie variabila a


care este afişată? Care ar trebui să fie funcţia f() care este apelată? Observaţi că
variabila nu este definită în blocul de unde este apelată. În ambele cazuri nu se
poate decide. În astfel de situaţii, veţi primi eroare la compilare.
namespace calcule
{ int a=1;
int f (int x)
{ return x*x-1; }
}
int a=2;

52
ELEMENTE DE LIMBAJ

int f (int x)
{ return x*x;
}
using namespace calcule;
int main()
{ cout<<a<<" "<<f(2); // eroare
}

 Există posibilitatea ca spaţiul de nume să conţină numai declaraţia unei


funcţii nu şi definiţia ei. Funcţia poate fi definită prin utilizarea operatorului de
rezoluţie, ca mai jos. Acest procedeu asigură separarea între interfaţă şi
implementarea propriu-zisă. În plus, nu este permis să adăugăm o nouă
funcţie spaţiului de nume dacă antetul funcţiei nu este inclus în acesta.
namespace definita
{ int f (int x); }

int definita::f(int x)
{ return x*x; }

OBSERVAŢIE. Toate librăriile standard ale limbajului C++ sunt în spaţiul de nume
std. Din acest motiv, la orice program am inclus directiva:
using namespace std;

ÎNTREBARE. Care dintre programele de mai jos este corect?


a) #include <iostream> b) #include <iostream>
int main() int main()
{ int a=2; { int a=2;
cout<<a; std::cout<<a;
} }

RĂSPUNS. În ambele lipseşte using namespace std;. Răspunsul corect este


b), pentru că a fost prefixat numele spaţiului de nume al librăriei standard a
limbajului C++, std.

LECŢIA 13. Instrucţiunile “break” şi “continue”

Deşi utile, instrucţiunile care sunt prezentate în această lecţie, nu sunt


descrise în manualele noastre pentru liceu. Motivul? Dacă le folosim, în cele mai
multe cazuri se încalcă principiile programării structurate.

 Instrucţiunea ”break;” are rolul de a forţa ieşirea dintr-un ciclu. În cazul în


care există cicluri imbricate (un ciclu în interiorul altui ciclu) forţează doar
ieşirea din primul ciclu (vezi secvenţa următoare, unde for-ul după j se
execută doar pentru j=1). Se foloseşte pentru instrucţiunile for, while, do
while şi, în particular, pentru switch - cazul a fost prezentat în manual.

53
PARTEA A II-A

Exemplu. Analizaţi secvenţa de mai jos: Se afişează:


{ for (int i=1;i<=4;i++) 1 1
for (int j=1;j<=4;j++) 2 1
{ if (j==2) break; 3 1
cout<<i<<" "<<j<<endl; 4 1
}
}

 Instrucţiunea ”continue;” este folosită în cazul ciclurilor (for, while, do


while) şi are rolul de a determina ca, după executarea ei, să se sară direct
la testul condiţiei de continuare. Astfel, se ignoră instrucţiunile care îi
urmează, în secvenţa care se repetă.

Exemplu. Secvenţa următoare afişează. 1, 3. 4. Dacă i ia valoarea 2, atunci se


execută continue şi se sare peste partea de afişare:
for (int i=1;i<=4;i++)
{ if (i==2) continue;
cout<<i<<endl;
}

EXERCIŢIU. Ce afişează secvenţa de mai jos?


for (int i=1;i<4;i++)
for (int j=1;j<4;j++)
{ if (i==3) break;
if (j==2) continue;
cout<<i<<" "<<j<<endl;
}

RĂSPUNS: 1 1; 1 3; 2 1; 2 3;

LECŢIA 14. Excepţii


Orice program utilizat în practică, chiar dacă este realizat după un algoritm
corect, poate să furnizeze rezultate nedorite sau să se întrerupă din cauza dateler
de intrare, care nu sunt întotdeauna cele aşteptate.

De exemplu, dacă un program citeşte vârsta unei persoane (în ani) şi


operatorul introduce, din greşeală, 1234, este clar că aceasta nu este o valoare
validă. Trebuie ca operatorul să fie înştiinţat că data este eronată şi eventual să se
ceară reintroducerea vârstei persoanei respective.

 În C++, prin excepţie se înţelege mecanismul prin care se separă partea de


identificare a unei erori de partea de tratare a erorii respective. Aceasta se
realizează prin instrucţiunea try/catch. Practic, depistarea erorii se face
în codul scris în partea try sau în funcţiile apelate de aici, iar tratarea lor se
face în codul scris în partea catch.

54
ELEMENTE DE LIMBAJ

 Forma generală a instrucţiunii try/catch este prezentată mai jos. Observaţi


că pot exista mai multe părţi catch corespunzătoare unei părţi try:
try
{
// instructiuni
}
catch (parametru)
// instructiuni care trateaza o anumita eroare
}
...
catch (parametru)
// instructiuni care trateaza o anumita eroare
}
catch (parametru)
// instructiuni care trateaza o anumita eroare
}

 Atunci când programul pe care-l scrieţi depistează o anumită eroare, pentru


a o semnala, se foloseşte instrucţiunea throw: sub forma de mai jos, caz în
care se execută instrucţiunile care tratează eroarea respectivă.
throw expresie;

Foarte important: instrucţiunile care se execută se găsesc în acel catch


care are parametrul de tipul expresiei returnate de throw.

Exemplu. Se citeşte o valoare despre care se ştie că este un întreg. Se va testa


dacă valoarea respectivă este între 1 şi 5. În caz afirmativ, se va afişa valoarea,
altfel se va afişa un mesaj care corespunde erorii. Indiferent de valoarea citită, la
sfârşit se va afişa ”Program terminat”.

Rezolvare. După citirea valorii, se intră în blocul try. Dacă valoarea este mai
mică decât 1, se va returna prin throw valoarea 1, dacă valoarea este mai mare
decât 5, se va returna prin throw valoarea 2, iar dacă valoarea este corectă, se
afişează. Imediat ce a fost returnată o anumită valoare prin throw, secvenţa
rămasă de instrucţiuni din try nu se mai execută. În continuare, se testează tipul
valorii. În cazul de faţă, nu se pot transmite prin throw decât valori de tip int. Prin
urmare, trebuie să existe un catch cu un parametru de tip int. Cum el există, în
secvenţa din catch se testează valoarea parametrului a, adică valoarea care a
fost transmisă prin throw. În funcţie de valoarea lui a, se afişează mesajul de
eroare corespunzător. Se observă că, după executarea instrucţiunii try/catch,
se trece la instrucţiunea următoare, cea care, în cazul de faţă, afişează mesajul
cerut (“Program terminat”).
#include <iostream>
using namespace std;
int main()
{ int x;
cin>>x;

55
PARTEA A II-A

try
{ if (x<1) throw 1;
else
if (x>5) throw 2;
cout<<x;
}
catch(int a)
{ if (a==1) cout<<"x<1"<<endl;
else cout<<"x>5"<<endl;
}
cout<<"Program terminat"<<endl;
}

 O funcţie apelată dintr-un bloc try poate returna valori atât prin return cât şi
prin throw. Dacă funcţia îşi termină execuţia prin return, valoarea
returnată de aceasta nu este tratată de instrucţiunile unui catch, dar dacă
îşi încheie execuţia printr-o expresie returnată cu throw, atunci, automat,
sunt apelate instrucţiunile din catch-ul cu parametrul de acelaşi tip cu tipul
expresiei returnate.

Exemplu. Reluăm programul precedent, dar, de această dată, utilizăm o funcţie.


Dacă x este între 1 şi 5, funcţia returnează pe x (prin return), altfel, dacă x este
mai mic decât 1, funcţia returnează o valoare de tip int (1), deci se apelează
instrucţiunile din catch-ul de parametru int, dacă x este mai mare decât 5,
atunci funcţia returnează un caracter, deci se apelează instrucţiunile din catch-ul
de parametru char, unde se afişează mesajul corespunzător.
#include <iostream>
using namespace std;
int test(int x)
{ if (x>=1 && x<=5) return x;
else
if (x<1) throw 1;
else throw 'a';
}
int main()
{ int x; cin>>x;
try
{ cout<<test(x)<<endl;
cout<<"x a fost afisat";
}
catch(int)
{ cout<<"x<1"; }
catch(char)
{ cout<<"x>5"; }
}

 Observaţi cât este de interesant? O funcţie de tipul int, poate să returneze


şi valori de alte tipuri, iar tratarea se face separat, în funcţie de tipul valorilor
returnate.

56
ELEMENTE DE LIMBAJ

 Instrucţiunile unui catch de forma catch(...) se execută automat,


indiferent de tipul valorii primite. Exerciţiu: daţi un exemplu în acest sens!

EXERCIŢII PROPUSE
1. Scrieţi un program care citeşte o valoare întreagă până când ea este mai mare
sau egală cu 1 şi mai mică sau egală cu 5. La citirea unei valori incorecte, să se
afişeze un mesaj de eroare şi să se reia citirea.
2. Scrieţi un program care citeşte o valoare care trebuie să fie un întreg între 1
şi 5. Dacă valoarea introdusă nu este de tip întreg (de exemplu, începe cu un
caracter alfabetic) sau dacă valoarea este numerică, dar nu este între 1 şi 5,
citirea se reia până când condiţiile cerute sunt îndeplinite.
Indicaţie: pentru citire, se va utiliza o variabilă de tip şir de caractere.
3. Scrieţi un program care citeşte, de pe prima linie a unui fişier text cu numele
“[Link]”, o valoare naturală între 1 şi 5. Dacă fişierul nu există sau dacă pe prima
linie nu este o valoare naturală sau dacă valoarea se găseşte, dar nu este între 1
şi 5, programul se opreşte şi se afişează un mesaj corespunzător. Dacă valoarea
se găseşte, atunci să se afişeze valoarea la pătrat.
Indicaţie. Declaraţia fstream f(...) returnează valoarea 0, dacă fişierul nu
există, în rest... ştiţi deja!

RĂSPUNSURI
1. 2.
#include <iostream> #include <iostream>
using namespace std; using namespace std;
int main() int main()
{int x; {char x[20],nr,semnal;
int semnal; do
do { {semnal=1;
semnal=1; try
cin>>x; { cin>>x;
try nr=atoi(x);
{ if (x<1) throw 1; if (nr==0 && x[0]!='0') throw 1;
if (x>5) throw 2; else if(nr<1) throw 2;
} else if (nr>5) throw 3;
catch(int a) }
{if (a==1) catch(int a)
cout<<"x<1"<<endl; {semnal=0;
else if (a==1)
cout<<"x>5"<<endl; cout<<"data introdusa nu
semnal=0; respecta formatul"<<endl;
} else if (a==2) cout<<"x<1"<<endl;
} while (semnal==0); else cout<<"x>5"<<endl;
cout<<x<<endl; }
} }while (semnal==0);
cout<<x<<endl;
}

57
PARTEA A III-A
Iniţiere în programarea orientată pe obiecte

LECŢIA 1. Ce este programarea orientată pe obiecte ?


Conceptul poate fi întâlnit sub diverse denumiri, cum ar fi: programare
orientată pe obiecte, programare obiectuală, programare orientată spre obiect sau,
pe scurt, ”OOP - Object Oriented Programming”.

În linii mari, se poate spune că OOP reuneşte trei concepte fundamentale ale
programării: încapsulare, moştenire şi polimorfism.
1. Prin încapsulare înţelegem mecanismul prin care datele (variabilele) şi funcţiile
(numite în acest caz şi metode) sunt plasate împreună, într-o unică structură. În
C++, o astfel de structură se numeşte clasă. Clasa generalizează noţiunea de tip
de dată. Fiind dat un tip de dată, putem avea variabile care sunt de un anumit tip,
adică acele variabile pot reţine numai date care respectă anumite cerinţe,
corespunzătoare tipului (char, float, int, etc). Fiind dată o clasă, putem obţine
obiecte rezultate prin instanţierea acelei clase. Pe lângă datele proprii, un obiect
conţine şi metode, adică funcţii care permit lucrul cu datele reţinute de obiectul
respectiv, dar şi cu date din afara obiectului.
2. Prin moştenire se înţelege acea proprietate a claselor prin care o clasă poate
prelua datele şi metodele unei (unor) clase anterior realizate. În C++, acest
mecanism mai este cunoscut sub numele de "derivare a claselor". Evident, clasei
nou construite i se pot adăuga noi date şi metode! Prin acest procedeu, se preia
soft deja implementat şi acesta se dezvoltă. Avantajul uriaş al acestui mecanism
este că persoana care preia un anumit soft trebuie să cunoască doar documentaţia
de utilizare a acestuia.
3. Polimorfismul este legat de moştenire. În linii mari, prin polimorfism înţelegem
posibilitatea ca atât clasa care este moştenită, cât şi moştenitoarea, să poată
utiliza metode cu acelaşi nume, dar diferite. Fie mai multe clase C0, C1.. Cn, unde
C1 moşteneşte pe C0, C2 pe C1, ..., Cn pe Cn-1. Toate aceste clase redefinesc o
metodă: f. Există posibilitatea ca, în timpul executării programului, să se ruleze
o metodă f sau alta, tot f, în funcţie de tipul datelor care intervin. Procedeul este
cu mult mai complex şi o iniţiere în utilizarea lui o veţi găsi în această parte a cărţii.

OBSERVAŢII
1. Limbajul C++ oferă un suport puternic pentru OOP!
2. În ziua de azi, în viaţa reală, este de neconceput să se programeze în absenţa
mecanismelor OOP.
3. În această carte veţi găsi o iniţiere în OOP. Pentru a programa cu adevărat în
OOP, trebuie să urmaţi cursurile specializate din facultăţile de profil.

58
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

4. Cu adevărat greu este să proiectaţi din punct de vedere logic o clasă şi alte
clase care o moştenesc pe aceasta, ş.a.m.d., cu alte cuvinte, să proiectaţi o
”ierarhie de clase”.
5. Este important ca, pornind de la documentaţie, să învăţaţi să utilizaţi o ierarhie
de clase!

LECŢIA 2. Clase

DEFINIŢIE.. Prin încapsulare înţelegem mecanismul prin care datele (variabilele)


şi funcţiile (în acest caz, numite şi metode) sunt plasate împreună, într-o unică
structură.

 În C++, încapsularea se realizează printr-un tip abstract, numit class. Se


face distincţie între clasă, care este de tip abstract şi obiectul propriu-zis, care
este instanţierea clasei respective. Iată forma simplificată a tipului clasă:
class
{
[private]
date;
metode;
[protected]
date;
metode;
[public]
date;
metode;
}

Între paranteze drepte sunt trecuţi modificatorii public, protected,


private. Un modificator are efect până la întâlnirea altuia, sau până la
sfârşitul definiţiei.

- modificatorul private are rolul de a interzice accesul la date şi metode


din afara clasei;

- modificatorul public are rolul de a permite accesul din afara obiectului


la date şi metode;

- modificatorul protected va fi prezentat în alt context.

În absenţa modificatorilor, dacă utilizăm structura class, este interzis


accesul la date şi metode din afara obiectului.

Exemplu. La matematică v-aţi întâlnit cu numere complexe! Ele sunt de forma:


z=x+iy, unde x,y. În C++ există mai multe tipuri care reţin numere reale!
Încercăm să construim singuri un tip care permite memorarea şi prelucrarea
numerelor complexe.

59
PARTEA A III-A

class complex Un număr complex cuprinde:


{ - partea reală -x;
public: - partea imaginară -y;
float x, y;
- o funcţie care returnează
float modul();
}; modulul: x 2  y 2 .

Descrierea numărului complex o vom face într-un tip separat, numit complex:

#include <cmath>
class complex
{
public:
float x, y;
float modul();
};

float complex::modul()
{ return sqrt (x*x+y*y);
}

Definirea funcţiei modul (metodă a clasei complex) s-a făcut în afara clasei
respective. Pentru a preciza faptul că funcţia este o metodă a clasei complex,
utilizăm în antetul definiţiei operatorul "::" (operator de rezoluţie):
float complex::modul()

OBSERVAŢIE
Practic, cu ajutorul clasei, am descris un tip nou, numit complex. Obiectul
este faţă de clasă ceea ce este o variabilă faţă de tipul ei. Un obiect de tip
complex va conţine partea reală, partea imaginară a numărului şi o metodă
(funcţie) care permite calculul modulului numărului complex.

 Obiectele unei clase se declară la fel cum se declară variabilele de un


anumit tip.

De exemplu, pentru a declara două obiecte ale clasei complex, numite z1 şi z2,
vom scrie: complex z1,z2;.

 Pornind de la un obiect, pentru a adresa datele şi metodele sale, se


utilizează notaţia cu “.”, întocmai cum se adresează câmpurile unei
structuri.

De exemplu, dacă z este un obiect de tip complex, declarat, aşa cum s-a
arătat, complex z;, atunci prin z.x se adresează partea reală a obiectului z, prin
z.y se adresează partea imaginară a obiectului z, iar prin [Link]() se
adresează metoda (funcţia) modul a obiectului z, metodă care returnează modulul
numărului complex.

60
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

Exemple:

 Programul următor utilizează un obiect complex pentru care atribuie valori


părţii reale şi imaginare, apoi le afişează împreună cu modulul acestuia:
#include <iostream>
#include "[Link]"
using namespace std;
int main()
{ complex z;
z.x=3;
z.y=4;
cout<<z.x<<" "<<z.y<<" "<<[Link]();
}

 Programul următor citeşte un vector de numere complexe şi afişează cel mai


mare modul al acestora:
#include <iostream>
#include "[Link]"
using namespace std;
int main()
{ complex v[100];
int n,i;
float max;
cout<<"n="; cin>>n;
for (i=0;i<n;i++)
{
cout<<"Numarul complex "<<i+1<<endl;
cout<<"partea reala "; cin>>v[i].x;
cout<<"partea imaginara "; cin>>v[i].y;
}
max=v[0].modul();
for (i=1;i<n;i++)
if (v[i].modul()>max) max=v[i].modul();
cout<<max;
}

 Atribuiri. Fiind date două obiecte, se pot efectua atribuiri la fel cum se
1
procedează în cazul variabilelor .

Exemplu:
complex z,z1;
...
z1=z2;

 Pointeri către obiecte. Se poate lucra, aşa cum suntem obişnuiţi, cu


pointeri către obiecte. Ca şi în cazul pointer-ilor către tipuri definite cu
struct, se poate utiliza operatorul “->“. Urmăriţi exemplul următor, în care

1
După cum vom vedea, nu întotdeauna astfel de atribuiri conduc la rezultatul aşteptat.
Aceste cazuri vor fi tratate în lecţia următoare.

61
PARTEA A III-A

se alocă dinamic un obiect de tip complex, i se atribuie valori datelor


membru şi se afişează modulul lui:

complex* adr=new complex;


adr->x=3;
adr->y=4;
cout<<adr->x<<" "<<adr->y<<" "<<adr->modul();

 Se pot folosi şi referinţe către obiecte. Analizaţi exemplul următor, în care z


şi z1 reprezintă unul şi acelaşi obiect:
complex z, &z1=z;
z.x=3;
z.y=4;
cout<<[Link]();

Până în prezent, pentru a defini o metodă a clasei respective am utilizat


o declaraţie a ei (în interiorul clasei) şi definiţia propriu-zisă, pe care am scris-o în
afara clasei. Întrebarea este: este posibil să definim funcţia în interiorul clasei?
Răspunsul este afirmativ.

 Dacă o metodă este definită în interiorul clasei, atunci ea este tratată de


compilator ca o metodă inline. Aceasta înseamnă că orice apel al ei este
înlocuit cu codul secvenţei. Prin urmare, în limbaj maşină, în loc să avem un
salt către instrucţiunile metodei, apoi, după executare, să se revină tot
printr-un salt, la prima instrucţiune de după apel, pur şi simplu se execută
direct metoda. O astfel de abordare are avantajul eliminării salturilor şi
aceasta conduce la economie de timp, dar şi dezavantajul unui necesar mai
mare de memorie, dacă, evident, sunt multe apeluri, în locuri diferite ale
programului. În concluzie, programatorul decide, în funcţie de modul de
utilizare, dacă o metodă este sau nu inline.

Exemplu. Codul următor prezintă o clasă cu metode inline:


class exemplu
{ public:
int a;
void cit()
{ cin>>a; }
void afis()
{ cout<<a; }
};
int main()
{ exemplu ob;
[Link]();
[Link]();
}

 Există posibilitatea să definim metodele inline în afara clasei. Deşi se


scrie puţin mai mult, procedând astfel, avem avantajul unui program lizibil. În
viaţa reală, programele sunt foarte mari...

62
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

Exemplu:
class exemplu
{ public:
int a;
void cit();
void afis();
};
inline void exemplu::cit() { cin>>a; }
inline void exemplu::afis() { cout<<a; }

int main()
{ exemplu ob;
[Link]();
[Link]();
}

 Există posibilitatea să declarăm metode const (vedeţi exemplul de mai jos).


Semnificaţia? O metodă declarată const nu poate modifica datele membru
ale obiectului. Tentativa de modificare a datelor obiectului de o metodă
const este sancţionată de către compilator.

Exemplu. Din cele două metode ale clasei următoare, doar una poate fi declarată
const, cea care afişează conţinutul datei membru:
class exemplu
{
public:
int a;
void cit();
void afis() const;
};
void exemplu::cit() { cin>>a; }
void exemplu::afis() const { cout<<a ; }

OBSERVAŢIE. Declararea unei metode ca fiind const are şi rolul de a informa un


eventual utilizator al clasei respective (care, în mod evident, analizează antetele
metodelor) că metoda respectivă nu modifică datele membre ale obiectului.

EXERCIŢII PROPUSE
1. Adăugaţi clasei complex o metodă (funcţie) prin care se adună două numere
complexe. Primul este reprezentat de obiectul curent, iar al doilea este reprezentat
de parametru. Rezultatul trebuie să fie de tipul complex.
2. Utilizând rezultatul de la problema precedentă, scrieţi o secvenţă prin care se
adună două numere complexe.
3. Scrieţi o clasă, numită rational, prin care se gestionează numere raţionale.
p
Acestea sunt numere de forma p, q  Z , q  0 .
q

63
PARTEA A III-A

4. Creaţi un vector cu n componente numere raţionale cu valori citite de la


tastatură şi sortaţi vectorul în ordine crescătoare.
5. Creaţi o clasă numită cerc, unde pentru un cerc se reţin coordonatele
centrului şi raza (numere reale). Clasa va conţine două metode prin care se
afişează aria cercului şi, respectiv, lungimea lui.
6. Fiind date două obiecte de tip cerc, scrieţi o funcţie care returnează 1 dacă
cercurile se intersectează şi 0, în caz contrar.
7. Creaţi o clasă numită prim. Clasa reţine un număr natural şi este înzestrată cu
o metodă care returnează cel mai mic număr prim care este mai mare decât
numărul reţinut.
8. Prin utilizarea clasei prim, afişaţi primele n (n - număr natural citit de la
tastatură) numere prime.

INDICAŢII
1. Clasei i se adaugă metoda de mai jos. Nu uitaţi, introduceţi în interiorul ei
antetul: complex adun(complex z)!
complex complex::adun(complex z)
{ complex man;
man.x=x+z.x;
man.y=y+z.y;
return man;
}

2.
complex z, z1,z2;
z.x=3; z.y=4;
z1.x=4; z1.y=7;
z2=[Link](z1);
cout<<z2.x<<" "<<z2.y;

LECŢIA 3. Constructori

Revenim asupra declaraţiilor obiectelor. Fie declaraţia: complex a,b;. Au


fost declarate două obiecte numite a şi b. Aşa cum am văzut, dacă dorim ca cele
două obiecte să reţină valori, trebuie să accesăm datele membru ale obiectelor,
prin instrucţiuni de genul: a.x=valoare1; şi a.y=valoare2;.

O astfel de manieră de lucru nu este elegantă… Ar fi mult mai convenabil


dacă ar fi permise declarări de forma complex z(2,3) sau z1(5), pentru
z=2+3i şi z1=5+0i=5.

 Pentru a permite o astfel de modalitate de iniţiere a obiectelor, clasele pot


conţine anumite metode (funcţii) care realizează aceasta, funcţii numite
constructori.

64
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

 Pentru realizarea unui constructor, se va ţine cont de:


- un constructor are întotdeauna numele clasei din care face parte;
- un constructor este o funcţie fără tip; cu toate acestea, în dreptul tipului
nu se trece cuvântul cheie void;
- constructorul este apelat automat la declararea obiectului.

 O clasă poate fi înzestrată cu mai mulţi constructori, diferind între ei prin


numărul parametrilor formali şi/sau prin tipul acestora. Este o consecinţă
directă a supraîncărcării funcţiilor.

Exemplu de clasă care utilizează constructori

Adăugăm clasei complex 3 constructori: unul fără parametri, unul cu un


parametru care permite declarări de genul complex z(2), pentru z=2+0i=2 şi
altul cu doi parametri care permite declarări de genul z(2,3), pentru z=2+3i.
Astfel, clasa aflată în fişierul [Link] devine:
#include <cmath>
class complex
{ public:
complex (float a, float b);
complex (float a);
complex ();
float x, y;
float modul();
};
complex::complex (float a, float b)
{ x=a;
y=b;
}
complex::complex (float a)
{ x=a;
y=0;
}

complex::complex (){}
float complex::modul()
{ return sqrt(x*x+y*y);
}

În continuare, este prezentat un mic program care testează clasa complex:


#include <iostream>
#include "[Link]"
using namespace std;
int main()
{ complex z(3,4),z1(3),z2;
cout<<[Link]()<<" "<<[Link]();
}

65
PARTEA A III-A

OBSERVAŢII
1. Dacă o clasă este înzestrată cu un constructor cu cel puţin un parametru,
atunci, implicit, nu se mai pot face declarări de obiecte fără parametri. Pentru ca
acest lucru să fie permis, este necesar să “înzestrăm” clasa cu un constructor fără
parametri. Acesta este motivul pentru care clasa complex conţine şi constructorul:
complex::complex (){}

2. În absenţa altor definiţii (dacă programatorul nu a scris niciun constructor),


clasei i se ataşează în mod implicit o metodă specială, numită, constructor, care
are rolul de a permite declarea obiectelor, alocând spaţiu pentru ele. Un astfel de
constructor este numit constructor generat implicit sau, pe scurt, constructor
2
implicit.

3. Există, implicit şi un constructor de copiere. Pentru a vă convinge, testaţi


secvenţa complex z(3,4), z1=z;, unde z1 a fost iniţializat cu z. O astfel de
copiere se mai numeşte copiere bit cu bit. Nu întotdeauna o astfel de copiere
este utilă, aşa cum va rezulta din studiul lecţiilor următoare.

EXERCIŢIU
Adăugaţi clasei rational, constructorii rational de forma
rational(int,int) şi rational(int).
Orice număr raţional se va reţine sub formă de fracţie ireductibilă. Astfel, vor fi
posibile declarări de forma rational p(6,8) şi se va reţine numărul 3/4 sau
rational(7) şi se va reţine numărul 7/1.

LECŢIA 4. Destructor

Un principiu de bază în proiectarea claselor afirmă că este bine ca accesul


la datele obiectului să se facă exclusiv prin metodele sale. În practică, motivul
este dat de faptul că obiectele conţin o mulţime de date între care există relaţii. O
intervenţie “neautorizată” a utilizatorului în datele obiectului poate avea consecinţe
neplăcute. Să dăm un exemplu, în care accesul la date se face exclusiv prin metode.

Exemplu. Se cere să se realizeze o clasă care să gestioneze o listă liniară simplu


înlănţuită. Presupunem că sunteţi familiarizaţi, din manual, cu modul în care se
creează o listă liniară simplu înlănţuită. Fiecare element al listei reţine un număr
întreg şi adresa elementului următor. Clasa se numeşte lista, se găseşte în
fişierul [Link], iar un obiect de tip lista reţine un pointer, numit vf, către

2
Nu confundaţi constructorul implicit cu constructorul fără parametri. Acesta din urmă a fost
scris de cel care a creat clasa respectivă.

66
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

vârful listei liniare simplu înlănţuite. Vârful vf nu este public, prin urmare un
utilizator al clasei nu are acces la el. În afara vârfului listei, clasa conţine:

 lista(); - constructor fără parametri care are rolul de a iniţializa vârful


cu valoarea 0 (nicio adresă);
 void adaug(int a) - adaugă la începutul listei un nod care reţine
întregul a;
 void afis() - afişează toate valorile numerice reţinute de listă.

#include <iostream>
using namespace std;
struct nod
{ int valoare;
nod* adr_urm;
};
class lista
{ nod *vf;
public:
lista();
void adaug(int a);
void afis();
};
void lista::afis()
{ nod* c=vf;
while (c)
{ cout<<c->valoare<<" ";
c=c->adr_urm;
}
}
void lista::adaug(int a)
{ nod* adr=new nod;
adr->valoare=a;
adr->adr_urm=vf;
vf=adr;
}

lista::lista()
{ vf=0;
}

Mai jos, este prezentat un program care creează şi afişează (5 2 7 1) o


astfel de listă:
#include "[Link]"
int main()
{
lista l;
[Link](1); [Link](7);
[Link](2); [Link](5);
[Link]();
}

67
PARTEA A III-A

Ce observăm? Utilizatorul nu mai are nevoie “să-şi bată capul” cu alocarea listei
sau cu afişarea ei. Pentru el, lista este un obiect care conţine 3 metode şi atât. Nu
lucrează cu datele obiectului decât prin intermediul metodelor sale.

Dar dacă, un obiect de tip lista, se află declarat într-o funcţie oarecare a
programului, alta decât main(). Durata de viaţă a acestui obiect este, la fel ca în
cazul variabilelor, timpul cât se execută funcţia respectivă. Prin urmare, după
ieşirea din funcţie, obiectul este şters. Dar, mare atenţie: este şters obiectul, care
nu conţine, ca dată, decât adresa vârfului de listă (vf), nu şi lista, care rămâne
alocată în memorie, ocupând astfel spaţiul inutil. Ce este de făcut?

 În C++ un obiect este şters de către o metodă specială, numită destructor.


Orice obiect este înzestrat cu un destructor implicit, care are rolul de a
şterge datele reţinute de obiect, atunci când durata de viaţă a obiectului a
expirat. Constructorul explicit se apelează automat.

 Ca şi în cazul constructorilor, există posibilitatea ca o clasă să fie înzestrată


cu un destructor, altul decât cel implicit:
- un destructor are întotdeauna numele clasei din care face parte, dar
precedat de caracterul ”~”;
- un destructor este o funcţie fără tip; cu toate acestea, în dreptul tipului
nu se trece cuvântul cheie void;
- destructorul este apelat automat atunci când durata de viaţă a obiectului
încetează, dar poate fi apelat şi explicit.

Exemplu. În cazul clasei lista, din cauza motivelor arătate, este necesar să
scriem un destructor. Acesta are rolul de a elibera memoria ocupată de lista liniară,
ştergând fiecare element al ei.
În interiorul clasei, se adaugă antetul: ~lista();.

Definiţa destructorului este:


lista::~lista()
{ nod *man=vf, *man1=vf->adr_urm;
while (man1)
{ delete man;
man=man1;
man1=man->adr_urm;
}
vf=0;
}
În secvenţa de mai jos, se creează un obiect lista, după care se şterge, prin
apelul explicit al destructorului:
lista l;
[Link](2); [Link](1) ;[Link](3);
[Link](); cout<<endl;
...
l.~lista();

68
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

EXERCIŢII PROPUSE
1. Refaceţi clasa complex, astfel încât numerele complexe să fie alocate dinamic.
Înzestraţi clasa cu mai mulţi constructori şi un destructor.
2. Refaceţi clasa rational, astfel încât numerele complexe să fie alocate
dinamic. Înzestraţi clasa cu mai mulţi constructori şi un destructor.

LECŢIA 5. Copierea obiectelor


 În C++, obiectele pot fi copiate direct prin utilizarea operatorului ”=”. Practic,
la utilizarea operatorului ”=” se copiază toate datele şi metodele obiectului
rezultat în urma evaluării expresiei din dreapta operatorului în obiectul aflat
în stânga operatorului.
Exemple. Pentru a exemplifica modul în care se copiază obiectele, vom utiliza cele
două clase prezentate în lecţiile anterioare, complex şi lista.

 Se creează două obiecte complex. Al doilea (z2) se iniţializează cu valorile


reţinute de primul (z1). Proba se efectuează prin afişarea părţii reale şi a
părţii imaginare ale obiectului z2. Acestea sunt cele ale lui z1, 2 şi 3.
complex z1(2,3), z2=z1;
cout<<z2.x<<" "<<z2.y;

 Se creează două obiecte complex, z1 şi z2. După executarea atribuirii, z1


va reţine datele lui z2:
complex z1(2,3), z2(4,5);
z1=z2;
cout<<z1.x<<" "<<z1.y;

Nu întotdeauna lucrurile sunt atât de simple. Uneori, pot apărea probleme


grave dacă obiectele se copiază astfel.
Exemple. În secvenţa de mai jos se creează un obiect lista, numit l. Ea va
conţine, în ordine, 3 elemente şi anume: 3, 1 şi 2. Pentru a proba aceasta, se
afişează elementele lui l. În continuare, se creează o altă listă, numită g şi se
efectuează atribuirea g=l, în urma căreia g va reţine aceeaşi listă liniară simplu
înlănţuită ca şi l. Pentru probă, se afişează g. Elementele ei sunt tot 3, 1 şi 2.
Apoi, vom apela destructorul lui l, pentru a o şterge. Rămâne g, pe care încercăm
s-o afişăm din nou. Surpriză ... nu se mai pot afişa elementele lui g, deşi ea nu a
fost ştearsă. Care este motivul pentru care se întâmplă aceasta?
lista l;
[Link](2); [Link](1); [Link](3);
cout<<"l "; [Link](); cout<<endl;
lista g=l;
cout<<"g "; [Link](); cout<<endl;
l.~lista();
cout<<"g "; [Link](); cout<<endl;

69
PARTEA A III-A

Prin atribuirea g=l, ambele obiecte g şi l reţin adresele de început ale


aceleaşi liste liniare simplu înlănţuite. Destructorul, aplicat pentru l (a fost
prezentat în lecţia precedentă) şterge lista liniară alocată dinamic. În urma acestei
ştergeri, g va reţine în vf adresa unei liste liniare inexistente.

vf
l 3 1 2

vf
g
Din cele prezentate, rezultă că nu întotdeauna ne putem baza pe
mecanismul implicit de copiere a obiectelor. Aceasta se întâmplă atunci când
obiectele reţin pointeri către stucturi alocate dinamic. Mecanismul implicit de
copiere a obiectelor se mai numeşte copiere bit cu bit şi, aşa cum s-a arătat, nu
întotdeauna poate fi folosit.

ÎNTREBARE. În cazul obiectelor care conţin pointeri, cum se poate face copierea?
Această situaţie va fi tratată separat, într-una din lecţiile următoare.

EXERCIŢII PROPUSE
1. Adăugaţi clasei lista o metodă, sterg(), care şterge ultimul element din
lista liniară simplu înlănţuită.

2. Ce se afişează în urma executării secvenţei de mai jos?


lista l;
[Link](2); [Link](1); [Link](3);
lista g=l;
[Link]();
cout<<"g "; [Link](); cout<<endl;

3. Ce se afişează în urma executării secvenţei de mai jos?


lista l;
[Link](2); [Link](1); [Link](3);
lista g=l;
[Link](7);
cout<<"g "; [Link](); cout<<endl;
cout<<"l "; [Link](); cout<<endl;

INDICAŢII
1.
void lista::sterg()
{ nod* p=vf;
while (p->adr_urm->adr_urm) p=p->adr_urm;
delete p->adr_urm->adr_urm;
p->adr_urm=0;
}

70
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

2. 3 1 - ultimul element al listei a fost şters, iar l şi g reţin aceeaşi adresă a


primului element al listei! Prin urmare, ultimul element figurează ca şters din
“ambele” liste!
3. Pentru g se afişează 7 3 1 2 şi pentru l se afişează 3 1 2. De fapt, metoda
adaug() adaugă un element la începutul listei. Astfel, g va reţine adresa către
primul element, 7, iar l adresa către al doilea element, 3. Deşi 7 şi 3 fac parte din
aceeaşi listă, aparent, l şi g reţin pointer-i către liste diferite.

LECŢIA 6. Funcţii prietene

Există posibilitatea ca anumite funcţii, care nu aparţin unei clase să poată


accesa datele private ale acesteia, caz în care funcţiile respective se numesc
"funcţii prieten".

 Pentru a ataşa unei clase o funcţie prietenă, introducem în interiorul definiţiei


sale prototipul funcţiei prietenă, precedat de cuvântul cheie friend.

 Funcţia prieten nu poate accesa datele direct, aşa cum poate o metodă a
clasei respective, ci prin intermediul parametrului de tip obiect transmis.
Practic, o funcţie prietenă poate accesa datele private ale clasei respective.

Exemplu (exclusiv didactic). Clasa numar conţine o dată membru v, care nu


este publică. Metoda void cNumar(int x) memorează o valoare în data membru
v. Valoarea memorată este afişată de funcţia prieten void tip(numar x);. Să
observăm că funcţia prieten poate accesa data privată, dar prin intermediul
obiectului număr, nu direct.

#include <iostream>
using namespace std;
class numar
{ int v;
public:
void cNumar(int x);
friend void tip(numar x);
};
void numar::cNumar(int x)
{ v=x;
}
void tip(numar x)
{ cout<<x.v;
}
int main()
{ numar x;
[Link](3);
tip(x);
}

71
PARTEA A III-A

ÎNTREBARE. La ce folosesc funcţiile prieten? Răspunsul îl găsiţi în Lecţia 8, dar


rezolvând exerciţiul care urmează, îl puteţi intui!

EXERCIŢIU. Creaţi, pentru clasa din exemplul precedent, două funcţii prieten care
calculează suma dintre valoarea reţinută de obiect şi întregul primit ca argument.
int adun (numar x, int n);
int adun (int n, numar x);

LECŢIA 7. Supraîncărcarea operatorilor cu metode


ale clasei. Generalităţi

Ca şi funcţiile, în C++ operatorii pot fi supraîncărcaţi.

 Dacă operatorul O (O poate fi unul dintre operatorii +,-,...) este supraîncărcat,


atunci metoda respectivă se numeşte operator O(). Dacă operatorul este
binar, atunci metoda are un parametru formal (celălalt este obiectul curent),
iar dacă operatorul este unar, atunci funcţia nu are niciun parametru, pentru
că se acţionează asupra obiectului curent. De asemenea, în expresia care
utilizează operatorul supraîncărcat, acesta va avea aceeaşi prioritate ca şi
operatorul clasic, cel care a fost supraîncărcat.

Exemple. Supraîncărcarea operatorului “+“. Pentru exemplificare, vom utiliza


clasele complex şi lista pentru că respectivele clase oferă posibilitatea
prezentării unor exemple semnificative.

 Metoda următoare adună două numere complexe. Ea este realizată prin


supraîncărcarea operatorului “+“. Să observăm că metoda returnează un
obiect complex:

complex complex::operator+(complex z)
{
complex t;
t.x=x+z.x;
t.y=y+z.y;
return t;
}

În aceste condiţii, se pot face adunări de numere complexe şi se poate


proba efectul, ca mai jos:

complex z1(2,3),z2(4,5),z3=z1+z2;
complex z4;
z4=z3+z3;
cout<<z3.x<<" "<<z3.y<<endl;
cout<<z4.x<<" "<<z4.y<<endl;

72
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

 Metoda următoare, realizată tot prin supraîncărcarea operatorului “+“,


returnează obiectul rezultat ca suma dintre numărul complex reţinut de
obiectul curent şi o valoare reală:
complex complex::operator+(float a)
{ complex t;
t.x=x+a;
t.y=y;
return t;
}

Prin utilizarea ei, se pot efectua operaţii precum cea de mai jos:
complex z1(2,3),z2;
z2=z1+2;

ATENŢIE. Prin utilizarea metodei prezentate nu se pot scrie apeluri de forma


z2=2+z1. Aceasta pentru că, pentru metoda de mai sus, primul parametru este
obiectul curent, iar al doilea este parametrul de tip float. Prin urmare, deşi din
punct de vedere matematic o astfel de operaţie are sens, întrucât adunarea
numerelor complexe este comutativă, prin utilizarea metodelor clasei respective nu
se poate realiza. Soluţia este dată de utilizarea funcţiilor prieten şi va fi prezentată
la momentul potrivit.

 Metoda următoare, realizată prin supraîncărcarea operatorului “+“,


concatenează două obiecte de tip lista. Altfel spus, obiectul nou creat
reţine o listă liniară simplu înlănţuită alcătuită din elementele primei liste,
urmate de elementele celei de-a doua liste. Evident, o astfel de operaţie nu
este comutativă.
lista lista::operator+(lista l)
{
lista s;
nod* p=vf,*q,*q1;
while (p)
{ q=new nod;
q->valoare=p->valoare;
if ([Link]==0) { [Link]=q; q->adr_urm=0; }
else q1->adr_urm=q;
q1=q;
p=p->adr_urm;
}
p=[Link];
q->adr_urm=0;
while (p)
{ q->adr_urm=new nod;
q->adr_urm->valoare=p->valoare;
q=q->adr_urm;
q->adr_urm=0;
p=p->adr_urm;
}
return s;
}

73
PARTEA A III-A

Un exemplu de utilizare este prezentat mai jos, unde se concatenează două


obiecte de tip lista. Se va afişa, 3 2 1 5 4.
lista l,g,h;
[Link](1); [Link](2); [Link](3);
[Link](4); [Link](5);
h=l+g;
[Link]();

OBSERVAŢIE. Pentru ca metoda să ruleze fără eroare, trebuie să renunţaţi la


destructorul prezentat în lecţia precedentă. De ce? Lista este creată, aşa cum am
văzut, într-o metodă. La părăsirea metodei, se apelează destructorul, care şterge
lista pe care tocmai am creat-o. Aveţi posibilitatea ca, pentru ştergerea listei, să
utilizaţi o metodă, de exemplu void sterg_lista(). Spre deosebire de
destructor, metoda se apelează numai ca urmare a unui apel, nu numai implicit,
aşa cum se apelează destructorul.

EXERCIŢII PROPUSE
1. Adăugaţi clasei complex o metodă care scade două numere complexe:
z1  z 2  x1  i  y1  x 2  i  y 2  x1  x 2  i  (y1  y 2 ).

2. Adăugaţi clasei complex o metodă care înmulţeşte două numere complexe.


z1  z 2  x1  i  y1 x 2  i  y 2   x1  x 2  y1y 2  i  (x1y 2  x2 y1 ).

3. Adăugaţi clasei complex o metodă care împarte două numere complexe.

z1 x  i  y1
 1
x  i  y1   x2  i  y 2   x1 x 2  y1y 2  ix2 y1  x1 y 2  .
 1
z2 x 2  i  y 2 x 2  i  y 2   x 2  i  y 2  x22  y 22

4. Se citesc z1 şi z2, numere complexe şi n, număr natural. Se cere, ca prin


utilizarea clasei complex, să se realizeze un program care calculează (z1/z2)n.

LECŢIA 8. Supraîncărcarea operatorilor prin funcţii


prietene
În lecţia precedentă, am învăţat faptul că operatorii pot fi supraîncărcaţi cu
metode ale clasei respective. În acest caz, dacă operatorul este binar, primul
operand este obiectul curent şi al doilea, cel transmis prin parametrul formal. Dacă
operatorul este unar, atunci singurul operand este parametrul formal.

 În C++, operatorii pot fi supraîncărcaţi cu funcţii prieten. În acest caz, dacă


operatorul este binar, funcţia care îl supraîncarcă are doi parametri formali,
iar dacă operatorul este unar, funcţia care îl supraîncarcă are un singur
parametru formal.

74
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

ÎNTREBARE: la ce foloseşte supraîncărcarea operatorilor prin funcţii prieten?


RĂSPUNS: anumite operaţii sunt comutative. De exemplu, dacă se doreşte să se
facă suma dintre un număr complex, z şi unul real, x. Suma se poate face fie ca
z+x, fie ca x+z. Dacă suma z+x se poate face prin supraîncărcarea operatorului
“+” printr-o metodă a clasei complex (primul parametru este obiectul curent),
suma x+z nu se mai poate face aşa. În schimb, dacă supraîncărcarea se face prin
funcţii prieten, atunci problema se poate rezolva foarte uşor: prin scrierea a două
funcţii care supraîncarcă operatorul “+”, fiecare cu doi parametri, unul de tip
complex şi altul de tip real, dar în care ordinea parametrilor este diferită.

Adăugăm clasei complex antetele funcţiilor prieten.

friend complex operator+(float a, complex z);


friend complex operator+(complex z, float a);

În afara clasei scriem funcţiile prieten:


complex operator+(complex z, float a)
{ z.x=z.x+a;
return z; }

complex operator+(float a, complex z)


{ z.x=z.x+a;
return z; }

ATENŢIE. Eliminaţi din cadrul clasei complex metoda realizată prin supraîncăr-
carea operatorului “+” pentru adunarea conţinutului obiectului curent cu un număr
real. Dacă nu faceţi acest lucru, vom avea ambiguităţi, în sensul că, de exemplu, la
expresia z+3 nu se poate decide ce să se apeleze: funcţia prieten sau metoda... În
aceste condiţii, se pot utiliza expresii precum:
complex z(1,1);
z=z+3;
z=6+z;

EXERCIŢII PROPUSE
1. Adăugaţi clasei complex funcţii prieten prin care se pot calcula expresii de
forma: a*z, z*a, a/z. z/a, unde z este un obiect al clasei complex, iar a, un
număr real.

2. Adăugaţi clasei lista funcţii prieten prin care se pot calcula expresii de
forma a+l, l+a, unde l este un obiect de tip lista, iar a, un număr natural. Prin
a+l se va obţine o listă liniară simplu înlănţuită formată din elementul care conţine
valoarea a, urmat de lista reţinută de l. Prin l+a se va obţine o listă liniară simplu
înlănţuită care se obţine din concatenarea listei reţinute de l şi elementul care
reţine valoarea a.

75
PARTEA A III-A

LECŢIA 9. Cuvântul cheie “this”

 Semnificaţia cuvântului cheie “this“ este de pointer către obiectul curent.


În linii mari, se poate spune că existenţa sa se justifică în cel puţin două
situaţii:

1. De multe ori, constructorii au unul sau mai mulţi parametri de acelaşi tip şi cu
acelaşi nume ca datele membru. În acest caz, pentru a se face diferenţa între
parametru şi data membru, se utilizează cuvântul cheie “this“. De exemplu, dacă
data membru este x, şi parametrul constructorului este tot x, atunci data membru
se adresează prin this->x.

2. Uneori se doreşte ca anumite metode să întoarcă fie obiectul curent, fie un


pointer către acesta, fie o referinţă către obiectul curent. În toate aceste cazuri,
valoarea de retur se obţine cu ajutorul operatorului “this“.

Exemplu. Să analizăm exemplul următor, care este exclusiv didactic:


class num
{ int x;
public:
num& inc();
num (int x);
void afis();
};
num::num(int x)
{ this->x=x; }
num& num::inc()
{ x++;
return *this;
}
void num::afis()
{ cout<<x<<endl; }
int main()
{ num a(3); [Link]();
[Link](); [Link]();
[Link]().inc(); [Link]();
}

Clasa num are o singură dată membru, x, care nu este publică. O primă
utilizare a lui “this“ o observăm în alcătuirea constructorului. Parametrul este tot
x, aşa că, pentru a accesa data membru, utilizăm expresia this->x. Metoda
membru inc() incrementează valoarea reţinută de data membru x şi returnează o
referinţă către obiectul curent, cu ajutorul căreia se poate, din nou, incrementa x.
De exemplu, dacă pentru obiectul a, x reţine 3, [Link](x) are ca efect faptul că x
reţine 4, dar este în acelaşi timp şi referinţă către x, căreia i se poate aplica din
nou inc() (vezi “[Link]().inc()“). Programul afişează 3, 4 şi 6.

76
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

EXERCIŢII PROPUSE
1. Înlocuiţi constructorul clasei complex cu unii pentru care numele parametrilor
de intrare coincide cu cel al datelor membru (x şi y).

2. Adăugaţi clasei complex o metodă care returnează o referinţă către obiectul


curent şi o alta care returnează un pointer către obiectul curent.

LECŢIA 10. Despre supraîncărcarea unor operatori unari

 Operatorii unari se pot supraîncărca cu metode ale clasei respective.


Parametrul este considerat obiectul curent şi cum este un singur parametru,
metoda nu va avea ataşat niciun parametru formal.

Exemplu. Fiind dat un obiect al clasei complex, z, care reţine numărul complex
x+iy, vom supraîncărca operatorul “!” pentru ca acesta să reţină conjugatul său,
adică x-iy. De asemenea, fiind dat un obiect al clasei complex, z1, care reţine
numărul complex x+iy, vom supraîncărca operatorul “-” (ştim că poate fi şi unar),
astfel încât acesta să reţină numărul -x-iy.

Pentru aceasta, adăugăm clasei complex antetele de mai jos:

complex operator!();
complex operator-();

Iată şi definiţia celor două metode:


complex complex::operator!()
{ y=-y;
return *this;
}

complex complex::operator-()
{ x=-x;
y=-y;
return *this;
}

Testaţi cele două metode cu ajutorul secvenţei următoare:


complex z(2,1),z1(-3,4);
!z;
-z1;
cout<<z.x<<" "<<z.y<<endl;
cout<<z1.x<<" "<<z1.y<<endl;

 Operatorii unari se pot supraîncărca cu funcţii prietene ale clasei respective.


De această dată, parametrul trebuie să figureze în antetul funcţiei.

77
PARTEA A III-A

Reluăm exemplul cu supraîncărcarea operatorilor “!“ şi “-“. Adăugaţi în


interiorul clasei cele două antete:
friend complex operator!(complex& z);
friend complex operator-(complex& z);

Iată şi definiţiile celor două funcţii:


complex operator!(complex& z)
{ z.y=-z.y;
return z;
}
complex operator-(complex& z)
{ z.x=-z.x;
z.y=-z.y;
return z;
}

Puteţi testa modul în care acestea se execută utilizând, de exemplu, aceeaşi


secvenţă ca în cazul supraîncărcării operatorilor unari cu metode ale clasei
complex.

EXERCIŢII PROPUSE
1. Adăugaţi clasei lista o metodă realizată prin supraîncărcarea operatorului
“-“, cu rolul ca fiecare element al listei liniare, care reţinea valoarea v, după rulare
să reţină -v.

2. La fel ca mai sus, numai că se cere ca problema să fie rezolvată prin utilizarea
unei funcţii prietene.
3. Adăugaţi clasei lista o metodă realizată prin supraîncărcarea operatorului
“-“, cu rolul de a inversa elementele din listă.

4. La fel ca mai sus, numai că se cere ca problema să fie rezolvată prin utilizarea
unei funcţii prietene.

LECŢIA 11. Supraîncărcarea operatorilor “++” şi “–-”

Operatorii “++“ şi “--“ sunt unari, pentru că acţionează asupra unui singur
operand. Pentru că un astfel de operator se poate afla, fie înainte, fie după
operand, operaţia de supraîncărcare a lui se particularizează.

 Atunci când operatorul este prefixat, se utilizează o metodă, fără parametri,


care-l supraîncarcă şi care acţionează doar asupra obiectului curent.

 Atunci când operatorul este postfixat, se utilizează, prin convenţie,


o metodă cu un singur parametru de tip int, care-l supraîncarcă şi care
acţionează, în fapt, doar asupra obiectului curent.

78
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

OBSERVAŢIE. Spre deosebire de modalitatea clasică de utilizare prefixată, în care


participarea la expresii se face cu valoarea incrementată, iar în cazul în care
operatorul este postfixat, participarea la expresie se face cu variabila
neincrementată, după care se realizează incrementarea, în cazul supraîncărcării
acestor operatori, întotdeauna se “incrementează” valoarea după care acestea
intră în calcul. Este normal să fie aşa, pentru că utilizăm termenii “incrementare”,
“decrementare” doar în sens simbolic. De exemplu, la un obiect de tip lista, prin
incrementare am putea înţelege adăugarea la începutul listei a unui element care
reţine 0.

Exemplu. Se scriu metodele care supraîncarcă operatorul “++“, pentru a


incrementa partea reală a unui număr complex:
complex complex::operator++()
{ x++; return *this; }

complex complex::operator++(int)
{ x++; return *this; }

În aceste condiţii, la executarea secvenţei de mai jos


complex z(2,1),z1,z2;
z1=++z;
z2=z++;
cout<<z.x<<" "<<z.y<<endl;
cout<<z1.x<<" "<<z1.y<<endl;
cout<<z2.x<<" "<<z2.y<<endl;

se afişează rezultatele:
4 1
3 1
4 1

 Operatorii “++“ şi “--“ se pot supraîncărca folosind şi funcţii prietene. Atunci


când operatorii sunt postfixaţi, primul parametru este obiectul, iar al doilea,
prin convenţie, este de tip int.

Exemplu. Pentru a obţine efectul supraîncărcării cu metode ale clasei (cea de mai
sus), în interiorul clasei se adaugă antetele:

friend complex operator++(complex& z, int);


friend complex operator++(complex& z);

Funcţiile sunt cele de mai jos. Puteţi testa secvenţa de la supraîncărcarea


operatorilor prin metode ale clasei. Efectele sunt aceleaşi.

complex operator++(complex& z, int )


{ z.x++; return z; }

complex operator++(complex& z)
{ z.x++; return z; }

79
PARTEA A III-A

OBSERVAŢII
1. Pentru ca partea reală reţinută de obiect să rămână incrementată, trebuie ca
obiectul să fie transmis prin referinţă.

2. În exemplul dat, supraîncărcarea se face fie prin metode ale obiectului, fie prin
funcţii prietene. Dacă se utilizează simultan ambele variante, vom avea erori
datorate ambiguităţilor (nu se ştie care metodă/funcţie trebuie aleasă).

EXERCIŢII PROPUSE
1. Adăugaţi clasei rational metode de incrementare şi decrementare a valorilor
reţinute de obiectele ei. Metodele vor supraîncărca operatorii ”++” şi ”--”.
2. La fel ca mai sus, numai că pentru incrementare se vor utiliza funcţii prietene.
3. Adăugaţi clasei lista mai multe metode realizate prin supraîncărcarea
operatorilor ”++” şi ”--” astfel: dacă l este un obiect de tip lista, atunci prin:
++l se adaugă la începutul listei un element care reţine 0;
l++ se adaugă la sfârşitul listei un element care reţine 0;
--l se elimină primul element al listei;
l-- se elimină ultimul element al listei.

LECŢIA 12. Supraîncărcarea operatorului “=”


Am învăţat faptul că, implicit, dacă se foloseşte operatorul ”=” pentru obiecte,
fără ca acesta să fie supraîncărcat, se realizează o copiere bit cu bit. De
asemenea, am învăţat faptul că nu în toate cazurile se poate utiliza cu succes
copierea bit cu bit. Exemplul ales a fost cu obiectele de tip lista, în care, prin
utilizarea implicită a operatorului ”=”, ambele obiecte reţineau adresa aceleiaşi liste
şi ... de aici, pot apărea o mulţime de neajunsuri. Pentru a le înlătura, în cazul
atribuirii vom utiliza o metodă rezultată din supraîncărcarea operatorului ”=”.

 Operatorul ”=” este binar. Dacă acesta este supraîncărcat prin utilizarea unei
metode membru, atunci primul operand este obiectul curent, iar al doilea
operand este parametrul metodei.
Exemplu. Considerăm clasa de mai jos, numită sir, memorată în fişierul text
[Link], în care fiecare obiect reţine un şir de caractere. Practic, data membru
adr reţine doar un pointer către şir, iar şirul a fost alocat cu new în zona de
memorie disponibilă. Clasa este înzestrată cu un constructor, care permite
declarări de forma sir s (”un exemplu de sir”);, un destructor, care
eliberează memoria de şirul alocat dinamic, o metodă care afişează şirul, afis()
şi o metodă de copiere realizată prin supraîncărcarea operatorului ”=”. Această
metodă are rolul ca la o atribuire de forma a=b, unde a şi b sunt obiecte de tip
sir, să elibereze memoria reţinută de şirul de caractere a cărui adresă o conţine
data membru adr a lui a, să aloce spaţiu pentru şirul reţinut de b şi să copieze

80
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

şirul reţinut de b în acest spaţiu. După această operaţie, şirul care era iniţial reţinut
numai de b este alocat în două locuri diferite în memoria disponibilă, iar pointerii
către cele două locuri sunt reţinuţi de adr a obiectului a şi adr a obiectului b. Să
observăm că metoda realizată prin supraîncărcarea operatorului ”=”, constructorul,
şi destructorul afişează câte un mesaj care ne permite ca, atunci când rulăm
programul, să ştim care dintre ele se execută:

#include <iostream>
using namespace std;

class sir
{ char *adr;
public:
~sir();
sir(char s[]);
void afis();
void operator=( sir& sirul);
};

sir::sir(char s[])
{ cout<<"Constructor"<<endl;
adr=new char[strlen(s)+1];
strcpy(adr,s);
}

sir::~sir()
{ cout<<"Destructor"<<endl;
delete [] adr;
adr=0;
}
void sir::afis()
{ cout<<adr<<endl;
}

void sir::operator=( sir& sirul)


{ cout<<"Operator="<<endl;
delete[] adr;
adr=new char[strlen([Link])+1];
strcpy(adr,[Link]);
}

În aceste condiţii, programul de mai jos este corect:


#include "[Link]"
int main()
{
sir s("un sir"),t("alt sir");
[Link]();
[Link]();
s=t;
t.~sir();
[Link]();
cout<<"Gata"<<endl;
}

81
PARTEA A III-A

Programul afişează:
Constructor
Constructor
un sir
alt sir
Operator=
Destructor
alt sir
Gata
Destructor
Destructor

Iniţial, au fost create două obiecte de tip sir, deci s-a afişat de două ori
mesajul constructor. Apoi au fost afişate cele două şiruri. La atribuirea s=t se
afişează Operator= şi de aici deducem că la această atribuire s-a apelat metoda
obţinută prin supraîncărcarea operatorului “=“. Apoi, se afişează Destructor
pentru că destructorul a fost apelat explicit pentru obiectul s. Apoi, se afişează şirul
reţinut de s (care iniţial era reţinut de t). De aici se deduce că atribuirea s-a
efectuat corect. Dacă n-ar fi aşa şi atribuirea s-ar fi efectuat fără metoda rezultată
prin supraîncărcarea operatorului “=“, iar prin apelul destructorului lui s, s-ar fi
pierdut şi şirul reţinut de t. Se afişează Gata şi, pentru că programul îşi încheie
executarea, se apelează, implicit, de două ori, constructorul pentru t şi pentru s.

EXERCIŢIU. Pentru obiecte de tip lista, creaţi o metodă care realizează atribuiri
de forma a=b, unde a şi b sunt de tip lista.

LECŢIA 13. Constructori de copiere


În această lecţie vom vedea că mai există o categorie de constructori şi
anume constructorii de copiere. Pentru a-i prezenta, utilizăm clasa descrisă în
lecţia anterioară: sir. Să considerăm următorul program:
#include "[Link]"
int main()
{ sir s("un sir"),t=s;
[Link](); [Link]();
t.~sir();
[Link]();
cout<<"Gata"<<endl;
}

Programul afişează la executare:


Constructor
un sir
un sir
Destructor
un rând liber
Destructor
Destructor

82
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

Să observăm că deşi am declarat două obiecte, t şi s, s-a apelat o singură


dată constructorul, pentru că s-a afişat o singură dată Constructor. Ce s-a
întâmplat la declararea lui t ca fiind copie a lui s: t=s? Nu s-a afişat
Constructor pentru t, dovadă că pentru t nu a fost apelat constructorul. Nu a
fost afişat nici Operator=, dovadă că nu a fost apelată nici metoda obţinută prin
supraîncărcarea operatorului “=“. În fapt, pentru obiectul t a fost rezervat, implicit,
spaţiu şi, tot implicit, s-a realizat o copiere bit cu bit a datelor din s în t. De aici
rezultă că după o astfel de declarare, atât s cât şi t reţin un pointer către acelaşi
şir de date. Să vedem ce urmează. Pentru t, se apelează explicit destructorul.
Aceasta înseamnă că şirul propriu-zis se şterge din memorie. Această afirmaţie
este adevărată pentru că, la tentativa de afişare a şirului pentru s, se afişează, de
fapt, un rând liber. În final, se afişează de două ori Destructor, atât pentru t cât
şi pentru s. Cu alte cuvinte, deşi la crearea lui t nu s-a apelat constructorul,
destructorul se apelează ...

Vă daţi seama că o astfel de anomalie poate avea consecinţe grave în


programare. Ce este de făcut? Răspuns: se creează un constructor de copiere.

 Un constructor de copiere se creează la fel ca unul obişnuit, doar că,


parametrul său este o referinţă către un obiect al clasei şi anume cel de
unde se efectuează copierea.

Înzestrăm clasa sir cu un constructor de copiere. Mai întâi, introducem în


interiorul clasei antetul:
sir(sir& sirul);

Adăugăm în afara clasei constructorul:

sir::sir(sir& sirul)
{
cout<<"Constructor de copiere"<<endl;
adr=new char[strlen([Link])+1];
strcpy(adr,[Link]);
}

După ce am modificat clasa, reluăm programul anterior. De această dată


se afişează ce vedeţi mai jos. Astfel, se demonstrează utilitatea constructorului
de copiere.

Constructor
Constructor de copiere
un sir
un sir
Destructor
un sir
Gata
Destructor
Destructor

83
PARTEA A III-A

EXERCIŢII PROPUSE
1. Adăugaţi clasei rational un constructor de copiere. Scrieţi un program care
utilizează constructorul de copiere.
2. Adăugaţi clasei lista un constructor de copiere. Scrieţi un program care
utilizează constructorul de copiere.
3. Are rost să înzestrăm clasa complex cu un constructor de copiere? Justificaţi
răspunsul ales!

LECŢIA 14. Supraîncărcarea operatorului “[ ]”

 Operatorul ”[]” este binar. De exemplu, dacă v este un vector, prin v[3] se
calculează expresia *(v+3). Operatorul ”[]” se poate supraîncărca printr-o
metodă a clasei. Primul operand este obiectul curent, iar al doilea operand
este parametrul transmis metodei.
Exemplu. Se cere să se scrie o metodă a clasei lista, realizată prin
supraîncărcarea operatorului ”[]”, astfel încât, dacă l este un obiect al clasei lista,
prin l[0] se înţelege valoarea numerică reţinută de primul element al listei, l[1]
este valoarea numerică reţinută de al doilea element al listei, ş.a.m.d. De
asemenea, se cere ca prin utilizarea aceleiaşi metode să se poată face atribuiri de
forma l[i]=k. În toate cazurile se presupune că, atunci când se adresează
valoarea reţinută de un element de indice i al listei, lista are cel puţin i+1
elemente.

Rezolvare. La adresarea l[i] se adresează, de fapt, elementul i+1 al listei. Prin


urmare, avem nevoie ca lista să fie parcursă până la acel element. Deoarece
valoarea returnată va fi valoarea reţinută de acel element, deducem că valoarea
returnată trebuie să fie de tip int sau int&. Faptul că trebuie să se facă atribuiri
de forma l[i]=k, rezultă în mod clar că funcţia trebuie să întoarcă o referinţă
către valoarea de tip int memorată de acel element, adică trebuie să aibă tipul
int&. Mai jos, vedeţi antetul care necesită a fi adăugat în class şi apoi metoda
rezultată prin supraîncărcarea operatorului ”[]”:

int& operator[](int k);

int& lista::operator[](int k)
{ nod* p=vf;
int i=0;
while (i!=k) { p=p->adr_urm; i++; }
return p->valoare;
}

În aceste condiţii, puteţi testa programul următor, dar nu uitaţi că lista este
creată în ordinea inversă introducerii elementelor.

84
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

#include "[Link]"
using namespace std;
int main()
{ lista l;
[Link](1); [Link](6);
[Link](3); [Link](12);
cout<<l[1]<<endl;
l[1]=9;
[Link]();
}

Iată cum, prin supraîncărcarea acestui operator, o listă poate fi privită ca un


vector. Reţineţi acest aspect, este semnificativ!

EXERCIŢIU. Adăugaţi clasei sir (vezi Lecţia 11) o metodă realizată prin supra-
încărcarea operatorului ”[]”, prin care, dacă s este un obiect sir, atunci prin
s[i] se returnează caracterul de indice i al şirului. De asemenea, se cere să fie
permise atribuiri de forma s[i]=c, unde c este un caracter.

LECŢIA 15. Supraîncărcarea operatorului “( )”


 Operatorul ”()” este numit şi operator de apel funcţie şi este binar.
Utilizarea clasică a lui este de forma expresie1(expresie2). De
exemplu, pentru un apel de funcţie precum suma(a,b), expresie1 este
suma şi după cum ştim, numele funcţiei este un pointer către funcţie, iar
expresie2 este (a,b). Datorită faptului că apelul unei funcţii poate avea
oricât de mulţi parametrii, rezultă că operatorul poate avea oricât de mulţi
parametri şi acest aspect este esenţial.

EXEMPLU DE UTILIZARE. Clasa matrice reţine un masiv tridimensional. Funcţia


care supraîncarcă operatorul ”()” va accesa orice element al masivului
tridimensional, aşa cum se vede mai jos. Întrucât se returnează o referinţă,
accesarea unui element al masivului se poate face cu uşurinţă, aşa cum suntem
obişnuiţi de la matematică.
#include <iostream>
using namespace std;
class matrice
{ int v[10][5][8];
public:
int& operator()(int i, int j, int k);
};
int& matrice::operator()(int i, int j, int k)
{ return v[i][j][k]; }
int main()
{ matrice a;
a(3,2,6)=10;
cout<<a(3,2,6);
}

85
PARTEA A III-A

OBSERVAŢIE. În practica programării, operatorul ”()” se supraîncarcă pentru a


accesa elementele unei structuri, care poate fi oricât de complexă. De asemenea,
o astfel de supraîncărcare este utilizată în STL (vezi ultima parte a acestei cărţi).

 Funcţii obiect (functori). Să observăm faptul că apelul unei metode


rezultate prin supraîncărcarea operatorului ”()” seamănă mult cu apelul unei
funcţii. Din acest motiv, un obiect al unei clase care conţine o astfel de
metodă se mai numeşte şi funcţie obiect sau, pe scurt, functor.

EXERCIŢII PROPUSE
1. Scrieţi o metodă care supraîncarcă operatorul ”()” şi care, dacă l este un obiect
al clasei lista, l(i) reprezintă o referinţă către elementul de indice i (la fel ca la
exemplul dat în lecţia anterioară referitor la supraîncărcarea operatorului ”[]”).

2. Lucrare complexă. Este cunoscut faptul că matricele ocupă mult spaţiu în


memorie. Uneori, spaţiul ocupat de o matrice poate fi micşorat. O astfel de situaţie
este aceea în care matricea are majoritatea elementelor nule, caz în care se
numeşte matrice rară. O matrice rară va fi memorată cu ajutorul a două liste
liniare simplu înlănţuite, una conţinând valorile nenule, alta numărul de ordine al
lor, număr rezultat prin parcurgerea pe linii a matricei.

Exemplu. Matricea următoare se memorează aşa cum se vede:

1.2 0 0 0  1.2 5 3
 
A   0 5 0 0
 0 0 0 3
  1 6 12

Scrieţi o clasă care ne ajută să lucrăm cu matrice rare.

LECŢIA 16. Membri statici ai unei clase


Până în prezent am învăţat că o clasă conţine date şi metode. Fiecare obiect
al clasei respective conţine toate datele şi toate metodele. Aşa să fie? În realitate,
membrii unei clase pot fi statici.

 Prin membru static înţelegem o dată a clasei sau o metodă a ei care nu se


regăseşte în fiecare obiect al clasei, ci este memorat o singură dată.
Membrul static este accesibil oricărui obiect al clasei. Evident, pe lângă
membrii statici, clasele pot conţine şi membri nestatici.
Raţiunea existenţei membrilor statici este economia de memorie, ei fiind
memoraţi o singură dată.

86
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

A) Date statice:
- definiţia lor este în interiorul clasei;
- definiţia unei date statice începe cu cuvântul cheie static;
- în absenţa modificatorului const, nu este permis ca definiţia unei date
statice să conţină şi iniţializarea ei;
- dacă este publică, data statică poate fi accesată din exteriorul clasei în
forma clasa::nume; în acest caz, ea poate fi iniţializată;
- data membru statică poate fi accesată în mod obişnuit de orice metodă
statică sau nestatică a clasei respective.

B) Metode statice:
- definiţia lor este în interiorul clasei;
- declararea unei metode statice (antetul ei, din interiorul clasei) începe cu
cuvântul cheie static;
- definiţia metodei statice (din exteriorul clasei) nu conţine cuvântul cheie
static;
- dacă este publică, metoda statică poate fi accesată din exteriorul clasei în
forma clasa::nume_functie(). De asemenea, o metodă statică poate fi
accesată pornind de la obiect, aşa cum se apelează metodele nestatice.

Exemple:

 Programul următor afişează de 3 ori 1. Clasa T conţine o dată statică şi o


metodă statică.
#include <iostream>
using namespace std;
class T
{ public:
static int v; // declararea datei statice
static void f(); // declararea metodei statice
};

int T::v = 1; // initializarea datei statice


// se poate face si asa
void T::f()
{ cout<<v<<endl; } // definitia metodei statice

int main()
{ cout<<T::v<<endl; // accesare data pornind de la
// numele clasei
T::f();// accesare metoda pornind de la numele clasei
T ob; ob.f(); // accesare metoda pornind de la un obiect
// al clasei
}

87
PARTEA A III-A

 Tot aşa, se creează şi se iniţializează date membru de tip şir de caractere:


class P
{ public:
static char sir[10];
};
char P::sir[10]="mama merge";
int main()
{ cout<<P::sir;
P o;
cout<<[Link];
}

OBSERVAŢIE. În programarea orientată pe obiecte, datele statice joacă un rol


asemănător cu cel al variabilelor globale în programarea clasică.

 Pentru a iniţializa o dată statică, se foloseşte modificatorul const.

Exemplu:
static const int cns=99;

EXERCIŢIU. Scrieţi o secvenţă prin care un constructor iniţializează o dată statică.

LECŢIA 17. Tratarea excepţiilor prin utilizarea claselor


 Mecanismul general de tratare a excepţiilor constă în crearea unei (unor)
clase specializate de tratare a erorilor. De regulă, constructorul clasei
primeşte un parametru prin care se identifică eroarea respectivă, iar clasa
conţine una sau mai multe metode de tratare a erorilor. Valoarea primită ca
parametru este memorată de obiectul clasei respective. Constructorul este
apelat prin thorow. În catch se primeşte obiectul respectiv şi, pentru
tratarea erorilor, este apelată o metodă a clasei scrisă special în acest scop.

Exemplu. Programul citeşte o valoare care trebuie să fie un întreg între 1 şi 5.


Dacă valoarea introdusă nu este de tip întreg (de exemplu, începe cu un caracter
alfabetic) sau dacă valoarea este numerică, dar nu este între 1 şi 5, citirea se
reia, până când condiţiile cerute sunt îndeplinite. Pentru fiecare eroare se afişează
un mesaj corespunzător.
#include <iostream>
using namespace std;
class eroare
{ int tip;
public:
eroare(int tip);
void tratare();
};

88
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

eroare::eroare(int tip)
{ this->tip=tip; }
void eroare::tratare()
{ if (tip==1) cout<<"data introdusa nu respecta formatul"<<endl;
else
if (tip==2)cout<<"x<1"<<endl;
else cout<<"x>5"<<endl;
}
int main()
{ char x[20],nr,semnal;
do
{ semnal=1;
try
{ cin>>x;
nr=atoi(x);
if (nr==0 && x[0]!='0') throw eroare(1);
else
if(nr<1) throw eroare(2);
else if (nr>5) throw eroare (3);
}
catch(eroare er)
{ semnal=0;
[Link]();
}
}while (semnal==0);
cout<<x<<endl;
}

EXERCIŢIU. Se citeşte un şir de caractere care trebuie să conţină numai literele


alfabetului, să înceapă cu literă mare şi, în rest, să conţină numai litere mici. Se
cere ca validarea datei de intrare să se facă prin utilizarea mecanismului excepţiilor
şi, în acest scop, se va scrie o clasă specializată.

LECŢIA 18. Clase derivate


 Fiind dată o clasă C, este posibil să obţinem o altă clasă C1, care să preia
automat toate datele şi toate metodele clasei C, dar căreia să-i putem
adăuga noi date şi noi metode? Răspunsul este afirmativ.

Terminologie: în această situaţie, se spune că:


a) Clasa C1 s-a obţinut prin derivarea clasei C;
b) Clasa C a fost derivată şi s-a obţinut clasa C1;
c) Clasa C1 moşteneşte clasa C;
d) Faptul că C1 moşteneşte pe C2 se notează ca mai jos:

C1 C

89
PARTEA A III-A

OBSERVAŢIE. Prin acest procedeu, se poate prelua software deja implementat


pentru o dezvoltare ulterioară. Avantajul uriaş al procedeului este dat de faptul că
persoana care preia un anumit software trebuie să cunoască doar documentaţia
de utilizare a acestuia.

 Forma simplificată prin care o clasă se obţine din derivarea altei clase este
prezentată mai jos, dar specificăm că problema va fi reluată pentru a explica
necesitatea folosirii modificatorului public. Mai jos, semnificaţia lui C este
de clasă care se derivează, iar a lui C1, de clasă care s-a obţinut în urma
derivării:
class C1:public C {...}
Exemplu. Clasa numar conţine o dată membru n şi o metodă membru tip().
Clasa numar_d este derivată din clasa numar. Prin urmare, un obiect obţinut prin
instanţierea acesteia are data membru n şi metoda tip(), prin care se afişează
valoarea lui n. Pe lângă acestea, clasa numar_d are o nouă metodă, numită
cit(), prin care se poate citi valoarea lui n. În program, se testează faptul că un
obiect al clasei numar_d conţine pe lângă propria metodă, data membru şi metoda
clasei numar.
#include <iostream>
using namespace std;
class numar
{ public:
int n;
void tip()
{ cout<<n<<endl;
}
};
class numar_d : public numar
{ public:
void cit()
{ cout<<"n=";
cin>>n; }
};
int main()
{ numar_d a;
[Link]();
[Link]();
}

 Fie clasele C şi C1, unde C1 s-a obţinut prin derivarea clasei C. Fie v o
variabilă de tip C şi v1 o variabilă de tip C1. Atunci:

a) atribuirea v=v1 este permisă;


b) atribuirea v1=v nu este permisă.

Motivul? Se presupune că o clasă derivată are date şi metode în plus. Ce valori ar


primi acestea în cazul unei astfel de atribuiri?

90
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

Exemple - pentru clasele definite în această lecţie numar şi numar_d:

 În urma executării secvenţei de mai jos, se afişează 7 şi 8:


numar v;
v.n=7;
[Link]();
numar_d v1;
v1.n=8;
v=v1;
[Link]();

 Secvenţa de mai jos va da eroare de compilare:


numar v;
v.n=7;
[Link]();
numar_d v1;
v1.n=8;
v1=v; // incorect

 Fie clasele C şi C1, unde C1 s-a obţinut prin derivarea clasei C. Dacă clasei
C1 i se adaugă o dată membru şi/sau o metodă membru cu una existentă în
C, atunci nu se creează o situaţie de ambiguitate. În acest caz, pornind de
la un obiect al clasei C1, se pot adresa atât datele/metodele clasei C1, cât şi
datele/metodele clasei C. Pentru ultima situaţie este necesară utilizarea
operatorului de rezoluţie, aşa cum rezultă din exemplul următor.
Exemplu. Clasa numar are data membru n şi metoda tip(). Clasa numar1 se
obţine prin derivarea clasei numar şi i se adaugă o dată membru numită tot n şi o
metodă numită tot tip().
#include <iostream>
using namespace std;
class numar
{ public:
int n;
void tip()
{ cout<<"numar "<<n<<endl; }
};
class numar1 : public numar
{ public:
int n;
void tip()
{ cout<<"numar1 "<<n<<endl; }
};
int main()
{ numar1 v1;
v1.n=3;
[Link](); // se afiseaza numar1 3
[Link]::n=2;
[Link]::tip(); // se afiseaza numar 2
}

91
PARTEA A III-A

 În C++, există posibilitatea ca o clasă să rezulte în urma derivării mai


multor clase (să moştenească mai multe clase). Vom spune că avem un
caz de moştenire multiplă. Noua clasă va conţine datele şi metodele
tuturor claselor pe care le moşteneşte. Fie clasa C care moşteneşte clasele
C1, C2, …, Cn. În acest caz, declaraţia este:
class C:public C1,C2,...Cn {...}

Grafic, moştenirea multiplă se reprezintă precum în schema de mai jos:

C1 C2 C2 Cn

 În situaţia în care unele dintre clasele C1, C2, …, Cn au date sau metode
cu acelaşi nume, la apelul lor se va utiliza operatorul de rezoluţie, aşa cum
rezultă din exemplul următor.

Exemplu. Atât clasa numar, cât şi clasa numar1 au data membru n comună şi
metoda membru tip() comună. Clasa numar2, moşteneşte atât clasa numar, cât
şi clasa numar1, dar nu i s-au mai adăugat date membru sau metode. Un obiect al
clasei numar2 poate apela datele membru şi metodele clasei numar şi ale clasei
numar1.
#include <iostream>
using namespace std;
class numar
{ public:
int n;
void tip()
{ cout<<"numar "<<n<<endl; }
};
class numar1
{ public:
int n;
void tip()
{ cout<<"numar1 "<<n<<endl; }
};
class numar2: public numar,public numar1
{};
int main()
{ numar2 v;
[Link]::n=1; v.numar1::n=2; // apel pentru membrii lui numar
[Link]::tip(); v.numar1::tip();// apel pentru membrii lui numar1
}

92
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

OBSERVAŢII
1. Utilizând mecanismul de moştenire, se pot crea ierarhii de clase. Aspectul
grafic al unei ierarhii de clase este acela al unui graf orientat.

2. Nu toate limbajele care permit utilizarea mecanismului de moştenire (derivare)


permit şi moştenirea multiplă. De exemplu, în Java este permisă numai
moştenirea simplă, adică o clasă nu poate moşteni decât o singură clasă, nu mai
multe. În acest caz, ierarhia claselor se prezintă sub forma unui arbore.

EXERCIŢII PROPUSE
Notă: în realizarea claselor de mai jos, nu se vor utiliza constructori/destructori.

1. Creaţi o clasă numită Matrice. Pentru fiecare matrice se vor reţine numărul de
linii şi de coloane. Clasa va fi înzestrată cu metode de citire şi afişare a matricelor,
iar matricele vor fi alocate dinamic.
2. Creaţi o clasă numită MatriceOp. Clasa se obţine din derivarea clasei
Matrice şi va conţine în plus metode care permit adunarea, scăderea şi
înmulţirea a două matrice.
Atenţie: în cazul în care două matrice, din cauza numărului de linii şi coloane pe
care le au, nu se pot aduna, scădea sau înmulţi, se va utiliza mecanismul
excepţiilor.
3. Creaţi clasa MatriceBin, care memorează numai matrice binare (elementele
sunt numai 0 şi 1). Clasa rezultă din derivarea clasei MatriceOP, dar va utiliza o
altă metodă, cu acelaşi nume, pentru citirea matricei.
4. Creaţi o clasă numită GrafOrientat rezultată prin derivarea clasei
MatriceBin. Clasa va memora numai matrice binare pătratice care au numai 0
pe diagonala principală. De asemenea, se va considera matricea memorată ca
fiind matricea de adiacenţă a unui graf neorientat.
Clasa va conţine, în plus, metode pentru afişarea numărului de componente
conexe, pentru parcurgerea BFS sau DFS a unei componente pornind de la un
nod al ei. Toate nodurile parcurse vor fi afişate pe ecran.
5. Creaţi o clasă numită GrafNeOrientat rezultată prin derivarea clasei
GrafOrientat. Un obiect al acestei clase va memora o matrice binară pătratică
care are numai 0 pe diagonala principală şi care este simetrică faţă de diagonala
principală.

93
PARTEA A III-A

LECŢIA 19. Accesul la membrii unei clase

Membrii unei clase (date şi metode) sunt de 3 feluri:

1. Publici - precedaţi de cuvântul cheie ”public”. Aceştia pot fi accesaţi atât din
interior cât şi din exteriorul clasei.

2. Protejaţi - precedaţi de cuvântul cheie “protected”. Până în prezent, aceştia


nu au fost explicaţi. Rolul lor apare în momentul în care o clasă este derivată.
Ei nu pot fi accesaţi din exterior (întocmai ca membrii privaţi), dar pot fi accesaţi
din clasele derivate.

3. Privaţi - precedaţi de cuvântul cheie ”private”. Să nu uităm că, implicit,


membrii unei clase sunt privaţi. Membrii privaţi nu pot fi accesaţi decât din
interiorul clasei respective.

Există 3 modalităţi prin care o clasă poate fi moştenită (derivată):

1. Precedată de cuvântul cheie ”public”:

class nume_nou :public nume


{
...
};

- Membrii publici ai clasei de bază rămân publici pentru clasa derivată. Aceasta
înseamnă că pot fi accesaţi atât din exteriorul clasei, cât şi din interiorul ei.

- Membrii protejaţi ai clasei de bază rămân protejaţi pentru clasa derivată.


Aceasta înseamnă că pot fi accesaţi din interiorul clasei derivate, dar nu pot fi
accesaţi din exteriorul ei.

- Membrii privaţi ai clasei de bază nu pot fi accesaţi nici din interiorul clasei
derivate şi nici din exteriorul ei.

2. Precedată de cuvântul cheie ”protected”:

class numar_nou :protected numar


{
...
};

În această situaţie, membrii publici şi cei protejaţi ai clasei de bază sunt protejaţi în
clasa de bază. Aceasta înseamnă că ei pot fi accesaţi din interiorul clasei derivate,
dar nu pot fi accesaţi din exteriorul ei. În acest timp, membrii privaţi ai clasei de
bază sunt inaccesibili atât din exteriorul clasei derivate, cât şi din interiorul ei.

94
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

3. Precedată de cuvântul cheie ”private”:


class numar_d :private numar
{
...
};

Membrii publici şi protejaţi ai clasei de bază devin membrii privaţi în noua clasă. În
această situaţie, comportamentul clasei este asemănător cu cel al clasei moştenite
prin protected. Adică, membrii publici şi protejaţi ai clasei de bază pot fi accesaţi
din interiorul clasei derivate, dar nu pot fi accesaţi din exteriorul ei. Diferenţa faţă
de cazul anterior, atunci când clasa a fost moştenită utilizând protected, este că
la o eventuală derivare a ei, în noua clasă aceşti membri nu mai pot fi accesaţi în
niciun fel.

 Nu este obligatoriu să reţineţi aceste noţiuni. Ele au fost introduse din


dorinţa de a vă pune la dispoziţie o documentaţie.

Dăm forma generală prin care se obţine o clasă derivată care are la bază n clase:
class nume : [public sau protected sau private] nume1,
: [public sau protected sau private] nume2,
...
: [public sau protected sau private] numen

{
...
}

EXERCIŢII PROPUSE
În testele care urmează, vom considera clasa următoare:
class numar
{ public:
const static int m=1;
protected:
const static int n=2;
private:
const static int p=3;
};

1. Fie programul:
class numar1:public numar
{ public:
void tip();
};
void numar1::tip()
{ cout<<"...";
}
int main()
{ numar1 ob;
... }

95
PARTEA A III-A

Care dintre instrucţiunile de mai jos sunt corecte (C) şi care sunt incorecte (I)?

a) cout<<ob.m;, introdusă în main(), în locul celor trei puncte;


b) cout<<ob.n;, introdusă în main(), în locul celor trei puncte;
c) cout<<ob.p;, introdusă în main(), în locul celor trei puncte;
d) înlocuim şirul din definiţia metodei tip() cu m şi instrucţiunea introdusă în
main(), în locul celor trei puncte, va fi [Link]();;
e) înlocuim şirul din definiţia metodei tip() cu n şi instrucţiunea introdusă în
main(), în locul celor trei puncte, este [Link]();;
f) înlocuim şirul din definiţia metodei tip() cu p şi instrucţiunea introdusă în
main(), în locul celor trei puncte, este [Link]();.

2. Fie programul:
class numar1:protected numar //clasa de la ex. 1
{ public:
void tip();
};

void numar1::tip()
{ cout<<"...";
}
int main()
{ numar1 ob;
... }

Care dintre instrucţiunile de mai jos sunt corecte (C) şi care sunt incorecte (I)?

a) cout<<ob.m;, introdusă în main(), în locul celor trei puncte;


b) cout<<ob.n;, introdusă în main(), în locul celor trei puncte;
c) cout<<ob.p;, introdusă în main(), în locul celor trei puncte;
d) înlocuim şirul din definiţia metodei tip() cu m şi instrucţiunea introdusă în
main(), în locul celor trei puncte, va fi [Link]();;
e) înlocuim şirul din definiţia metodei tip() cu n şi instrucţiunea introdusă în
main(), în locul celor trei puncte, este [Link]();;
f) înlocuim şirul din definiţia metodei tip() cu p şi instrucţiunea introdusă în
main(), în locul celor trei puncte, este [Link]();.

3. Fie programul:
class numar1:private numar //clasa de la ex. 1
{ public:
void tip();
};
void numar1::tip()
{ cout<<"...";
}
int main()
{ numar1 ob;
... }

96
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

Care dintre instrucţiunile de mai jos sunt corecte (C) şi care sunt incorecte (I)?
a) cout<<ob.m;, introdusă în main(), în locul celor trei puncte;
b) cout<<ob.n;, introdusă în main(), în locul celor trei puncte;
c) cout<<ob.p;, introdusă în main(), în locul celor trei puncte;
d) înlocuim şirul din definiţia metodei tip() cu m şi instrucţiunea introdusă în
main(), în locul celor trei puncte, va fi [Link]();;
e) înlocuim şirul din definiţia metodei tip() cu n şi instrucţiunea introdusă în
main(), în locul celor trei puncte, este [Link]();;
f) înlocuim şirul din definiţia metodei tip() cu p şi instrucţiunea introdusă în
main(), în locul celor trei puncte, este [Link]();.

RĂSPUNSURI:
1. a) C; b) I; c) I; d) C; e) C; f) I.
2. a) I; b) I; c) I; d) C; e) C; f) I.
3. a) I; b) I; c) I; d) C; e) C; f) I.

LECŢIA 20. Comportamentul constructorilor şi


desctructorilor în cazul derivării claselor
Pentru o clasă, programatorul poate să scrie sau nu unul sau mai mulţi
constructori. Chiar şi în cazul în care el nu a scris niciun constructor, clasa va
conţine unul, generat automat, numit constructor general implicit (constructor
implicit). Prin urmare, orice clasă are cel puţin un constructor. Problema care se
pune în continuare este: ce se întâmplă cu constructorii în cazul în care o clasă
este derivată?

 Regula generală este: la instanţierea unui obiect al clasei derivate, mai întâi,
automat, se apelează constructorul clasei de bază, apoi este apelat
constructorul clasei moştenitoare. Motivaţia? Teoretic, un obiect al clasei
moştenitoare conţine faţă de datele clasei de bază, date în plus. Prin
urmare, mai întâi se completează datele clasei de bază, apoi datele clasei
moştenitoare.
Astfel, pot apărea situaţiile:
1. Clasa de bază are numai constructorul generat implicit şi clasa moştenitoare
are numai constructorul generat implicit. Această situaţie este singura pe care am
întâlnit-o în lecţiile anterioare. Practic, la instanţierea unui obiect al clasei
moştenitoare, mai întâi se rulează constructorul generat implicit al clasei de bază,
apoi constructorul generat implicit al clasei moştenitoare.

2. Clasa de bază are numai constructorul generat implicit sau un constructor


fără parametri. În această situaţie, la instanţierea unui obiect al clasei
moştenitoare, cu oricare din constructorii pe care îi are, mai întâi se rulează, dacă
există, constructorul fără parametri al clasei de bază, apoi constructorul clasei
moştenitoare, iar dacă acesta nu există, se rulează mai întâi constructorul generat
implicit al clasei de bază, apoi constructorul clasei moştenitoare.

97
PARTEA A III-A

EXEMPLE
 Pentru clasa de bază s-a scris un constructor fără parametri, iar pentru clasa
moştenitoare nu s-a scris niciun constructor. La instanţierea unui obiect al
clasei derivate, se apelează, automat, constructorul fără parametri al clasei
de bază. Prin urmare, programul afişează: baza.
class baza
{ public:
baza();
};

baza::baza()
{ cout<<"baza "<<endl; }

class mosten :public baza


{ };

int main()
{ mosten a; }

 Atât clasa de bază, cât şi clasa care o moşteneşte conţin câte un


constructor fără parametri. La apelul constructorului clasei moştenitoare se
apelează, mai întâi, constructorul clasei de bază. Prin urmare, se afişează
baza şi mosten.
class baza
{ public:
baza();
};

baza::baza()
{ cout<<"baza "<<endl; }

class mosten :public baza


{ public:
mosten();
};

mosten::mosten()
{ cout<<"mosten"; }

int main()
{ mosten a; }

 Clasa de bază are un constructor fără parametri, iar clasa moştenitoare are
un constructor cu un parametru. Mai întâi se rulează constructorul fără
parametri al clasei de bază, apoi constructorul cu un parametru al clasei
moştenitoare. Prin urmare, se afişează baza şi mosten.
class baza
{ public:
baza();
};

98
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

baza::baza()
{ cout<<"baza "<<endl; }

class mosten :public baza


{ public:
mosten( int a);
};

mosten::mosten(int a)
{ cout<<"mosten"; }

int main()
{ mosten a(1); }

3. Clasa de bază are mai mulţi constructori, dintre care unul fără parametri şi cel
puţin unul cu parametri. În acest caz, apare o problemă: la instanţierea unui obiect
al clasei moştenitoare, care constructor al clasei de bază va fi apelat? Implicit, va fi
apelat constructorul fără parametri. Dacă programatorul doreşte să fie apelat alt
constructor, atunci acesta trebuie să fie trecut în definiţia constructorului clasei
moştenitoare sub forma:
nume_contructor(...):nume_constructor_baza(..)

Exemple

 Clasa de bază are doi constructori, unul fără parametri şi altul cu un


parametru. Clasa moştenitoare are un singur constructor, cu un parametru.
La instanţierea unui obiect al clasei moştenitoare, mai întâi, se apelează,
implicit, constructorul fără parametri al clasei de bază. Programul afişează
baza mosten.

class baza
{ public:
baza();
baza(int a);
};

baza::baza(int a)
{ cout<<"baza "<<a<<endl; }

baza::baza()
{ cout<<"baza "<<endl; }

class mosten :public baza


{ public:
mosten(int a);
};

mosten::mosten (int a)
{ cout<<"mosten"; }

int main()
{ mosten x(1);
}

99
PARTEA A III-A

 Constructorul clasei moştenitoare apelează explicit constructorul cu un


parametru al clasei de bază. Se va afişa baza 1 mosten.
class baza
{ public:
baza();
baza(int a);
};
baza::baza(int a)
{ cout<<"baza "<<a<<endl; }
baza::baza()
{ cout<<"baza "<<endl; }
class mosten :public baza
{ public:
mosten(int a);
};
mosten::mosten (int a):baza(a)
{ cout<<"mosten"; }
int main()
{ mosten x(1); }

 În ce priveşte destructorii, implicit, întâi este apelat destructorul clasei


moştenitoare, apoi este apelat destructorul clasei de bază. Practic, datele se
distrug în ordinea inversă în care au fost create.

EXERCIŢII PROPUSE
1. Fie clasele:
class baza
{ public:
baza();
~baza();
};
baza::baza()
{ cout<<3; }
baza::~baza()
{ cout<<2; }
class mosten :public baza
{ public:
mosten();
~mosten();
};
mosten::mosten ()
{ cout<<"1"; }
mosten::~mosten ()
{ cout<<"4"; }

Ce se se afişează la declaraţia: mosten x; ?


a) 1234; b) 3124; c) 3142; d) 1324.

100
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

2. Fie clasele:

class C
{ public:
C();
C(int a);
C(int a,int b);
};
C::C()
{ cout<<3; }
C::C(int a)
{ cout<<2; }
C::C(int a, int b)
{ cout<<5; }

class D :public C
{ public:
D();
D(int a);
D(int a, int b);
};
D::D ()
{ cout<<"1"; }
D::D (int a)
{ cout<<"4"; }
D::D (int a, int b)
{ cout<<"6"; }

2.1 Ce se afişează la declaraţia D ob;?


2.2 Ce se afişează la declaraţia D ob(7);?
2.3 Ce se afişează la declaraţia D ob(1,1);?
2.4 Modificaţi o singură linie, astfel încât la declaraţia D ob(1,1); să se afişeze 26.
2.5 Modificaţi o singură linie, astfel încât la declaraţia D ob(1); să se afişeze 54.
2.6 Modificaţi o singură linie, astfel încât la declaraţia D ob; să se afişeze 21.

RĂSPUNSURI
1.c).

2. 2.1 31; 2.2 34; 2.3 36;


2.4 D::D (int a, int b)  D::D (int a, int b):C(a)
2.5 D::D (int a)  D::D (int a):C(a,1)
2.6 D::D ()  D::D ():C(1)

101
PARTEA A III-A

LECŢIA 21. Pointeri către obiecte

 În C++, obiectele se pot aloca dinamic cu ajutorul operatorului new şi


memoria ocupată de ele poate fi eliberată cu operatorul delete. Dacă C
este o clasă, o variabilă care poate reţine un pointer la obiect este de tipul
C*. Pornind de la un pointer către obiect, datele membru şi metodele
obiectului se pot accesa cu ajutorul operatorului “->”.

Exemplu. Fie C o clasă şi p o variabilă care reţine un pointer către un obiect al


clasei C, alocat dinamic. Pornind de la p, se accesează metoda afis() a
obiectului. Testaţi!
#include <iostream>
using namespace std;
class C
{ public:
void afis();
};
void C::afis()
{ cout<<"Eu sunt metoda afis() din clasa C"; }
int main()
{ C* p=new C;
p->afis();
}

Am învăţat faptul că dacă C este o clasă şi C1 este derivata ei, atunci o


variabilă de tip C, poate reţine un obiect al clasei C, dar şi un obiect al clasei C1. În
schimb, o variabilă de tip C1 nu poate reţine un obiect al clasei C. Ce se întâmplă
în cazul pointer-ilor?

 Dacă C este o clasă şi C1 este derivata ei şi dacă p este o variabilă de tipul


C*, atunci p poate reţine un pointer către un obiect al clasei C, dar şi un
pointer către un obiect al clasei C1. În schimb, dacă p1 este de tip C1*,
atunci p1 poate reţine un pointer către un obiect al clasei C1, dar nu poate
reţine un pointer către un obiect al clasei C.

Exemplu. Analizaţi programul de mai jos:


#include <iostream>
using namespace std;
class C
{ public:
void afis();
};
void C::afis()
{ cout<<"Eu sunt metoda afis() din clasa C"; }

102
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

class C1 :public C
{ public:
void afis();
};

void C1::afis()
{ cout<<"Eu sunt metoda afis() din clasa C1"; }

int main()
{ C* p=new C1; p->afis(); // ok
C1* p1=new C;// eroare
}

Observaţi faptul că, atât clasa de bază, cât şi clasa derivată, conţin metoda
afis(). În ambele clase, metoda are acelaşi tip (void) şi acelaşi număr de
parametri (0).

 Întrebarea este: în cazul apelului metodei pornind de la un pointer către


clasă, reţinut de variabila p, care metodă afis() este apelată? Cea din C,
sau cea din C1? Se disting 4 cazuri care sunt exemplificate pe clasele de
mai sus:

1) p este de tip C*, pointer-ul este către obiecte de tipul C. Se apelează


metoda clasei C.
Exemplu:
C* p=new C;
p->afis();

2) p este de tip C1*, pointer-ul este către obiecte de tipul C1. Se apelează
metoda clasei C1.
Exemplu:
C1* p=new C1;
p->afis();

3) p este de tip C*, pointer-ul este către obiecte de tipul C1. Se apelează
metoda clasei C.
Exemplu:
C1* p=new C1;
p->afis();

4) p este de tip C1*, pointer-ul este către obiecte de tipul C. Aşa cum s-a
arătat, se semnalează eroare!

DEFINIŢIE. Prin polimorfism se înţelege posibilitatea ca atât clasa de bază, cât şi


cea (cele) care o moştenesc să poată utiliza metode cu acelaşi nume, dar cu
acţiuni diferite.

103
PARTEA A III-A

LECŢIA 22. Metode virtuale

Fie C o clasă şi C1 derivata ei. Ambele clase conţin câte o metodă


void afis(), fără parametri (vedeţi clasele din lecţia precedentă). Fie p o
variabilă de tipul C*. Reluăm cazul în care p reţine un pointer către un obiect al
clasei C1. Am văzut că, în acest caz, pornind de la p, se apelează metoda afis()
a clasei C.

ÎNTREBARE. Oare nu există o posibilitate ca, în acest caz, dacă p reţine un pointer
către un obiect al clasei C1, să fie apelată metoda afis() a clasei C1, iar dacă p
reţine un pointer către un obiect al clasei C, să fie apelată metoda afis() a clasei C?
Răspunsul este afirmativ şi pentru a obţine acest efect trebuie ca metoda afis()
a clasei C (de bază) să fie virtuală.

 O metodă virtuală se obţine scriind în faţa antetului metodei cuvântul cheie


“virtual“. Aceasta indică compilatorului să selecteze metoda în funcţie de
tipul obiectului indicat de către pointer.
Exemplu. Metoda afis() a clasei de bază este virtuală. Prin urmare, va fi
apelată metoda clasei care dă tipului obiectului a cărui pointer către el este reţinut
de p. Astfel, în program, dacă n citit este diferit de 0, se apelează metoda clasei
C1, iar dacă el este 0, se apelează metoda clasei C. Testaţi!

#include <iostream>
using namespace std;
class C
{ public:
virtual void afis();
};
void C::afis()
{ cout<<"Eu sunt metoda afis() din clasa C"; }
class C1 :public C
{ public:
void afis();
};
void C1::afis()
{ cout<<"Eu sunt metoda afis() din clasa C1"; }
int main()
{ int n;
cout<<"n="; cin>>n;
C* p;
if (n) p=new C1;
else p=new C;
p->afis();
}

104
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

 O astfel de legare (selectare) a metodelor care se execută poartă numele de


legare dinamică, pentru că selectarea metodei se face în timpul executării
programului. Mai sus, este selectată metoda în funcţie de valoarea citită
pentru n.

Prin utilizarea legării dinamice a metodelor, se pot realiza programe care se


adaptează cu uşurinţă datelor de intrare. Practic, în funcţie de datele de intrare,
se rulează o metodă sau alta şi asta fără intervenţia celui care a scris programul
care utilizează o ierarhie de clase.

Exemplu. În organizarea unui şantier, personalul este ierarhizat. Astfel, o echipă


este condusă de un maistru şi este alcătuită din mai mulţi muncitori. Mai multe
echipe sunt conduse de un inginer. Fiecare persoană angajată este plătită cu o
anumită sumă pe ora de lucru. De exemplu, un muncitor este plătit cu 2 lei pe oră,
un maistru, cu 3 lei pe oră, iar un inginer, cu 4 lei pe oră. Datele se pot organiza
astfel: avem o clasă, muncitor, iar aceasta conţine data membru ore_lucrate şi
este înzestrată cu un constructor cu un parametru care are rolul de a iniţializa
obiectul cu numărul orelor lucrate. Clasa maistru rezultă din derivarea clasei
muncitor, deci va reţine şi ea numărul orelor lucrate de un maistru. La fel, clasa
inginer este derivată din clasa maistru şi va reţine numărul orelor lucrate de un
inginer. Toate clasele conţin câte o metodă de calcul a salariului. Pentru că modul
de calcul al salariului este diferit în funcţie de rolul persoanei respective (muncitor,
maistru, inginer), metodele salariu() sunt virtuale. Programul citeşte o valoare
întreagă. Dacă această valoare este 1, înseamnă că persoana este muncitor, dacă
este 2, persoana este maistru, iar dacă este 3, persoana este inginer. Programul
afişează salariul pe 10 ore pentru un muncitor, maistru sau inginer.
#include <iostream>
using namespace std;
class muncitor
{ protected:
int ore_lucrate;
public:
muncitor (int ore);
virtual int salariu();
};
muncitor::muncitor(int ore)
{ ore_lucrate=ore; }
int muncitor::salariu()
{ return ore_lucrate*2; }
class maistru:public muncitor
{ public:
maistru (int ore);
virtual int salariu();
};
maistru::maistru(int ore):muncitor(ore){}
int maistru::salariu()
{ return ore_lucrate*3; }

105
PARTEA A III-A

class inginer:public maistru


{ public:
inginer (int ore);
virtual int salariu();
};
inginer::inginer(int ore):maistru(ore){ }
int inginer::salariu()
{ return ore_lucrate*4; }
int main()
{ int n; cin>>n;
muncitor* p;
switch (n)
{ case 1: p=new muncitor(10); break;
case 2: p=new maistru(10); break;
case 3: p=new inginer(10); break;
}
cout<<p->salariu();
}

LECŢIA 23. Metode virtuale pure

Am studiat backtracking nerecursiv standardizat. Am văzut acolo că o unică


secvenţă, apela anumite funcţii care întotdeauna aveau acelaşi nume, dar care se
modificau de la un program la altul.

 Problema este următoarea: cum facem ca rutina unică să fie scrisă o singură
dată, iar funcţiile init(), succesor()... să fie redefinite pentru fiecare caz
în parte. Evident, toate acestea prin utilizarea mecanismului OOP!

Care este dificultatea? Ea constă în faptul că funcţia de bază, pe care o vom


numi Run(), apelează rutinele succesor(), init(), etc., ceea ce înseamnă că
obiectul care o reţine trebuie să conţină şi aceste subprograme. Apoi, acestea vor
fi redefinite şi după această operaţie, trebuie ca funcţia de bază să le apeleze pe
cele redefinite.

Iniţial, declarăm clasa bkt, în care funcţiile init(), succesor(), etc.,


sunt declarate ca virtuale. Întrucât ele nu vor fi apelate niciodată din cadrul acestei
clase, au "corpul" vid. Astfel, obţinem aşa numitele funcţii virtuale pure.
class bkt
{ public:
int st[10],n,k;
virtual void Init()=0;
virtual int Am_Succesor ()=0;
virtual int E_Valid()=0;
virtual int Solutie()=0;
virtual void Tipar()=0;
void bkt::run();
};

106
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

void bkt::run()
{ int AS;
k=1; Init();
while (k>0)
{ do {} while ((AS=Am_Succesor()) && !E_Valid());
if (AS)
if (Solutie()) Tipar();
else { k++; Init(); }
else k--;
}
}

 Se cere ca, prin utilizarea clasei bkt să se genereze toate permutările


mulţimii {1, 2, ..., n}.

 Pentru a folosi clasa bkt vom defini o alta, care o moşteneşte şi care
redefineşte funcţiile specifice. Ea va fi înzestrată cu un constructor.
Constructorul are şi rolul de a-l citi pe n. Astfel, se obţine tipul următor:
#include <iostream.h>
#include "[Link]"
class permut : public bkt
{ public:
permut (int v) { n=v; }
virtual void Init();
virtual int Am_Succesor();
virtual int E_Valid();
virtual int Solutie();
virtual void Tipar();
};
void permut::Init()
{ st[k]=0; }
int permut::Am_Succesor ()
{ if (st[k]<n)
{ st[k]++;
return 1;
}
else return 0;
}
int permut::E_Valid()
{ for (int i=1;i<k;i++)
if (st[i]==st[k]) return 0;
return 1;
}
int permut::Solutie()
{ return k==n; }
void permut::Tipar()
{ for (int i=1;i<=n;i++) cout<<st[i];
cout<<endl;
}

107
PARTEA A III-A

De acum, scrierea programului devine o simplă formalitate...


#include "[Link]"
#include "[Link]"
int main()
{
permut x(3);
[Link]();
}

 Prin utilizarea claselor deja create, se cere să se rezolve problema celor n


dame.

După cum ştim, rezolvarea acestei probleme diferă faţă de generarea


permutărilor doar printr-o singură metodă: valid(). Atunci, aceasta va fi redefinită
şi se obţine:
#include "[Link]"
#include <math.h>

class dame: public permut


{
public:
dame( int v): permut(v) {};
virtual int dame::E_Valid();
};

int dame::E_Valid()
{
for (int i=1;i<k;i++)
if ( st[i]==st[k] || abs(st[k]-st[i])==abs(k-i) )
return 0;
return 1;
}

Scrierea programului de generare a damelor este o simplă formalitate:

#include "[Link]"
#include "[Link]"

int main()
{
dame x(8);
[Link]();
}

108
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

LECŢIA 24. Clasa “string”

Acum, după ce am studiat câteva elemente esenţiale referitoare la OOP,


este momentul să prezentăm o clasă a limbajului C++, numită string, care
facilitează lucrul cu şirurile de caractere.

1. Constructori

 string() - construieşte un obiect de tip string care reţine şirul vid.

Exemplu: string sir;


 string(char* s) - permite crearea unui obiect de tip string pornind de
la un şir clasic, aşa cum am folosit până acum.

Exemple. În secvenţele următoare, se afişează “Un sir de caractere”:


1)
char sir[]="Un sir de caractere";
string s=string(sir);
cout<<"s="<<s;

2)
string s=string("Un sir de caractere");
cout<<"s="<<s;

3)
char* sir="Un sir de caractere";
string s=string(sir);
cout<<"s="<<s;

2. Concatenarea şirurilor de caractere

 Pentru a concatena două şiruri de caractere, se foloseşte o metodă care


supraîncarcă operatorul “+“. După cum ştim, operaţia de concatenare nu
este comutativă.

Exemplu. În secvenţa următoare, se afişează:


“cuvant1cuvant2” şi “cuvant2cuvant1”.

string s1="cuvant1", s2="cuvant2";


string c1=s1+s2;
string c2=s2+s1;
cout<<c1<<endl;
cout<<c2<<endl;

109
PARTEA A III-A

3. Adresarea caracterelor prin indice

Se poate adresa un caracter al unui şir de caractere prin indicele său. Primul
caracter are indicele 0, al doilea caracter are indicele 1, ..., ş.a.m.d.

Exemplu. La executarea secvenţei următoare, se afişează caracterul ”a”:


string s="soare";
cout<<s[2];

4. Lungimea unui şir de caractere

Aceasta este returnată de metoda size_type length()3.

Exemplu. La executarea secvenţei de mai jos, se afişează ”5”:


string s="soare";
cout<<[Link]();

5. Returnarea şirului de caractere sub forma clasică

 Metoda const char* c_str() returnează un pointer către şirul de


caractere reţinut de obiect. Cu ajutorul acestui pointer, se poate gestiona
şirul în mod clasic, aşa cum suntem obişnuiţi.

Exemplu. Şirul ”este vara” este copiat din cadrul obiectului de tip string
la adresa reţinută de variabila sir:
string s="este vara";
char *sir="";
strcpy(sir,s.c_str())

6. Ştergerea unui subşir de caractere

 Metoda string& erase(size_type pozitie, size_type lungime); are


rolul de a şterge din şirul reţinut de obiectul de tip şir, un subşir care începe
cu indicele pozitie şi are lungimea lungime. Funcţia returnează o
referinţă către obiectul de tip şir.

Exemplu. În secvenţa următoare, se şterge din şirul iniţial reţinut de obiectul de tip
şir s, subşirul al cărui prim caracter are indicele 1 şi care are lungimea 3. Se va
afişa ”e noapte”.
string s="este noapte";
[Link](1, 3);
cout<<s;

3
size_type este un tip cu semnificaţie de întreg fără semn.

110
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

7. Înlocuirea unui subşir cu un şir

 Metoda string& replace(size_type pozitie, size_type lungime,


const string& sir); are rolul de a înlocui în şirul reţinut de obiect
subşirul al cărui prim caracter are indicele pozitie şi care are lungimea
lungime cu şirul reţinut de obiectul de tip şir s1.

Exemplu. La executarea secvenţei de mai jos, se afişează:


”afara este tare cald”.

string s="afara este foarte cald", s1="tare";


[Link](11, 6, s1);
cout<<s;

8. Inserarea unui subşir

 Metoda string& insert(size_type pozitie, const string& sir);


are rolul de a insera în şirul reţinut de obiectul curent, începând cu poziţia
pozitie, şirul reţinut de obiectul sir.

Exemplu. La executarea secvenţei de mai jos, se afişează:


”azi este iar sarbatoare”.

string s="azi este sarbatoare", s1="iar ";


[Link](9, s1);
cout<<s;

9. Identificarea subşirurilor

 Metoda size_type find(const string& sir, size_type pozitie);


caută în şirul reţinut de obiectul curent subşirul reţinut de obiectul sir.
Căutarea se face începând cu indicele reţinut de pozitie. În cazul în care
subşirul nu este găsit, se returnează un număr întreg dat de o constantă din
clasa string, numită npos (string::npos). În cazul în care căutarea se
efectuează cu succes, metoda returnează indicele de început al primei
apariţii a subşirului. Dacă se doreşte indicele de început al ultimei apariţii a
subşirului utilizaţi rfind, cu aceiaşi parametri.

Exemplu. La executarea secvenţei de mai jos se afişează ”4”. Dacă subşirul nu ar


fi fost identificat, s-ar fi afişat un mesaj corespunzător.
string s="azi este sarbatoare", s1="este";
int semnal=[Link](s1,0);
if(string::npos==semnal)
cout<< "Sir neidentificat";
else cout<<semnal;

111
PARTEA A III-A

10. Returnarea unui subşir care începe la o anumită poziţie şi are o


anumită lungime

 Metoda string substr(size_type pozitie, size_type lung); are


rolul de a returna un obiect care conţine un subşir al şirului reţinut de
obiectul curent, subşir care se identifică prin poziţia primului caracter,
pozitie şi lungimea, lung.

Exemplu. La executarea secvenţei de mai jos, se afişează ”azi”:


string s="azi este sarbatoare";
cout<<[Link] (0,3);

11. Citirea şirurilor de caractere

 Citirea cu metoda care supraîncarcă operatorul ”>>” - şirul se presupune


că este introdus de la tastatură şi este citit până la întâlnirea primului
caracter alb.

Exemplu. Dacă la executarea secvenţei de mai jos, se introduce şirul de caractere


”afara ploua”, se va afişa ”afara”:
string s;
cin>>s;
cout<<s;

 Funcţia prieten istream& getline(istream& numeF, string& sir,


char delim = '\n'); are rolul de a citi un şir introdus dintr-un flux de
intrare. Memorarea şirului citit se face în obiectul de tip string numit sir.
Citirea se face din fluxul numeF şi se termină dacă una dintre condiţiile de
mai jos a fost îndeplinită:

a) s-a depistat sfârşitul fişierului (în cazul fişierelor);


b) s-a întâlnit caracterul transmis prin parametrul delim. Dacă acest
parametru lipseşte, se presupune că este ”\n”.

Funcţia returnează o referinţă către fluxul de intrare. Dacă ea este nenulă,


atunci se mai pot efectua citiri (mai există date în flux). Această valoare este
utilizată pentru detectarea sfârşitului de fişier.

Exemple:

 În secvenţa următoare, se citeşte un şir de caractere care este introdus de la


tastatură:

string linie;
cin>>linie;

112
INIŢIERE ÎN PROGRAMAREA ORIENTATĂ PE OBIECTE

 Programul următor citeşte şi afişează toate liniile unui fişier text:


#include <fstream>
#include <iostream>
#include <string>
using namespace std;
int main()
{ fstream f ("[Link]", ios::in);
string linie;
while (getline(f,linie,'\n')) cout<<linie<<endl;
}

12. Scrierea şirurilor de caractere


 Scrierea se realizează, aşa cum suntem deja obişnuiţi, cu ajutorul unei
metode care supraîncarcă operatorul “<<“. Practic, prin apelul acestei
metode se scrie întreg şirul de caractere.
Exemplu. Programul următor creează un fişier text cu 4 linii. Acestea din urmă
sunt citite de la tastatură.
#include <fstream>
#include <iostream>
#include <string>
using namespace std;
int main()
{ fstream f ("[Link]",ios::out);
string linie;
for (int i=1;i<=4;i++)
{ getline(cin,linie); f<<linie<<endl; }
[Link]();
}

13. Compararea şirurilor de caractere


 Pentru compararea lexicografică a şirurilor de caractere se folosesc metode
care supraîncarcă operatorii: “>“, “>=“, “<“, “<=“ şi “<“.
Exemplu. La executarea secvenţei de mai jos, se afişează ”0” şi ”1”:
fstream f ("[Link]");
string l1="abc", l2="bc";
cout<<(l1>l2)<<endl;
cout<<(l2>l1);

14. Atribuirea şirurilor de caractere


 Unei variabile de tip string i se poate atribui un şir de caractere printr-o
metodă rezultată în urma supraîncărcării operatorului “=“.
Exemplu. La executarea secvenţei următoare se afişează “un sir “:
string l;
l="un sir";
cout<<l;

113
PARTEA A III-A

EXERCIŢII
1. Se citesc n cuvinte. Să se sorteze în ordine crescătoare.
#include <iostream>
#include <string>
using namespace std;
string v[100];
int main()
{ int i,n,inv; string man;
cout<<"n="; cin>>n;
cout<<"Introduceti cuvintele"<<endl;
for(i=0;i<n;i++) { cout<<"cuvantul "<<i<<"="; cin>>v[i]; }
do
{ inv=0;
for (i=0;i<n-1;i++)
if (v[i]>v[i+1]) { man=v[i]; v[i]=v[i+1]; v[i+1]=man; inv=1;}
} while (!inv);
for (i=0;i<n;i++)
cout<<v[i]<<endl;
}

2. Se citeşte un şir şi un caracter. Să se afişeze indicii tuturor apariţiilor


caracterului citit în şir.
#include <iostream>
#include <string>
using namespace std;
int main()
{ string s;
cout<<"introduceti sirul="; getline(cin,s);
char ch;
cout<<"introduceti caracterul="; cin>>ch;
for (int i=0;i<[Link]();i++)
if (s[i]==ch) cout<<i<<" ";
}

3. Se cere ca dintr-un şir dat să se elimine toate apariţiile unui subşir, de


asemenea, dat.
#include <iostream>
#include <string>
using namespace std;
int main()
{ string sir,subsir;
cout<<"introduceti sirul="; getline (cin,sir);
cout<<"introduceti subsirul="; getline(cin,subsir);
int indice;
do
{ indice=[Link](subsir,0);
if (indice!=string::npos) [Link](indice,[Link]());
} while (indice!=string::npos);
cout<<sir;
}

Prin utilizarea clasei string, puteţi rezolva orice problemă cu şiruri, din
manual sau din culegeri!

114
PARTEA A IV-A. Iniţiere în programarea generică

LECŢIA 1. Ce este programarea generică?


La informatică aţi studiat o mulţime de algoritmi. De multe ori, ideea care stă
la baza unui algoritm este aceeaşi, indiferent de tipul de date cu care lucrează
algoritmul.
Exemple:
- dacă se calculează valoarea maximă între două valori, algoritmul este
acelaşi, indiferent dacă avem maxim pentru două valori de tip int, două de
char sau două de tip double;
- în cazul în care se interschimbă două valori, algoritmul este acelaşi,
indiferent dacă valorile sunt de tip char, float sau un tip neomogen
(obţinut cu struct).
- evident, există o mulţime de algoritmi de sortare, dar fiecare are la bază
aceeaşi idee, indiferent de tipul valorilor pe care le sortează;
- dacă utilizăm o stivă, ideea de bază este acceaşi, indiferent de tipul datelor
memorate în stivă.
Şirul exemplelor ar putea continua. Apare însă o întrebare firească: oare nu
se poate să scriem funcţii şi clase care să funcţioneze corect, indiferent de tipul
valorilor cu care lucrează? Pentru limbajul C++, răspunsul este afirmativ!

 Prin programare generică înţelegem acea metodă de programare în care,


funcţiile şi clasele au parametri formali de tip. La apel, se precizează
parametrii efectivi de tip. În unele cazuri, parametrii efectivi de tip se deduc
din tipul valorilor pe care o funcţie le primeşte ca parametri efectivi. În C++,
se poate utiliza programarea generică prin utilizarea mecanismului template
(model, şablon).

OBSERVAŢII:
1. De multe ori, un parametru formal de tip va fi notat cu T.
2. În această parte a cărţii, vom studia funcţii template şi clase template.
3. Programarea generică stă la baza STL-ului (vezi ultima parte a acestei cărţi).
4. În STL, pentru a lucra cu diverse structuri de date (vectori, liste liniare simplu şi
dublu înlănţuite, stive, cozi, etc.) se utilizează clasele template.
5. În STL, algoritmii standard care pot fi folosiţi pentru obţinerea unor programe
performante şi lizibile sunt sub forma unor funcţii template.
6. Nu există niciun motiv pentru care, un elev care doreşte să înveţe programare,
să folosească pe scară largă funcţiile şi clasele template.

115
PARTEA A IV-A

LECŢIA 2. Funcţii “template”

Prin funcţii template, înţelegem funcţii model, şablon. De ce este nevoie


de funcţii template, cu ce ne ajută ele în programare? De multe ori, algoritmii sunt
aceiaşi, pentru mai multe tipuri de date. Clasic, pentru fiecare dintre tipurile de date
cu care lucrează o funcţie, ar trebui scrisă câte o funcţie. În loc să facem acest
lucru, scriem o singură funcţie template şi o utilizăm pentru tipul (tipurile) de date
dorit(e). Pentru a rezolva această problemă, funcţia poate primi un parametru de
un tip formal, să-l numim T. La apel, se va preciza care este tipul efectiv, adică cel
folosit în locul lui T. La faza de compilare, se va genera codul pentru tipul precizat.
Prin urmare, utilizarea funcţiilor template nu afectează eficienţa programului.

 O funcţie template se defineşte ca mai jos:

template <class T> tip_functie nume_functie (lista parametri formali)


{ ... }

OBSERVAŢIE. Parametrii formali pot fi de tipul formal T. De asemenea, tipul


funcţiei poate fi şi el de tipul formal T.

 Forma de apel a unei funcţii template cu un singur tip formal este prezentată
mai jos, unde prin tip_efectiv înţelegem tipul care va fi utilizat în locul
tipului formal T:
nume_functie<tip_efectiv> (lista parametri efectivi)

Exemple

 Programul de mai jos defineşte şi utilizează o funcţie template care


calculează cea mai mare valoare dintre cele două, primite ca parametri
efectivi. Un apel este pentru două valori de tip int, apoi pentru două valori
de tip float:

#include <iostream>
using namespace std;

template <class T>


T maxim (T a, T b)
{
if (a>b) return a;
else return b;
}

int main()
{
int a=2,b=3;
cout<<maxim<int>(a,b)<<endl;
cout<<maxim<double>(2.9,6.7)<<endl;
}

116
INIŢIERE ÎN PROGRAMAREA GENERICĂ

 Programul de mai jos defineşte şi utilizează o funcţie template care


interschimbă două valori, de tipul formal T, primite ca parametri efectivi.
Apelul este pentru două valori de tip int.
#include <iostream>
using namespace std;
template <class T>
void interschimb (T& a, T& b) {
T man;
man=a;
a=b;
b=man;
}
int main() {
int a=2,b=3;
interschimb<int>(a,b);
cout<<a<<" "<<b;
}

 Nu întotdeauna este necesar ca, la apel, să fie transmise tipurile efective.


Tipurile efective se pot deduce din tipurile parametrilor efectivi.

Exemplu. Reluăm exemplul funcţiei care calculează maximul. Apelul se poate


realiza şi aşa, fără precizarea tipului efectiv. În cazul primului apel, dacă a şi b sunt
de tipul int, atunci T=int. În cazul celui de-al doilea apel, constantele sunt de tip
double. Prin urmare, T=double.
cout<<maxim(a,b)<<endl;
cout<<maxim(2.9,6.7)<<endl;

OBSERVAŢIE. Dacă, de exemplu, pentru funcţia care calculează maximul, se


transmit parametri efectivi de tip diferit, cum ar fi char şi int, deşi maximul se
poate calcula clasic pentru două date cu aceste tipuri, întrucât compilatorul nu
poate decide dacă T=char sau T=int, programul va da eroare la compilare:
char a='b'; int b=5;
cout<<maxim(a,b)<<endl;

 Există posibilitatea ca funcţiile template să utilizeze mai multe tipuri formale.


În acest caz, tipurile formale sunt separate prin virgulă. De asemenea,
tipurile care se folosesc efectiv sunt şi ele separate prin virgulă.
Exemplu. Programul următor, strict didactic, utilizează o funcţie care adună două
valori de tipurile U şi V şi returnează rezultatul sub forma tipului T. La rulare,
T=float, U=int şi V=char.

#include <iostream>
using namespace std;
template <class T, class U, class V>
T adun (U a, V b)
{ return a+b; }

117
PARTEA A IV-A

int main() {
int a=2; char b='a';
cout<<adun<float,int,char> (a,b);
}

 În C++, funcţiile template pot fi supraîncărcate, adică se pot declara mai


multe astfel de funcţii cu acelaşi nume, care însă, diferă prin lista parametrilor
de tip formali.

Exemplu. Mai jos, aveţi două funcţii template cu acelaşi nume:


template <class T, class U, class V>
T adun (U a, V b)
{ return a+b; }

template <class T, class U>


T adun (U a, U b)
{ return a+b; }

EXERCIŢII PROPUSE
1. Scrieţi o funcţie template care sortează crescător un vector cu n componente
de tipul T.

2. Scrieţi o funcţie template care interclasează doi vectori (evident, deja sortaţi
crescător) cu elemente de tipul T.

LECŢIA 3. Clase “template”

La fel ca şi o funcţie template, o clasă template se utilizează atunci când


respectiva clasă foloseşte cel puţin un tip de dată care nu este cunoscut de la
început. Ca şi în cazul funcţiei template, clasa template este numai un model de
clasă, iar clasa propriu-zisă, în sensul pe care-l cunoaştem, se obţine după
compilare, atunci când tipul efectiv este cunoscut.

 Pentru a defini o clasă template, se utilizează forma de mai jos, unde T este
tipul formal, iar membrii clasei (date şi metode) pot utiliza tipul T:
template <class T> class nume_clasa
{ ... }

 Antetul unei metode se declară în interiorul clasei aşa cum suntem obişnuiţi,
iar definiţia, din afara clasei, se declară la fel ca o funcţie template, numai că
evident, va fi necesar să specificăm clasa din care aceasta face parte.
Vedeţi mai jos, unde T este tipul formal utilizat de clasă:
template <class T> tip_metoda
nume_clasa<T>::nume_metoda (lista parametri formali)
{ ... }

118
INIŢIERE ÎN PROGRAMAREA GENERICĂ

 Pentru a defini un obiect (să-l numim ob) al unei clase template cu tipul
formal T, vom proceda aşa cum suntem obişnuiţi, în funcţie de constructorul
clasei, numai că numele clasei va conţine tipul efectiv:

Exemple:
nume_clasa<int> ob;
nume_clasa<float> ob;

Aplicaţia 1. Se cere să se scrie o clasă care memorează o valoare de tipul T.


Clasa va conţine o dată v, de tipul T, constructorul Valoare(T v) care are ca
efect iniţializarea lui v şi o metodă afis() care afişează valoarea memorată de v.
Clasa se testează pentru T=int şi pentru T=float.
include <iostream>
using namespace std;

template <class T> class Valoare {


T v;
public:
Valoare (T v);
void afis();
};

template <class T> Valoare<T>::Valoare(T v)


{ this->v=v; }

template <class T> void Valoare<T>::afis()


{ cout<<v<<endl; }

int main()
{ Valoare<int> a(3);
[Link]();
Valoare<double> b(-2.75);
[Link]();
}

Aplicaţia 2. Se cere să se scrie o clasă (numită vector) care gestionează un


vector cu n componente de tip T, vector alocat dinamic. Se cere ca accesul la
elementele vectorului să se realizeze aşa cum suntem obişnuiţi. Clasa se testează
pentru T=int şi pentru T=double.

Clasa conţine:

 vector(int n); - constructor. Alocă dinamic un vector cu n


componente de tip T, iar data membru adr va reţine un pointer către acesta;
 ~vector(); - destructor. Are rolul de a elibera memoria ocupată de
vector;
 T& operator[](int n); - metoda returnează o referinţă la
componenta de indice k a vectorului.

119
PARTEA A IV-A

#include <iostream>
using namespace std;
template <class T> class vector {
int n;
T* adr;
public:
vector(int n);
~vector();
T& operator[](int k);
};
template <class T> vector<T>::vector(int n)
{ this->n=n;
adr=new T[n];
}
template <class T> vector<T>::~vector()
{ delete[] adr; }
template <class T> T& vector<T>::operator[](int k)
{ return adr[n]; }
int main()
{ vector<int> v(10);
v[1]=3;
cout<<v[1]<<endl;
vector<double> w(5);
w[0]=77;
cout<<w[0];
}

Aplicaţia 3. Se cere să se scrie o clasă (numită stiva) care gestionează o stivă


memorată ca listă liniară simplu înlănţuită. Fiecare nod al listei reţine, pe lângă
informaţia de adresă, o valoare de tipul formal T. Clasa se testează pentru T=int
şi pentru T=float. Prin urmare, cu această clasă se pot gestiona stive care conţin
elemente dintr-o mulţime de tipuri.

Rezolvare. Evident, orice nod conţine o informaţie de adresă şi valoarea


propriu-zisă. Structura unui nod este conţinută de clasă, este definită cu struct şi
se numeşte Nod. Orice nod va conţine valoarea de tip T, numită info şi adresa
nodului precedent, numită adr_prec.

Clasa conţine:

 varf – dată membru – are rolul de a reţine adresa ultimului nod introdus în
stivă;
 stiva() – constructor – are rolul de a iniţializa o stivă vidă; practic, varf
va reţine 0 (nicio adresă);
 void push(T v) – metodă – are rolul de a adăuga listei un nod nou.
Nodul adăugat va conţine, pe lângă informaţia de adresă şi o valoare de tip
T. Cu alte cuvinte, adaugă valoarea v în stivă.

120
INIŢIERE ÎN PROGRAMAREA GENERICĂ

 T pop() – metodă – are rolul de a returna conţinutul lui info din ultimul
nod al listei. Acesta din urmă este şters din listă; cu alte cuvinte, metoda
extrage ultima informaţie introdusă;
 int vida() – metodă – are rolul de a testa dacă stiva este vidă sau nu;
în caz afirmativ, returnează o valoare nenulă.

Programul este următorul:


#include <iostream>
using namespace std;
template <class T> class stiva {
struct Nod
{T info;
Nod* adr_prec;
};
Nod *varf;
public:
stiva();
void push(T v);
T pop();
int vida();
};
template <class T> stiva<T>::stiva()
{ varf=0; }
template <class T> void stiva<T>::push(T v)
{ Nod* c=new Nod;
c->info=v;
c->adr_prec=varf;
varf=c;
}
template <class T> T stiva<T>::pop()
{ Nod *c=varf;
T nd;
varf=varf->adr_prec;
nd=c->info;
delete c;
return nd;
}
template <class T> int stiva<T>::vida()
{ if (varf==0) return 1;
else return 0;
}
int main()
{ stiva<int> st;
[Link](1); [Link](2); [Link](3);
while (![Link]()) cout<<[Link]()<<endl;
stiva<float> st1;
[Link](3.23); [Link](-12);
while (![Link]()) cout<<[Link]()<<endl;
}

121
PARTEA A IV-A

 O clasă template poate avea mai mulţi parametri de tip, la fel ca şi funcţiile
template. De asemenea, este posibil ca, pentru fiecare parametru de tip, să
avem o valoare implicită. Pentru aceasta, în definiţia clasei, după
parametrul de tip, se trece, precedat de “=”, numele implicit al tipului.
Exemplu. Clasa test_par_implicit conţine metoda minim, care returnează
cea mai mică valoare dintre cele două de tip T, pe care le primeşte ca parametri de
intrare. Pentru că cele două valori sunt de tip T, trebuie stabilit ce înseamnă “mai
mic”. Dacă T este un tip numeric, atunci se foloseşte o metodă a clasei compar,
numită Less. Această metodă utilizează operatorul clasic, “<“. Dacă însă, cele
două valori de comparat sunt şiruri de caractere, se utilizează o altă metodă,
numită tot Less, prin care se compară două şiruri de caractere. Ea este conţinută
de clasa siruri. Prin urmare, test_par_implicit conţine doi parametri de tip:
primul T, cu valoarea implicită int şi al doilea, C, cu valoarea implicită compar<T>
(pentru tipuri numerice). Acum: dacă dorim să comparăm două tipuri numerice, se
declară un obiect al clasei test_par_implicit cu un singur parametru de tip,
dar dacă dorim să comparăm două şiruri, se declară un obiect al clasei
test_par_implicit cu doi parametri de tip. Analizaţi programul următor:
#include <iostream>
using namespace std;
class siruri
{ public:
static int Less(char* a, char* b) // o metoda Less
{ if (strcmp(a,b)<0) return 1;
else return 0;
}
};
template <class T> class compar
{ public:
static int Less(T a, T b) { return a<b; } // alta metoda Less
};
template < class T=int, class C=compar<T> >
class test_par_implicit
{ public:
T minim (T a, T b)
{ if (C::Less(a, b)) return a;
else return b;
}
};
int main()
{ float a=2.1,b=1.75;
test_par_implicit<float> ob;
cout<<[Link](a,b)<<endl;
test_par_implicit<char*,siruri>ob1;
cout<<[Link]("arbore", "banca")<<endl;
}

OBSERVAŢIE. Procedeul prezentat aici este mult utilizat în STL (vezi ultima parte
a acestei cărţi).

122
INIŢIERE ÎN PROGRAMAREA GENERICĂ

 Există posibilitatea ca o clasă să conţină parametrii prin care un simbol să


preia o anumită valoare de un anumit tip. Un astfel de parametru formal se
declară sub forma: tip Nume_simbol, iar parametrul efectiv este o valoare
de tip tip.
Exemplu. Clasa Vector, conţine un vector cu N (N de tip int) elemente de tipul
T. Parametrii sunt: <class T, int N>. La apel se precizează care este tipul T şi
care este valoarea lui N.
#include <iostream>
using namespace std;
template <class T, int N > class Vector
{ public:
T V[N]; };
int main()
{ Vector<float,20> X; // avem float V[20]
X.V[12]=12;
cout<<X.V[12];
Vector<char,1000> X1; // avem char V[1000]
}

 În C++, clasele template nu pot fi supraîncărcate.

EXERCIŢII PROPUSE
1. Creaţi o clasă (numită listaT) prin care se poate lucra cu liste liniare simplu
înlănţuite în care nodurile, pe lângă informaţia de adresă, conţin o informaţie de tip T.

2. Creaţi o clasă (numită coadaT) prin care se poate lucra cu cozi implementate
ca liste liniare simplu înlănţuite în care nodurile, pe lângă informaţia de adresă,
conţin o informaţie de tip T.

LECŢIA 4. Derivarea în cazul claselor template


Nu există restricţii în ceea ce priveşte derivarea claselor template. Regulile
sunt aceleaşi.

 Antetul unei clase C1 care moşteneşte (public) o clasă C, unde ambele


depind de tipul formal T, este:
template <class T> class C1:public C1<T> {...}

 Antetul definiţiei unui constructor al clasei moştenitoare C1, care apelează


un constructor al clasei de bază C, unde ambele clase depind de tipul formal
T, este:
template <class T> C1<T>::C1(lista parametri
formali):C<T>(lista parametri efectivi){...};

123
PARTEA A IV-A

Exemple:

 Clasa Valoare este o clasă template care depinde de tipul formal T. Ea


are ca dată membru pe v, de tipul T. De asemenea, clasa este înzestrată cu
un constructor cu un singur parametru, care are rolul de a-i atribui o valoare
lui v. Clasa Val moşteneşte clasa Valoare, iar constructorul ei, apelează
constructorul clasei Valoare. De asemenea, clasa a fost înzestrată cu o
metodă care afişează valoarea reţinută de v.

#include <iostream>
using namespace std;

template <class T> class Valoare


{ protected:
T v;
public:
Valoare(T v);
};
template <class T> Valoare<T>::Valoare(T v)
{ this->v=v; }

template <class T> class Val:public Valoare<T>


{ public:
void afisez();
Val(T v);
};

template <class T> Val<T>::Val(T v):Valoare<T>(v){};


template <class T> void Val<T>::afisez()
{ cout<<this->v<<endl; }

int main()
{ Val<int> ob(11);
[Link]();
}

 Clasa Matrice permite lucrul cu matrice în care elementele au tipul formal


T. Matricele sunt alocate dinamic. Numărul de linii m şi cel de coloane, n sunt
citite de la tastatură, iar memoria alocată pentru o astfel de matrice ţine cont
de valorile lui m şi n.

a) Se ştie că limbajul permite să se aloce dinamic un vector cu un număr variabil


de elemente, dar nu şi o matrice. Cum trebuie procedat pentru a aloca dinamic o
matrice cu un număr variabil de linii şi coloane? Un obiect de tip Matrice va avea
o dată membru adr, de tip T**. Data membru adr reţine adresa unui vector cu m
componente de tip T*. Fiecare dintre cele m componente ale vectorului reţine
adresa unui vector cu n componente de tip T. Astfel, s-a rezolvat problema
memorării unei matrice cu componente de un tip formal T şi cu un număr variabil
de linii şi coloane. Toată această problemă este rezolvată de constructorul clasei,
cel cu doi parametri de tip int.

124
INIŢIERE ÎN PROGRAMAREA GENERICĂ

[0] [0] [1] [n-1]

adr [1] [0] [1] [n-1]

[m-1] [0] [1] [n-1]

b) Clasa conţine şi o metodă obţinută prin supraîncărcarea operatorului “()”. Ea


returnează o referinţă către un element al matricei. Cu ajutorul ei, dacă A este
obiectul rezultat prin instanţierea clasei, adresarea unui element, de exemplu
pentru o atribuire, se poate face prin A(2,3)=5, iar afişarea lui se va face prin
cout<<A(2,3).

c) Clasa MatrOp moşteneşte clasa Matrice. Ea conţine, în plus, o metodă


pentru adunarea a două matrice. Matricea sumă este alocată în memorie. Pentru a
o putea copia fără a dubla spaţiul necesar memorării matricei, vedeţi copierea bit
cu bit, am înzestrat clasa cu un constructor fără parametri. Acesta rezervă spaţiu
pentru adr, dar nu alocă spaţiu pentru matrice. Astfel, o atribuire nu face altceva
decât să copieze pointer-ul reţinut de adr pentru matricea sumă.

d) Programul citeşte două matrice cu acelaşi număr de linii şi de coloane şi


afişează suma lor:

#include <iostream>
using namespace std;

template <class T> class Matrice


{ T** adr;
protected:
int m, n;
public:
Matrice();
Matrice(int m, int n);
T& operator()(int i,int j);
};

template <class T> Matrice<T>::Matrice() { };


template <class T> Matrice<T>::Matrice(int m, int n)
{ this->m=m;
this->n=n;
adr=new T*[m];
for (int i=0;i<m;i++) adr[i]=new T[n];
}

template <class T> T& Matrice<T>::operator() (int i, int j)


{ T* adr_linie=adr[i];
return adr_linie[j];
}

125
PARTEA A IV-A

template <class T> class MatrOp:public Matrice<T>


{ public:
MatrOp();
MatrOp(int m,int n);
MatrOp operator+(MatrOp M);
};

template <class T> MatrOp<T>::MatrOp(){}

template <class T> MatrOp<T>::MatrOp(int m,int n):Matrice<T>(m,n)


{}
template <class T> MatrOp<T> MatrOp<T>::operator+(MatrOp M)
{ MatrOp<T> S(this->m,this->n);
for(int i=0;i<this->m;i++)
for (int j=0;j<this->n;j++) S(i,j)=(*this)(i,j)+M(i,j);
return S;
}

int main()
{ int m,n,i,j;
cout<<"m="; cin>>m;
cout<<"n="; cin>>n;
cout<<"Creez si citesc matricea A"<<endl;
MatrOp<int> A(m,n);
for (i=0;i<m;i++)
for (j=0;j<n;j++)
{ cout<<"A("<<i<<","<<j<<")=";
cin>>A(i,j);
}
cout<<"Creez si citesc matricea B"<<endl;
MatrOp<int> B(m,n);
for (i=0;i<m;i++)
for (j=0;j<n;j++)
{ cout<<"B("<<i<<","<<j<<")=";
cin>>B(i,j);
}
cout<<"Afisez suma matricelor"<<endl;
MatrOp<int> C=A+B;
for (i=0;i<m;i++)
{ for (j=0;j<n;j++) cout<<C(i,j)<<" ";
cout<<endl;
}
}

EXERCIŢII PROPUSE
1. Adăugaţi clasei MatrOp o metodă care permite înmulţirea a două matrice.

2. Utilizaţi clasa MatrOp pentru citirea unei matrice pătratice cu elemente de tip T
şi afişarea matricei An.

126
PARTEA A V-A
Iniţiere în STL (Standard Template Library)

LECŢIA 1. Ce este STL ?


Creatorul limbajului C++, Bjarne Stroustrup, şi-a pus problema să adauge
1
limbajului anumite elemente astfel încât scrierea programelor să se facă rapid ,
2 3
strandardizat şi programele obţinute să fie performante .

Atunci când scriem programe, utilizăm două noţiuni fundamentale: algoritmi


şi structuri de date. De aici, rezultă că:

1. Limbajul trebuie să conţină anumite clase care să gestioneze structurile de date


frecvent utilizate. Astfel, vom avea clase specializate în gestionarea vectorilor,
listelor liniare, stivelor, cozilor, heap-urilor, etc. Astfel de clase se numesc clase de
tip container, iar obiectele generate prin instanţierea lor se numesc containere.
Prin urmare, o structură de tip listă liniară este un container, o structură de tip stivă
este un container, ş.a.m.d.
2. Întrucât o structură de date trebuie să opereze cu date de orice tip, clasele de tip
container utilizează din plin mecanismul template. Astfel, la modul general, un
container se defineşte ca un obiect care poate reţine alte obiecte.
3. Aşa cum am văzut, structurile de date nu sunt suficiente pentru realizarea
programelor. Din acest motiv, este necesar să avem anumite funcţii care
“incorporează” algoritmii cei mai des folosiţi. Funcţiile care incorporează algoritmii,
trebuie să poată lucra cu orice tip de dată (la modul general cu orice obiect).
Cerinţa este satisfăcută dacă aceste funcţii utilizează şi ele mecanismul template.
De asemenea, este foarte important ca aceste funcţii să poată lucra cu datele
stocate în containere.
Ca terminologie, vom utiliza termenul “algoritm” şi cu semnificaţia de funcţie
template care rezolvă o anumită problemă des întâlnită în activitatea curentă şi
care, de regulă, prelucrează date (obiecte) stocate în containere.

1
Un program se realizează cu mult mai repede dacă limbajul dispune de funcţii şi clase,
care pot fi adaptate uşor pentru rezolvarea unor probleme concrete, cerute de practică.
2
Scrierea programelor nu mai este de mult o activitate “artizanală”. Este esenţial ca un
program să poată fi înţeles şi de alţi programatori, nu numai de cei care l-au scris. De
exemplu, o firmă cu acest profil, trebuie să-şi poată uşor modifica programele, chiar dacă
unii dintre cei care le-au realizat nu mai lucrează acolo.
3
Pentru a obţine programe performante prin utilizarea funcţiilor şi claselor puse la dispoziţie
de limbaj, este esenţial ca acestea din urmă să aibă, la rândul lor, la bază algoritmi
performanţi.

127
PARTEA A V-A

4. O problemă foarte importantă este dată de accesul la datele (obiectele) aflate în


container. Dacă la un container de tip vector, accesul se poate realiza uşor, el este
mai dificil la un container de tip listă liniară sau la unul de tip de arbore de căutare.
Din acest motiv, s-au generalizat pointerii şi s-a ajuns la iteratori. Un iterator indică
un anumit obiect din container. Pornind de la el se pot accesa datele şi, dacă este
cazul, metodele obiectului memorat. De asemenea, se poate obţine imediat, printr-o
simplă incrementare, un iterator care indică obiectul următor din container.

FOARTE IMPORTANT. Pentru a accesa datele stocate în containere, algoritmii


utilizează iteratorii. Acesta este motivul pentru care se spune că “iteratorii
reprezintă un liant între containere şi algoritmi”.

 Prin STL (Standard Template Library) înţelegem o colecţie de clase


de tip container, algoritmi şi iteratori care poate fi folosită pentru a realiza
rapid programe performante şi standardizate. Orice compilator actual de
4
C++ trebuie să permită utilizarea STL-ului . Prin folosirea STL-ului, obţinem
programe portabile (pot fi compilate, teoretic, cu orice compilator de C++) şi,
mai ales, uşor de descifrat pentru oricine este familiarizat cu acest mod
5
de lucru .

Pentru început, citiţi lecţiile 2-4 doar cu titlu informativ, dar, pe măsura
studierii STL-ului, reveniţi asupra lor.

Învăţaţi să vă autodocumentaţi! Prezentăm o adresă la care puteţi găsi o


documentaţie pentru STL:

“[Link]

 După cum aţi văzut, containerele conţin obiecte. De cele mai multe ori,
acestea sunt de tipuri simple, predefinite, cum ar fi: int, float, char... Mai
mult, aceste obiecte sunt supuse unor operaţii de comparare. De asemenea,
anumite containere sunt de tip vector, listă liniară, ş.a.m.d. Din acest motiv,
vom folosi uneori termenii de dată, valoare pentru obiecte sau de
componentă în cazul vectorului, element în cazul listelor (înţelegând prin
aceasta, atât obiectul memorat, cât şi informaţia de adresă). Sperăm ca
astfel de exprimări să nu fie considerate o "crimă"...

4
În varianta Borland C++, STL-ul nu poate fi folosit decât parţial. Atunci când a fost realizat
compilatorul (la modul general, mediul de programare), STL-ul era încă în faza de
proiectare. Acesta este unul dintre motivele pentru care recomandăm utilizarea
compilatoarelor prezentate în prima parte a cărţii.
5
Elevii sunt avantajaţi dacă utilizează STL-ul în concursurile şcolare. Teoretic, nimeni nu-i
poate opri să-l utilizeze, este un standard. Totuşi, utilizarea STL-ului nu-i scuteşte de
pregătirea teoretică. Se poate spune că, între doi “olimpici” cu aceleaşi cunoştinţe, cel care
cunoaşte STL-ul are un anumit avantaj. Nu recomandăm utilizarea STL-ului la examenul de
bacalaureat, pentru că se riscă depunctarea!

128
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

LECŢIA 2. Mai mult despre containere


 Containere. Un container este un obiect care reţine alte obiecte. El rezultă
prin instanţierea unei clase de tip container. Containerele sunt de două mari
categorii: containere secvenţă şi containere asociative. De asemenea,
există o categorie aparte de containere, numite adaptori.

A) Containere secvenţă - reţin obiectele într-o ordine oarecare. Ordinea


obiectelor este stabilită de cel care foloseşte containerul respectiv. Containerele de
tip secvenţă permit inserarea şi ştergerea obiectelor.
Exemple de clase ale căror obiecte sunt containere de tip secvenţă: vector
(gestionează un vector unde fiecare componentă reţine un obiect) şi
list (gestionează o listă liniară dublu înlănţuită, unde fiecare element al listei
reţine un obiect).

B) Containere asociative - fiecare obiect are o cheie şi poate fi regăsit rapid


după aceasta. Regăsirea, ştergerea şi inserarea unui obiect se fac rapid, în
O(log n), unde n este numărul obiectelor care se găsesc în container. Pentru
astfel de containere se preferă memorarea obiectelor sub forma unor arbori de
căutare echilibraţi, motiv pentru care operaţiile amintite se efectuează atât de
eficient. În mod evident, inserarea unui obiect se face automat, acolo unde îi este
locul, conform cheii pe care o are, nu acolo unde doreşte programatorul, ca în
cazul containerelor secvenţă.
Exemple de clase ale căror obiecte utilizează containere asociative: set
gestionează o mulţime de obiecte, unde fiecare obiect coincide cu cheia sa, unică;
map gestionează mai multe perechi de forma (cheie, valoare), unde cheia este
unică; multiset, la fel ca set, dar pot exista mai multe obiecte cu aceeaşi cheie;
multimap, la fel ca map, dar pot exista mai multe perechi cu aceeaşi cheie.

C) Adaptori - sunt tot containere, dar nu sunt definiţi separat, ci pornind de la


alte containere se restricţionează accesul numai la anumite metode. De exemplu,
dacă dorim să lucrăm cu o stivă, se poate folosi un vector cu acces numai la unul
dintre capete.
Exemple de adaptori: stack (stivă), queue (coadă), priority_queue (coadă
cu prioritate sau, aşa cum este cunoscut din cursul de algoritmi, structura heap).

NOTAŢII STANDARD. Atunci când prezentăm containerele, metodele de acces


la obiectele memorate, utilizăm următoarele notaţii:

size_type un tip întreg fără semn


key_type tipul obiectului care constituie cheia
tipul obiectului memorat - în cazul obiectelor de tip
value_type
map şi multimap, este alcătuit din cheie şi valoare

129
PARTEA A V-A

LECŢIA 3. Mai mult despre iteratori

 Iteratori. Pentru a se accesa cu uşurinţă obiectele unui container (“a naviga”


printre ele) se utilizează iteratorii. Iteratorii sunt asemănători pointer-ilor, dar
cu ei se pot face mult mai multe lucruri. Dacă în cazul containerelor de tip
secvenţă, iteratorii sunt aproape la fel ca pointerii, în cazul containerelor
asociative, aceştia permit “navigarea” uşoară, printr-o simplă incrementare,
printre nodurile unui arbore de căutare.

DE REŢINUT:
1) Iteratorii sunt obiecte ale unor clase.

2) Dacă it este o variabilă care reţine un iterator care indică un anumit obiect al
containerului, atunci:
 prin *it se adresează obiectul;
 prin it->metoda / it->data_membru se adresează o metodă / dată
membru a obiectului memorat;
 prin ++it se obţine adresa obiectului “următor”. Dacă semnificaţia
incrementării este uşor de explicat în cazul containerelor secvenţiale
(++it va indica obiectul următor în secvenţă), în cazul containerelor
asociative semnificaţia poate fi de a indica următorul obiect cu cheia “mai
6
mare” sau “mai mică” .
3) Prin convenţie, s-a stabilit că toate containerele secvenţiale şi asociative să
aibă un prim element şi un ultim element. Acesta din urmă nu conţine niciun
obiect, el este folosit pentru a marca sfârşitul elementelor din container. Nu există
ca în cazul pointer-ilor un iterator nul, există numai un iterator care indică ultimul
element din structură.
4) Din punct de vedere al sensului de parcurgere a obiectelor din container,
iteratorii sunt de două feluri:
a) iterator - prin aplicarea repetată a operatorului “++“ se parcurg
obiectele în ordinea stabilită, cea secvenţială, în cazul containerelor
secvenţiale şi cea dată de relaţia de ordine pe mulţimea cheilor, în cazul
containerelor asociative;
b) reverse_iterator - prin aplicarea repetată a operatorului “++“ se
parcurg obiectele în ordinea inversă impusă de secvenţă, în cazul
containerelor secvenţiale şi ordinea inversă relaţiei de ordine pe mulţimea
cheilor, în cazul containerelor asociative.

6
În cazul tipurilor numerice, semnificaţia lui “mai mare” sau “mai mic” este evidentă. În
cazurile altor tipuri, semnificaţia lui “ mai mare” sau “mai mic” trebuie precizată (programată).
Pe parcursul lecţiilor următoare, veţi înţelege şi cum se realizează aceasta.

130
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

5) Clasele care utilizează containeri conţin unele sau toate metodele de mai jos:

iterator begin()
returnează un iterator care indică primul
obiect al containerului
iterator end()
returnează un iterator care indică
ultimul element al containerului
returnează un reverse_iterator care
reverse_iterator rbegin() indică primul obiect al containerului, dacă le
considerăm pe acestea în ordine inversă
returnează un reverse_iterator care
reverse_iterator rend() indică ultimul element al containerului dacă
le considerăm pe acestea în ordine inversă

6) Declararea unei variabile care reţine un iterator se face prin utilizarea


operatorului de rezoluţie, ca mai jos:
a) nume_clasa_container::iterator nume_iterator;
b) nume_clasa_container::reverse_iterator nume_iterator;

7) Parcurgerea cu ajutorul iteratorilor a obiectelor dintr-un container, se poate face


ca mai jos:
for(it=ob_container.begin();it!=ob_container.end();it++)
... // unde it este iterator
for(it=ob_container.rbegin();it!=ob_container.rend();it++)
... // unde it este reverse_iterator

8) În funcţie de operaţiile care se pot efectua cu ei, principial, iteratorii se pot


împărţi în următoarele categorii:
a) Iteratori de tip Input - permit accesul la obiectele din container. Operaţiile
permise cu aceştia sunt: ++ (incrementare, pentru a indica următorul obiect), =*p
(citiri de obiecte din container), -> (accesul la datele sau metodele obiectului
indicat de iterator), == şi != (testarea egalităţii sau a inegalităţii a doi iteratori).
b) Iteratori de tip Output - permit memorarea obiectelor în container. Operaţiile
permise cu aceştia sunt: ++, *p= (atribuiri de obiecte în container).
c) Iteratori de tip Forward - permit operaţiile care se pot efectua cu iteratorii de tip
Input şi Output.
d) Iteratori de tip Bidirectional - permit operaţiile care se pot efectua cu iteratorii
de tip Forward şi, în plus, este permisă şi operaţia -- (se obţine un iterator care
indică obiectul care precede în secvenţă obiectul indicat de iteratorul memorat).
e) Iteratori de tip Random - permit operaţiile care se pot efectua cu iteratorii de tip
Bidirectional, dar, în plus, se permit operaţii genul iterator+k, unde k este un
întreg, adică se poate accesa direct un obiect al containerului, fără a fi necesar să
parcurgem toate elementele care îl preced. De asemenea, accesul direct la obiecte
se poate obţine prin supraîncărcarea operatorului “[]“.
În final, menţionăm că diversele containere permit lucrul cu o anumită
categorie de iteratori. De asemenea şi algoritmii lucrează cu anumiţi iteratori.

131
PARTEA A V-A

LECŢIA 4. Un exemplu mult simplificat pentru


înţelegerea noţiunilor de container, iterator şi algoritm

Această lecţie este facultativă şi se adresează doar acelor elevi care vor să
înţeleagă principiile care stau la baza programării prin utilizarea STL-ului. În ea,
vom prezenta o clasă, de tip container, care permite lucrul cu vectori. Clasa
o vom numi vectoras<T>. Pentru a putea lucra cu vectori, vom utiliza iteratori
obţinuţi prin instanţierea clasei iterator. De asemenea, se va da un exemplu de
algoritm pentru a vedea, în linii mari, cum au fost aceştia obţinuţi.
Menţionăm că, în vederea atingerii scopului propus, vom utiliza procedee cu
mult simplificate faţă de cele care au stat la baza obţinerii STL-ui. Totuşi, parcur-
gerea acestei lecţii constituie şi o recapitulare a cunoştinţelor însuşite în lecţiile
precedente.
Începem prin a prezenta sursa clasei vectoras<T>:
template <class T> class vectoras
{ // numarul de componente
int n;
// pointer catre vectorul alocat dinamic
T* h;
public:
// clasa interioara ce are rolul de a genera obiectele iterator
class iterator
{ T* p;
public:
iterator() {p=0;}
iterator(T* adr) {p=adr;};
T& operator*() {return *p;}
void operator++()
{ p++; }
void operator=(iterator it1) {p=it1.p;}
int operator !=(iterator it1) {return p!=it1.p;}
};
// metoda care returneaza un iterator catre prima componenta a
// vectorului
iterator begin();

// metoda care returneaza un iterator catre ultima componenta a


// vectorului
iterator end();

// constructorul clasei
vectoras(int n);

// metoda de acces la componentele vectorului


T& operator[](int k);

// metoda de adaugare a unei componente la sfarsitul unui vector


void push_back (T x);
};

132
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

template <class T> vectoras<T>::vectoras(int n)


{ this->n=n;
h=new T[n+1];
}

template <class T> T& vectoras<T>::operator[](int k)


{ return h[k]; }

template <class T> typename vectoras<T>::iterator


vectoras<T>::begin()
{ return h; }

template <class T> typename vectoras<T>::iterator


vectoras<T>::end()
{ return h+n; }

template <class T> void vectoras<T>::push_back(T x)


{
T* adr=new T[n+1];
int i;
for (i=0;i<n;i++) adr[i]=h[i];
adr[n]=x;
delete[] h;
n++;
h=adr;
}

Să analizăm clasa prezentată!

1. Cerinţa este ca vectorul să poată conţine componente de orice tip. Din acest
motiv, clasa este una de tip template, iar componentele vectorului au tipul formal T.

2. O altă cerinţă este ca numărul de componente să fie cunoscut în timpul


executării programului. De aici, rezultă că este necesar ca vectorul să fie alocat
dinamic. Prin urmare, clasa va conţine data membru h, care reţine un pointer către
prima componentă a vectorului. De asemenea, data membru n va reţine numărul
de componente ale vectorului. Alocarea vectorului în memorie şi iniţializarea
datelor membru este făcută de către constructorul clasei:
template <class T> vectoras<T>::vectoras(int n)
{
this->n=n;
h=new T[n+1];
}

3. Pentru a accesa componentele vectorului în mod clasic, aşa cum suntem


obişnuiţi, vom supraîncărca operatorul ”[]” astfel încât metoda rezultată să
întoarcă o referinţă către componenta de indice k.
template <class T> T& vectoras<T>::operator[](int k)
{ return h[k]; }

Din acest moment, se pot scrie deja primele programe care permit lucrul cu vectori.

133
PARTEA A V-A

Exemplu. Iată secvenţele prin care se citesc şi se afişează doi vectori. Unul cu
componente de tip float (stânga), altul cu componente de tip char (dreapta):
vectoras<float> v(5); int i; vectoras<char> v(5); int i;
for (i=0;i<5;i++) cin>>v[i]; for (i=0;i<5;i++) cin>>v[i];
for (i=0;i<5;i++) cout<<v[i]; for (i=0;i<5;i++) cout<<v[i];

4. Clasa trebuie înzestrată cu iteratori. Ce sunt ei? Nişte pointeri, dar mai evoluaţi.
E drept, efectul utilizării iteratorilor este evident în cazul altor structuri, mai puţin
cea de vector. Oricum, pentru simplitate, am preferat să exemplificăm crearea
acestora pentru vectori.

Ce ştim despre iteratori?


- Ei sunt obiecte - prin urmare, vom considera clasa iterator, care este
înzestrată cu un constructor fără parametri. Clasa iterator este inclusă în clasa
vectoras<T>. Iat-o:
class iterator
{ T* p;
public:
iterator() { p=0; }
iterator(T* adr) { p=adr ;};
T& operator*() { return *p; }
void operator++() { p++; }
void operator=(iterator it1) { p=it1.p; }
int operator !=(iterator it1) { return p!=it1.p; }
};

- Un iterator indică un anumit obiect al containerului. În cazul de faţă, iteratorul


va indica o anumită componentă a vectorului. Prin urmare, clasa va avea ca dată
membru un pointer către tipul formal T, adică tipul elementelor vectorului, numit p.
Constructorul iniţializează cu 0 data membru.
- În vederea parcurgerii elementelor structurii, variabilele de tip iterator se pot
incrementa. Semnificaţia incrementării este de a indica adresa următorului obiect
al containerului. În cazul de faţă, semnificaţia este de a indica următoarea
componentă a vectorului. Pentru a fi permisă incrementarea, clasa conţine o
metodă care supraîncarcă operatorul “++“. În fapt, ea va incrementa pointer-ul din
data membru p, pointer ce reţine adresa unei componente.
- Iteratorii suportă comparări de genul != (inegal). Din acest motiv, clasa
conţine o metodă care supraîncarcă operatorul “!=“ şi returnează o valoare diferită
de 0 în cazul în care iteratorul obiectului curent nu este egal cu iteratorul transmis
ca parametru. În fapt, se compară conţinuturile datelor membru, p.
- Între variabilele care conţin iteratori sunt permise atribuiri. Prin urmare, clasa
va conţine o metodă care supraîncarcă operatorul “=” şi care permite copierea în
obiectul curent a conţinutului altui iterator.
- Pornind de la un iterator, se poate adresa conţinutul obiectului, la fel cum
pornind de la un pointer, se poate adresa conţinutul unei variabile indicată de el.
Pentru a realiza aceasta, vectoras<T> conţine o clasă care supraîncarcă
operatorul “*“ şi care întoarce o referinţă la obiectul (componenta) indicat de iterator.

134
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

- În plus, clasa iterator este înzestrată cu un constructor de copiere. Acesta


va fi folosit, aşa cum veţi vedea, de alte metode ale clasei vectoras<T>. Practic,
constructorul de copiere obţine un obiect iterator pornind de la un pointer către un
obiect al containerului.

5. Aşa cum am învăţat în lecţia precedentă, clasa container conţine, de regulă,


două metode care returnează adresa primului obiect din container şi adresa
ultimului element (cel care nu reţine nicio valoare). Pentru aceasta, containerul va
avea două metode: iterator begin() şi iterator end(). Prima returnează
un iterator ce conţine adresa primei componente a vectorului, conţinutul datei
membru h. A doua returnează adresa componentei n+1 care, în mod convenţional,
nu conţine nici un obiect. Să observăm faptul că cele două metode returnează un
obiect de tip iterator, dar valoarea returnată este de fapt de tipul T*. Este posibil,
deoarece am înzestrat clasa iterator cu un constructor de copiere.
template <class T> typename vectoras<T>::iterator
vectoras<T>::begin()
{ return h; }
template <class T> typename vectoras<T>::iterator
vectoras<T>::end()
{ return h+n; }

De acum, putem accesa obiectele prin intermediul iteratorilor pe care i-am


construit.
Exemplu. Iată secvenţele prin care, cu ajutorul iteratorilor, se citesc şi se afişează
doi vectori: unul, cu componente de tip float (stânga) şi altul, cu componente de
tip char (dreapta):

vectoras<float> v(5); vectoras<char> v(5);


vectoras<float>::iterator it; vectoras<char>::iterator it;
for (it=[Link]();it!=[Link](); for (it=[Link]();it!=[Link]();
++it) cin>>*it; ++it) cin>>*it;
for (it=[Link]();it!=[Link](); for (it=[Link]();it!=[Link]();
++it) cout<<*it<<" "; ++it) cout<<*it<<" ";

6. De regulă, clasele care generează containere secvenţiale conţin o metodă prin


care se poate adăuga la sfârşitul unui container un obiect. În cazul nostru, se poate
adăuga la sfârşitul unui vector o componentă cu o anumită valoare. Metoda se
numeşte push_back(). De fapt, vă daţi seama, algoritmul constă în alocarea
dinamică a unui vector cu n+1 componente de tipul T, copierea valorilor primelor n
componente în vectorul nou alocat, adăugarea noii valori, transmisă ca parametru,
pe poziţia n+1, eliberarea memoriei ocupate de vechiul vector, memorarea în n a
noului număr al componentelor şi în h, un pointer către noul vector.
template <class T> void vectoras<T>::push_back(T x)
{ T* adr=new T[n+1]; int i;
for (i=0;i<n;i++) adr[i]=h[i];
adr[n]=x; delete[] h; n++;
h=adr;
}

135
PARTEA A V-A

Iată şi un exemplu de utilizare a acestei metode:


vectoras<int> v(5);
int i;
for (i=0;i<5;i++) v[i]=i+1;
v.push_back(6);
for (i=0;i<6;i++) cout<<v[i];

OBSERVAŢIE. Pentru vectori, deşi prezentă, metoda este un exemplu de


ineficienţă. Desigur, nu se poate spune acelaşi lucru dacă am avea o aceeaşi
metodă pentru o listă liniară dublu înlănţuită.

Rămâne să exemplificăm modul în care se realizează un algoritm. Algoritmul


accesează elementele containerului (în exemplu, vectorul) prin intermediul
iteratorilor. El este sub forma unei funcţii template şi calculează suma elementelor
din container:
long suma(vectoras<int>::iterator st, vectoras<int>::iterator fin)
{ long s=0; vectoras<int>::iterator it;
for(it=st;it!=fin;++it) s+=*it;
return s;
}

Iată şi o secvenţă care utilizează algoritmul: se citeşte vectorul şi se afişează


suma elementelor:
vectoras<int> v(5);
vectoras<int>::iterator it;
for (it=[Link]();it!=[Link](); ++it) cin>>*it;
cout<<"Suma este "<<suma([Link](),[Link]());

PROBLEME PROPUSE
1. Adăugaţi clasei vectoras<T> metoda int size() care returnează numărul
de componente ale vectorului.
2. Adăugaţi clasei vectoras<T> metoda int empty() care returnează
valoarea 1, dacă vectorul nu conţine nicio componentă şi 0, în caz contrar.
3. Adăugaţi clasei vectoras<T> metoda push_front( T x) care adaugă o
primă componentă vectorului, care conţine obiectul x.
4. Adăugaţi clasei vectoras<T> metoda swap(vectoras<T>& v) care are
rolul de a interschimba vectorul reţinut de obiectul curent cu cel transmis ca
parametru.
5. Lucrare în colectiv. Creaţi o clasă listuta<T> care are rolul de a crea şi
gestiona o listă liniară dublu înlănţuită care reţine obiecte de tipul T. Clasa va
conţine aceleaşi metode, cu acelaşi nume şi aceiaşi parametri ca şi clasa
vectoras<T>. De asemenea, se cere posibilitatea de a utiliza iteratorii exact în
acelaşi mod în care îi utilizăm pentru vectoras<T>. Se vor construi şi metodele
propuse ca probleme pentru vectoras<T>.

136
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

LECŢIA 5. Clasa vector<T>

Până în prezent, am utilizat de nenumărate ori vectori. De fiecare dată când


defineam un vector, precizam tipul elementelor de bază, ca în exemplele de mai jos:
Exermple:
a) int V[00] // vector cu 100 de componente de tip int
b) double V[50] // vector cu 50 de componente de tip double
c) struct persoana
{ string nume;
int varsta;
};
persoana P[200]; // vector cu 200 de componente de tip persoana

 În C++ avem o clasă template numită vector<T>, prin care se pot declara
vectori în care, evident, toate componentele au acelaşi tip, T. Clasa vector
este de tip container şi este înzestrată cu iteratori. Este utilă pentru că:
- deşi toate elementele vectorului trebuie să fie de acelaşi tip (T), tipul T
poate fi unul oarecare;
- numărul de componente al vectorului este variabil; se pot adăuga şi se
pot şterge componente.

Clasa este înzestrată cu următoarele metode:

 vector () – creează un vector vid (vector fără nici o componentă).


Evident, după creare, vectorului i se pot adăuga componente.
 vector(size_type n) - se creează un vector cu n componente.
 vector(size_type n, const T& t) - se creează un vector cu n
componente iniţializate cu valoarea t.
 vector(const vector&) - constructor de copiere.
 ~vector() – destructor.
 void swap(vector&) - interschimbă conţinutul vectorului reţinut de
obiectul curent cu cel primit ca parametru. Iată că şi conţinuturile a doi
vectori pot fi interschimbate...
Exemple:
a) vector<float> V(10); - se creează un vector, numit V, cu 10 compo-
nente de tip float;
b) vector<int> V(4,1); - se creează un vector V cu 4 componente de tip
int, unde fiecare componentă reţine valoarea 1.
c) vector<int> V1=vector<int>(V); - V este un vector deja creat şi se
creează V1 cu acelaşi conţinut ca şi V.

137
PARTEA A V-A

 T& operator[](size_type i) - are rolul de a întoarce o referinţă


către elementul de indice i. Metoda este realizată prin supraîncărcarea
operatorului “[]“.
 void pop_back() - şterge ultima componentă a vectorului.
 void push_back(const T&) - adaugă o componentă la sfârşitul vectorului.

 size_type size() const - returnează numărul de componente ale


vectorului.
 bool empty() - returnează true (1) dacă vectorul reţinut de obiectul
curent este vid (nu conţine nicio componentă).
Exemple:

 Abordarea clasică, care utilizează supraîncărcarea operatorului “[]“. Se


creează un vector cu 3 componente. Acestea se citesc şi se afişează. În
programul din stânga, vectorul are toate componentele de tip int, iar în cel
din dreapta, toate componentele de tip char. Observaţie: size_t este un
tip întreg fără semn. Pentru a evita avertismentele la compilare, variabila i
este tot de tipul size_t.

#include <iostream> #include <iostream>


#include <vector> #include <vector>
using namespace std; using namespace std;
int main() int main()
{ vector<int> V(3); int i; { vector<char> V(3); size_t i;
for (i=0;i<[Link]();i++) for (i=0;i<[Link]();i++)
cin>>V[i]; cin>>V[i];
for (i=0;i<[Link]();i++) for (i=0;i<[Link]();i++)
cout<<V[i]; cout<<V[i];
} }

 Crearea vectorului prin adăugarea elementelor la sfârşit. Metoda este


ineficientă, pentru că fiecare adăugare a unui element necesită crearea unui
alt vector, recopierea, etc. În programul din stânga, vectorul are toate
componentele de tip int, iar în cel din dreapta, toate componentele de
tip char.

#include <iostream> #include <iostream>


#include <vector> #include <vector>
using namespace std; using namespace std;
int main() int main()
{ vector<int> V; { vector<char> V;
V.push_back(10); V.push_back('a');
V.push_back(3); V.push_back('b');
V.push_back(7); V.push_back('c');
for (size_t i=0;i<3;i++) for (size_t i=0;i<3;i++)
cout<<V[i]<<" "; cout<<V[i]<<" ";
} }

138
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

 Pentru a accesa componentele unui vector, se pot utiliza iteratori. Aceştia


sunt de două feluri: iterator (utilizaţi pentru parcurgerea vectorului de la
început către sfârşit) şi reverse_iterator (utilizaţi pentru parcurgerea
vectorului de la sfârşit către început). Pentru a declara un iterator, se
procedează în conformitate cu declaraţiile:

a) vector<T>::iterator nume_iterator;
b) vector<T>::reverse_iterator nume_iterator;

Pentru parcurgerea vectorului se utilizează şi metodele:

 iterator begin() - returnează o valoare de tip iterator care indică


primul element al vectorului.
 iterator end() - returnează o valoare de tip iterator care indică
sfârşitul vectorului. Atenţie: se presupune că vectorul conţine o ultimă
componentă care nu reţine nicio valoare. În această logică, evident,
ultima valoare reţinută de vector este în penultima componentă. Practic,
metoda returnează un iterator către ultima componentă.

 reverse_iterator rbegin() - returnează o valoare de tip


reverse_iterator care indică primul element al vectorului, dacă acesta
este privit ca având componentele în ordine inversă.
 reverse_iterator rend() - returnează o valoare de tip
reverse_iterator care indică ultimul element al vectorului, dacă acesta
este privit cu componentele în ordine inversă.

OBSERVAŢII FOARTE IMPORTANTE:


a) Pornind de la valoarea reţinută de variabila iterator, se poate accesa conţinutul
componentei, cu ajutorul operatorului “*“, la fel ca în cazul variabilelor care reţin
pointeri. Astfel, dacă variabila iterator it indică prima componentă a vectorului,
atunci prin *it obţinem conţinutul primei componente a vectorului. De asemenea,
dacă obiectele reţinute de vector conţin metode sau mai multe date membru,
pornind de la it, acestea pot fi adresate, aşa cum suntem obişnuiţi, cu ajutorul
operatorului “->“.
b) Dacă o variabilă iterator it reţine adresa componentei de indice k, atunci, în
urma atribuirii it=it+i (it+=i), variabila it va indica componenta de indice k+i.
c) Nu sunt permise comparări între conţinuturile a două variabile iterator cu
operatorii “<“ , “>“ , “<=“ sau “>=“.

Exemplu. Programul din stânga citeşte şi afişează un vector folosind iteratori şi


cel din dreapta, la citire foloseşte iterator, iar la scriere reverse_iterator.
Prin urmare, acesta va afişa componentele vectorului în ordine inversă.

139
PARTEA A V-A

#include <iostream> #include <iostream>


#include <vector> #include <vector>
using namespace std; using namespace std;
int main() int main()
{vector<int> V(3); {vector<int> V(3);
vector<int>::iterator it; vector<int>::iterator it;
for (it=[Link]();it!=[Link](); vector<int>::reverse_iterator it1;
++it) cin>>*it; for (it=[Link]();it!=[Link]();
for(it=[Link]();it!=[Link](); ++it) cin>>*it;
++it) cout<<*it; for (it1=[Link]();it1!=[Link]();
} ++it1) cout<<*it1;
}

Am văzut că adăugarea unor componente la sfârşitul vectorului (containerului),


este o operaţie ineficientă. Se poate face ceva care să sporească eficienţa acestei
operaţii? Răspunsul este afirmativ. Să analizăm următoarele două metode:

 void reserve(size_type n) - rezervă memorie pentru vector astfel


încât acesta să poată avea n componente. Dacă vectorul are deja un număr
mai mare ca n de componente, efectul acestei metode este nul. Dacă nu, de
fapt, după executare, vectorul va avea deja alocate n componente, dar
acestea nu sunt “văzute” de metoda size() şi nici de iteratori. În schimb,
dacă se utilizează push_back(), operaţia se face foarte rapid pentru că se
va utiliza o componentă deja existentă (nu sunt necesare operaţii precum
alocarea, copierea, etc).

 size_type capacity() const - returnează numărul de componente


alocate la un moment dat pentru vector.

Exemplu. Analizaţi programul următor:

#include <iostream>
#include <vector>
using namespace std;
int main()
{ vector<int> V; [Link](100);
// V are alocate 100 de componente
V.push_back(3); V.push_back(7);
// Lui V i se adauga la sfarsit doua componente
// In fapt, se folosesc primele doua componente ale lui V
cout<<[Link]()<<endl;
// S-a afisat 2
cout<<[Link]()<<endl;
// S-a afisat 100
vector<int>::iterator it;
for (it=[Link]();it!=[Link]();it++) cout<<*it<<" ";
// S-au afisat 3 si 7
}

OBSERVAŢIE FOARTE IMPORTANTĂ. Atunci când utilizaţi clasa vector<T>, dacă


doriţi să aveţi programe performante, rezervaţi vectorului un număr suficient de
componente. Toate operaţiile se vor executa mult mai rapid.

140
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

Alte metode ale clasei vector<T>:

 void insert(iterator it, const T& x) are rolul de a insera o


componentă în vector, plasată înaintea componentei a cărei adresă este
reţinută de it şi va reţine valoarea transmisă prin parametrul x. După
inserare, it va indica aceeaşi componentă, adică cea care urmează după
componenta inserată.
Exemplu. Dacă V=(1,2,3,4,5), după executarea apelului de mai jos, V reţine:
(1,2,9,3,4,5).
[Link]([Link]()+2, 9);

 iterator erase(iterator it) are rolul de a şterge componenta


indicată de it. După ştergere, it va indica componenta următoare celei
care a fost ştearsă.
Exemplu. Dacă V=(1,2,3,4,5), după executarea apelului de mai jos V reţine:
(1,2,4,5).
[Link]([Link]()+2);

 iterator erase(iterator it1, iterator it2) - şterge


componentele începând de la cea indicată de it1, până la cea indicată de
it2. Atenţie: componenta indicată it2 nu este ştearsă. După ştergere, cei
doi iteratori vor indica componenta care iniţial era indicată de it2, adică
prima componentă care urmează ultimei componente şterse.
Exemplu. Dacă V=(1,2,3,4,5), după executarea apelului de mai jos, V reţine:
(1,4,5).
[Link]([Link]()+1,[Link]()+3);

OBSERVAŢIE. Metodele insert() şi erase() nu afectează capacitatea


vectorului (cea indicată de capacity()).
Exemplu. Se citeşte n, număr natural, un vector cu n componente numere întregi
şi un întreg k. Programul elimină toate componentele care-l conţin pe k.
#include <iostream>
#include <vector>
using namespace std;
int main()
{ int n,k,i;
cout<<"n="; cin>>n;
cout<<"k="; cin>>k;
vector<int> V(n);
for (i=0;i<n;i++) { cout<<"V["<<i<<"]="; cin>>V[i]; }
vector<int>::iterator it;
for (it=[Link]();it!=[Link]();++it)
if (*it==k) { [Link](it); --it; }
for (it=[Link]();it!=[Link]();++it) cout<<*it<<" ";
}

141
PARTEA A V-A

 Cu ajutorul clasei vector<T> se poate lucra cu matrice? Răspunsul este


afirmativ. Se utilizează constructorul următor:
vector(size_type n, const T& t)

Iniţial, se declară un vector cu componente de tipul de bază al elementelor


matricei, de exemplu int: vector<int> lin(n);. Un astfel de vector are, în
exemplu, n componente. Apoi, se declară un alt vector cu m componente de tip
vector<T>, în exemplu mat, iar componentele acestuia se iniţializează cu
vectorul lin: vector<vector<int>> mat(m,lin);. În concluzie, am obţinut o
matrice cu m linii şi n coloane.

Exemplu. Programul următor citeşte şi afişează elementele unei matrice cu m linii


şi n coloane. Elementele matricei au tipul int.
#include <iostream>
#include <vector>
using namespace std;
int main()
{ int m,n,i,j;
cout<<"m="; cin>>m;
cout<<"n="; cin>>n;
vector<int> lin(n);
vector<vector<int> > mat(m,lin);
for (i=0;i<m;i++)
for (j=0;j<n;j++)
{ cout<<"Mat["<<i<<','<<j<<"]=";
cin>>mat[i][j];
}
for (i=0;i<m;i++)
{ for (j=0;j<n;j++) cout<<mat[i][j]<<" ";
cout<<endl;
}
}

Clasa vector<T> are două funcţii prietene:

 bool operator==(const vector&, const vector&) -


returnează true dacă cei doi vectori sunt egali;
 bool operator<(const vector&, const vector&) - compară
lexicografic doi vectori şi returnează true (1) dacă primul vector este
în ordine lexicografică mai mic decât al doilea, altfel returnează false (0).

Exemplu. Secvenţa următoare afişează 0. Pentru că v1[0]=9 şi v2[0]=3, v1


este în ordine lexicografică mai mic decât v2.
vector<int> v1(3), v2(3);
v1[0]=9;
v2[0]=3;
v2[1]=2;
v2[2]=9;
cout<<(v1<v2);

142
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

PROBLEME PROPUSE
1. Se citeşte un vector cu n componente de tip double. Se cere să se afişeze
valorile reţinute de acesta în ordine descrescătoare.

2. Fie structura următoare care descrie partea reală şi partea imaginară a unui
număr complex: struct complex { double x,y;};. Scrieţi un program care
citeşte şi afişează un vector cu n numere complexe. Programul se va realiza în
două variante: clasic, prin utilizarea unui indice şi prin utilizarea iteratorilor.
3*. Citiţi şi afişaţi o matrice cu m linii şi n coloane utilizând exclusiv iteratorii.

4. Fiind dată o matrice cu m linii şi n coloane, obţineţi o matrice în care liniile sunt
sortate lexicografic.
5. Fiind dată o matrice cu m linii şi n coloane, obţineţi o matrice în care coloanele
sunt sortate lexicografic.
6*. Fiind dată o matrice cu n linii şi n coloane şi un număr natural k<n, obţineţi o
nouă matrice prin eliminarea liniei k şi a coloanei k. Se va utiliza metoda erase().

7. Lucrare de tip referat. Studiaţi clasa de tip container bit_vector. Un obiect


al acestei clase este un vector de valori booleene (1=true, 0=false). Rolul ei este
ca, în cazul masivelor booleene, să se facă economie de memorie reţinând pentru
fiecare element 1 bit. Daţi exemple de utilizare ale acestei clase în teoria grafurilor
(pentru elevii din clasa a XI-a). Documentaţia o găsiţi la adresa indicată la lecţia 1.

INDICAŢII
2. Vectorul se defineşte astfel: vector<complex> v(10);. În cazul iteratorilor
utilizaţi operatorul “->“, de exemplu: v[2]->x este partea reală a numărului
complex reţinut de componenta de indice 2.
3. O matrice cu m linii şi n coloane este un vector de vectori. Elementele vectorului
mat sunt vectori (linii). Există m linii, iar fiecare linie are n elemente.
Vom avea un iterator de linie, numit linie:
vector<vector<int> >::iterator linie;

Vom avea un iterator care parcurge elementele unei linii, numit coloana:
vector<int>::iterator coloana;

O linie este un vector cu n componente, deci este un obiect de tip vector<int>.


Fiind dat un iterator care indică linia, linie, atunci pentru a obţine linia
propriu-zisă va trebui să scriem *linie şi să apelăm, de exemplu, metoda
begin(). Atunci, va trebui să scriem (*linie).begin(). Aşa cum ştim, în locul
acestei adresări greoaie vom scrie, ca şi în cazul pointer-ilor, linie->begin().

143
PARTEA A V-A

#include <iostream>
#include <vector>
using namespace std;
int main()
{ int m,n;
cout<<"m="; cin>>m;
cout<<"n="; cin>>n;
vector<int> lin(n);
vector<vector<int> > mat(m,lin);
vector<vector<int> >::iterator linie;
vector<int>::iterator coloana;
for (linie=[Link]();linie!=[Link]();++linie)
for (coloana=linie->begin();coloana!=linie->end();++coloana)
cin>>*coloana;
for (linie=[Link]();linie!=[Link]();++linie)
{ for (coloana=linie->begin();coloana!=linie->end();++coloana)
cout<<*coloana<<" ";
cout<<endl;
}
}

LECŢIA 6. Clasa list<T>


Un obiect al clasei list reprezintă o listă liniară dublu înlănţuită. Prin
utilizarea obiectelor acestei clase, lucrul cu listele liniare dublu înlănţuite este cu
mult mai uşor faţă de modul cum suntem deja obişnuiţi. Programatorul nu mai
gestionează informaţiile de adresă pentru elementele ei. Practic, prin utilizarea
unor metode ale clasei, toate operaţiile clasice cu liste se pot realiza imediat.

 Clasa list este înzestrată cu următorii constructori (vedeţi mai jos), dar şi
cu destructorul ~list().

 list() - creează o listă vidă;


 list(size_type n) - creează o listă cu n elemente;
 list(size_type n, const T& t) - creează o listă cu n elemente
iniţializate cu o anumită valoare;
 list(const list&) - constructor de copiere; creează o listă pornind de
la o altă listă, deja creată.

Exemple:
 list<int> L; creează o listă vidă. Fiecare element al ei reţine o valoare
întreagă.
 list<int> L(5); - creează o listă cu 5 elemente.
 list<int> L(5, 7); - creează o listă cu 5 elemente şi toate
elementele reţin valoarea 7.
 list<int> Li=L; - creează o listă, numită Li, rezultată în urma copierii
listei L.

144
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

 Există metode care permit accesul la începutul şi sfârşitul listei.

 void push_back(const T&) - metoda adaugă un element la sfârşitul


unei liste; valoarea pe care acesta o reţine este transmisă ca parametru;
 void push_front(const T&) - metoda adaugă un element la începutul
unei liste; valoarea pe care acesta o reţine este transmisă ca parametru;
 void pop_front() - şterge primul element al listei;
 pop_back() - şterge ultimul element al listei;
 T& front() - returnează o referinţă la primul element;
 T& back() - returnează o referinţă la ultimul element al listei.

Exemple:

 Dacă L este o listă cu elemente de tip int, atunci L.push_back(7);


adaugă la sfârşitul listei un element care reţine 7.
 Dacă L este o listă cu elemente de tip int, atunci L.push_front(7);
adaugă la începutul listei un element care reţine 7.
 Dacă L este o listă, atunci prin L.pop_front(); L.pop_back(); se
şterg primul şi ultimul element din listă.
 Dacă L este o listă, atunci prin cout<<[Link](); se afişează valoarea
reţinută de primul element al ei, iar prin [Link]()=100;, primul element
al listei ia valoarea 100.

 Şi în cazul listelor se pot utiliza iteratori. Aceştia sunt de două feluri:


iterator (utilizaţi pentru parcurgerea listei de la început către sfârşit) şi
reverse_iterator (utilizaţi pentru parcurgerea listei de la sfârşit către
început). Pentru parcurgerea listei se utilizează şi metodele:

 iterator begin() - returnează o valoare de tip iterator care indică


începutul listei;
 iterator end() - returnează o valoare de tip iterator care indică
sfârşitul listei;
 reverse_iterator rbegin() - returnează o valoare de tip
reverse_iterator care indică primul element al listei, dacă aceasta este
privită ca listă inversată (în sens clasic, indică ultimul element al listei);
 reverse_iterator rend() - returnează o valoare de tip
reverse_iterator care indică ultimul element al listei, dacă aceasta este
privită ca listă cu elementele în ordine inversă.

OBSERVAŢIE. Dacă x este o variabilă de tip iterator care reţine o anumită valoare,
atunci se poate utiliza x++, x--, dar nu sunt permise expresii de genul x+k (unde
k este un întreg).

145
PARTEA A V-A

Exemplu. Programul următor creează o listă liniară dublu înlănţuită cu 10


elemente şi afişează valorile reţinute de acestea. Lista este parcursă de la stânga
la dreapta şi de la dreapta la stânga.
#include <iostream>
#include <list>
using namespace std;
int main()
{ list<int> L;
int i;
for (i=1;i<=10;i++) L.push_front(i);
list<int>::iterator it;
for(it=[Link]();it!=[Link]();it++) cout<<*it<<" ";
cout<<endl;
list<int>::reverse_iterator it1;
for(it1=[Link]();it1!=[Link]();it1++) cout<<*it1<<" ";
}

 Clasa este înzestrată cu o serie de metode care permit inserarea şi


ştergerea unor elemente.

 iterator insert(iterator p, const T& v); - inserează


elementul x în listă înaintea elementului indicat de iterator şi returnează o
valoare de tip iterator care indică elementul inserat.

Exemplu. Fie lista L care reţine elementele 1 2 3 4 5 6... În urma executării


secvenţei de mai jos, L va reţine: 1 2 3 9 4 5 6...
for (i=1;i<=3;i++) it++;
[Link](it, 9);

 iterator erase(iterator p) - şterge elementul indicat de iterator şi


returnează o valoare de tip iterator care indică elementul care urmează
elementului şters.

Exemplu. Fie lista L care reţine elementele 1 2 3 4 5 6... În urma executării


secvenţei de mai jos, L va reţine: 1 2 3 5 6...
for (i=1;i<=3;i++) it++;
[Link](it);

 iterator erase(iterator st, iterator fin) - şterge o


secvenţă de elemente din listă, cuprinsă între valoarea indicată de st şi cea
indicată de fin. Valoarea indicată de fin nu este ştearsă.

Exemplu. Fie lista L care reţine elementele 1 2 3 4 5 6... În urma executării


secvenţei următoare, L va reţine: 1 2 5 6...
list<int>::iterator it=[Link](),it1;
for (i=1;i<=2;i++) it++;
it1=it;
for (i=1;i<3;i++) it1++;
[Link](it,it1);

146
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

 void remove(const T& v) - şterge din listă toate elementele care


reţin obiectul transmis ca parametru.

Exemplu. Fie lista L care reţine elementele 1 2 3 4 3. În urma executării


secvenţei [Link](3);, L va reţine: 1 2 4.

 Reţineţi şi următoarele metode:

 void swap(list&) - interschimbă conţinuturile a două liste.

Exemplu. Programul următor creează două liste, le interschimbă elementele şi le


afişează:
#include <iostream>
#include <list>
using namespace std;
int main()
{ list<int> L,L1;
int i;
for (i=1;i<=5;i++) L.push_back(i);
for (i=6;i<=8;i++) L1.push_back(i);
[Link](L1);
list<int>::iterator it;
cout<<"Lista L"<<endl;
for(it=[Link]();it!=[Link]();it++)
cout<<*it<<" ";
cout<<endl<<"Lista L1"<<endl;
for(it=[Link]();it!=[Link]();it++)
cout<<*it<<" ";
}

 void clear() - şterge toate elementele unei liste.


 bool empty() - returnează true (1) dacă lista este vidă şi false (0),
în caz contrar.

 Clasa conţine şi următoarele două funcţii prietene:


 bool operator==(const list&, const list&) - testează
egalitatea între două liste;
 bool operator<(const list&, const list&) - compară
lexicografic două liste.

EXERCIŢII PROPUSE
1. Un fişier text conţine pe prima linie un număr natural n>10, iar pe următoarele
linii, n numere naturale. Scrieţi funcţii care:
a) creează o listă care conţine toate numerele naturale de pe linia 2 (în ordinea
inversă în care se găsesc pe linie);

147
PARTEA A V-A

b) elimină din listă toate elementele care reţin aceeaşi valoare cu al treilea
element al listei;
c) afişează elementele listei în ordine inversă;
d) şterge primele k elemente de la începutul listei şi le adaugă la sfârşitul ei.
2. Creaţi o listă liniară cu 3 obiecte. Fiecare obiect conţine numele şi vârsta unei
persoane.
3. Considerăm lista creată la 2. Scrieţi o secvenţă care modifică numele reţinut
de persoana memorată de ultimul element al listei (care reţine date referitoare la o
persoană).
4. Creaţi o listă în care fiecare element reţine un vector cu n componente de tip
float. Afişaţi conţinutul ca pe matrice. Prima linie va conţine elementele
vectorului din primul element al listei, a doua linie va conţine elementele vectorului
din al doilea element al listei, ş.a.m.d.
5. Construiţi o listă liniară cu elemente de tip double. Sortaţi-le astfel încât
acestea să se găsească în listă în ordine descrescătoare.
7. Lucrare de tip referat. Studiaţi clasa de tip container slist<T>. Un obiect al
acestei clase este o listă liniară simplu înlănţuită. Daţi exemple de utilizare
ale acesteia. Documentaţia o găsiţi la adresa indicată la lecţia 1.

INDICAŢII
1.a). Adăugaţi elementele în listă cu push_front(). c). Utilizaţi remove()!
2. Programul este următorul:
#include <iostream>
#include <list>
using namespace std;
struct pers
{ string nume;
int varsta;
}p;
int main()
{ list<pers> L;
int i;
for (i=1;i<=3;i++)
{ cout<<"Pers "<<i<<endl;
cout<<"nume ";cin>>[Link];
cout<<"varsta ";cin>>[Link];
L.push_back(p);
}
list<pers>::iterator it;
for (it=[Link]();it!=[Link]();it++)
cout<<it->nume<<" "<<it->varsta<<endl;
}

3. De exemplu, front().nume="Ion";. Vedeţi cât de elegant se pot utiliza


referinţele?

148
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

LECŢIA 7. Clasa set<T,Comp>


Containerele acestei clase reţin mulţimi. Evident, toate elementele unei
mulţimi trebuie să fie de acelaşi tip. După cum se ştie, o mulţime conţine numai
elemente distincte. Aceasta înseamnă că dacă unei mulţimi i se adaugă
un element pe care acesta îl conţine, mulţimea rămâne neschimbată.

 Fiind vorba de o mulţime, am fi tentaţi să presupunem că pentru a o memora


se utilizează vectorul caracteristic. În realitate, pentru memorare se
7
folosesc arbori de căutare echilibraţi. Arborii de căutare echilibraţi permit
căutarea cheii, adică identificarea unui element al mulţimii, în O(log(n)).
Deoarece este un algoritm extrem de rapid, va fi util să utilizăm această
clasă în memorarea mulţimilor. Mai mult, parcurgerea acestora în inordine
(stânga, vârf, dreapta) are ca efect afişarea cheilor (a elementelor mulţimii)
în ordine crescătoare. Toate aceste avantaje se obţin fără un efort deosebit
de programare, doar utilizând clasa set!

 Pentru accesarea elementelor unei mulţimi, se pot utiliza, aşa cum suntem
obişnuiţi, iteratorii. Aceştia sunt de tip iterator, pentru parcurgerea
elementelor mulţimii în ordine crescătoare şi reverse_iterator, pentru
parcurgerea elementelor mulţimii în ordine descrescătoare. Asupra
variabilelor care reţin iteratori se pot aplica operatorii ”++” şi ”--”, cu
semnificaţia de a indica elementul următor, respectiv elementul precedent.
Pentru utilizarea iteratorilor, se pot folosi metodele de mai jos:

 iterator begin() - returnează un iterator către cel mai mic element


din listă;
 iterator end() - returnează un iterator către ultimul element al
structurii, cel care nu conţine nicio valoare;
 reverse_iterator rbegin() - returnează un iterator către elementul
cel mai mare al mulţimii (primul dacă ordinea elementelor este inversată);
 reverse_iterator rend() - returnează un iterator către ultimul
element al structurii, dacă se consideră elementele mulţimii în ordine
descrescătoare;

 Clasa set este înzestrată cu mai multe metode, care sunt prezentate în
continuare:

 set() – constructor – creează o mulţime vidă;


 set(const set&) – constructor de copiere;
 insert(const T& x) - metodă care se utilizează pentru inserarea unui
element; inserarea se efectuează în O(log(n)).

7
Arborii de căutare sunt prezentaţi în manualul de Informatică, Intensiv, pentru clasa a XI-a.

149
PARTEA A V-A

 void erase(iterator it) - are rolul de a şterge elementul indicat de


iterator;
 size_type erase(T& k) - are rolul de a şterge (dacă există) elementul
transmis ca parametru. Returnează o valoare diferită de 0 dacă elementul a
fost şters şi 0, în caz contrar. Ştergerea se efectuează în O(log(n));
 iterator find(const T& k) - caută obiectul primit ca parametru.
Dacă este găsit, returnează un iterator către el, altfel, returnează iteratorul
către elementul care nu conţine nicio valoare (cel indicat de end()).
Căutarea se efectuează în O(log(n));
 key_compare key_comp() const - returnează un pointer către
functorul care compară 2 elemente în vederea inserării, ştergerii, etc. Acesta
poate fi utilizat în vederea testării. După cum vom vedea, functorul poate fi
modificat de către programator pentru a efectua exact operaţia dorită de
acesta.

Exemple de utilizare:
cout<<Mult.key_comp()(1,2)<<endl; // afiseaza 1
cout<<Mult.key_comp()(2,1)<<endl; // afiseaza 0

 void erase(iterator first, iterator last) - şterge secvenţa


cuprinsă între cei doi indicatori transmişi ca parametri.

Exemple:
 Mai jos, puteţi observa două programe care creează şi afişează o mulţime
de numere reale, de tip float şi o mulţime de obiecte de tip string.
Afişarea se face în ordine crescătoare pentru T=float şi lexicografic pentru
T=string.
#include <set> #include <set>
#include <iostream> #include <iostream>
using namespace std; using namespace std;
int main() int main()
{ set<float> Mult; { set<char> Mult;
set<float>::iterator it; set<char>::iterator it;
set<float>::reverse_iterator set<char>::reverse_iterator
it1; it1;
[Link](3.7); [Link]('x');
[Link](1); [Link]('a');
[Link](3.7); [Link]('m');
[Link](4.2); [Link]('b');
[Link](1); [Link]('b');
for (it=[Link](); for (it=[Link]();
it!=[Link]();it++) it!=[Link]();it++)
cout<<*it<<" "; cout<<*it<<" ";
cout<<endl; cout<<endl;
for (it1=[Link](); for (it1=[Link]();
it1!=[Link]();it1++) it1!=[Link]();it1++)
cout<<*it1<<" "; cout<<*it1<<" ";
} }

150
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

 În condiţiile de mai sus, pentru a afişa cel mai mic element al uneia dintre
mulţimi, se execută comanda: cout<<*[Link]();

 În condiţiile de mai sus, pentru a afişa cel mai mare element al uneia dintre
mulţimi, se execută comanda: cout<<*[Link]();

 În condiţiile de mai sus, la executarea secvenţei următoare, se şterge cel


mai mic element al mulţimii:
it=[Link]();
[Link](it);

 În condiţiile de mai sus, la executarea instrucţiunii următoare, se şterge


elementul corespunzător:

[Link](1); sau Mult,erase(“a”);

 Programul următor utilizează o funcţie, numită apartine, care testează


dacă un element citit aparţine sau nu mulţimii. Practic, dacă find()
returnează iteratorul către ultima componentă a structurii, cea care nu
conţine niciun element, atunci elementul nu aparţine mulţimii, dar dacă
returnează o valoare care indică un alt element al structurii, atunci acel
element aparţine mulţimii.
#include <set>
#include <iostream>
using namespace std;

int apartine (set<int> Mult, int x)


{ return [Link](x)!=[Link](); }

int main()
{ set<int> Mult;
set<int>::iterator it;
[Link](3); [Link](1);
[Link](9); [Link](8);
[Link](12);
cout<<[Link](111);
int x; cin>>x;
if (apartine(Mult,x)) cout<<x;
else cout<<"nu apartine";
}

7. Programul următor citeşte două mulţimi şi calculează intersecţia lor. Practic, se


parcurg cu ajutorul unui iterator toate elementele unei mulţimi şi, pentru fiecare
element parcurs, se testează dacă aparţine celeilalte mulţimi. În caz afirmativ,
acesta este afişat. Să observăm că dacă prima mulţime are m elemente şi a doua
mulţime, n elemente, deoarece testul de apartenenţă la o mulţime se face în
O(log(n)), algoritmul are complexitatea O(mlog(n)). Dacă mulţimile ar fi
memorate cu ajutorul vectorului caracteristic, atunci algoritmul ar avea complexitatea
O(mn)...

151
PARTEA A V-A

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

int apartine (set<int> Mult, int x)


{ return [Link](x)!=[Link]();
}

void inters(set<int> M,set<int> M1)


{ set<int>::iterator it;
for (it=[Link]();it!=[Link]();it++)
if (apartine(M1,*it)) cout<<*it<<" ";
}

void citMult(set<int>& M)
{ int n,i,elm;
cout<<"Mutime "<<endl<<"n=";
cin>>n;
for (i=1;i<=n;i++)
{ cout<<"elem=";
cin>>elm;
[Link](elm);
}
}

set<int> M,M1;

int main()
{ citMult(M);
citMult (M1);
inters(M,M1);
}

 Pentru a putea crea o mulţime, elementele trebuie să fie distincte. În plus,


între ele trebuie să existe o relaţie de ordine: dacă x şi y sunt două elemente
ale mulţimii, atunci: fie x<y, fie y<x. Dacă pentru tipuri numerice condiţia
este îndeplinită fără probleme, ce se întâmplă în cazul altor tipuri, de
exemplu cele neomogene (create cu struct)? Ce semnificaţie are pentru
aceste tipuri operatorul “<“? În astfel de situaţii, trebuie precizată
semnificaţia operatorului “<“. Pentru aceasta, vom proceda astfel:

a) se va crea o clasă care conţine o metodă care supraîncarcă operatorul


“()“, adică se creează o funcţie obiect (functor). Pentru o viteză sporită de
executare, este indicat ca metoda să fie tip inline. Ea va avea doi
parametri de tipul elementelor din mulţime şi va returna o valoare diferită de
0, dacă primul element este “strict mai mic” decât al doilea element.
Semnificaţia acestei expresii “strict mai mic” o stabileşte programatorul când
scrie metoda.

b) Declararea unui obiect al clasei set va conţine un al doilea parametru


de tip, numele clasei care conţine metoda (vezi clasele template).

152
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

Exemple:

 Primul exemplu este dat mai mult ca amuzament. Considerăm că


elementele mulţimii sunt de tip int. Schimbăm semnificaţia lui "strict mai
mic” cu “strict mai mare”. Rulaţi programul şi veţi vedea că totul este în
regulă, numai că afişarea mulţimii se face în ordine strict crescătoare!

#include <set>
#include <iostream>
using namespace std;
class redefinesc
{ public:
bool operator()(int a, int b)
{ return a>b; }
};
int main()
{ set<int,redefinesc> Mult;
set<int>::iterator it;
set<int>::reverse_iterator it1;
[Link](3);[Link](1);
[Link](4); [Link](7);
for (it=[Link]();it!=[Link]();it++)
cout<<*it<<" ";
}

 Elementele mulţimii sunt de tip persoana. O variabilă de tip persoana


conţine numele persoanei şi vârsta. Dacă x şi y sunt două variabile de acest
tip, atunci x<y dacă, lexicografic (alfabetic), numele lui x este mai mic decât
numele lui y. Programul construieşte mulţimea şi o afişează.
Răspuns. În astfel de cazuri, cheia este dată de o metodă conţinută într-o
altă clasă, (lexicografic), iar metoda este obţinută de supraîncărcarea
operatorului “()“. Analizaţi programul următor, în care mulţimea este
formată din elemente de tip persoana, iar relaţia de ordine dintre ele este
dată de metoda operator() (care supraîncarcă operatorul “()“):

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

struct persoana
{ string nume;
int varsta;
} p;

class lexicografic
{ public:
bool operator()(persoana p1, persoana p2)
{ return [Link]<[Link]; }
};

153
PARTEA A V-A

int main()
{ set<persoana,lexicografic> Mult;
set<persoana,lexicografic>::iterator it;
[Link]="Ion";
[Link]=32;
[Link](p);

[Link]="Ancu";
[Link]=17;
[Link](p);

[Link]="Titi";
[Link]=18;
[Link](p);

[Link]="Sorin";
[Link]=21;
[Link](p);

for (it=[Link]();it!=[Link]();it++)
cout<<(*it).nume<<" "<<(*it).varsta<<endl;
}

PROBLEME PROPUSE
1. Citiţi două mulţimi de numere reale şi calculaţi reuniunea lor.
2. Citiţi două mulţimi de numere reale şi calculaţi diferenţa lor.
3. Citiţi un cuvânt şi afişaţi dacă literele lui sunt distincte.
4. Citiţi n cuvinte şi afişaţi dacă sunt distincte sau nu.

LECŢIA 8. Clasa multiset<T,Comp>

Un container de tip multiset<T,Comp> este asemănător unui container de


tip set<T,Comp>, numai că este permis ca un element să se găsească de mai
multe ori. De exemplu, un astfel de container poate reţine: 1 2 2 3 4 4.

 Clasa conţine metodele de mai jos:

 multiset() - constructor;
 iterator insert(const T& x) - inserează un obiect în container;
 iterator find(const T& k) const - caută un obiect în container şi
returnează un iterator către el; dacă elementul nu este găsit, returnează un
iterator către ultimul element din structură;
 void erase(iterator it) - şterge obiectul indicat de iteratorul
transmis ca parametru;

154
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

 iterator lower_bound(const T& k) const - caută primul obiect


al containerului care este mai mare sau egal cu k. Dacă nu există un astfel
de element, se returnează un iterator către ultimul element al structurii (cel
returnat de end()).

Exemplu. Secvenţa următoare caută primul element al structurii care este mai
mare sau egal cu 12. Dacă acesta este găsit, va fi afişat, iar în caz contrar, nu se
afişează nimic:
it=Mult.lower_bound(12);
if (it!=[Link]()) cout<<*it;

 iterator upper_bound(const T& k) const - returnează un


iterator către primul element care este mai mare decât k. Dacă nu există un
astfel de element, se returnează un iterator către ultimul element al structurii
(cel returnat de end()).

Exemple:
 Dacă un container de tip multiset reţine: 1 1 1 3 3 4, atunci secvenţa de
mai jos afişează 1 3 :
cout<<*Mult.lower_bound(1)<<” “<<*Mult.upper_bound(1);

 În secvenţa următoare, se caută primul element al containerului care este


mai mare decât 4. Dacă acesta este găsit, va fi afişat, iar în caz contrar, nu
se afişează nimic.
it=Mult.upper_bound(4);
if (it!=[Link]()) cout<<*it;

Următoarele funcţii sunt prietene ale clasei multiset:

 bool operator<(const multiset&, const multiset&) - returnează


true (valoarea 1) dacă, în ordine lexicografică, obiectele primului container,
considerate în ordine crescătoare, sunt mai mici decât elementele celuilalt
container, altfel returnează false (0).

Exemplu:
multiset<float> Mult, Mult1;
...
cout<<(Mult1<Mult);

 bool operator==(const multiset&, const multiset&) - testează


egalitatea obiectelor reţinute de două astfel de containere;

 key_compare key_comp() const - returnează un pointer către functorul


care compară două obiecte. Pornind de aici, se poate apela functorul, de
exemplu, pentru a-l testa pentru diverse valori.

Exemplu. Dacă Mult este o astfel de structură, cu elemente de tip int, atunci
prin cout<<Mult.key_comp()(1,2)<<endl; se afişează 1.

155
PARTEA A V-A

EXERCIŢII REZOLVATE
1. Se creează şi se afişează o structură de tip multiset. Elementele sunt
afişate, mai întâi, în ordine crescătoare, apoi în ordine descrescătoare.

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

int main()
{ multiset<float> Mult;
set<float>::iterator it;
set<float>::reverse_iterator it1;
[Link](3);
[Link](1);
[Link](1);
[Link](3);
[Link](4);
[Link](1);
for (it=[Link]();it!=[Link]();it++)
cout<<*it<<" ";
cout<<endl;
for (it1=[Link]();it1!=[Link]();it1++)
cout<<*it1<<" ";
}

2. Fiind dat un container de tip multiset, creat ca mai sus, să se citească o


valoare de acelaşi tip cu elementele structurii şi să se afişeze de câte ori valoarea
respectivă este regăsită în container.

Rezolvare. Pentru rapiditatea executării, nu se parcurge întregul container, ci se


identifică prima apariţie a elementului în container, după care, repetitiv, la fiecare
pas, se numără un element şi se trece la următorul:
int s=0;
it=[Link](x);
while (*it==x)
{ s++;
it++;
}
cout<<s;

3. Fiind dată o structură de tip multiset, creată ca mai sus, să se citească o


valoare de acelaşi tip cu elementele structurii şi să se şteargă toate apariţiile
acestei valori în structură.

it=[Link](x);
while (*it==x)
{ [Link] (it);
it++;
}

156
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

PROBLEME PROPUSE
1. Scrieţi un subprogram care afişează toate valorile reţinute de un container
multiset (valori de tip întreg) care se găsesc în intervalul [a,b).
2. Scrieţi un subprogram care afişează, pentru fiecare valoare reţinută de un
container multiset (valori de tip float), numărul apariţiilor ei.
3. Creaţi un container de tip multimap care reţine date de tip s (vezi mai jos).
Datele se memorează în relaţia de ordine impusă de câmpul multicheie.
struct s
{ int multicheie;
string z;
};

LECŢIA 9. Clasa map<Cheie,Data,Compar>

Obiectele reţinute de o structură de tip map sunt perechi de forma:

(key_type cheie, data_type valoare).


Cheia trebuie să fie unică. Informaţiile se pot regăsi rapid O(log N) după
cheie, pentru că structura este sub forma unui arbore echilibrat de căutare.
O pereche precum cea de mai sus se reţine într-un obiect al clasei template
pair <tip1,tip2>. Fiind dat un astfel de obiect, primul argument se preia din
data membru first, iar al doilea argument, din data membru second.

Pentru a accesa elementele structurii se utilizează iteratori, la fel cum am


utilizat, de exemplu, pentru obiectele clasei set. Pornind de la un iterator it, se
poate accesa cheia prin it->first şi valoarea propriu-zisă prin it->second.
Motivul? Iteratorul indică adresa unui obiect al containerului şi printr-o astfel de
adresare se selectează datele membru ale obiectului.

 Clasa este înzestrată cu mai multe metode, dintre care, cele mai importante
sunt prezentate în continuare:

 map() - constructor;

 data_type& operator[](const key_type& k) - returnează o referinţă


către valoarea reţinută de perechea memorată; metoda este obţinută prin
supraîncărcarea operatorului ”[]”;
 void erase(iterator pos) - şterge elementul indicat de iterator;

 size_type erase(const key_type& k) - şterge elementul de cheie dată;

 void erase(iterator first, iterator last) - şterge elementele


aflate între cei doi iteratori;

157
PARTEA A V-A

 iterator find(const key_type& k) - returnează un iterator către


elementul de cheie dată; dacă nu există un obiect cu această cheie,
returnează valoarea întoarsă de end();
 iterator lower_bound(const key_type& k) - returnează un
iterator către primul element care are o cheie mai mare sau egală decât k;
 iterator upper_bound(const key_type& k) - returnează un
iterator către primul element care are o cheie strict mai mare decât k;
 void clear() - şterge toate elementele reţinute de structură;
 pair<iterator, bool> insert(const value_type& x) - metodă
ce inserează un obiect acătuit din ansamblul (cheie, valoare).

Prezentăm şi două funcţii prietene:

 bool operator==(const map&, const map&) - testează dacă două


containere de tipul map sunt sau nu egale;
 bool operator<(const map&, const map&) - compară lexicografic două
obiecte de tipul map.

EXERCIŢII REZOLVATE
1. Programul următor creează o structură de tip map, unde cheia fiecărui element
este de tip string şi reprezintă numele unei persoane, iar data asociată fiecărei
persoane este tot de tip string şi reprezintă numărul de telefon al persoanei
respective. Cu ajutorul unui iterator, se parcurge întreaga structură şi, pentru
fiecare element, se afişează cheia şi valoarea asociată.

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

int main()
{ map<string, string> agenda;
agenda["Ionescu Marian"]="0822101123";
agenda["Bojian Mihaela"]= "0823222700";
agenda["Sandu Rajala"]="0823422567";
map<string,string>::iterator it;
for (it=[Link]();it!=[Link]();it++)
cout<<it->first<<" "<<it->second<<endl;
}

O altă variantă a programului, care realizează acelaşi lucru, dar utilizează


insert() pentru inserare:

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

158
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

int main()
{ map<string, string> agenda;
[Link]
(pair<string,string>("Ionescu Marian", "0822101123"));
[Link]
(pair<string,string>("Bojian Mihaela", "0823222700"));
[Link]
(pair<string,string>("Sandu Rajala", "0823422567"));
map<string,string>::iterator it;
for (it=[Link]();it!=[Link]();it++)
cout<<it->first<<" "<<it->second<<endl;
}

2. În condiţiile de la exerciţiul 1, mai jos este prezentată secvenţa prin care,


pornind de la nume, se afişează numărul de telefon al unei persoane (dacă
persoana de cheie dată se găseşte în structură):

it=[Link]("Sandu Rajala");
if (it!=[Link]())
cout<<it->second<<endl;

3. Fie un container de tip map, creat precum cel de mai jos. Se citeşte k. Scrieţi o
secvenţă prin care se afişează câte elemente au valoarea k.

map<int, int> tabel;


tabel[1]=3;
tabel[7]=5;
tabel[2]=2;
tabel[9]=3;
tabel[5]=3;
tabel[8]=1;

Rezolvare. Secvenţa este:

int k,s=0; cin>>k;


map<int,int>::iterator it;
for (it=[Link]();it!=[Link]();it++)
if (it->second==k) s++;
cout<<s;

 Şi structura de tip map permite să stabilim noi relaţia de ordine în care se


găsesc cheile. Acest fapt este util în unele aplicaţii, când cheia propriu-zisă
este parte a unei înregistrări create cu struct.

Exemplu. Avem mai multe persoane. Pentru fiecare persoană se cunoaşte


numele şi vârsta. Dorim să obţinem o situaţie în care, pentru fiecare persoană să
cunoaştem locul de muncă. În acest caz, cheia va fi persoana, iar data propriu-zisă
va fi locul de muncă. Dar, pentru comparare, vom utiliza numai numele persoanei,
iar ordinea este cea alfabetică (lexicografică). În acest caz, vom utiliza clasa
template cu 3 tipuri formale: cheia, informaţia legată de cheie şi functorul care
compară două chei. Revedeţi şi exemplul dat pentru set.

159
PARTEA A V-A

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

struct persoana
{ string nume;
int varsta;
} p;

class alfabetic
{ public:
bool operator()(persoana p1, persoana p2)
{ return [Link]<[Link];
}
};

int main()
{ map<persoana,string,alfabetic> link;
map<persoana,string, alfabetic>::iterator it;
[Link]="Ion";
[Link]=32;
link[p]="Firma 1";
[Link]="Ancu";
[Link]=17;
link[p]="Firma 2";
[Link]="Titi";
[Link]=18;
link[p]="Firma 2";
for (it=[Link]();it!=[Link]();it++)
cout<<it->[Link]<<" "<<it->[Link]<<" "
<<it->second<<endl;
}

PROBLEME PROPUSE
1. Un container de tip multiset, numit Persoane, reţine mai multe nume de
persoane (şiruri de caractere). Să se creeze o astfel de structură.
2. Pornind de containerul Persoane, creaţi un alt container, de tip map, astfel
încât pentru fiecare persoană să se ştie numărul de apariţii în containerul
Persoane.

3. Lucrare de tip referat. Studiaţi din documentaţia indicată în prima lecţie clasa
multimap<Cheie,Data,Compar> şi realizaţi un referat despre aceasta.

4. Scrieţi un program care creează un container de tip multimap.

5. Creaţi un container de tip multimap care reţine pentru fiecare nod al unui graf
neorientat nodurile adiacente cu el. Un obiect memorat este de forma: (nod,
nod_adiacent). Teoria o găsiţi în manualul pentru clasa a XI-a!
6. Utilizând containerul de la problema 5, parcurgeţi graful în adâncime.

160
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

LECŢIA 10. Clasa stack<T>


Structura de date de tip stivă este studiată în liceu. În această lecţie,
prezentăm clasa stack. Obiectele realizate prin instanţierea ei sunt stive cu
elemente de orice tip.

 Principalele metode ale clasei stack<T> sunt:

 stack() - constructor;
 void push(const value_type& x) - inserează un element în stivă;
 void pop() - şterge ultimul element introdus în stivă.
 value_type& top() - returnează o referinţă către ultimul element
introdus în stivă.
 bool empty() const - returnează true dacă stiva este vidă şi false,
în caz contrar.
 size_type size() const - returnează numărul de elemente din stivă.

Exemplu. Programul următor exemplifică principalele operaţii care se pot efectua


cu o stivă:
#include <stack>
#include <iostream>
using namespace std;
int main()
{ stack<int> stiva;
// pun in stiva 1 2 3
[Link](1);
[Link](2);
[Link](3);
// in top am 3
cout<<[Link]()<<endl;
//sterg elementul din top
[Link]();
// in top am 2;
cout<<[Link]()
// afisez numarul de elemente care se gasesc in stiva: 2
cout<<[Link]();
}

PROBLEME PROPUSE
1. Scrieţi un program care citeşte un şir de numere şi îl afişează în ordine inversă.
2. Rezolvaţi funcţia lui Ackerman (vedeţi manualul pentru clasa a X-a, intensiv sau
a XI-a, neintensiv). Programul va utiliza clasa stack<T> şi nu va fi recursiv.

161
PARTEA A V-A

LECŢIA 11. Clasa queue<T>

Structura de date de tip coadă este tratată în manuale. Aici, prezentăm clasa
queue<T>. Prin instanţierea elementelor ei, putem construi cozi în care tipul
valorilor memorate să fie la alegere.

 Principalele metode care se pot utiliza pentru a lucra cu o coadă sunt:

 queue() - constructor;
 void push(const value_type&) - adaugă cozii un element;
 value_type& front() - returnează o referinţă către elementul aflat în
faţa cozii;
 value_type& back() - returnează o referinţă către ultimul element
introdus în coadă;
 void pop() - şterge elementul aflat în faţa cozii;
 bool empty() const - returnează true dacă coada este vidă;
 size_type size() const - returnează numărul de elemente din coadă.

Exemplu. Programul următor exemplifică principalele operaţii care pot fi efectuate


cu o coadă:
#include <queue>
#include <iostream>
using namespace std;
int main()
{ queue<int> coada;
// pun in coada 1 2 3
[Link](1); [Link](2); [Link](3);
// afisez elementul din fata (1)
cout<<[Link]()<<endl;
// sterg elementul din fata (1)
[Link]();
// acum elementul din fata va fi 2
cout<<[Link]()<<endl;
}

PROBLEME PROPUSE
(vedeţi manualul pentru clasa a XI-a)
1*. Memoraţi matricea de adiacenţă a unui graf, cu un container de tip bit_vector
şi parcurgeţi în lăţime graful utilizând o coadă ca obiect al clasei queue.
2*. Pentru graful de la problema 1, găsiţi distanţa minimă între două noduri i şi j
ale grafului. Se presupune că distanţa între oricare două noduri adiacente este 1.
3*. Pentru graful de la problema 1, afişaţi matricea de adiacenţă.

162
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

LECŢIA 12. Clasa priority_queue<T,Sequence,Compar>

Cu obiectele acestei clase se pot crea cozi cu prioritate (heap-uri, aşa cum
sunt prezentate în manualul pentru clasa a XI-a, intensiv). Tipul formal T este al
obiectelor memorate de heap, tipul formal Sequence este pentru a preciza tipul de
container utilizat pentru memorarea heap-ului (de obicei vector<T>), iar tipul
formal Compar este pentru a preciza functorul care se utilizează pentru
compararea obiectelor.

Observaţie foarte importantă: implicit, se lucrează cu un maxheap, deci vom


avea acces întotdeauna la obiectul "cel mai mare".

 Principalele metode sunt:


 priority_queue() - constructor;
 void push(const value_type&) - adaugă un element la heap;
 void pop - şterge elementul aflat în top (cel mai mare, la maxheap şi cel
mai mic, la minheap);
 const value_type& top() const - returnează valoarea aflată în top;
 bool empty() const - testează dacă heap-ul este sau nu vid.

Exemple:

 Programul următor exemplifică lucrul cu un maxheap:


#include <queue>
#include <iostream>
using namespace std;
int main()
{ priority_queue<int> heap;
// pun in coada 3 2 5
[Link](3); [Link](2); [Link](5);
// afisez elementul din fata (5)
cout<<[Link]()<<endl;
[Link]();
// afisez 3
cout<<[Link]()<<endl;
}

 Programul următor exemplifică lucrul cu un minheap:


#include <queue>
#include <iostream>
class maimic
{ public:
bool operator()(int n1, int n2)
{ return n1>n2; }
};
using namespace std;

163
PARTEA A V-A

int main()
{ priority_queue<int,vector<int>,maimic> heap;
// pun in coada 3 2 5
[Link](3); [Link](2); [Link](5);
// afisez elementul din fata (2)
cout<<[Link]()<<endl;
[Link]();
// afisez 3
cout<<[Link]()<<endl;
}

EXERCIŢIU. Sortaţi n numere reale prin utilizarea unui minheap.

LECŢIA 13. Cum se scrie un algoritm ?


Începând cu Lecţia 16, vom prezenta mai mulţi algoritmi standard din STL.
Pentru a-i folosi, includeţi fişierul antet “algorithm”: #include <algorithm>.
Aşa cum vom vedea, cu ei se lucrează relativ uşor. Înainte de a-i studia, trebuie să
ne facem o idee asupra modului în care aceştia au fost realizaţi. Desigur,
mecanismul pe care îl prezentăm este mult simplificat.
Problemă. Să se scrie un algoritm, numit maxim, care să calculeze maximul dintre
valorile aflate într-un container. De exemplu, putem avea un container de tip
list<T> sau un container de tip vector<T>.

 Un algoritm va fi scris sub forma unei funcţii template.

Atunci putem construi o funcţie template, precum cea de mai jos. Parametrii
sunt de tipul formal inputIterator, iar funcţia este de tip T pentru că returnează
cea mai mare valoare a tipului de bază: T maxim<inputIterator,T>.

a) Mai jos, este prezentat un program care utilizează algoritmul menţionat pe o


listă cu elemente de tip int:
#include <iostream>
#include <list>
using namespace std;
// algoritmul maxim
template <class T, class inputIterator>
T maxim (inputIterator st, inputIterator fin)
{ T max=*st;
inputIterator it=++st;
for(it=st;it!=fin;++it) if (max<*it) max=*it;
return max;
}
int main()
{ list<int> l;
l.push_back(2); l.push_back(3); l.push_back(5);
l.push_back(8); l.push_back(2);
int x=maxim<int,list<int>::iterator>([Link](),[Link]());
cout<<x;
}

164
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

b) Mai jos, este prezentat un program care utilizează algoritmul menţionat pe un


vector cu elemente de tip double:
int main()
{
vector<double> v;
v.push_back(2.1); v.push_back(3); v.push_back(5);
v.push_back(8.22); v.push_back(-12.5);
double x=
maxim<double,vector<double>::iterator>([Link](),[Link]());
cout<<x;
}

c) Mai jos, este prezentat un program care utilizează algoritmul menţionat pe un


container de tip set cu elemente de tip float:
int main()
{
set<float> Mult;
[Link](3.7);[Link](1);
[Link](4.2); [Link](1);
float x=
maxim<float,set<float>::iterator>([Link](),[Link]());
cout<<x;
}

Sperăm că exemplele prezentate v-au convins asupra generalităţii algoritmului


propus. Totuşi, apare o problemă: la fiecare apel al algoritmului, trebuie precizaţi
parametrii efectivi de tip. Nu se poate evita aceasta? Răspunsul este afirmativ!

 Tipurile parametrilor efectivi ai funcţiei se depistează automat, în momentul


apelului.

În aceste condiţii, apelul este mai simplu: nu mai este necesar să transmitem
tipul acestora. Mai jos, este reluat apelul de la c), fără a mai transmite tipul efectiv
pentru inputIterator:

float x=maxim<float> ([Link](),[Link]());.

Este drept, apelul s-a simplificat, dar tot trebuie să transmitem tipul efectiv al
funcţiei. Se poate evita aceasta? Răspunsul este din nou afirmativ!

 Pornind de la tipul iteratorului se pot obţine mai multe informaţii, cum ar fi, de
exemplu, tipul obiectelor din container. Mai jos, prezentăm natura
informaţiilor care pot fi obţinute precum şi modul în care se obţin acestea.
Menţionăm că există şi informaţii de altă natură, pe care nu le prezentăm
aici, dar care pot fi luate din documentaţia indicată în Lecţia 1.

- Tipul obiectelor din secvenţă:

typename inputIterator::value_type.

165
PARTEA A V-A

- Tipul referinţei la un obiect al secvenţei:


typename inputIterator::reference.

- Tipul întreg necesar pentru memorarea numărului de obiecte din secvenţe:


typename inputIterator::difference_type.

Exemple:

 Refacerea algoritmului maxim, astfel încât să nu mai fie necesar să


transmitem ca tip efectiv valoarea returnată de algoritm.

Aplicând cele prezentate, noul algoritm maxim este cel de mai jos. Evident,
valoarea maximă are acelaşi tip cu tipul obiectelor memorate, iar acest tip
poate fi obţinut pornind de la tipul iteratorului. Iată cum arată noua formă a
algoritmului maxim:
template <class inputIterator>
typename inputIterator::value_type maxim (inputIterator st,
inputIterator fin)
{ typename inputIterator::value_type max=*st;
inputIterator it=++st;
for(it=st;it!=fin;++it)
if (max<*it) max=*it;
return max;
}

Programul care urmează utilizează această nouă formă a algoritmului pe un


vector cu componente de tip float:
#include <iostream>
#include <vector>
using namespace std;
// algoritmul maxim in noua forma
int main()
{ vector<float> v;
v.push_back(2); v.push_back(3.7); v.push_back(5.2);
v.push_back(8.9); v.push_back(2);
float x=maxim([Link](),[Link]());
cout<<x;
}

Programul care urmează utilizează această nouă formă a algoritmului pe o


listă cu componente de tip int:
...
int main()
{ list<int> l;
l.push_back(2); l.push_back(3); l.push_back(5);
l.push_back(8); l.push_back(2);
int x=maxim([Link](),[Link]());
cout<<x;
}

166
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

 Uneori, un parametru de intrare trebuie să fie de acelaşi tip cu cel al


obiectelor din secvenţă. Pentru a asigura generalitatea algoritmului, acesta
trebuie să funcţioneze pentru secvenţe cu obiecte de orice tip. Prin urmare,
tipul parametrului de intrare se deduce pornind de la tipul iteratorului.

De exemplu, se doreşte scrierea unui algoritm, numit gaseste, care


returnează un iterator ce indică prima apariţie a valorii transmise ca
parametru, altfel returnează un iterator către obiectul indicat de fin.

Iată algoritmul:
template <class inputIterator>
inputIterator gaseste(inputIterator st, inputIterator fin,
typename inputIterator::value_type v)
{ inputIterator it=st;
while ( *st!=v && st!=fin) st++;
return st;
}

Mai jos este prezentat un program care testează algoritmul gaseste pe un


vector cu componente de tip float, unde se caută valoarea 5.2:
...
int main()
{ vector<float> v;
v.push_back(2); v.push_back(3.7); v.push_back(5.2);
v.push_back(8.9); v.push_back(2);
vector<float>::iterator it;
it=gaseste([Link](),[Link](),5.2);
if (it!=[Link]())
cout<<"Valoarea cautata se gaseste";
else
cout<<"Valoarea cautata NU se gaseste";
}

 Uneori, este necesar ca un algoritm să întoarcă o referinţă către un obiect al


containerului. Mai jos este prezentat algoritmul ref, care primeşte un
iterator şi returnează o referinţă către obiectul indicat:
template <class Iterator>
typename Iterator::reference ref(Iterator it)
{ return *it; }

În programul următor se testează algoritmul ref, prin modificarea primului


element al unui vector cu componente de tip float:
...
int main()
{ vector<float> v;
v.push_back(2); v.push_back(3.7); v.push_back(5.2);
v.push_back(8.9); v.push_back(2);
ref([Link]())=9;
cout<<*[Link]();
}

167
PARTEA A V-A

 Anumiţi algoritmi numără obiectele din container sau obiectele care au o


anumită proprietate. Este clar, în astfel de cazuri, trebuie returnată o valoare
întreagă. Dar, mai precis, care este tipul ei? Pentru a stabili tipul, algoritmul
va returna un tip stabilit pe baza diferenţei iteratorilor (văzuţi ca pointeri).
De exemplu, algoritmul de mai jos, numar, returnează numărul de apariţii
ale unei valori într-un container. Desigur, puteam să stabilim ieşirea de tip
int sau long. Dar aşa, lucrăm standardizat!
template <class Iterator>
typename Iterator::difference_type numar(Iterator st,Iterator
fin, typename Iterator::value_type v)
{ typename Iterator::difference_type s=0;
for(Iterator it=st;it!=fin;it++)
if (*it==v) s++;
return s;
}

Programul următor testează algoritmul numar! Se afişează numărul


apariţiilor valorii 2 într-un container de tip vector cu elemente de tip float:
...
int main()
{ vector<float> v;
v.push_back(2); v.push_back(3.7); v.push_back(5.2);
v.push_back(8.9); v.push_back(2);
int nr_ap=numar([Link](),[Link](),2 );
cout<<nr_ap;
}

PROBLEME PROPUSE
Pentru fiecare dintre problemele de mai jos, scrieţi algoritmul care o rezolvă
şi testaţi-l pe containere de tip vector şi list. Obiectele memorate de containere
pot avea diverse tipuri.

1. Algoritm care găseşte valoarea minimă dintr-o secvenţă şi o returnează.

2. Algoritm care găseşte valoarea minimă dintr-o secvenţă şi returnează un


iterator către ea.
3. Algoritm care aranjează în ordine crescătoare valorile într-o secvenţă (sortare).

4. Algoritm care compară lexicografic două secvenţe şi returnează true dacă


prima secvenţă este mai mică sau egală cu a doua secvenţă.
5. Algoritm care interclasează două secvenţe crescătoare.

6. Algoritm care pornind de la o secvenţă, returnează secvenţa următoare, mai


mare, în ordine lexicografică.

168
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

LECŢIA 14. Algoritmii şi predicatele

Înaintea prezentării lecţiei propriu-zise, exemplificăm un procedeu prin care


se poate transmite, unui subprogram, un obiect. Priviţi exemplul de mai jos:
#include <iostream>
using namespace std;
class clasa
{ public:
int x;
clasa() { x=1; };
};
void f(clasa p)
{ cout<<p.x<<endl; }
int main()
{ f(clasa()); }

Avem o clasă, numită clasa. Ea conţine o dată membru x, de tip int şi un


constructor fără parametri, care iniţializează data membru cu valoarea 1. Avem, de
asemenea, funcţia f, care primeşte ca parametru de intrare un obiect de tip clasa
şi-i afişează data membru (tipăreşte 1).

 O modalitate de transmitere a unui obiect ca parametru efectiv unei funcţii,


constă în a scrie parametrul sub forma nume_clasa(). Practic, un astfel de
apel invocă constructorul, fie el şi cel implicit (nu şi în acest caz, când se
apelează constructorul fără parametri) şi, prin urmare, se transmite obiectul.

Dacă rulaţi programul de mai sus, veţi observa că funcţia f poate afişa conţinutul
datei membru a obiectului (1), de unde rezultă că obiectul a fost transmis.

Revenim acum la studiul algoritmilor. Uneori, este necesar ca algoritmii să


primească ca parametru un obiect al unei clase, de exemplu un predicat.

DEFINIŢIE. Un predicat este un functor care returnează o valoare de tip boolean.


 Este esenţial ca algoritmii să poată primi ca parametru un predicat. Uneori,
este necesar ca un anumit criteriu după care se face compararea, adică
predicatul, să fie scris explicit de către utilizator. Exemplul care urmează vă
va convinge.

Problemă. Să se scrie un algoritm (numit maxim_if) care are trei parametri


formali: st, un iterator către începutul secvenţei de valori, pentru care se
calculează maximul, fin, iterator către ultima valoare din secvenţă, care nu intră
în calcul şi predicatul p, cu ajutorul căruia algoritmul face comparări în vederea
calculului valorii maxime. Algoritmul va avea antetul:
template <class inputIterator, class predicat>
maxim_if (inputIterator st, inputIterator fin, predicat p)

169
PARTEA A V-A

Rezolvare. Predicatul se foloseşte pentru comparare:


// algoritmul maxim_if
template <class inputIterator, class predicat>
typename inputIterator::value_type
maxim_if (inputIterator st, inputIterator fin, predicat p)
{ typename inputIterator::value_type max=*st;
inputIterator it=++st;
// pentru comparare se foloseste predicatul
for(it=st;it!=fin;++it)
if (p(max,*it)) max=*it;
return max;
}

Exemple de utilizare ale algoritmului maxim_if

 Ca amuzament, vom folosi un predicat care returnează true, dacă x>y.


Prin urmare, cu acest predicat algoritmul va returna, de fapt, valoarea
minimă din secvenţă. Observaţi cum am transmis ca parametru efectiv un
obiect al clasei compar<int>, apelând constructorul acestei clase. Proba
se face utilizând o secvenţă dintr-o listă liniară dublu înlănţuită cu elemente
(obiecte) de tip int. Programul va afişa cea mai mică valoare din secvenţă.

#include <iostream>
#include <list>
using namespace std;
template <class T>
class compar
{ public:
bool operator()(T x, T y)
{ return x>y; }
};
// introduceti algoritmul maxim_if aici
int main()
{ list<int> l;
l.push_back(2); l.push_back(3); l.push_back(5);
l.push_back(8); l.push_back(2);
int x=maxim_if([Link](),[Link](),compar<int>());
cout<<x;
}

 Dacă primul exemplu a fost mai mult pentru a ne amuza, al doilea justifică
necesitatea acceptării predicatelor ca parametri pentru algoritmi. De
exemplu, considerăm că secvenţa conţine obiecte de un tip creat cu
struct. Fiecare astfel de obiect reţine numele şi vârsta unei persoane. Se
cere să se afişeze datele referitoare la persoana care are cel “mai mare”
nume în ordine lexicografică (ultima, în ordinea alfabetică a numelor). În
acest caz, predicatul compară numele a două persoane. Evident, algoritmul
clasic, fără predicat, n-ar putea rezolva această problemă. Testarea
algoritmului cu acest predicat se face pe un container de tip vector.

170
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

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

struct persoana
{ string nume;
int varsta;
}p;

template <class T>


class compar_nume
{ public:
int operator()(T x, T y)
{ return [Link]<[Link];
}
};

// introduceti algoritmul maxim_if aici

int main()
{ vector<persoana> v;
[Link]="Mihai"; [Link]=29; v.push_back(p);
[Link]="Zicu"; [Link]=15; v.push_back(p);
[Link]="Rose"; [Link]=23; v.push_back(p);
persoana x=maxim_if([Link](),[Link](),
compar_nume<persoana>());
cout<<[Link]<<" "<<[Link];
}

8
 STL conţine câteva predicate des utilizate : ”equal_to” testează egalitatea
parametrilor (x==y), ”not_equal_to” (x!=y), ”less” (x<y), ”less_equal”
(xy), ”greater” (x>y), ”greater_equal” (xy), ”logical_or” (x||y),
”logical_and” (x&&y) şi ”logical_not” (!x).

Exemplu. În programul de mai jos, maxim_if utilizează un astfel de predicat:

#include <iostream>
#include <functional>
#include <list>
using namespace std;

// introduceti algoritmul maxim_if aici

int main()
{ list<int> l;
l.push_back(2); l.push_back(3); l.push_back(5);
l.push_back(8); l.push_back(2);
int x=maxim_if([Link](),[Link](),less<int>());
cout<<x;
}

8
Standardul presupune că, pentru a folosi aceste predicate, trebuie inclus fişierul antet
”functional”: #include <functional>. În realitate, utilizând mediile prezentate în
prima parte a lucrării, nu este necesară includerea acestui fişier antet.

171
PARTEA A V-A

PROBLEMĂ PROPUSĂ
Scrieţi un algoritm care, pentru o secvenţă dată şi un obiect v, de acelaşi tip
cu obiectele din secvenţă, în funcţie de un predicat p, returnează numărul de valori
din secvenţă pentru care obiectul predicat returnează true(1). Exemplificaţi
utilizarea algoritmului pentru situaţiile următoare:
a) să numere toate valorile egale cu v;
b) să numere toate valorile mai mici ca v;
c) dacă tipul lui v (şi al obiectelor din secvenţă) este obţinut cu struct şi
conţine oraşul, de tip string, temperatura medie anuală, de tip float,
trebuie să numere oraşele care au temperatura medie anuală mai mică sau
egală cu temperatura reţinută de v.

LECŢIA 15. Algoritmii şi elementele de asociere


Există cazuri când un algoritm are ca parametri de intrare începutul unei
secvenţe, sfârşitul ei şi un predicat cu un singur argument. Întrebarea este:
poate selecta un astfel de algoritm obiectele care îndeplinesc o anumită condiţie în
funcţie de o valoare? Atenţie: valoarea nu figurează în lista parametrilor formali.
Răspunsul este afirmativ.
Să analizăm exemplul de mai jos, unde algoritmul numar, prezentat în
continuare, numără obiectele pentru care predicatul cu un singur argument, p,
returnează true:

template <class Iterator, class predicat>


int numara(Iterator st, Iterator fin, predicat p)
{ int s=0;
for (Iterator it=st; it!=fin; ++it)
if (p(*it)) s++;
return s;
}

Să presupunem că, utilizând algoritmul de mai sus, pornind de la o secvenţă cu


elemente de tip int, dorim să numărăm valorile (obiectele) care sunt strict mai
mari decât 5. Ce avem de făcut? Scriem predicatul corespunzător, cu un singur
argument şi utilizăm algoritmul. Programul de mai jos numără şi afişează obiectele
dintr-o listă cu obiecte de tip int care sunt mai mari decât 5.
#include <iostream>
#include <functional>
#include <list>
using namespace std;
template <class T>
class pred
{ public:
bool operator() (T x) { return x>5; }
};

172
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

// introduceti algoritmul numar aici


int main()
{ list<int> l;
l.push_back(7); l.push_back(3);l.push_back(5);
l.push_back(8);l.push_back(6);
cout<<numara([Link](),[Link](),pred<int>() );
}

Dar dacă am fi dorit numărul obiectelor mai mari decât 7 sau numărul obiectelor
mai mici ca 3 sau numărul obiectelor egale cu 4? În toate cazurile vrem să folosim
algoritmul numar, fără să-i modificăm antetul. Nu există o posibilitate pentru a
rezolva elegant această problemă? Răspunsul este afirmativ: se utilizează
asociatorul de mai jos.
bind2nd(predicat_binar,valoare).

Pentru a utiliza asociatorii, trebuie inclus fişierul antet funcţional:


#include<functional>

 Asociatorul bind2nd(predicat_binar,valoare). Fie un predicat binar


pb(arg1,arg2), o valoare v şi un predicat unar p(arg), utilizat în
algoritm. Dorim ca atunci când se apelează p(arg) să se apeleze, de fapt,
pb(arg,v). În astfel de cazuri, predicatul primit de algoritm trebuie să fie
bind2nd(pb<>(),v).

Exemplu. Utilizând algoritmul numar şi pornind de la o secvenţă cu elemente de tip


int, dorim să numărăm valorile (obiectele) care sunt strict mai mari decât 5. Vom
utiliza predicatul binar predefinit greater<>. Iată programul obţinut:
// introduceti algoritmul numar aici
int main()
{ list<int> l;
l.push_back(7); l.push_back(3); l.push_back(5);
l.push_back(8); l.push_back(6);
cout<<numara ([Link](),[Link](),bind2nd(greater<int>(),5));
}

 Asociatorul bind1st(predicat_binar,valoare). Fie un predicat binar


pb(arg1,arg2), o valoare v şi un predicat unar p(arg), utilizat în
algoritm. Dorim ca atunci când se apelează p(arg) să se apeleze, de fapt,
pb(v,arg). În astfel de cazuri, predicatul primit de algoritm trebuie să fie
bind2nd(pb<>(),v).

Exemplu. Dacă în programul anterior se utilizează apelul de mai jos, atunci


algoritmul va efectua compararea greater(5,*it), deci se va afişa numărul
obiectelor strict mai mici decât 5.
cout<<numara([Link](),[Link](),bind1st(greater<int>(),5));

173
PARTEA A V-A

LECŢIA 16. Algoritmul for_each()


Începând cu această lecţie, vom studia unii dintre algoritmii standard,
incluşi în STL.

Prin functie_unara înţelegem o clasă care conţine o metodă obţinută prin


supraîncărcarea operatorului ”()” care are un singur parametru. Desigur, pe lângă
această metodă, clasa poate conţine date membru, alte metode, etc.

 Algoritmul for_each() parcurge secvenţa de obiecte indicate de iteratorii din


intervalul [st,fin). Fiecare obiect din secvenţă este transmis ca parametru
funcţiei unare. În final, se returnează obiectul rezultat din instanţierea funcţiei
unare.
template<class InputIterator,class functie_unara)
functie_unara for_each(InputIterator st, InputIterator fin,
functie_unara f);

Exemple:

 Mai jos, puteţi observa un program care afişează valorile reţinute de o listă
liniară cu obiecte de tip int. Aici, metoda operator() afişează valoarea
primită ca parametru.
#include <iostream>
#include <list>
using namespace std;
template <class T>
class afis
{ public:
void operator()(T x) { cout << x << ' '; }
};
int main()
{ list<int> l;
l.push_back(2); l.push_back(3); l.push_back(5);
l.push_back(8); l.push_back(2);
for_each([Link](), [Link](), afis<int>());
}

 Se cere să se calculeze suma elementelor conţinute de o listă cu elemente


de tip int. Aici, funcţia unară este dată de clasa suma<T>. Ea conţine data
membru s care, în final, va reţine suma elementelor, un constructor, care are
rolul de a iniţializa pe s cu 0 şi metoda operator() care adună fiecare
valoare la data membru s. Algoritmul returnează obiectul funcţiei unare.
Pornind de la el, se afişează conţinutul lui s.
#include <iostream>
#include <list>
#include <algorithm>
using namespace std;

174
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

template <class T>


class suma
{ public:
T s;
suma() { s=0; }
void operator() (T x) { s+=x; }
};

int main()
{ list<int> l;
l.push_back(2); l.push_back(3);
l.push_back(5); l.push_back(8);
suma<int> ob=for_each([Link](), [Link](), suma<int>());
cout<<"suma ="<<ob.s;
}

PROBLEME PROPUSE
1. Se consideră un vector (listă) cu elemente de tipul float. Se cere să se
calculeze media aritmetică a valorilor reţinute.
2. Pentru fiecare persoană se cunoaşte numele şi vârsta. Elementele unui vector
(listă) conţin mai multe înregistrări de persoane. Se cere să se afişeze media de
vârstă a acestora.
3. Se consideră un vector (listă) cu elemente de tipul double. Afişaţi valoarea
maximă reţinută.

LECŢIA 17. Algoritmi de tip find


 Algoritmul find caută prima apariţie a valorii v în secvenţa reţinută de
container. Dacă valoarea este găsită, returnează un iterator către ea, altfel
returnează iteratorul fin.

template <class InputIterator, classs T>


InputIterator find(InputIterator st, InputIterator fin,
const T& v);

Exemplu. În programul următor se caută prima apariţie a unei valori într-un vector
(listă). Dacă valoarea este găsită se afişează, altfel se afişează un mesaj
corespunzător.

#include <iostream> #include <iostream>


#include <vector> #include <list>
#include <algorithm> #include <algorithm>
using namespace std; using namespace std;
int main() int main()
{ vector<int> V; { list<int> l;
[Link](5); l.push_back(2);
V.push_back(2); l.push_back(3);
V.push_back(3); l.push_back(5);

175
PARTEA A V-A

V.push_back(5); l.push_back(8);
V.push_back(8); l.push_back(2);
V.push_back(2); list<int>::iterator it;
vector<int>::iterator it; it= find([Link](), [Link](),3);
it=find([Link](),[Link](),12); if( it!=[Link]()) cout<<*it;
if( it!=[Link]()) cout<<*it; else cout<<"Element negasit";
else cout<<"Element negasit"; }
}

 Algoritmul find_if caută în secvenţa [st,fin) prima apariţie pentru care


predicatul returnează valoarea true. Dacă respectiva valoare este găsită,
se returnează un iterator către ea, iar contrar, un interator către fin.
template <class InputIterator, classs T>
InputIterator find_if(InputIterator st, InputIterator fin,
Predicat p);

Exemple:

 Se caută, într-o listă care reţine numere întregi, prima apariţie a unei valori
strict mai mari decât 5. Dacă este găsită se afişează, altfel se afişează un
mesaj corespunzător. În acest caz, predicatul a fost scris explicit. El este dat
de clasa predicat.

#include <iostream>
#include <list>
#include <algorithm>
using namespace std;
template <class T>
class predicat
{ public:
bool operator() (T x)
{ return x>5; }
};
int main()
{ list<int> l;
l.push_back(2); l.push_back(3); l.push_back(5);
l.push_back(8); l.push_back(2);
list<int>::iterator it;
it=find_if([Link](), [Link](), predicat<int>());
if (it!=[Link]()) cout<<* it;
else cout<<" Valoarea nu a fost gasita";
}

 La fel ca mai sus, numai că se utilizează un predicat predefinit


(greater<T>). După cum se ştie, acest predicat are 2 parametri. Unul, mai
precis primul parametru, este transmis implicit de către algoritm. Pentru a-l
transmite pe al doilea, se utilizează elementul de asociere binder2nd,
unde primul parametru este predicatul, iar al doilea este valoarea cu care se
compară primul parametru (revedeţi lecţia 15).

176
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

#include <iostream>
#include <list>
#include <functional>
#include <algorithm>
using namespace std;
int main()
{
list<int> l;
l.push_back(2); l.push_back(3); l.push_back(5);
l.push_back(8); l.push_back(2);
list<int>::iterator it;
it=find_if([Link](), [Link](), bind2nd(greater<int>(),5));
if (it!=[Link]()) cout<<* it;
else cout<<" Valoarea nu a fost gasita";
}

PROBLEME PROPUSE
1. Scrieţi un program care citeşte două valori întregi a şi b şi returnează prima
apariţie dintr-un vector a unei valori x care îndeplineşte condiţia: a<x<b.

2. O înregistrare de tip persoana conţine numele mic al unei persoane, vârsta şi


sexul (băiat sau fată). O listă conţine mai multe înregistrări de tip persoana. Găsiţi
prima fată a cărei nume începe cu ‘M’ şi are vârsta în intervalul [17,20).

LECŢIA 18. Algoritmi de tip count

 Algoritmul count() numără apariţiile unei valori într-o secvenţă:

template <class InputIterator, classs T>


typename InputIterator::difference_type
count(InputIterator st, InputIterator fin, const T& v);

Exemplu. În programul de mai jos se numără apariţiile valorii 2 într-o listă cu


elemente de tip int. La sfârşit, se afişează numărul apariţiilor:

#include <iostream>
#include <list>
#include <algorithm>
using namespace std;

int main()
{
list<int> l;
l.push_back(2); l.push_back(3); l.push_back(5);
l.push_back(8); l.push_back(2);
int n=count([Link](), [Link](), 2);
cout<<n;
}

177
PARTEA A V-A

 Algoritmul count_if() numără apariţiile unor valori din secvenţă pentru


care predicatul returnează valoarea true:
template <class InputIterator, classs T>
typename InputIterator::difference_type
count_if(InputIterator first,InputIterator last,Predicat p);

Exemplu. În programul următor se numără valorile dintr-o listă care sunt mai mici
sau egale cu 3:
#include <iostream>
#include <list>
#include <functional>
#include <algorithm>
using namespace std;
int main()
{ list<int> l;
l.push_back(2); l.push_back(3); l.push_back(5);
l.push_back(8); l.push_back(2);
int n=count_if([Link](),[Link](),bind2nd(less_equal<int>(),3));
cout<<n;
}

PROBLEME PROPUSE
1. Scrieţi un program care citeşte două valori întregi a şi b şi returnează numărul
de apariţii care îndeplinesc condiţia: a<x<b.
2. O înregistrare de tip persoana conţine numele mic al unei persoane, vârsta
şi sexul (băiat sau fată). O listă conţine mai multe înregistrări de tip persoana.
Găsiţi numărul fetelor pe care le cheamă “Lavinia” şi au 25 de ani.

LECŢIA 19. Algoritmi de tip search


 Algoritmul search, sub forma de mai jos, caută secvenţa marcată de
iteratorii [st2, fin2) în secvenţa marcată de iteratorii [st1,fin1); Dacă
secvenţa este găsită, atunci se returnează un iterator către începutul ei,
altfel se returnează fin1. Se pare că majoritatea implementărilor folosesc
un algoritm în O(nm) unde n este lungimea primei secvenţe, iar m este
lungimea celei de-a doua secvenţe.
template <class ForwardIterator1, class ForwardIterator2>
ForwardIterator1 search(ForwardIterator1 st1,
ForwardIterator1 fin1,
ForwardIterator2 st2,
ForwardIterator2 fin2);

Exemplu. Programul următor verifică dacă secvenţa 5, 8, 2, dintr-un vector, este


subsecvenţă a secvenţei 2, 3, 5, 8, 2, dintr-o listă.

178
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

#include <iostream>
#include <list>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{ list<int> l;
l.push_back(2); l.push_back(3); l.push_back(5);
l.push_back(8); l.push_back(2);
vector<int>v; v.push_back(5); v.push_back(8); v.push_back(2);
list<int>::iterator it;
it=search([Link](),[Link](),[Link](),[Link]());
if (it!=[Link]()) cout<<"am gasit subsecventa";
else cout<<"nu am gasit subsecventa";
}

 A doua formă o generalizează pe prima, în sensul că nu se mai testează


egalitatea implicit, ci se utilizează un predicat binar. Predicatul primeşte ca
parametri, în această ordine, un obiect al primei secvenţe şi un obiect al
celei de-a doua secvenţe.
template <class ForwardIterator1, class ForwardIterator2,
class Predicat_binar>
ForwardIterator1 search(ForwardIterator1 st1,
ForwardIterator1 fin1, ForwardIterator2 st2,
ForwardIterator2 fin2, Predicat_binar p);

Exemplu. În program se verifică dacă în listă există o subsecvenţă în care fiecare


element este mai mare sau egal decât elementul corespunzător din subsecvenţa
din vector. Secvenţa din listă este: 1 6 9 8 2. Secvenţa din vector este: 5 8 2.
Am găsit o subsecvenţă în listă care îndeplineşte cerinţele: 6 9 8 (65, 98, 82).
#include <iostream>
#include <list>
#include <vector>
#include <algorithm>
using namespace std;
template <class T>
class predicat
{ public:
bool operator() (T x, T y)
{ return x>=y; }
};
int main()
{ list<int> l;
l.push_back(1); l.push_back(6); l.push_back(9);
l.push_back(8); l.push_back(2);
vector<int>v; v.push_back(5); v.push_back(8); v.push_back(2);
list<int>::iterator it;
it=search([Link](),[Link](),[Link](),[Link](),predicat<int>());
if (it!=[Link]()) cout<<"am gasit subsecventa"<<endl;
else cout<<"nu am gasit subsecventa";
}

179
PARTEA A V-A

PROBLEME PROPUSE
1. Se citeşte un şir de n numere naturale şi un altul de m<n numere naturale.
Există, în primul şir, o secvenţă de m numere naturale care are aceeaşi ultimă cifră
ca şi şirul celor m numere naturale?
2. Se citeşte un şir de n numere naturale şi un altul de m<n numere naturale. De
câte ori se regăseşte secvenţa celui de-al doilea şir de caractere în secvenţa
primului şir de caractere?

LECŢIA 20. Algoritmi de tip mismatch şi equal


 Algoritmul mismatch testează dacă două secvenţe diferă sau nu. În cazul
în care se găsesc două obiecte aflate pe aceeaşi poziţie relativă şi care
diferă, se returnează o pereche de iteratori care indică aceste obiecte.
Primul iterator din pereche, first, indică elementul din prima secvenţă, iar
al doilea, second, indică elementul din cea de-a doua secvenţă. Dacă
primul iterator este egal cu fin1, înseamnă că pe lungimea primei secvenţe
nu s-au detectat obiecte care diferă. Dacă al doilea operator este egal cu
sfârşitul celei de-a două secvenţe, end(), înseamnă că pe lungimea celei
de-a doua secvenţe nu s-au detectat obiecte care diferă.
template <class InputIterator1, class InputIterator2>
pair<InputIterator1, InputIterator2>
mismatch(InputIterator1 st1, InputIterator1 fin1,
InputIterator2 st2);

Exemplu. Prima secvenţă din listă este 2 3 5 8 2, iar a doua secvenţă din
vector este 2 3 2. Se cere să se verifice dacă a doua secvenţă este prefixul
primei secvenţe. În caz afirmativ, se va afişa un mesaj corespunzător, altfel, se va
afişa perechea de obiecte care diferă, în ordinea următoare: obiectul din prima
secvenţă şi obiectul din a doua secvenţă. Programul va afişa 5 şi 2.
#include <iostream>
#include <list>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{ list<int> l;
l.push_back(2); l.push_back(3); l.push_back(5);
l.push_back(8); l.push_back(2);
vector<int> v;
v.push_back(2); v.push_back(3); v.push_back(2);
pair<list<int>::iterator,vector<int>::iterator> x;
x=mismatch([Link](),[Link](),[Link]());
if ([Link] !=[Link]())
cout<<"valori prin care difera "<<*[Link]<<" "<<*[Link];
else cout<<"a doua secventa este prefixul primei secvente";
}

180
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

 A doua formă a algoritmului mismatch are în plus un parametru care indică


predicatul binar după care se compară două elemente din secvenţă.

template <class InputIterator1, class InputIterator2,


class Predicat_binar>

pair<InputIterator, InputIterator>
mismatch(InputIterator st1, InputIterator fin1,
InputIterator fin2, Predicat_binar p);

Exemplu. Considerăm că o secvenţă din vector este prefixul secvenţei din listă
dacă orice element din secvenţa din listă este mai mare sau egal decât elementul
corespunzător din secvenţa din vector. Se cere să se scrie un program care
verifică acest fapt. Pentru rezolvare, în programul anterior, înlocuiţi atribuirea
x=..., cu atribuirea de mai jos:
x=mismatch([Link](),[Link](),[Link](),greater_equal<int>());

 Algoritmul equal compară două secvenţe caracter cu caracter în scopul


verificării egalităţii lor pe lungimea primei secvenţe. Valoarea de retur este
true dacă prima secvenţă a fost parcursă integral şi nu s-au depistat
obiecte care diferă, altfel se returnează false.
template <class InputIterator1, class InputIterator2>
bool equal(InputIterator1 st1, InputIterator1 sf1,
InputIterator2 st2);

Exemple:

 Dacă S1= 1 2 şi S2= 1 2, se returnează true;


 Dacă S1= 1 2 şi S2= 1 2 3, se returnează true;
 Dacă S1= 1 2 şi S2= 1, se returnează false;
 Dacă S1= 1 2 3 şi S2= 1 2 4 3, se returnează false.

 Forma de mai jos a algoritmului equal ne permite să alegem un predicat


binar pentru a stabili noi “ce înseamnă =”:

template <class InputIterator1, class InputIterator2,


class Predicat_binar>
bool equal(InputIterator1 st1, InputIterator1 fin1,
InputIterator2 st2, Predicat p);

PROBLEME PROPUSE
1. Se citeşte un şir de n numere naturale şi un altul de m<n numere naturale.
Determinaţi dacă al doilea şir de caractere se găseşte exact la sfârşitul primului şir
de caractere.
2. Se citesc două şiruri de numere naturale. Să se determine dacă cele două şiruri
coincid sau nu.

181
PARTEA A V-A

LECŢIA 21. Algoritmi de tip copy

 Algoritmul copy copiază secvenţa [st,fin) în secvenţa care începe cu


r. Dacă containerul receptor nu conţine, începând de la r, suficiente
componente, atunci se copiază în limita componentelor disponibile (nu se
adaugă componente la sfârşit). Evident, obiectele peste care se face
copierea, se pierd. Se returnează un iterator către obiectul următor celui
peste care s-a efectuat ultima copiere.

template <class InputIterator, class OutputIterator>


OutputIterator copy(InputIterator st, InputIterator fin,
OutputIterator r);

Exemplu. În listă avem secvenţa: 1 2 3. În vector, avem secvenţa 4 5 6. Se


copiază secvenţa din listă în vector, începând cu componenta a doua a vectorului.
După copiere, vectorul va conţine 4 1 2 (nu a mai fost în vector o componentă
peste care să se scrie). În acest caz, se returnează un iterator către [Link]().
După copiere, conţinutul listei rămâne nemodificat.

#include <iostream>
#include <list>
#include <vector>
#include <algorithm>

using namespace std;


int main()
{ list<int> l;
l.push_back(1); l.push_back(2); l.push_back(3);
vector<int> v;
v.push_back(4); v.push_back(5); v.push_back(6);
copy([Link](),[Link](),[Link]()+1);
for (size_t i=0; i<[Link](); i++) cout<<v[i]<<" ";
}

 Algoritmul copy_backward copiază secvenţa sf1-1, sf1-2,..., st în


secvenţa r-1, r-2, ..., în limita numărului de obiecte din secvenţă aflate
înaintea obiectului indicat de iteratorul r.
template <class BidirectionalIterator1,
class BidirectionalIterator2>
BidirectionalIterator copy_backward(BidirectionalIterator1 st,
BidirectionalIterator1 fin, BidirectionalIterator2 r);

Exemplu. Lista l conţine elementele: 1 2 3, iar vectorul v conţine elementele: 4 5


6 7. Aunci, după executarea algoritmului de mai jos, vectorul va conţine: 2 3 6 7:

copy_backward([Link](),[Link](), [Link]()+2);

182
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

LECŢIA 22. Algoritmi de tip replace

 Algoritmul replace înlocuieşte, în secvenţa [st,fin), toate apariţiile unui


obiect v cu obiectul v_nou:
template <class ForwardIterator, class T>
void replace(ForwardIterator st, ForwardIterator fin,
const T& v, const T& v_nou)

Exemplu. Dacă v conţine 4 5 6 5 şi se execută algoritmul de mai jos, atunci v


va conţine: 4 9 6 9.

replace([Link](), [Link](), 5, 9);

 Algoritmul replace_if înlocuieşte, în secvenţa [st,fin), toate apariţiile


pentru care predicatul p returnează true cu v_nou:
template <class ForwardIterator, class Predicat, class T>
void replace_if(ForwardIterator st, ForwardIterator fin,
Predicat p, const T& v)

Exemplu. Vectorul v conţine elementele: 6 2 4 8. Dorim să înlocuim toate


elementele mai mari decât 5 cu 9. După executarea algoritmului de mai jos, v va
conţine 9 2 4 9 (dacă scrieţi programul, nu uitaţi să includeţi fişierul antet
functional).
v.push_back(6); v.push_back(2); v.push_back(4); v.push_back(8);
replace_if([Link](), [Link](), bind2nd(greater<int>(),5), 9);

 Algoritmul replace_copy copiază obiectele din secvenţa [st,fin) peste


obiectele din secvenţa al cărei prim obiect este indicat de r. Dacă în
procesul de copiere, în [st,fin) se găseşte obiectul v, în locul lui se
copiază v1.
template <class InputIterator, class OutputIterator, class T>
OutputIterator replace_copy(InputIterator st, InputIterator
fin, OutputIterator r, const T& v, const T& v1);

Exemplu. Vectorul v conţine valorile: 0 1 2 3 4 5 6 7 8 9. După executarea


alogoritmului de mai jos, v va reţine: 0 1 2 3 4 0 1 13 8 9:

replace_copy([Link](),[Link]()+3,[Link]()+5,2,13);

 Algoritmul replace_copy_if este asemănător algoritmului replace_copy,


dar se înlocuiesc cu v1 doar obiectele din [st,fin) pentru care predicatul
binar p returnează true.
template <class InputIterator, class OutputIterator,
class Predicate, class T>
OutputIterator replace_copy_if(InputIterator st,
InputIterator fin,OutputIterator r,Predicat p,const T& v1)

183
PARTEA A V-A

Exemplu. Vectorul v conţine valorile: 0 1 2 3 4 5 6 7 8 9. După executarea


algoritmului de mai jos, v reţine: 0 1 2 3 4 13 13 2 8 9.
replace_copy_if([Link](),[Link]()+3,[Link]()+5,
bind2nd(less<int>(),2),13);

LECŢIA 23. Algoritmi de tip remove

 Algoritmul remove şterge din secvenţa [st,fin) toate apariţiile valorii v.


Se returnează un iterator către primul element care nu face parte din
secvenţa rămasă. Evident, pornind de la st şi de la iteratorul returnat, se
poate obţine uşor secvenţa nouă. În schimb, containerul iniţial va reţine
acelaşi număr de obiecte. După secvenţa care a fost modificată, restul
obiectelor rămân pe aceeaşi poziţie.
template <class ForwardIterator, class T>
ForwardIterator remove(ForwardIterator st,
ForwardIterator fin(), const T& value);

Exemplu. Fie vectorul V=1 2 3 4 5 6 7 8 9 10. Se execută secvenţa:


vector<int>::iterator it,it1;
it=remove([Link](), [Link](), 2);
for (it1=[Link](); it1!=it; ++it1) cout<<*it1;
for (size_t i=0; i<[Link](); i++) cout<<v[i]<<" ";

Se afişează: 1 3 4 5 6 7 8 9 10.

În schimb, vectorul este 1 3 4 5 6 7 8 9 10 10.

 Algoritmul remove_if este asemănător cu algoritmul remove, dar se şterg


valorile pentru care predicatul binar returnează true:
template <class ForwardIterator, class Predicate>
ForwardIterator remove_if(ForwardIterator st,
ForwardIterator fin, Predicat p);

Exemplu. Se aplică remove_if, în loc de remove, cu acelaşi efect ca în


exemplul de la remove:
remove_if([Link](), [Link](), bind2nd(equal_to<int>(),2));

 Algoritmul remove_copy copiază din secvenţa [st,fin) toate obiectele


care nu sunt egale cu v, în secvenţa care începe cu iteratorul r. Se copiază
atâtea elemente câte încap, nu se adaugă obiecte la sfârşitul containerului.
Se returnează un indicator care indică primul element peste care nu s-a
copiat nicio valoare. Pornind de la r şi de la acest iterator, se poate obţine
secvenţa copiată.
template <class InputIterator, class OutputIterator, class T>
OutputIterator remove_copy(InputIterator st,
InputIterator fin, OutputIterator r, const T& v);

184
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

Exemplu. Fie vectorul V=1 2 3 4 5 6 7 8 9 10. Se execută:


remove_copy([Link](), [Link]()+5, [Link]()+6, 2);

Astfel, se obţine: V=1 2 3 4 5 6 1 3 4 5.

 Algoritmul remove_copy_if funcţionează la fel ca remove_copy, numai


că în locul lui v se consideră acele valori pentru care predicatul binar
returnează true:
template <class InputIterator, class OutputIterator,
class Predicat>
OutputIterator remove_copy_if(InputIterator st,
InputIterator fin, OutputIterator r, Predicat p);

Exemplu. Se aplică remove_copy_if cu acelaşi efect ca în exemplul de la


remove_copy:
remove_copy_if([Link](), [Link]()+5, [Link]()+6,
bind2nd(equal_to<int>(),2));

LECŢIA 24. Algoritmi de tip transform


 Algoritmul transform, în forma de mai jos, preia obiectele din secvenţa
[[Link]), le prelucrează cu o funcţie unară care returnează obiectele
modificate şi le include într-o altă secvenţă pentru care valoarea de început
este dată de iteratorul r.
template <class InputIterator, class OutputIterator,
class functie_unara>
OutputIterator transform(InputIterator st, InputIterator fin,
OutputIterator r, functie_unara f);

Exemplu. În programul de mai jos se preiau elementele de tip int dintr-o listă
liniară simplu înlănţuită, se incrementează şi se copiază într-un vector. La sfârşit,
se afişează valorile memorate de vector.
#include <iostream>
#include <list>
#include <vector>
#include <algorithm>
using namespace std;
template <class T> class increment
{ public:
T operator()(T x) { return x+1; }
};
int main()
{ list<int> l;
l.push_back(1); l.push_back(2); l.push_back(3);
vector<int> v([Link]());
transform([Link](),[Link](),[Link](),increment<int>());
for (size_t i=0;i<[Link]();i++) cout<<v[i]<<" ";
}

185
PARTEA A V-A

 Algoritmul transform, în forma de mai jos, preia obiectele din secvenţa


[st1,fin1) şi împreună cu obiectele corespunzătoare din secvenţa care
începe cu st2 obţine, printr-o funcţie binară, obiecte care sunt memorate în
secvenţa care începe cu r:
template <class InputIterator1, class InputIterator2,
class OutputIterator, class functie_binara>
OutputIterator transform(InputIterator1 st1, InputIterator1
sf1,InputIterator2 st2,OutputIterator r,functie_binara f);

Exemplu. În secvenţa de mai jos se creează o listă şi un vector cu componente de


tip int. Cu ajutorul algoritmului transform, elementele se adună şi sunt
depozitate în vectorul rez. În final, se afişează componentele lui rez:
...
template <class T>
class adun
{ public:
T operator()(T x, T y) { return x+y; }
};
int main()
{ list<int> l;
l.push_back(1); l.push_back(2); l.push_back(3);
vector<int> v(3),rez(3);
v[0]=1; v[1]=7; v[2]=5;
transform([Link](),[Link](),[Link](),[Link](),adun<int>());
for (size_t i=0;i<[Link]();i++)
cout<<rez[i]<<" ";
}

LECŢIA 25. Algoritmi de tip sort(sortare)

 Algoritmul sort sortează crescător elementele din secvenţa [st,fin).


După cum observaţi, trebuie ca iteratorii să permită accesul direct la
elemente. Din acest motiv, se poate utiliza sort pentru vectori, dar nu şi
pentru liste. Metoda de sortare este quicksort, deci algoritmul are
complexitatea medie O(nlog(n)).

template <class RandomAccessIterator>


void sort(RandomAccessIterator st, RandomAccessIterator fin);

Exemplu. Se sortează crescător vectorul v:

vector<int> v(6);
v[0]=1;v[1]=7;v[2]=5;
v[3]=3;v[4]=8;v[5]=5;
sort([Link](),[Link]());
for (size_t i=0;i<[Link]();i++)
cout<<v[i]<<" ";

186
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

 Mai jos, algoritmul sort este asemănător primului, doar că permite să


introducem drept criteriu de sortare un predicat. Astfel, dacă dorim, putem
sorta secvenţa descrescător:
template <class RandomAccessIterator, class Predicat>
void sort(RandomAccessIterator st, RandomAccessIterator fin,
Predicat p);

Exemplu. Vectorul din exemplul anterior se sortează descrescător:


sort([Link](), [Link](), greater<int>());

LECŢIA 26. Căutarea binară

 Algoritmul binary_search presupune că secvenţa [st,fin) este


ordonată crescător şi caută, utilizând căutarea binară, valoarea v. Dacă
valoarea este găsită se returnează true, în caz contrar, false. După cum
se ştie, căutarea binară se realizează în O(log(n)).

template <class ForwardIterator, class T>


bool binary_search(ForwardIterator st, ForwardIterator fin,
const T& v);

Exemplu. Fie v un vector sortat crescător. Se caută valoarea “11”:

if (binary_search([Link](), [Link](), 11))


cout<<"Gasit";
else cout<<"Negasit";

 Algoritmul binary_search prezentat mai jos este asemănător


precedentului, numai că presupune vectorul sortat după predicatul p, iar
căutarea se face conform acestui predicat:
template <class ForwardIterator, class T, class Predicat>
bool binary_search(ForwardIterator st, ForwardIterator fin,
const T& v, Predicat p);

Exemplu. În programul următor, un vector se sortează crescător (în mod


intenţionat am scris predicatul în acest scop) şi, după sortare, se verifică dacă
vectorul conţine o anumită valoare:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

template <class T> class comp


{ public:
bool operator()(T x, T y) { return x>y; }
};

187
PARTEA A V-A

int main()
{ vector<int> v(6);
v[0]=1; v[1]=7; v[2]=5; v[3]=3; v[4]=8; v[5]=5;
sort([Link](),[Link](),comp<int>());
for (size_t i=0;i<[Link]();i++) cout<<v[i]<<" ";
cout<<endl;
if (binary_search([Link](),[Link](), 1, comp<int>()))
cout<<"Gasit";
else cout<<"Negasit";
}

 lower_bound (forma 1) presupune că obiectele din secvenţa [st,fin)


sunt sortate crescător şi returnează un iterator care indică prima poziţie
unde poate fi inserată o valoare (obiect) v, astfel încât secvenţa să rămână
sortată crescător. Algoritmul efectuează o căutare binară:
template <class ForwardIterator, class T>
ForwardIterator lower_bound(ForwardIterator st,
ForwardIterator fin, const T value);

Exemplu. Vectorul v se presupune sortat crescător. Se cere să se insereze


valoarea 4, astfel încât v să rămână sortat crescător. Dacă, de exemplu, în secvenţă
se găsesc doi de 4, lower_bound returnează un iterator către primul 4 (lower).
vector<int>::iterator it=lower_bound([Link](), [Link](), 4);
[Link](it,4);
cout<<endl;
for (size_t i=0;i<[Link]();i++) cout<<v[i]<<" ";

 Algoritmul lower_bound (forma 2) funcţionează la fel ca lower_bound


(forma 1), dar presupune că secvenţa [st,fin) este sortată cu predicatul
p (de exemplu, vectorul este sortat descrescător) şi permite utilizarea
predicatului pentru regăsirea iteratorului cu semnificaţia din prima formă:
template <class ForwardIterator, class T, class Predicat>
ForwardIterator lower_bound(ForwardIterator st,
ForwardIterator fin, const T& value, Predicat p);

Exemplu. Vectorul se presupune sortat descrescător şi se inserează valoarea 5,


astfel încât vectorul să rămână sortat descrescător:
vector<int>::iterator it;
it=lower_bound([Link](), [Link](), 5, greater<int>() );
[Link](it,5);

 Algoritmul upper_bound (forma 1) funcţionează la fel ca lower_bound


(forma 1) numai că returnează un iterator către ultima componentă unde se
poate insera valoarea v, astfel încât vectorul să rămână sortat crescător.

template <class ForwardIterator, class T>


ForwardIterator upper_bound(ForwardIterator st,
ForwardIterator fin, const T& v);

188
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

 Algoritmul upper_bound (forma 2) funcţionează la fel ca lower_bound


(forma 2) numai că returnează un iterator către ultima componentă unde se
poate insera valoarea v, astfel încât vectorul să rămână sortat conform
predicatului p:
template <class ForwardIterator, class T, class Predicat>
ForwardIterator upper_bound(ForwardIterator st,
ForwardIterator fin, const T& value, Predicat p);

LECŢIA 27. Ordinea lexicografică


 Algoritmul lexicographical_compare compară lexicografic secvenţele
[st1,fin1) şi [st2,fin2) şi returnează true (1), dacă prima secvenţă
este mai mică în ordine lexicografică decât cea de-a doua:
template <class InputIterator1, class InputIterator2>
bool lexicographical_compare(InputIterator1 st1,
InputIterator1 fin1, InputIterator2 st2,
InputIterator2 fin2);

Exemplu. Secvenţa următoare afişează “1”:

vector<int> v(3),v1(2);
v[0]=1; v[1]=2; v[2]=3;
v1[0]=3; v1[1]=1;
cout<<lexicographical_compare([Link](),[Link](),
[Link](),[Link]());

 Algoritmul lexicographical_compare de mai jos funcţionează la fel ca


primul algoritm de acest tip prezentat, numai că atunci când se compară
două elemente, se utilizează predicatul binar p:

template <class InputIterator1, class InputIterator2,


class Predicat>
bool lexicographical_compare(InputIterator1 st1,
InputIterator1 fin1, InputIterator2 st2,
InputIterator2 fin2, Predicat p);

 Algoritmul next_permutation (forma 1) porneşte de la secvenţa


[st,fin) şi o modifică astfel încât ea să conţină secvenţa următoare strict
mai mare în ordine lexicografică. Dacă există o astfel de secvenţă, se
returnează true, în caz contrar, false:

template <class BidirectionalIterator>


bool next_permutation(BidirectionalIterator st,
BidirectionalIterator fin);

Exemplu. Programul următor generează şi afişează toate permutările mulţimii


{1,2,3}.

189
PARTEA A V-A

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{ vector<int> v(3);
v[0]=1; v[1]=1; v[2]=3;
do
{ for (size_t i=0;i<[Link]();i++) cout<<v[i]<<" ";
cout<<endl;
} while (next_permutation([Link](),[Link](),less<int>()));
}

 Algoritmul next_permutation (forma 2) apelabil ca mai jos, funcţionează


la fel ca precedentul, numai că pentru comparare se utilizează predicatul p:
template <class BidirectionalIterator, Predicat p>
bool next_permutation(BidirectionalIterator st,
BidirectionalIterator fin, Predicat p);

 Algoritmul prev_permutation (forma 1) porneşte de la secvenţa


[st,fin) şi o modifică astfel încât ea să conţină secvenţa anterioară,
distinctă şi strict mai mică în ordine lexicografică. Dacă există o astfel de
secvenţă, se returnează true, în caz contrar, false:
template <class BidirectionalIterator>
bool prev_permutation(BidirectionalIterator st,
BidirectionalIterator fin);

 Algoritmul prev_permutation (forma 2) apelabil ca mai jos, funcţionează


la fel ca precedentul, numai că pentru comparare se utilizează predicatul p:
template <class BidirectionalIterator, Predicat p>
bool prev_permutation(BidirectionalIterator st,
BidirectionalIterator Predicat p);

LECŢIA 28. Partiţionare


Prin partiţionarea unei secvenţe vom înţelege aşezarea mai întâi a tuturor
obiectelor din secvenţă care îndeplinesc o anumită condiţie, urmate de obiectele
din secvenţă care nu o îndeplinesc.

 Metoda partiton partiţionează secvenţa [st,fin) şi returnează un


iterator care indică primul element din noua secvenţă pentru care predicatul
returnează false:

template <class ForwardIterator, class Predicat>


ForwardIterator partition(ForwardIterator st,
ForwardIterator fin, Predicat p)

190
INIŢIERE ÎN STL (STANDARD TEMPLATE LIBRARY)

Exemplu. Programul următor creează un vector cu elemente de tip int şi “aşează


în faţă” toate elementele impare:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template <class T>
class divizibil
{ public:
bool operator() (T x)
{ return x%2; }
};
int main()
{ vector<int> v(6);
v[0]=1; v[1]=1; v[2]=3; v[3]=6; v[4]=5; v[5]=8;
vector<int>::iterator it;
it=partition([Link](), [Link](), divizibil<int>() );
for (size_t i=0; i<[Link](); i++) cout<<v[i]<<" ";
// s-a afisat 1 1 3 6 8
cout<<endl<<*it; // afiseaza 6
}

LECŢIA 29. Algoritmi de tip fill

 Algoritmul fill de mai jos, atribuie fiecărui element (obiect) al secvenţei


[st,fin) valoarea v:
template <class ForwardIterator, class T>
void fill(ForwardIterator st, ForwardIterator fin,
const T& v);

Exemplu. În secvenţa de mai jos, se iniţializează toate componentele vectorului v


cu valoarea ”15”:
vector<int> v(6);
...
fill([Link](),[Link](),15);

 Algoritmul fill_n iniţializează primele n componente (obiecte) ale unei


secvenţe cu valoarea v. Secvenţa începe cu componenta indicată de
iteratorul st:
template <class OutputIterator, class Size, class T>
OutputIterator fill_n(OutputIterator st, Size n, const T& v);

Exemplu. Se iniţializează primele trei componente ale unui vector cu ”15”:


vector<int> v(6);
...
fill_n([Link](),3,15);

191
PARTEA A V-A

LECŢIA 30. Algoritmi de tip unique


 Algoritmul unique de mai jos, elimină duplicatele adiacente din secvenţa
[st,fin). Practic, secvenţa obiectelor fără duplicate se va afla între [st,
iterator_returnat), unde iterator_returnat este iteratorul întors
de algoritm:
template <class ForwardIterator>
ForwardIterator unique(ForwardIterator st,
ForwardIterator fin);

Exemplu. În urma executării secvenţei, se afişează: 1 6 5 1.


vector<int> v(6);
v[0]=1; v[1]=1; v[2]=1; v[3]=6; v[4]=5; v[5]=1;
vector<int>::iterator it,it1;
it=unique([Link](),[Link]());
for(it1=[Link]();it1!=it;++it1) cout<<*it1;

 Algoritmul unique de mai jos, funcţionează asemănător cu cel anterior,


numai că semnificaţia a două “elemente egale” este dată de predicatul binar p:
template <class ForwardIterator, Predicat p>
ForwardIterator unique(ForwardIterator st,
ForwardIterator fin, predicat_binar p);

Exemplu. Se elimină duplicatele adiacente dintr-un vector. Vom considera că două


elemente sunt egale dacă dau acelaşi rest la împărţirea la 2. În aceste condiţii,
programul va afişa 1 6 5:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template <class T>
class lafel
{ public:
bool operator()(T x, T y)
{ return x%2==y%2; }
};
int main()
{ vector<int> v(6);
v[0]=1; v[1]=1; v[2]=1; v[3]=6; v[4]=5; v[5]=1;
vector<int>::iterator it,it1;
it=unique([Link](),[Link](),lafel<int>());
for(it1=[Link]();it1!=it; ++it1) cout<<*it1;
}

192

S-ar putea să vă placă și