Autor: Răzvan Cristea
Studentul va fi capabil la finalul acestui laborator să:
În acest laborator vom aprofunda utilizarea claselor abstracte și a interfețelor, concentrându-ne pe un aspect important al POO și anume gestionarea colecțiilor de date neomogene. Dacă până acum am lucrat cu pointeri pentru a manipula obiecte derivate din clase abstracte sau interfețe, acum vom explora cum putem organiza și gestiona aceste obiecte utilizând colecții de date, cum ar fi vectorii. Pentru o mai bună înțelegere a noțiunilor legate de clase abstracte și interfețe se recomandă citirea laboratorului 9.
Un vector de obiecte neomogene este o structură de date utilizată pentru a stoca și gestiona obiecte de tipuri diferite, dar care împărtășesc o relație comună definită printr-o clasă abstractă sau o interfață. Această abordare este posibilă datorită conceptului de late binding, care permite apelarea metodelor potrivite pentru fiecare pointer în funcție de tipul său concret, stabilit la momentul execuției.
Acest tip de vector nu stochează direct obiectele, ci pointeri la obiecte. Acest lucru asigură flexibilitatea de a lucra cu tipuri diferite de obiecte, cu condiția ca acestea să implementeze toate metodele virtuale sau virtual pure definite în clasa abstractă sau interfața comună.
Avantajele utilizării unui vector de obiecte neomogene sunt:
Astfel, vectorii de obiecte neomogene reprezintă un instrument esențial în gestionarea colecțiilor de date diverse, utilizând avantajele oferite de polimorfism și de principiile POO. Această tehnică facilitează crearea de sisteme modulare și extensibile, esențiale pentru aplicații complexe.
Pentru a putea construi un vector de obiecte neomogene mai întâi avem nevoie de o ierarhie de clase iar exemplul din acest laboartor este realizat cu ajutorul claselor ProdusElectronic care este o interfață și respectiv Laptop și SmartPhone care sunt clase concrete ce implementează interfața anterior menționată.
Interfața ProdusElectronic conține metodele virtual pure getPret
și respectiv getProducator
și un destructor virtual pur.
class ProdusElectronic { public: virtual ~ProdusElectronic() = 0; // destructor virtual pur virtual float getPret() const = 0; virtual char* getProducator() const = 0; };
Prin urmare vom furniza o implementare pentru destructorul clasei ProdusElectronic după cum urmează în secțiunea de cod de mai jos.
ProdusElectronic::~ProdusElectronic() { // chiar daca nu scriem nimic este necesar sa existe pentru a putea functiona corect dezalocarea memoriei }
Clasele Laptop și SmartPhone pot fi observate mai jos.
class Laptop : public ProdusElectronic { float pret; char* producator; public: Laptop(); Laptop(const float& pret, const char* producator); ~Laptop(); float getPret() const override; char* getProducator() const override; };
class SmartPhone : public ProdusElectronic { float pret; char* producator; public: SmartPhone(); SmartPhone(const float& pret, const char* producator); ~SmartPhone(); float getPret() const override; char* getProducator() const override; };
Iar implementările metodelor celor două clase se pot observa în blocurile de mai jos.
Laptop::Laptop() { pret = 0.0f; producator = nullptr; } Laptop::Laptop(const float& pret, const char* producator) { this->pret = pret; if (producator != nullptr) { this->producator = new char[strlen(producator) + 1]; strcpy(this->producator, producator); } else { this->producator = nullptr; } } Laptop::~Laptop() { if (producator != nullptr) { delete[] producator; } } float Laptop::getPret() const { return pret; } char* Laptop::getProducator() const { return producator; }
SmartPhone::SmartPhone() { pret = 0.0f; producator = nullptr; } SmartPhone::SmartPhone(const float& pret, const char* producator) { this->pret = pret; if (producator != nullptr) { this->producator = new char[strlen(producator) + 1]; strcpy(this->producator, producator); } else { this->producator = nullptr; } } SmartPhone::~SmartPhone() { if (producator != nullptr) { delete[] producator; } } float SmartPhone::getPret() const { return pret; } char* SmartPhone::getProducator() const { return producator; }
Pentru a putea declara un vector de obiecte neomogene vom folosi un dublu pointer la tipul interfeței ProdusElectronic pe care îl vom aloca dinamic folosind operatorul new după cum urmează în blocul de cod de mai jos.
#include "Laptop.h" #include "SmartPhone.h" int main() { int nrProduse = 5; ProdusElectronic** produse = new ProdusElectronic * [nrProduse]; // vector de obiecte neomogene produse[0] = new Laptop(5499.99f, "HP"); // late binding produse[1] = new SmartPhone(2499.99f, "Motorola"); produse[2] = new Laptop(3000.0f, "Asus"); produse[3] = new Laptop(7999.99f, "Lenovo"); produse[4] = new SmartPhone(3999.99f, "Apple"); for (int i = 0; i < nrProduse; i++) { std::cout << "Pretul produsului este: " << produse[i]->getPret() << '\n'; std::cout << "Numele producatorului este: " << produse[i]->getProducator() << "\n\n"; } return 0; }
Tot ceea ce mai trebuie să facem acum este să dezalocăm memoria corespunzător. Dacă mai întâi am alocat vectorul și apoi am alocat câte un slot de memorie pentru fiecare pointer din vector la eliberarea memoriei vom porni în ordinea inversă după cum se poate observa în codul de mai jos.
#include "Laptop.h" #include "SmartPhone.h" int main() { int nrProduse = 5; ProdusElectronic** produse = new ProdusElectronic * [nrProduse]; produse[0] = new Laptop(5499.99f, "HP"); // late binding produse[1] = new SmartPhone(2499.99f, "Motorola"); produse[2] = new Laptop(3000.0f, "Asus"); produse[3] = new Laptop(7999.99f, "Lenovo"); produse[4] = new SmartPhone(3999.99f, "Apple"); for (int i = 0; i < nrProduse; i++) { std::cout << "Pretul produsului este: " << produse[i]->getPret() << '\n'; std::cout << "Numele producatorului este: " << produse[i]->getProducator() << "\n\n"; } for (int i = 0; i < nrProduse; i++) { delete produse[i]; // eliberam fiecare slot din vector } delete[] produse; // stergem vectorul return 0; }
Pentru a înțelege mai bine structura unui vector de obiecte neomogene, este esențial să vizualizăm modul în care acesta este organizat în memorie.
În contextul exemplului prezentat anterior, ar fi foarte elegant să putem afișa elementele vectorului folosind operatorul <<, ceea ce ar simplifica și uniformiza procesul de afișare. Totuși, o problemă fundamentală apare din faptul că acest operator poate fi supraîncărcat, dar nu și suprascris.
Prin urmare operatorul << nu poate fi declarat virtual, astfel încât să permită apelul unei implementări specifice clasei derivate atunci când este utilizat printr-un pointer sau o referință la clasa de bază. Soluția implică de obicei definirea unei metode virtuale în clasa abstractă și utilizarea acesteia în supraîncărcarea operatorului <<.
Această abordare oferă o separare clară între logica specifică de afișare și mecanismul operatorului <<, respectând în același timp principiile polimorfismului.
Așadar vom declara o metodă virtual pură în interfața ProdusElectronic și vom anunța compilatorul că vom supraîncărca și operatorul de afișare.
class ProdusElectronic { protected: virtual void afisare(std::ostream& out) const = 0; // va fi folosita pentru operatorul << public: virtual ~ProdusElectronic() = 0; virtual float getPret() const = 0; virtual char* getProducator() const = 0; friend std::ostream& operator<<(std::ostream& out, const ProdusElectronic* const& produsElectronic); };
Iar implementarea operatorului de afișare este evidentă acum.
std::ostream& operator<<(std::ostream& out, const ProdusElectronic* const& produsElectronic) { produsElectronic->afisare(out); // ne folosim de late binding return out; }
Tot ce ne mai rămâne acum de făcut este să implementăm metoda virtual pură în clasele derivate și de restul se va ocupa late binding-ul.
class Laptop : public ProdusElectronic { float pret; char* producator; protected: void afisare(std::ostream& out) const override; public: Laptop(); Laptop(const float& pret, const char* producator); ~Laptop(); float getPret() const override; char* getProducator() const override; };
class SmartPhone : public ProdusElectronic { float pret; char* producator; protected: void afisare(std::ostream& out) const override; public: SmartPhone(); SmartPhone(const float& pret, const char* producator); ~SmartPhone(); float getPret() const override; char* getProducator() const override; };
Iar implementările celor 2 metode se pot observa în blocurile de cod de mai jos.
void Laptop::afisare(std::ostream& out) const { out << "Pretul laptopului este: " << pret << '\n'; out << "Numele producatorului de laptopuri este: "; if (producator != nullptr) { out << producator << '\n'; } else { out << "N/A\n"; } }
void SmartPhone::afisare(std::ostream& out) const { out << "Pretul smartphone-ului este: " << pret << '\n'; out << "Numele producatorului de smartphone-uri este: "; if (producator != nullptr) { out << producator << '\n'; } else { out << "N/A\n"; } }
Iar în funcția main vom afișa detaliile din vector folosind operatorul <<.
#include "Laptop.h" #include "SmartPhone.h" int main() { int nrProduse = 5; ProdusElectronic** produse = new ProdusElectronic * [nrProduse]; produse[0] = new Laptop(5499.99f, "HP"); produse[1] = new SmartPhone(2499.99f, "Motorola"); produse[2] = new Laptop(3000.0f, "Asus"); produse[3] = new Laptop(7999.99f, "Lenovo"); produse[4] = new SmartPhone(3999.99f, "Apple"); for (int i = 0; i < nrProduse; i++) { std::cout << produse[i] << '\n'; // se foloseste operatorul << definit in interfata ProdusElectronic } for (int i = 0; i < nrProduse; i++) { delete produse[i]; } delete[] produse; return 0; }
Acest lucru se realizează prin definirea unor funcții virtual pure în clasa de bază, care stabilesc comportamentul operatorului respectiv în subclase. Clasele derivate vor implementa aceste funcții virtuale, asigurând astfel logica specifică operatorului în funcție de cerințele fiecărei clase. Această abordare permite un design flexibil și robust, bazat pe mecanismul de run time polymorphism.
În acest laborator am aprofundat utilizarea claselor abstracte și a interfețelor, aplicând aceste concepte pentru a înțelege și a lucra cu vectori de obiecte neomogene. Acest tip de colecție permite stocarea de obiecte de tipuri diferite, având la bază relația de moștenire și utilizând polimorfismul pentru manipularea lor unitară.
Am înțeles cum să declarăm și să populăm un vector de obiecte neomogene, folosind pointeri către clase abstracte sau interfețe. Acest lucru ne-a permis să lucrăm eficient cu obiecte care împărtășesc o interfață comună, dar pot avea implementări distincte. Vectorii de acest tip reprezintă o continuare firească a conceptelor de clase abstracte și interfețe, aprofundate în laboratorul anterior.
Am discutat și implementat un destructor virtual pur, subliniind importanța sa în ierarhiile de clase. Un destructor virtual permite apelarea corectă a destructorilor din clasele derivate în momentul eliberării memoriei pentru obiectele stocate sub forma pointerilor la clasa de bază abstractă. Prin aceasta, am evitat scurgerile de memorie și alte comportamente neașteptate.
Am analizat și implementat supraîncărcarea operatorului << pentru a simplifica afișarea elementelor din vectorul de obiecte. Acest proces a evidențiat diferența fundamentală dintre supraîncărcare (overloading) și suprascriere (overriding). Deoarece operatorii nu pot fi suprascriși, ci doar supraîncărcați, am adaptat codul pentru a permite funcționarea acestuia cu obiecte de tipuri diferite.
Conceptul de late binding, explorat anterior, a fost aplicat pentru a apela metodele obiectelor din vectorul de obiecte neomogene. Acest lucru ne-a permis să tratăm obiecte de tipuri diferite într-o manieră uniformă, demonstrând puterea polimorfismului în proiectarea și implementarea aplicațiilor scalabile.
Activitatea din acest laborator a consolidat noțiunile teoretice și practice legate de clase abstracte, interfețe și polimorfism, oferindu-ne o bază solidă pentru a lucra cu colecții de obiecte complexe și eterogene.
Acest laborator ne-a demonstrat cum putem combina conceptele avansate de POO pentru a rezolva probleme reale, asigurând un design robust, flexibil și extensibil al codului.