Autor: Răzvan Cristea
Studentul va fi capabil la finalul acestui laborator să:
Până acum am învățat să scriem clase, fiecare clasă având un scop bine definit în cadrul unui program, reprezentând fundația pentru o organizare structurată a codului. De asemenea, am aprofundat utilizarea claselor template, care se dovedesc a fi extrem de utile în contextul programării generice, permițându-ne să creăm structuri de date și algoritmi care nu depind de tipul de date.
Toate aceste cunoștințe fundamentale despre programarea orientată obiect și programarea generică ne conduc către un pas natural și esențial în dezvoltarea aplicațiilor moderne și anume utilizarea bibliotecii Standard Template Library (STL). STL reprezintă o colecție puternică și versatilă de structuri de date, algoritmi și funcționalități reutilizabile, bazată pe conceptele de template-uri și moștenire.
Astfel, ceea ce am învățat până acum devine indispensabil pentru a înțelege și folosi corect STL-ul. Vom vedea cum principiile fundamentale ale POO și programării generice sunt integrate în STL, oferindu-ne instrumente eficiente pentru a rezolva probleme complexe într-un mod elegant și intuitiv. Acest laborator reprezintă o continuare logică a ceea ce am studiat până acum, punând în practică toate cunoștințele dobândite.
Standard Template Library a apărut în 1994, fiind creată de Alexander Stepanov și Meng Lee. Inițial, STL-ul a fost dezvoltat ca o bibliotecă independentă, iar în 1998, odată cu standardizarea limbajului C++ (ISO/IEC 14882:1998), STL-ul a fost inclus oficial ca parte a bibliotecii standard din limbajul C++.
Această introducere a STL-ului a reprezentat un moment revoluționar în istoria limbajului, oferind programatorilor un set bogat de structuri de date generice (precum vectori, liste, seturi, și map-uri), algoritmi și funcționalități asociative, toate bazate pe concepte precum template-uri și iteratori.
De atunci, STL-ul a evoluat și a fost extins în versiunile ulterioare ale standardului C++ (C++11, C++14, C++17, C++20, etc.), fiind integrată cu noile caracteristici ale limbajului.
Utilizarea bibliotecii STL în programele noastre oferă multiple avantaje care fac dezvoltarea mai eficientă, clară și profesională:
În loc să pierdem timp implementând de la zero structuri de date și algoritmi bine-cunoscuți, putem folosi implementările standard oferite de STL. Aceste implementări sunt create de experți, sunt testate riguros și sunt optimizate pentru performanță. Astfel, putem să ne concentrăm pe logica specifică a aplicației noastre, lăsând în seama STL-ului gestionarea eficientă a colecțiilor și algoritmilor.
Folosind STL-ul, codul nostru devine mai scurt și mai expresiv. În loc să definim structuri complexe sau să scriem algoritmi în detaliu, putem apela direct funcționalități standardizate. Acest lucru face ca programul să fie mai ușor de citit și de întreținut, permițând altor programatori să înțeleagă rapid intențiile noastre. Practic, biblioteca STL promovează un stil de programare modern, care pune accent pe lizibilitate și simplitate.
Pe scurt, STL-ul ne ajută să scriem un cod robust, eficient și ușor de înțeles, economisind timp prețios în procesul de dezvoltare. Totuși trebuie menționat faptul că o bună cunoaștere a conceptului de structuri de date și algoritmi va simplifca masiv înțelegerea modului în care componentele acestei biblioteci funcționează.
Biblioteca standard C++ (Standard C++ Library) cuprinde toate bibliotecile standard C precum si biblioteci dedicate C++ (ex: STL).
Ca o scurtă alcătuire această bibliotecă cuprinde:
În continuare vom prezenta diferite moduri de utilizare a câtorva clase din librăria standard pentru a înțelege conceptul de programare modernă în limbajul C++.
Așa cum am menționat mai sus clasa string este o clasă specială în C++ pentru lucrul cu șiruri de caractere. Această clasă a fost introdusă pentru a simplifca masiv lucrul cu datele de tip char*
. Un string își actualizează dimensiunea dinamic în funcție de ce operații suferă iar la final, când durata de viață a acestuia se încheie, este chemat automat destructorul care dezalocă automat memoria.
În continuare vom prezenta un tabel unde sunt descrise principalele metode și operatori care există în clasa string.
Denumire metodă / Operator | Descriere |
---|---|
append() | Adaugă un șir la sfârșitul stringului curent. |
insert() | Inserează un șir sau un caracter la o anumită poziție în string. |
erase() | Șterge o secțiune din string, specificată prin poziție și lungime. |
replace() | Înlocuiește o secțiune din string cu un alt șir. |
substr() | Returnează un substring pe baza unei poziții de început și a unei lungimi. |
find() | Găsește prima apariție a unui șir sau caracter și returnează poziția acestuia. |
rfind() | Găsește ultima apariție a unui șir sau caracter și returnează poziția acestuia. |
compare() | Compară stringul curent cu un alt string și returnează o valoare specifică. |
empty() | Verifică dacă stringul curent este gol. |
size() / length() | Returnează lungimea stringului curent. |
clear() | Șterge conținutul stringului, făcându-l gol. |
resize() | Modifică dimensiunea stringului, adăugând sau eliminând caractere. |
at() | Returnează caracterul de la o poziție specifică (cu verificare de limite). |
operator= | Atribuie un string altui string. |
operator[ ] | Returnează caracterul de la o poziție specifică (fără verificare de limite). |
operator+ | Concatenează două stringuri sau un string și un șir de caractere. |
operator+= | Adaugă un string sau un șir de caractere la stringul curent. |
operator==, !=, <, >, <=, >= | Comparații lexicografice între stringuri. |
operator>>, << | Citire și scriere pe bază de fluxuri. |
c_str() | Returnează un pointer la un șir de caractere constant (stil C). |
data() | Similar cu c_str() , dar poate include caractere nule interne. |
find_first_of() | Caută primul caracter dintr-o listă de caractere specificată. |
find_last_of() | Caută ultimul caracter dintr-o listă de caractere specificată. |
find_first_not_of() | Găsește prima poziție cu un caracter care nu face parte dintr-o listă specificată. |
find_last_not_of() | Găsește ultima poziție cu un caracter care nu face parte dintr-o listă specificată. |
În continuare vom prezenta un scurt exemplu de utilizare a acestei clase în C++. Pentru aceasta trebuie să includem fișierul corespunzător pentru această clasă și anume #include <string>
.
#include <string> #include <iostream> int main() { std::string text = "Salut"; std::cout << text << '\n'; text += " sunt un string."; std::cout << text << '\n'; std::cout << "Al patrulea caracter din string este: " << text[3] << '\n'; std::cout << "Al patrulea caracter din string este: " << text.at(3) << '\n'; std::cout << "Lungimea stringului este: " << text.length() << '\n'; std::string sir = "Acum am continutul altui string"; text = sir; if (text == sir) { std::cout << "Stringuri identice\n"; } else { std::cout << "Stringuri diferite\n"; } sir.clear(); if (sir.empty()) { std::cout << "Variabila sir este goala\n"; } std::cout << text << '\n'; text.clear(); std::cout << text << '\n'; return 0; }
\n
ne putem folosi de funcția getline.
#include <string> #include <iostream> int main() { std::string sir; std::cout << "Introduceti o propozitie: "; std::cin >> sir; std::cout << "Propozitia introdusa este: " << sir << "\n\n"; std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // necesar pentru a goli buffer-ul std::cout << "Introduceti o propozitie: "; std::getline(std::cin, sir); std::cout << "Propozitia introdusa este: " << sir << '\n'; return 0; }
În continuare vom explora câteva din clasele pe care STL-ul le pune la dispoziție. Dar înainte de a trece la acestă parte propunem un tabel cu principalele structuri de date pe care le găsim în această colecție.
Structura de Date | Numele STL | #include |
---|---|---|
Vector | std::vector | <vector> |
Listă dublu înlănțuită | std::list | <list> |
Deque (coadă cu 2 capete) | std::deque | <deque> |
Stivă | std::stack | <stack> |
Coadă | std::queue | <queue> |
Coadă cu priorități | std::priority_queue | <queue> |
Set | std::set | <set> |
Multiset | std::multiset | <set> |
Map | std::map | <map> |
Multimap | std::multimap | <map> |
Unordered Set | std::unordered_set | <unordered_set> |
Unordered Multiset | std::unordered_multiset | <unordered_set> |
Unordered Map | std::unordered_map | <unordered_map> |
Unordered Multimap | std::unordered_multimap | <unordered_map> |
Pereche | std::pair | <utility> |
Array fix | std::array | <array> |
Toate structurile de date prezentate în tabelul de mai sus în STL poartă denumirea de containere. Aceste containtere sunt de mai multe tipuri după cum urmează în descrierea de mai jos.
Containerele secvențiale sunt structuri de date care păstrează o succesiune de elemente de același tip T. Fiecare element din container are o poziție specifică, iar ordinea elementelor este determinată de ordinea în care au fost inserate în container. Aceste containere permit accesul la elemente într-un mod secvențial, pe baza poziției lor în container.
Există mai multe tipuri de containere secvențiale în C++:
Adaptoarele de containere sunt clase care oferă o interfață specifică pentru manipularea unui container de bază, de obicei un container secvențial sau asociativ. Acestea modifică comportamentul containerelor tradiționale pentru a răspunde unor nevoi particulare, cum ar fi accesul într-un mod specific sau implementarea unor tipuri de structuri de date cu comportamente specifice, cum ar fi stive, cozi sau cozi cu prioritate.
Cele mai frecvente adaptoare de containere sunt:
Containerele asociative stochează colecții de elemente în care poziția fiecărui element depinde de valoarea sa, conform unui criteriu de sortare specificat. Aceste containere permit acces rapid la elemente prin intermediul unei chei unice, ceea ce le face ideale pentru situațiile în care este necesară găsirea rapidă a unui element pe baza unei valori asociate (cheia). În general, operarea pe aceste containere are un timp de complexitate logaritmică O(log n) datorită implementării lor, adesea printr-un arbore binar de căutare echilibrat (AVL).
Containerele asociative din STL includ:
Introduse în C++11, containerele asociative neordonate stochează elemente care sunt accesibile rapid prin chei, dar nu sunt sortate într-o ordine specifică. În schimb, aceste containere folosesc o funcție de hash pentru a organiza elementele. Astfel, accesul la elemente este efectuat pe baza unui hash, ceea ce face căutările, inserările și ștergerile mult mai rapide (în medie O(1), dar în cel mai rău caz O(n)).
Containerele asociative neordonate includ:
Acum că avem clar în minte ce reprezintă fiecare tip de container putem trece la exemple concrete cu câteva dintre aceste clase template.
Pentru clasa vector propunem următorul tabel ca și referință:
Denumire metodă/Operator | Descriere |
---|---|
push_back() | Adaugă un element la sfârșitul vectorului. |
pop_back() | Elimină ultimul element din vector. |
at() | Returnează elementul de la o anumită poziție, cu verificare de limite. |
operator[ ] | Returnează elementul de la o anumită poziție, fără verificare de limite. |
size() | Returnează numărul curent de elemente din vector. |
capacity() | Returnează capacitatea totală a vectorului, adică numărul maxim de elemente pe care le poate stoca fără a realoca memorie. |
empty() | Verifică dacă vectorul este gol. |
resize() | Schimbă dimensiunea vectorului, adăugând sau eliminând elemente. |
reserve() | Modifică capacitatea minimă a vectorului, fără a-i schimba dimensiunea curentă. |
clear() | Șterge toate elementele din vector. |
insert() | Inserează unul sau mai multe elemente la o poziție specificată. |
erase() | Elimină unul sau mai multe elemente din vector. |
front() | Returnează o referință la primul element al vectorului. |
back() | Returnează o referință la ultimul element al vectorului. |
begin() | Returnează un iterator la primul element din vector. |
end() | Returnează un iterator la poziția de după ultimul element al vectorului. |
rbegin() | Returnează un iterator invers la ultimul element din vector. |
rend() | Returnează un iterator invers la poziția dinaintea primului element din vector. |
emplace_back() | Construiește un element direct la sfârșitul vectorului, evitând copierea. |
emplace() | Construiește un element la o poziție specificată din vector. |
shrink_to_fit() | Redimensionează capacitatea vectorului pentru a se potrivi exact cu dimensiunea curentă. |
assign() | Înlocuiește toate elementele vectorului cu un anumit număr de copii ale unui element sau cu o altă secvență de elemente. |
swap() | Schimbă conținutul vectorului cu cel al unui alt vector. |
Iar ca și exemplu simplu de utilizare a clasei vector propunem următorul bloc de cod.
#include <vector> #include <iostream> int main() { std::vector<int> numere; numere.push_back(5); numere.push_back(10); numere.push_back(1); for (int i = 0; i < numere.size(); i++) { std::cout << numere[i] << ' '; } numere.pop_back(); std::cout << '\n'; for (int i = 0; i < numere.size(); i++) { std::cout << numere[i] << ' '; } std::vector<int> copie; copie.swap(numere); copie.push_back(100); copie.push_back(20); std::cout << '\n'; for (int i = 0; i < copie.size(); i++) { std::cout << copie[i] << ' '; } copie.clear(); return 0; }
Pentru clasa list propunem următorul tabel ca și referință.
Denumire metodă | Descriere |
---|---|
push_back() | Adaugă un element la sfârșitul listei. |
push_front() | Adaugă un element la începutul listei. |
pop_back() | Elimină ultimul element din listă. |
pop_front() | Elimină primul element din listă. |
size() | Returnează numărul de elemente din listă. |
empty() | Verifică dacă lista este goală. |
clear() | Șterge toate elementele din listă. |
front() | Returnează o referință la primul element din listă. |
back() | Returnează o referință la ultimul element din listă. |
insert() | Inserează unul sau mai multe elemente la o poziție specificată. |
erase() | Elimină unul sau mai multe elemente din listă. |
begin() | Returnează un iterator la primul element din listă. |
end() | Returnează un iterator la poziția de după ultimul element al listei. |
rbegin() | Returnează un iterator invers la ultimul element al listei. |
rend() | Returnează un iterator invers la poziția dinaintea primului element din listă. |
emplace_back() | Construiește un element direct la sfârșitul listei, evitând copierea. |
emplace_front() | Construiește un element direct la începutul listei, evitând copierea. |
emplace() | Construiește un element la o poziție specificată din listă. |
assign() | Înlocuiește conținutul listei cu un anumit număr de copii ale unui element sau cu o altă secvență de elemente. |
remove() | Elimină toate elementele egale cu o anumită valoare. |
remove_if() | Elimină toate elementele care satisfac o anumită condiție. |
sort() | Sortează lista în ordine crescătoare (implică compararea elementelor). |
reverse() | Inversează ordinea elementelor din listă. |
merge() | Combină două liste sortate într-una singură. |
splice() | Mută elemente dintr-o listă în alta la o poziție specificată. |
unique() | Elimină elementele duplicate consecutive. |
swap() | Schimbă conținutul listei cu cel al unei alte liste. |
Iar acum pentru o mai bună înțelegere propunem următorul exemplu.
#include <list> #include <iostream> int main() { std::list<int> lista; lista.push_back(-1); lista.push_back(2); lista.push_back(0); lista.push_back(4); lista.push_front(10); lista.push_front(22); for (const int& element : lista) // bucla de tip foreach { std::cout << element << ' '; } lista.push_front(90); lista.pop_back(); // parcurgerea listei de la cap la coada std::list<int>::iterator it; // declaram un iterator de tipul listei pe care o parcurgem std::cout << '\n'; for (it = lista.begin(); it != lista.end(); it++) { std::cout << *it << ' '; } // parcurgerea listei de la coada la cap std::list<int>::reverse_iterator rit; // declaram un iterator invers de tipul listei pe care o parcurgem std::cout << '\n'; for (rit = lista.rbegin(); rit != lista.rend(); rit++) { std::cout << *rit << ' '; } lista.clear(); return 0; }
Pentru clasa stack propunem următorul tabel ca și referință.
Denumire metodă | Descriere |
---|---|
push() | Adaugă un element în vârful stivei. |
pop() | Elimină elementul din vârful stivei. |
top() | Returnează o referință la elementul din vârful stivei, fără a-l elimina. |
size() | Returnează numărul de elemente din stivă. |
empty() | Verifică dacă stiva este goală. |
emplace() | Construiește un element direct în vârful stivei, evitând copierea. |
swap() | Schimbă conținutul stivei cu cel al unei alte stive. |
Iar pentru clasa queue propunem tabelul de mai jos.
Denumire metodă | Descriere |
---|---|
push() | Adaugă un element la sfârșitul cozii. |
emplace() | Construiește un element direct la sfârșitul cozii, evitând copierea. |
pop() | Elimină elementul din fața cozii. |
front() | Returnează o referință la elementul din fața cozii, fără a-l elimina. |
back() | Returnează o referință la elementul de la sfârșitul cozii. |
size() | Returnează numărul de elemente din coadă. |
empty() | Verifică dacă coada este goală. |
swap() | Schimbă conținutul cozii cu cel al unei alte cozi. |
Iar ca și exemple de utilizare a acestor clase propunem următorul bloc de cod.
#include <stack> #include <queue> #include <string> #include <iostream> int main() { std::queue<float> coada; std::stack<std::string> stiva; stiva.push("Raul"); stiva.push("Alex"); stiva.push("Ioana"); coada.push(-5.5f); coada.push(10.23f); coada.push(22.0f); while (!stiva.empty()) { std::cout << stiva.top() << ' '; stiva.pop(); } std::cout << '\n'; while (!coada.empty()) { std::cout << coada.front() << ' '; coada.pop(); } return 0; }
Pentru clasa map propunem următorul tabel ca și referință.
Denumire metodă/Operator | Descriere |
---|---|
operator[ ] | Accesează sau inserează un element cu cheia specificată. |
at() | Returnează o referință la valoarea asociată unei chei, aruncând o excepție dacă cheia nu există. |
insert() | Inserează un element (pereche cheie-valoare) în map. |
emplace() | Construiește un element direct în map, evitând copierea. |
erase() | Elimină un element din map pe baza cheii sau a unui iterator. |
find() | Returnează un iterator către elementul cu cheia specificată sau end() dacă nu este găsit. |
count() | Returnează 1 dacă cheia există în map, altfel 0. |
size() | Returnează numărul de elemente din map. |
empty() | Verifică dacă map-ul este gol. |
clear() | Elimină toate elementele din map. |
swap() | Schimbă conținutul map-ului cu cel al unui alt map. |
lower_bound() | Returnează un iterator către primul element care nu este mai mic decât cheia specificată. |
upper_bound() | Returnează un iterator către primul element care este mai mare decât cheia specificată. |
Iar pentru clasa set propunem tabelul de mai jos.
Denumire metodă | Descriere |
---|---|
insert() | Inserează un element în set, păstrând ordinea și unicitatea. |
emplace() | Construiește un element direct în set, evitând copierea. |
erase() | Elimină un element din set pe baza valorii sau a unui iterator. |
find() | Returnează un iterator către elementul specificat sau end() dacă nu este găsit. |
count() | Returnează 1 dacă elementul există în set, altfel 0. |
size() | Returnează numărul de elemente din set. |
empty() | Verifică dacă set-ul este gol. |
clear() | Elimină toate elementele din set. |
swap() | Schimbă conținutul set-ului cu cel al unui alt set. |
lower_bound() | Returnează un iterator către primul element care nu este mai mic decât valoarea specificată. |
upper_bound() | Returnează un iterator către primul element care este mai mare decât valoarea specificată. |
equal_range() | Returnează o pereche de iteratoare care delimitează intervalul elementelor egale cu o valoare specificată. |
Iar ca și exemple de utilizare a acestor două clase propunem următorul bloc de cod.
#include <map> #include <set> #include <string> #include <iostream> int main() { std::map<int, std::string> mapa; mapa[1] = "Unu"; mapa[2] = "Doi"; mapa[3] = "Trei"; std::cout << "Elementele din mapa sunt afisate mai jos\n"; for (const std::pair<const int, std::string>& pair : mapa) { std::cout << "Cheie: " << pair.first << ", Valoare: " << pair.second << '\n'; } int keyToFind = 2; if (mapa.find(keyToFind) != mapa.end()) { std::cout << "Cheia " << keyToFind << " exista si valoarea asociata este: " << mapa[keyToFind] << '\n'; } else { std::cout << "Cheia " << keyToFind << " nu exista.\n"; } mapa.erase(2); std::cout << "Mapa dupa stergerea cheii 2\n"; for (const auto& pair : mapa) { std::cout << "Cheie: " << pair.first << ", Valoare: " << pair.second << '\n'; } std::set<int> arbore; arbore.insert(10); arbore.insert(20); arbore.insert(30); arbore.insert(20); // Ignorat automat deoarece 20 deja exista std::cout << "\nElementele din set sunt afisate mai jos.\n"; for (int value : arbore) { std::cout << value << ' '; } std::cout << '\n'; int valueToFind = 20; if (arbore.find(valueToFind) != arbore.end()) { std::cout << "Valoarea " << valueToFind << " exista în set.\n"; } else { std::cout << "Valoarea " << valueToFind << " nu exista în set.\n"; } arbore.erase(10); std::cout << "Set-ul dupa stergerea valorii 10:\n"; for (const int& value : arbore) { std::cout << value << ' '; } std::cout << '\n'; mapa.clear(); arbore.clear(); return 0; }
Pentru a folosi algoritmii din STL vom include fișierul antet algorithm
. Aceast fișier cuprinde algoritmi de sortare, căutare, numărare și mulți alții pe care nu mai trebuie să îi rescriem noi de la zero. Propunem un exemplu simplu de utlizare a funcțiilor sort și count în următorul bloc de cod C++.
#include <vector> #include <iostream> #include <algorithm> int main() { std::vector<int> numere; numere.push_back(100); numere.push_back(5); numere.push_back(2); numere.push_back(15); numere.push_back(22); numere.push_back(6); numere.push_back(8); numere.push_back(18); numere.push_back(27); numere.push_back(18); std::sort(numere.begin(), numere.end()); // se sorteaza vectorul crescator std::cout << "Vectorul sortat crescator este: "; for (const int& numar : numere) { std::cout << numar << ' '; } int target = 18; // valoarea careia vrem sa ii aflam numarul de aparitii din vector std::cout << "\nNumarul de aparitii pentru " << target << " este: " << std::count(numere.begin(), numere.end(), target) << '\n'; // se numara de cate ori apare in vector valoarea variabilei target return 0; }
În acest laborator am înțeles importanța fundamentelor solide în programare, pe care le-am construit pe parcursul a aproape trei semestre. Aceste cunoștințe ne permit acum să lucrăm eficient cu biblioteca STL, aplicând conceptele și tehnicile învățate anterior fără a fi nevoie să reinventăm algoritmi sau să implementăm structuri de date de la zero.
Am explorat principalele clase și metode din STL, precum std::vector, std::list, std::stack, std::queue, std::map și std::set. Am analizat modul în care acestea sunt proiectate pentru a oferi eficiență și claritate în rezolvarea diverselor probleme. Înțelegerea și utilizarea corectă a STL-ului ne permit să scriem cod mai concis, mai lizibil și mai ușor de întreținut, păstrând în același timp performanța.
Un punct cheie al laboratorului a fost reprezentat de analiza modului în care biblioteca STL îmbină conceptul de programare generică cu cel de eficiență. Am discutat avantajele utilizării STL-ului, precum reutilizarea codului bine testat și optimizat. De asemenea, am evidențiat cum se pot rezolva probleme mai complexe folosind STL-ul, pornind de la manipularea colecțiilor de date până la utilizarea algoritmilor predefiniți.
Acest laborator ne-a oferit o perspectivă practică asupra puterii STL-ului, consolidând în același timp legătura dintre cunoștințele teoretice acumulate anterior și aplicarea lor eficientă în rezolvarea problemelor. Pe scurt, am făcut un pas important spre a scrie un cod mai bun, bazat pe principiile programării moderne în C++.