This shows you the differences between two versions of the page.
poo-is-ab:laboratoare:09 [2024/11/30 20:20] razvan.cristea0106 [Vector de obiecte neomogene] |
poo-is-ab:laboratoare:09 [2025/09/23 20:14] (current) razvan.cristea0106 |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ===== Laborator 10 - Vectori de obiecte neomogene ===== | + | ===== Laborator 09 - Clase abstracte și interfețe ===== |
**Autor: Răzvan Cristea** | **Autor: Răzvan Cristea** | ||
Line 7: | Line 7: | ||
Studentul va fi capabil la finalul acestui laborator să: | Studentul va fi capabil la finalul acestui laborator să: | ||
- | * recunoască un vector de obiecte neomogene | + | * recunoască și să definească o funcție virtuală |
- | * știe cum să aloce și să dezaloce memorie pentru un astfel de vector | + | * recunoască și să definească o funcție virtual pură |
- | * înțeleagă importanța utilizării acestui vector corelat cu notiunea de suprascriere | + | * înțeleagă importanța utilizării claselor abstracte și a interfețelor în diferite contexte ale POO |
- | * prelucreze o colecție neomogenă de date cu ajutorul noțiunilor dobândite în laboratorul precedent | + | * înțeleagă conceptul de "late binding" corelat cu run time polymorphism |
- | * supraîncarce diverși operatori pentru clasa de bază pentru a-i putea folosi în prelucrarea vectorului | + | * înțeleagă diferența dintre supraîncărcare și suprascriere |
==== Introducere ==== | ==== Introducere ==== | ||
- | Î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 [[poo-is-ab:laboratoare:08|laboratorului 9]]. | + | Până în prezent, am explorat conceptul de **polimorfism timpuriu (early polymorphism sau compile-time polymorphism)**, care se manifestă atunci când **funcții/metode** cu **același nume** sunt **diferențiate prin numărul sau tipul parametrilor**. Acest lucru stă la baza conceptului de **supraîncărcare a funcțiilor (overloading)**, care permite definirea mai multor variante ale unei funcții în cadrul **aceleiași clase**, fiecare având un **comportament specific** în funcție de **semnătura sa**. Acest tip de polimorfism este decis la **compilare**, ceea ce înseamnă că alegerea funcției care va fi apelată se face de către **compilator** pe baza tipului argumentelor oferite. |
- | ==== Vector de obiecte neomogene ==== | + | De exemplu, dacă avem o funcție **''adunare''** supraîncărcată pentru a lucra cu numere întregi și cu numere reale, **compilatorul** determină **automat** care versiune a funcției urmează să fie apelată, în funcție de **tipul** datelor primite ca argumente. Avantajul acestui tip de polimorfism este **viteza de execuție**, deoarece decizia a fost deja luată **înainte** ca programul să ruleze. |
- | 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 obiect în funcție de tipul său concret, stabilit la momentul execuției. | + | Cu toate acestea, există cazuri în care decizia pentru funcția/metoda ce trebuie apelată **nu** poate fi luată de către compilator, ci doar în timpul execuției. Acest procedeu este cunoscut sub denumirea de **polimorfism întârziat (late binding sau run-time polymorphism)**. Acest concept este strâns legat de **suprascrierea funcțiilor (overriding)** și de utilizarea mecanismelor de **moștenire** și **funcții virtuale** în C++. |
- | 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ă. | + | ==== Overloading vs Overriding ==== |
- | Avantajele utilizării unui vector de obiecte neomogene sunt: | + | În continuare vom prezenta un tabel care pune în evidență diferențele clare între cele două forme de polimorfism. |
- | + | ||
- | * **Polimorfism**: Gestionarea comportamentelor variate ale obiectelor printr-o singură interfață | + | |
- | * **Scalabilitate**: Adăugarea de noi tipuri de obiecte devine ușoară, fără a modifica codul existent | + | ^ **Overloading (Compile-Time Polymorphism)** ^ **Overriding (Run-Time Polymorphism)** ^ |
+ | | Se aplică funcțiilor/metodelor din **aceeași clasă** | Apare în ierarhiile de clase (**moștenire**) | | ||
+ | | Funcțiile/metodele au **același nume**, dar **diferă** prin **numărul sau tipul parametrilor** | O funcție/metodă dintr-o clasă derivată **suprascrie** comportamentul unei **funcții virtuale** din clasa de bază | | ||
+ | | Alegerea funcției este făcută de către **compilato**r pe baza semnăturii acesteia | Alegerea funcției care va fi apelată este făcută la **momentul execuției**, în funcție de **tipul dinamic** al obiectului | | ||
+ | | **Nu** necesită funcții virtuale/virtual pure. | **Necesită** utilizarea funcțiilor virtuale/virtual pure în clasa de bază. | | ||
+ | | Este o formă de polimorfism timpuriu (**early binding**) | Este o formă de polimorfism întârziat (**late binding**) | | ||
+ | | Are un impact **redus** asupra performanței, deoarece decizia se ia la compilare | Are un impact **ușor mai mare** asupra performanței, deoarece decizia se ia în timpul execuției | | ||
- | 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. | + | **Suprascrierea** (**overriding**) implică păstrarea aceluiași antent al funcției – adică același tip de return și aceeași semnătura (numele și lista de parametri) – dar permite redefinirea completă a comportamentului acesteia într-o **clasă derivată**. Prin această abordare, o funcție virtuală din clasa de bază poate fi adaptată pentru a răspunde nevoilor specifice ale clasei derivate. Acest proces este esențial pentru **polimorfismul la timp de execuție**, oferind flexibilitate și posibilitatea de a extinde funcționalitățile într-un mod dinamic. |
- | 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ă. | + | Pe de altă parte, **supraîncărcarea** (**overloading**) presupune existența mai multor funcții cu același nume în cadrul aceleiași clase, dar care diferă prin numărul sau tipul parametrilor. Deși semnăturile sunt distincte, logica generală a funcțiilor rămâne similară, acestea fiind utilizate pentru a oferi funcționalități variate în contexte diferite. Alegerea variantei corespunzătoare este realizată la timpul de compilare, ceea ce asigură o execuție rapidă. |
- | Interfața **ProdusElectronic** contine metodele virtual pure ''**getPret**'' și respectiv ''**getProducator**'' și un destructor virtual pur. | + | <note important>Astfel **suprascrierea** permite modificarea profundă a comportamentului unei funcții în cadrul unei **ierarhii de clase**, în timp ce **supraîncărcarea** oferă posibilitatea reutilizării aceluiași nume de funcție pentru a gestiona scenarii variate, păstrând însă consistența logicii.</note> |
+ | |||
+ | ==== Funcții virtuale ==== | ||
+ | |||
+ | Funcțiile **virtuale** reprezintă principalul mecanism prin care putem evidenția conceptul de **suprascriere (override)** în programarea orientată pe obiecte. Atunci când o clasă conține **cel puțin o metodă virtuală**, pentru acea clasă este generată o structură denumită **tabelă de pointeri la funcții virtuale (vtable)**. Această tabelă stochează adresele funcțiilor virtuale definite în clasa de bază, respectiv în clasele derivate. | ||
+ | |||
+ | Fiecare obiect al unei clase care derivă din clasa de bază cu metode virtuale va primi un **pointer la vtable**, ceea ce permite **legarea dinamică** a funcțiilor. Prin urmare, în momentul în care se apelează o **funcție virtuală**, sistemul determină în momentul execuției care implementare specifică (din clasa de bază sau din clasa derivată) trebuie utilizată, în funcție de **tipul dinamic al obiectului**. | ||
+ | |||
+ | Acest mecanism stă la baza conceptului de **late binding** sau **legare dinamică**, care presupune că alegerea funcției ce urmează să fie executată **nu** este stabilită în timpul compilării, ci **în timpul execuției**, oferind astfel o flexibilitate sporită și suport pentru polimorfismul la timp de execuție. | ||
+ | |||
+ | <note warning>**Stabilirea tipului de date** în cazul **funcțiilor virtuale** se aplică exclusiv **pointerilor la obiecte**, **nu** și obiectelor utilizate direct (prin valoare). Acest lucru se datorează faptului că **legarea dinamică** (**late binding**) funcționează doar atunci când accesul la funcțiile virtuale se face prin intermediul unui pointer.</note> | ||
+ | |||
+ | <note>În C++ există **două** tipuri de funcții virtuale și anume: **funcții virtuale** și **funcții virtual pure**. Diferența între cele două tipuri constă în faptul că o funcție virtual pură **nu** are implementare în clasa de bază.</note> | ||
+ | |||
+ | În limbajul C++ pentru a declara o funcție virtuală într-o clasă se utilizeaza cuvântul cheie **virtual**. Dacă am spus funcție virtuală asta înseamnă că în clasa de bază acea funcție va avea implementare. Ca și exemplu vom propune clasele **Animal** și **Caine**. | ||
<code cpp> | <code cpp> | ||
- | class ProdusElectronic | + | class Animal |
{ | { | ||
public: | public: | ||
- | virtual ~ProdusElectronic() = 0; // destructor virtual pur | + | void afisare() const; |
- | + | ||
- | virtual float getPret() const = 0; | + | |
- | virtual char* getProducator() const = 0; | + | |
}; | }; | ||
+ | |||
+ | void Animal::afisare() const | ||
+ | { | ||
+ | std::cout << "Sunt un animal!\n"; | ||
+ | } | ||
</code> | </code> | ||
- | <note important>În C++, unui **destructor virtual pur** trebuie să îi oferim o **implementare** deoarece va fi **întotdeauna apelat** atunci când un **obiect derivat** este **distrus**. Această cerință se bazează pe mecanismul de distrugere a obiectelor, care implică apelarea destructorilor **în ordine inversă** a **constructorilor**, inclusiv pentru clasa de bază.</note> | + | Și respectiv clasa **Caine**. |
- | + | ||
- | Prin urmare vom furniza o implementare pentru destructorul clasei **ProdusElectronic** după cum urmează în secțiunea de cod de mai jos. | + | |
<code cpp> | <code cpp> | ||
- | ProdusElectronic::~ProdusElectronic() | + | class Caine : public Animal |
{ | { | ||
- | // chiar daca nu scriem nimic este necesar sa existe pentru a putea functiona corect dezalocarea memoriei | + | public: |
+ | |||
+ | void afisare() const; | ||
+ | }; | ||
+ | |||
+ | void Caine::afisare() const | ||
+ | { | ||
+ | std::cout << "Sunt un caine!\n"; | ||
} | } | ||
</code> | </code> | ||
- | Clasele Laptop și Smartphone pot fi observate mai jos. | + | În funcția **main** vom demostra faptul că deși câinele este un animal se vor apela metodele specifice tipurilor de date din cauza faptului că în clasa Animal metoda **''afisare''** nu este declarată ca fiind virtuală. |
<code cpp> | <code cpp> | ||
- | class Laptop : public ProdusElectronic | + | int main() |
{ | { | ||
- | float pret; | + | Animal animal; |
- | char* producator; | + | animal.afisare(); // se apeleaza afisarea din Animal |
- | public: | + | Caine caine; |
+ | caine.afisare(); // se apeleaza afisarea din Caine | ||
- | Laptop(); | + | animal = caine; |
- | Laptop(const float& pret, const char* producator); | + | animal.afisare(); // se apeleaza afisarea din Animal |
- | ~Laptop(); | + | |
- | float getPret() const override; | + | Animal* pAnimal = new Animal(); |
- | char* getProducator() const override; | + | pAnimal->afisare(); // se apeleaza afisarea din Animal |
- | }; | + | |
+ | Caine* pCaine = new Caine(); | ||
+ | pCaine->afisare(); // se apeleaza afisarea din Caine | ||
+ | |||
+ | Animal* pa = pCaine; | ||
+ | pa->afisare(); // se apeleaza afisarea din Animal desi ar fi trebui sa se apeleze cea din Caine | ||
+ | |||
+ | delete pAnimal; | ||
+ | delete pCaine; | ||
+ | |||
+ | return 0; | ||
+ | } | ||
</code> | </code> | ||
+ | |||
+ | Soluția este să marcăm metoda de **''afisare''** ca fiind virtuală pentru a putea permite **legături întârziate**. | ||
<code cpp> | <code cpp> | ||
- | class SmartPhone : public ProdusElectronic | + | class Animal |
{ | { | ||
- | float pret; | ||
- | char* producator; | ||
- | |||
public: | public: | ||
- | SmartPhone(); | + | virtual void afisare() const; // metoda afisare este acum virtuala ceea ce inseamna ca vom putea avea legari dinamice |
- | SmartPhone(const float& pret, const char* producator); | + | |
- | ~SmartPhone(); | + | |
- | + | ||
- | float getPret() const override; | + | |
- | char* getProducator() const override; | + | |
}; | }; | ||
</code> | </code> | ||
- | Iar implementările metodelor celor două clase se pot observa în blocurile de mai jos. | + | Iar dacă vom testa acum codul din **funcția main** vom vedea că se va produce o **legare dinamică** atunci când vom chema metoda **''afisare''** prin intermediul pointerului **''pa''**. |
<code cpp> | <code cpp> | ||
- | Laptop::Laptop() | + | int main() |
{ | { | ||
- | pret = 0.0f; | + | Animal animal; |
- | producator = nullptr; | + | animal.afisare(); // se apeleaza afisarea din Animal |
- | } | + | |
- | Laptop::Laptop(const float& pret, const char* producator) | + | Caine caine; |
- | { | + | caine.afisare(); // se apeleaza afisarea din Caine |
- | this->pret = pret; | + | |
+ | animal = caine; | ||
+ | animal.afisare(); // se apeleaza afisarea din Animal deoarece animal nu este pointer | ||
- | if (producator != nullptr) | + | Animal* pAnimal = new Animal(); |
- | { | + | pAnimal->afisare(); // se apeleaza afisarea din Animal |
- | this->producator = new char[strlen(producator) + 1]; | + | |
- | strcpy(this->producator, producator); | + | Caine* pCaine = new Caine(); |
- | } | + | pCaine->afisare(); // se apeleaza afisarea din Caine |
- | else | + | |
- | { | + | Animal* pa = pCaine; |
- | this->producator = nullptr; | + | pa->afisare(); // se apeleaza afisarea din Caine |
- | } | + | |
+ | delete pAnimal; | ||
+ | delete pCaine; | ||
+ | |||
+ | return 0; | ||
} | } | ||
+ | </code> | ||
+ | |||
+ | Pentru a fi și mai riguroși putem marca în **clasa derivată** metoda de **''afisare''** cu **override** pentru a anunța compilatorul că metoda care provine din **superclasă** urmează să fie **suprascrisă**. | ||
- | Laptop::~Laptop() | + | <code cpp> |
+ | class Caine : public Animal | ||
{ | { | ||
- | if (producator != nullptr) | + | public: |
- | { | + | |
- | delete[] producator; | + | |
- | } | + | |
- | } | + | |
- | float Laptop::getPret() const | + | void afisare() const override; // acum este mentionat explicit faptul ca metoda din Caine o va suprascrie pe cea din Animal |
+ | }; | ||
+ | </code> | ||
+ | |||
+ | <note important>Cuvântul cheie **override** în **C++** este folosit pentru a specifica în mod **explicit** că o funcție membră dintr-o **clasă derivată** suprascrie o metodă **virtuală** din **clasa de bază**. Acest mecanism oferă mai multă siguranță în ceea ce privește **suprascrierea** funcțiilor și ajută la prevenirea **erorilor de programare**.</note> | ||
+ | |||
+ | ==== Clase abstracte ==== | ||
+ | |||
+ | În limbajul C++, o **clasă abstractă** este o clasă care conține **cel puțin** o **metodă virtuală pură**. **Metoda virtuală pură** este o funcție declarată în clasa de bază, dar care **nu are o implementare** în această clasă, **obligând** astfel **clasele derivate** să o **suprascrie**. O **clasă abstractă** este utilizată pentru a defini un comportament **general** care trebuie să fie specificat în **mod detaliat** în **clasele derivate**. | ||
+ | |||
+ | <note warning>O clasă abstractă **nu** poate fi folosită pentru a crea **obiecte**. Este concepută să fie doar o bază pentru alte clase care o vor moșteni. În schimb se pot instanția **pointeri** de tipul acestei clase care să fie inițializati cu ajutorul **claselor derivate**. Un alt aspect ce trebuie menționat este faptul că **orice** clasă derivată dintr-o clasă abstractă **trebuie** să implementeze **toate** metodele virtual pure, altfel va deveni ea însăși o **clasă abstractă**.</note> | ||
+ | |||
+ | O clasă abstractă poate avea membri și metode precum constructori, getteri, setteri și destructor care vor fi apelate în clasele derivate. În continuare vom prezenta modul în care putem pune în evidență conceptul de **late binding** transformând clasa **Animal** într-o clasă abstractă. | ||
+ | |||
+ | <code cpp> | ||
+ | class Animal | ||
{ | { | ||
- | return pret; | + | char* nume; |
- | } | + | int varsta; |
- | char* Laptop::getProducator() const | + | public: |
+ | |||
+ | Animal(); | ||
+ | Animal(const char* nume, const int& varsta); | ||
+ | Animal(const Animal& animal); | ||
+ | ~Animal(); | ||
+ | |||
+ | char* getNume() const; | ||
+ | int getVarsta() const; | ||
+ | |||
+ | virtual void afisare() const = 0; // metoda virtual pura | ||
+ | }; | ||
+ | </code> | ||
+ | |||
+ | Iar clasa **Caine** moștenește clasa **Animal** și implementează metoda virtual pură din clasa de bază. | ||
+ | |||
+ | <code cpp> | ||
+ | class Caine : public Animal | ||
{ | { | ||
- | return producator; | + | char* rasa; |
- | } | + | |
+ | public: | ||
+ | |||
+ | Caine(); | ||
+ | Caine(const char* nume, const int& varsta, const char* rasa); | ||
+ | Caine(const Caine& animal); | ||
+ | ~Caine(); | ||
+ | |||
+ | void afisare() const override; // implementam metoda din clasa de baza | ||
+ | }; | ||
</code> | </code> | ||
+ | |||
+ | Iar implementările metodelor din clasa **Caine** se pot observa mai jos. | ||
<code cpp> | <code cpp> | ||
- | SmartPhone::SmartPhone() | + | Caine::Caine() : Animal() |
{ | { | ||
- | pret = 0.0f; | + | rasa = nullptr; |
- | producator = nullptr; | + | |
} | } | ||
- | SmartPhone::SmartPhone(const float& pret, const char* producator) | + | Caine::Caine(const char* nume, const int& varsta, const char* rasa) : Animal(nume, varsta) |
{ | { | ||
- | this->pret = pret; | + | if (rasa != nullptr) |
- | + | ||
- | if (producator != nullptr) | + | |
{ | { | ||
- | this->producator = new char[strlen(producator) + 1]; | + | this->rasa = new char[strlen(rasa) + 1]; |
- | strcpy(this->producator, producator); | + | strcpy(this->rasa, rasa); |
} | } | ||
else | else | ||
{ | { | ||
- | this->producator = nullptr; | + | this->rasa = nullptr; |
} | } | ||
} | } | ||
- | SmartPhone::~SmartPhone() | + | Caine::Caine(const Caine& caine) : Animal(caine) |
{ | { | ||
- | if (producator != nullptr) | + | if (caine.rasa != nullptr) |
+ | { | ||
+ | rasa = new char[strlen(caine.rasa) + 1]; | ||
+ | strcpy(rasa, caine.rasa); | ||
+ | } | ||
+ | else | ||
{ | { | ||
- | delete[] producator; | + | rasa = nullptr; |
} | } | ||
} | } | ||
- | float SmartPhone::getPret() const | + | Caine::~Caine() |
{ | { | ||
- | return pret; | + | if (rasa != nullptr) |
+ | { | ||
+ | delete[] rasa; | ||
+ | } | ||
} | } | ||
- | char* SmartPhone::getProducator() const | + | void Caine::afisare() const |
{ | { | ||
- | return producator; | + | std::cout << "Nume: " << (getNume() ? getNume() : "Anonim") << '\n'; |
+ | std::cout << "Varsta: " << getVarsta() << " ani\n"; | ||
+ | std::cout << "Rasa: " << (rasa ? rasa : "Necunoscuta") << "\n\n"; | ||
} | } | ||
</code> | </code> | ||
- | 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ă. | + | Iar mai jos propunem un exemplu de testare a acestor clase. |
<code cpp> | <code cpp> | ||
- | #include "Laptop.h" | ||
- | #include "Smartphone.h" | ||
- | |||
int main() | int main() | ||
{ | { | ||
- | int nrProduse = 5; | + | Caine caine("Zeus", 3, "labrador"); |
- | ProdusElectronic** produse = new ProdusElectronic * [nrProduse]; // vector de obiecte neomogene | + | caine.afisare(); // valid |
- | produse[0] = new Laptop(5499.99f, "HP"); // late binding | + | Animal* pAnimal = new Caine(); |
- | produse[1] = new SmartPhone(2499.99f, "Motorola"); | + | pAnimal->afisare(); // valid deoarece metoda afisare este virtuala |
- | 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++) | + | delete pAnimal; // incorect nu se cheama si destructorul din Caine, memory leak |
- | { | + | |
- | std::cout << "Pretul produsului este: " << produse[i]->getPret() << '\n'; | + | pAnimal = &caine; // valid datorita relatiei de tip "is-a" |
- | std::cout << "Numele producatorului este: " << produse[i]->getProducator() << "\n\n"; | + | pAnimal->afisare(); // valid |
- | } | + | |
return 0; | return 0; | ||
Line 205: | Line 283: | ||
</code> | </code> | ||
- | 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 blocul de cod de mai jos. | + | <note warning>În momentul de față, codul de test din **funcția main** produce un **memory leak** deoarece, în mod normal, la eliberarea memoriei, destructorii ar trebui să se apeleze în ordinea inversă apelării constructorilor. Dacă, în cazul nostru, mai întâi s-a apelat constructorul fără parametri pentru clasa **Animal** și apoi cel din clasa **Caine**, atunci, la **distrugerea** obiectului, se va apela destructorul **Animal** fără a-l apela și pe cel al clasei **Caine**. |
- | <code cpp> | + | Acest comportament apare deoarece destructorul din clasa de bază **nu** este declarat **virtual**. În cazul ierarhiilor de clase care folosesc **metode virtuale** sau **metode virtual pure**, este absolut necesar ca **destructorul clasei de bază** să fie declarat **virtual**, astfel încât să asigure eliberarea corectă a resurselor. Când destructorul este declarat **virtual**, el va garanta apelarea în mod **corect** a destructorilor pentru toate **clasele derivate** implicate.</note> |
- | #include "Laptop.h" | + | |
- | #include "Smartphone.h" | + | |
- | int main() | + | Așadar pentru a elibera memoria **corect** vom declara destructorul clasei **Animal** ca **funcție virtuală**. |
+ | |||
+ | <code cpp> | ||
+ | class Animal | ||
{ | { | ||
- | int nrProduse = 5; | + | char* nume; |
- | ProdusElectronic** produse = new ProdusElectronic * [nrProduse]; | + | int varsta; |
- | produse[0] = new Laptop(5499.99f, "HP"); // late binding | + | public: |
- | 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++) | + | Animal(); |
- | { | + | Animal(const char* nume, const int& varsta); |
- | std::cout << "Pretul produsului este: " << produse[i]->getPret() << '\n'; | + | Animal(const Animal& animal); |
- | std::cout << "Numele producatorului este: " << produse[i]->getProducator() << "\n\n"; | + | virtual ~Animal(); // destructor virtual |
- | } | + | |
- | for (int i = 0; i < nrProduse; i++) | + | char* getNume() const; |
- | { | + | int getVarsta() const; |
- | delete produse[i]; // eliberam fiecare slot din vector | + | |
- | } | + | |
- | delete[] produse; // stergem vectorul | + | virtual void afisare() const = 0; // metoda virtual pura |
- | + | }; | |
- | return 0; | + | |
- | } | + | |
</code> | </code> | ||
- | 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. | + | Iar acum codul de test din funcția main **nu** va mai genera **scurgeri de memorie** fiind apelați destructorii în ordinea inversă apelării constructorilor. Codul complet cu implementările celor două clase poate fi descărcat de {{:poo-is-ab:laboratoare:clasa_abstracta.zip|aici}}. |
- | {{ :poo-is-ab:laboratoare:vector_neomogen.jpg?direct&600 |}} | + | ==== Interfețe ==== |
- | ==== Supraîncărcarea operatorului << pentru o clasă abstractă ==== | + | În limbajul C++, o **interfață** este o formă specializată de **clasă abstractă** care conține **exclusiv** metode **virtual pure** și, de regulă, un destructor **virtual pur**. Fiind o clasă destinată exclusiv definirii de funcționalități, o interfață **nu** conține **membri** și **nici constructori**, deoarece scopul său **nu** este să stocheze starea unui obiect, ci să specifice un **set de comportamente** pe care **clasele derivate** le vor implementa. |
- | Î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**. | + | În continuare vom prezenta un tabel cu caracteristicile interfețelor și ale claselor abstracte pentru a putea identifica eventuale asemănări și deosebiri și pentru a putea înțelege când avem nevoie de fiecare în parte în practică. |
- | 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 %%<<%%**. | + | ^ Caracteristică ^ Interfață ^ Clasă abstractă ^ |
+ | | **Metode** | Conține doar metode virtual pure | Poate conține metode virtual pure și metode concrete | | ||
+ | | **Membri de date** | Nu poate avea membri | Poate avea membri | | ||
+ | | **Constructori** | Nu poate avea constructori | Poate avea constructori | | ||
+ | | **Moștenire multiplă** | Este utilizată pentru moștenirea multiplă, mai ales pentru a defini contracte comune | Este utilizată în ierarhii simple sau complexe, dar poate genera ambiguități în moștenirea multiplă | | ||
+ | | **Scop** | Definește un contract strict pentru clasele derivate | Definește un comportament parțial și oferă reutilizarea codului | | ||
+ | | **Destructor** | Necesită destructor virtual pur atunci când în clasele derivate există membri de tip pointer | Destructorul virtual care trebuie implementat când există pointeri în clasă | | ||
+ | | **Instanțiere** | Nu poate fi instanțiată, dar se pot utiliza pointeri la tipul acesteia | Nu poate fi instanțiată, dar poate avea constructori pentru clase derivate | | ||
- | Această abordare oferă o separare clară între logica specifică de afișare și mecanismul **operatorului %%<<%%**, respectând în același timp principiile polimorfismului. | + | Ca și exemplu propunem clasa **FiguraGeometrica** care va fi o interfață ce conține metodele **''getArie''** și **''getPerimetru''**, iar ca și clase derivate vom lucra cu **Cerc** și **Patrat**. |
- | + | ||
- | 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. | + | |
<code cpp> | <code cpp> | ||
- | class ProdusElectronic | + | class FiguraGeomertica // interfata |
{ | { | ||
- | protected: | ||
- | |||
- | virtual void afisare(std::ostream& out) const = 0; // va fi folosita pentru operatorul << | ||
- | |||
public: | public: | ||
- | virtual ~ProdusElectronic() = 0; | + | virtual float getArie() const = 0; |
- | + | virtual float getPerimetru() const = 0; | |
- | virtual float getPret() const = 0; | + | |
- | virtual char* getProducator() const = 0; | + | |
- | + | ||
- | friend std::ostream& operator<<(std::ostream& out, const ProdusElectronic* const& produsElectronic); | + | |
}; | }; | ||
</code> | </code> | ||
- | Iar implementarea operatorului de afișare este evidentă acum. | + | Iar în continuare vom prezenta clasele **Cerc** și **Patrat**. |
<code cpp> | <code cpp> | ||
- | std::ostream& operator<<(std::ostream& out, const ProdusElectronic* const& produsElectronic) | + | class Cerc : public FiguraGeomertica |
{ | { | ||
- | produsElectronic->afisare(out); | + | int raza; |
- | return out; | + | |
- | } | + | |
- | </code> | + | |
- | + | ||
- | 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**. | + | |
- | + | ||
- | <code cpp> | + | |
- | class Laptop : public ProdusElectronic | + | |
- | { | + | |
- | float pret; | + | |
- | char* producator; | + | |
- | + | ||
- | protected: | + | |
- | + | ||
- | void afisare(std::ostream& out) const override; | + | |
public: | public: | ||
- | Laptop(); | + | Cerc(const int& raza = 0); |
- | Laptop(const float& pret, const char* producator); | + | |
- | ~Laptop(); | + | |
- | float getPret() const override; | + | float getArie() const override; |
- | char* getProducator() const override; | + | float getPerimetru() const override; |
}; | }; | ||
</code> | </code> | ||
+ | |||
+ | Iar mai jos sunt prezentate implementările metodelor aferente clasei **Cerc**. | ||
<code cpp> | <code cpp> | ||
- | class SmartPhone : public ProdusElectronic | + | Cerc::Cerc(const int& raza) |
{ | { | ||
- | float pret; | + | this->raza = raza; |
- | char* producator; | + | } |
- | protected: | + | float Cerc::getArie() const |
+ | { | ||
+ | return 3.14f * raza * raza; | ||
+ | } | ||
- | void afisare(std::ostream& out) const override; | + | float Cerc::getPerimetru() const |
+ | { | ||
+ | return 2 * 3.14f * raza; | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | Declarația clasei **Patrat** se poate observa mai jos. | ||
+ | |||
+ | <code cpp> | ||
+ | class Patrat : public FiguraGeomertica | ||
+ | { | ||
+ | int latura; | ||
public: | public: | ||
- | SmartPhone(); | + | Patrat(const int& latura = 0); |
- | SmartPhone(const float& pret, const char* producator); | + | |
- | ~SmartPhone(); | + | |
- | float getPret() const override; | + | float getArie() const override; |
- | char* getProducator() const override; | + | float getPerimetru() const override; |
}; | }; | ||
</code> | </code> | ||
- | Iar implementările celor 2 metode se pot observa în blocurile de cod de mai jos. | + | Iar implementările metodelor sunt disponibile mai jos. |
<code cpp> | <code cpp> | ||
- | void Laptop::afisare(std::ostream& out) const | + | Patrat::Patrat(const int& latura) |
{ | { | ||
- | out << "Pretul laptopului este: " << pret << '\n'; | + | this->latura = latura; |
- | out << "Numele producatorului de laptopuri este: "; | + | |
- | + | ||
- | if (producator != nullptr) | + | |
- | { | + | |
- | out << producator << '\n'; | + | |
- | } | + | |
- | else | + | |
- | { | + | |
- | out << "N/A\n"; | + | |
- | } | + | |
} | } | ||
- | </code> | ||
- | <code cpp> | + | float Patrat::getArie() const |
- | void SmartPhone::afisare(std::ostream& out) const | + | |
{ | { | ||
- | out << "Pretul smartphone-ului este: " << pret << '\n'; | + | return(float) latura * latura; |
- | out << "Numele producatorului de smartphone-uri este: "; | + | } |
- | if (producator != nullptr) | + | float Patrat::getPerimetru() const |
- | { | + | { |
- | out << producator << '\n'; | + | return(float) 4 * latura; |
- | } | + | |
- | else | + | |
- | { | + | |
- | out << "N/A\n"; | + | |
- | } | + | |
} | } | ||
</code> | </code> | ||
- | Iar în funcția **main** vom afișa detaliile din vector folosind **operatorul %%<<%%**. | + | Iar ca și exemplu de testare a funcționalităților în funcția main avem: |
<code cpp> | <code cpp> | ||
- | #include "Laptop.h" | ||
- | #include "Smartphone.h" | ||
- | |||
int main() | int main() | ||
{ | { | ||
- | int nrProduse = 5; | + | FiguraGeomertica* pfg1 = new Cerc(4); |
- | ProdusElectronic** produse = new ProdusElectronic * [nrProduse]; | + | |
- | produse[0] = new Laptop(5499.99f, "HP"); | + | std::cout << "Aria cercului este: " << pfg1->getArie() << '\n'; |
- | produse[1] = new SmartPhone(2499.99f, "Motorola"); | + | std::cout << "Perimetrul cercului este: " << pfg1->getPerimetru() << '\n'; |
- | 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++) | + | FiguraGeomertica* pfg2 = new Patrat(5); |
- | { | + | |
- | std::cout << produse[i] << '\n'; // se foloseste operatorul << definit in interfata ProdusElectronic | + | |
- | } | + | |
- | for (int i = 0; i < nrProduse; i++) | + | std::cout << "\nAria patratului este: " << pfg2->getArie() << '\n'; |
- | { | + | std::cout << "Perimetrul patratului este: " << pfg2->getPerimetru() << '\n'; |
- | delete produse[i]; | + | |
- | } | + | |
- | delete[] produse; | + | delete pfg1; |
+ | delete pfg2; | ||
return 0; | return 0; | ||
Line 394: | Line 430: | ||
</code> | </code> | ||
- | <note tip>În mod similar, procedăm și pentru ceilalți operatori, indiferent dacă sunt supraîncărcați ca **funcții membre** sau ca **funcții friend**. Acești operatori vor fi supraîncărcați **exclusiv** în **clasa de bază**, iar comportamentul lor specific poate fi ulterior personalizat în **clasele derivate** prin intermediul **suprascrierii**. | + | <note important>În cazul clasei **FiguraGeometrica** nu este necesară implementarea unui destructor virtual pur deoarece în clasele derivate **nu** există membri de tip pointer **alocați dinamic**. Prin urmare ordinea de apel a destructorilor este cea **corectă**, adică sunt apelați mai întâi destructorii claselor derivate (**Cerc** și **Patrat**) și la final va fi apelat destructorul superclasei.</note> |
- | 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**.</note> | ||
==== Concluzii ==== | ==== Concluzii ==== | ||
- | Î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ă. | + | Acest laborator a abordat concepte avansate legate de **POO** în limbajul C++, punând accent pe mecanismele de **virtualizare**, diferențele dintre **overloading** și **overriding**, și utilizarea **claselor abstracte** și a **interfețelor**. |
+ | |||
+ | == Virtualizarea în C++ == | ||
+ | |||
+ | * **Metodele virtuale** oferă flexibilitatea necesară pentru implementarea **polimorfismului dinamic**, unde comportamentul funcțiilor este stabilit în timpul execuției (**late binding**). | ||
+ | |||
+ | * **Metodele virtual pure** obligă clasele derivate să implementeze funcționalitățile esențiale, asigurând astfel că fiecare clasă derivată își definește comportamentul specific. | ||
+ | |||
+ | == Overloading vs Overriding == | ||
- | == Vectori de obiecte neomogene == | + | * **Overriding** este asociat cu polimorfismul dinamic (**run-time polymorphism**) și presupune redefinirea comportamentului unei metode virtuale din clasa de bază în clasele derivate. |
- | Am înțeles cum să declarăm și să populăm un vector de obiecte neomogene, folosind pointeri către clase abstracte. 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. | + | * **Overloading** este asociat cu polimorfismul timpuriu (**compile-time polymorphism**) și permite mai multe metode cu același nume, dar semnături diferite, în cadrul aceleiași clase. |
- | == Destructor virtual pur == | + | * Este important să folosim cuvântul cheie **override** pentru a face codul mai sigur și mai lizibil, prevenind erorile legate de semnături greșite sau lipsa suprascrierii. |
- | 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. | + | == Clase Abstracte și Metode Virtual Pure == |
- | == Supraîncărcarea operatorului de afișare == | + | * **Clasele abstracte** oferă un punct de plecare pentru proiectarea ierarhiilor complexe, combinând funcționalitățile comune și abstractizarea. |
- | 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. | + | * O **metodă virtual pură** este o metodă **fără** implementare în clasa de bază, iar clasa devine abstractă dacă conține cel puțin o astfel de metodă. |
- | == Polimorfism aplicat == | + | * Destructorii virtuali sunt obligatorii în clasele abstracte pentru a asigura o eliberare corectă a resurselor în ierarhiile de moștenire. |
- | 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. | + | == Interfețele în C++ == |
- | == Continuitatea dintre laboratoare == | + | * **Interfețele** sunt un caz particular de clase abstracte care conțin **doar** metode virtual pure. |
- | 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. | + | * Ele definesc **contracte stricte** între clase și sunt utilizate pentru a implementa moștenirea multiplă într-un mod clar și organizat, fără ambiguități. |
==== ==== | ==== ==== | ||
- | 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. | + | Acest laborator a evidențiat rolul important al mecanismului de **virtualizare**, al **claselor abstracte** și al **interfețelor** în proiectarea sistemelor software flexibile și extensibile. Am înțeles diferențele esențiale dintre **overloading** și **overriding** și importanța implementării corecte a **metodelor virtuale**, pentru a asigura funcționarea corectă și eficientă a aplicațiilor **orientate obiect**. |