Differences

This shows you the differences between two versions of the page.

Link to this comparison view

poo-is-ab:laboratoare:08 [2024/11/17 22:30]
razvan.cristea0106 [Introducere]
poo-is-ab:laboratoare:08 [2025/09/23 20:06] (current)
razvan.cristea0106
Line 1: Line 1:
-===== Laboratorul ​08 - Clase abstracte ​și interfețe ​=====+===== Laborator ​08 - Funcții ​și clase template ​=====
  
 **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ă și să definească ​funcție virtuală +  * recunoască și să definească funcții template 
-  * recunoască și să definească ​o funcție virtual pură +  * recunoască și să definească ​clase template 
-  * înțeleagă importanța ​utilizării claselor abstracte și a interfețelor în diferite contexte ale POO +  * înțeleagă importanța ​conceptului de programare generică 
-  * înțeleagă conceptul de "late binding"​ corelat cu run time polymorphism +  * organizeze ​în fișiere header și .cpp codul pentru funcțiile și clasele template
-  * înțeleagă diferența dintre supraîncărcare ​și suprascriere+
  
 ==== Introducere ==== ==== Introducere ====
  
-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.+Pe parcursul primului an de studiu, în cadrul disciplinei ​**Proiectarea Algoritmilor**, ați avut ocazia să explorați o gamă variată de **structuri de date** și **algoritmi**. Majoritatea implementărilor de **algoritmi** și **structuri de date** studiate s-au bazat pe un **tip de date specific** ​– de exemplu, structuri de date care funcționau doar cu valori ​de tip întreg sau doar cu șiruri ​de caractere. ​**Programarea generică** își propune să depășească aceste limitări și să ofere soluții care pot fi **adaptate pentru orice tip de date**, fără a fi nevoie să rescriem codul pentru fiecare tip nou.
  
-De exempludacă 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.+În C++, **programarea generică** este realizată prin intermediul ​**template-urilor**. Un template este un **model reutilizabil (șablon)** care poate fi definit **o singură dată** și utilizat pentru o **gamă variată de tipuri de date****Template-urile** permit astfel crearea ​de **funcții** și **clase** care pot funcționa genericpentru orice tip. De exemplu, o funcție de sortare implementată cu template-uri poate fi aplicată atât pe liste de întregi, cât și pe liste de numere în virgulă mobilă sau pe liste de obiecte de orice tip care suportă **operatorul de comparație**.
  
-Cu toate acestea, există cazuri în care decizia pentru funcția/​metoda ce trebuie apelată **nu** poate fi luată la 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++.+Principalele avantaje ​și caracteristici ale **programării generice** în C++ sunt:
  
-==== Overloading vs Overriding ====+  * **Reutilizarea codului** <=> Cu template-uri,​ putem crea funcții și clase care nu depind de un anumit tip de date. Codul devine mai flexibil și reutilizabil,​ reducând efortul de scriere și întreținere. 
 +  
 +  * **Flexibilitate și extensibilitate** <=> **Funcțiile** și **clasele** template permit adaptarea ușoară la tipuri noi de date și comportamente diferite, fără modificări majore în structura codului. 
 +  
 +  * **Tipizarea la timp de compilare** <=> C++ folosește template-urile într-un mod specific, denumit **generare la timp de compilare**. Asta înseamnă că tipul de date efectiv este determinat atunci când se realizează apelul **funcției** sau **metodei**,​ optimizând performanța și ajutând la identificarea erorilor de tip încă din faza de compilare.
  
-În continuare vom prezenta un tabel care prezintă diferențele clare între cele două forme de polimorfism.+  * **Specializare de template-uri** <​=> ​În anumite situații, se poate utiliza specializarea **template-urilor**,​ o tehnică prin care se definește un comportament specific al funcției sau clasei pentru un anumit tip de date. Aceasta permite adăugarea ​de optimizări sau modificări atunci când un tip special necesită un tratament diferit.
  
-^        ​**Overloading (Compile-Time Polymorphism)**      ​^ ​       **Overriding (Run-Time Polymorphism)** ​          ^ +**Programarea generică** în C++ îmbină aceste concepte pentru a realiza soluții mai **scalabile****eficiente** și **organizat structurate**. În cadrul acestui laborator, veți învăța cum să creațfuncții și clase template, cum să organizați și să utilizați template-uri specializate și cum să structurați proiectele pentru a încorpora aceste practici ​de programare generică.
-| 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 |+
  
-**Suprascrierea** (overriding) implică păstrarea aceluiași antent al funcției – adică același tip de return și aceiaș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.+==== Funcții template ====
  
-Pe de altă parte, ​**supraîncărcarea** (overloading) presupune existența mai multor ​funcții cu același nume în cadrul aceleiași clasedar 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 compilareceea ce asigură o execuție rapidă.+**Funcțiile template** sunt similare cu funcțiile obișnuiteînsă oferă un avantaj important: permit crearea de funcții genericecare pot lucra cu diferite ​tipuri de dateÎn loc să definim funcții separate pentru fiecare tip de date (de exemplu**''​int''​**,​ **''​float''​**,​ **''​double''​**),​ o funcție template ne permite să scriem ​singură funcție care să funcționeze pentru toate aceste tipuri
  
-<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>​+Să luăm spre exemplu o funcție care face suma două numere de același tip primite ca parametru și întoarce un rezultat de același tip. Vom implementa două funcții de adunare pentru numere întregi și pentru numere de tip **''​float''​** după cum urmează mai jos.
  
-==== Funcții virtuale ====+<code cpp> 
 +#include <​iostream>​
  
-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. ​+int adunare(const int& aconst int& b) 
 +
 + return a + b; 
 +}
  
-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ă** ​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**.+float adunare(const float& ​a, const float& b) 
 +
 + return a + b; 
 +}
  
-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.+int main() 
 +
 + int sumaIntregi = adunare(23); 
 + float sumaFloaturi = adunare(3.5f8.25f);
  
-<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>+ std::​cout ​<< ​"Suma numerelor intregi este: " << sumaIntregi << '​\n';​ 
 + std::cout << "Suma numerelor reale este: " << sumaFloaturi << '​\n';​
  
-<​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>+ return 0; 
 +
 +</code>
  
-Î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**.+Se poate observa că singurele diferențe între cele 2 funcții sunt **tipul de return** și **tipul parametrilor** acestora. Prin urmare putem spune că avem un cod duplicat care ne crește numărul de linii. Soluția mai elegantă și mai corectă este să implementăm o **funcție template** care va respecta structura celor 2 funcții de mai sus cu avantajul că va fi scrisă o singură dată pentru o gamă mai extinsă de tipuri de date. 
 + 
 +=== Declararea și implementarea funcțiilor template === 
 + 
 +În C++ pentru a declara o **funcție ​generică** se folosesc cuvintele ​cheie **template** și respectiv **typename** deasupra antetului ​funcției. Să reimplementăm acum funcția de **adunare** a două numere de același tip dar de acestă dată ca **funcție template**.
  
 <code cpp> <code cpp>
-class Animal+template <​typename T> 
 +T adunare(const T& a, const T& b)
 { {
-public:+ return a + b; 
 +
 +</​code>​
  
- void afisare() const; +<note important>​**T-ul** reprezintă tipul de date folosit de **funcția template**, atât ca **tip de returnare**,​ cât și ca **tip pentru parametrii funcției**. Observăm că am definit **o singură** funcție generică, iar la **momentul compilării**,​ aceasta își va adapta **automat** tipul de date pe baza tipurilor parametrilor primiți. Astfel, funcția poate fi folosită cu diferite tipuri de date **fără** a necesita rescrierea pentru fiecare tip în parte, asigurând **flexibilitate** și reducând **duplicarea codului**.</​note>​ 
-};+ 
 +Apelarea funcției se poate face în maniera următoare. 
 + 
 +<code cpp> 
 +#include <​iostream>​
  
-void Animal::​afisare(const+template <​typename T> 
 +T adunare(const ​T& a, const T& b)
 { {
- std::cout << "Sunt un animal!\n";+ return a + b; 
 +
 + 
 +int main() 
 +
 + int sumaIntregi = adunare(2, 3); 
 + /*int sumaIntregi = adunare<​int>​(2,​ 3); // corect si asa deoarece este pus explicit*/​ 
 + 
 + /*float sumaFloaturi = adunare(3.5f,​ 8.25f); // valid*/ 
 + float sumaFloaturi = adunare<​float>​(3.5f,​ 8.25f); // valid 
 + 
 + std::cout << "Suma numerelor intregi este: " << sumaIntregi << '\n'; 
 + std::cout << ​"Suma numerelor reale este: " << sumaFloaturi << '​\n';​ 
 + 
 + return 0;
 } }
 </​code>​ </​code>​
  
-Șrespectiv clasa **Caine**.+Dacă am vrea să adunăm două tipuri diferite de date șsă returnăm suma lor într-un alt tip de date am putea folosi următoarea formă de funcție template.
  
 <code cpp> <code cpp>
-class Caine : public Animal+template <​typename T1, typename T2, typename T3> 
 +T1 adunare(const T2& a, const T3& b)
 { {
-public:+ return (T1)a + b; 
 +
 +</​code>​
  
- void afisare() const; +**T1** reprezintă tipul de return al funcției iar **T2** și respectiv **T3** reprezintă tipurile de parametri pe care funcția îi poate primi. Funcția realizează suma valorilor celor 2 parametri și o convertește la tipul **T1** înainte de a o returna. Ca și exemple de apel putem scrie în felul următor.
-};+
  
-void Caine::​afisare(const+<code cpp> 
 +#include <​iostream>​ 
 + 
 +template <​typename T1, typename T2, typename T3> 
 +T1 adunare(const ​T2& a, const T3& b)
 { {
- std::cout << "Sunt un caine!\n";+ return (T1)a + b; 
 +
 + 
 +int main() 
 +
 + double suma = adunare<​double>​(2,​ 7.5f); 
 + /*double suma = adunare<​double,​ int, float>​(2,​ 7.5f); // corect de asemenea*/​ 
 + /*double suma = adunare(2, 7.5f); // incorect deoarece compilatorul nu stie ce tip de return sa utilizeze*/​ 
 + 
 + std::cout << "Suma este: " << suma << '\n'; 
 + 
 + return 0;
 } }
 </​code>​ </​code>​
  
-Î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ă.+Astfel, putem observa că **funcțiile template** oferă o formă de **polimorfism** cunoscută sub numele de **polimorfism static** sau **polimorfism la compilare** (**compile time polymorphism** sau **early polymorphism**). ​În loc să definim mai multe **funcții supraîncărcate** pentru fiecare tip de date posibil, folosim ​**un singur model generic**, iar **compilatorul generează automat** versiunile corespunzătoare pentru fiecare tip de date specific **atunci când funcția este apelată**. Aceasta este o abordare eficientă pentru a obține **flexibilitate** ​și **reutilizare a codului**, asigurând totodată **performanță optimă**, deoarece tipurile sunt determinate și validate la **compilare**,​ eliminând nevoia de verificări suplimentare la run time. 
 + 
 +<note warning>​În cazul exemplului de mai sus a fost **obligatorie** speficarea **tipului ​de date returnat** între **parantezele unghiulare**,​ deoarece compilatorul **nu** ar fi știut în ce tip de date să facă **conversia** rezultatului obținut în urma operației de adunare. Pentru celelalte două tipuri de date **nu a fost necesară** menționarea lor între **parantezele unghiulare**,​ deoarece compilatorul a știut să pună **automat** tipurile de date corecte pe baza valorilor parametrilor funcției **adunare** în momentul în care aceasta a fost apelată în **funcția main**.</​note>​ 
 + 
 +=== Supraîncărcarea unei funcții template === 
 + 
 +O **funcție generică** poate fi, într-adevăr,​ **supraîncărcată** folosind același nume, dar să difere prin **numărul sau tipul parametrilor**. Aceasta înseamnă ​că putem avea **mai multe versiuni** ale unei **funcții generice**, fiecare destinată unui **caz particular**,​ dar accesibile sub **același nume**. Astfel, **compilatorul** va selecta **automat** varianta corespunzătoare ​în funcție de tipurile de date și de numărul argumentelor transmise. 
 + 
 +Ca și exemplu propunem două **funcții generice** de **interschimbare** a două valori, una cu parametri transmiși prin **referință** și cea de a doua cu parametri transmiși prin **pointer**.
  
 <code cpp> <code cpp>
-int main()+#include <​iostream>​ 
 + 
 +template <​typename T> 
 +void interschimbare(T& x, T& y)
 { {
- Animal animal+ T aux = x
- animal.afisare()// se apeleaza afisarea din Animal+ x = y; 
 + y = aux; 
 +}
  
- Caine caine; +template <​typename T> 
- caine.afisare(); // se apeleaza afisarea din Caine+void interschimbare(T* x, T* y) 
 +
 + if (x == nullptr || y == nullptr) 
 +
 + return; 
 + }
  
- animal ​caine+ T aux *x
- animal.afisare(); // se apeleaza afisarea din Animal+ *x = *y; 
 + *y = aux; 
 +
 + 
 +int main() 
 +
 + int a = 22; 
 + int b = 6;
  
- Animal* pAnimal = new Animal(); + interschimbare(a, b);
- pAnimal->​afisare();​ // se apeleaza afisarea din Animal+
  
- Caine* pCaine ​new Caine()+ std::cout << "​a ​" << a << '​\n'​
- pCaine->​afisare()// se apeleaza afisarea din Caine+ std::cout << "b = " << b << '​\n'​;
  
- Animal* pa = pCaine; + interschimbare(&a, &b);
- pa->​afisare(); // se apeleaza afisarea din Animal desi ar fi trebui sa se apeleze cea din Caine+
  
- delete pAnimal+ std::cout << "\na = " << a << '​\n'​
- delete pCaine;+ std::cout << "b = " << b << '​\n'​;
  
  return 0;  return 0;
Line 113: Line 189:
 </​code>​ </​code>​
  
-Soluția este să marcăm metoda ​de **''​afisare''​** ca fiind virtuală pentru a putea permite ​**legături întârziate**.+Prin acest mecanism de **supraîncărcare a funcțiilor template**, am reușit să extindem funcționalitatea **codului generic** pentru a acoperi **cazuri specifice**,​ păstrând totodată **lizibilitatea** și **coerența** codului. 
 + 
 +=== Separarea declarației de implementărea unei funcții template === 
 + 
 +Până acum, în exemplele de cod cu **funcții template**, am realizat atât declarația, cât și implementarea în același fișier. Totuși, pentru a îmbunătăți organizarea codului și a facilita reutilizarea,​ intenționăm ​să separăm aceste componente. Separarea declarației și implementării funcțiilor template este o practică utilă, mai ales în proiectele ​de mari dimensiuni, deoarece oferă o structură mai clară și face codul mai ușor de întreținut.  
 + 
 +În C++, spre deosebire de funcțiile obișnuite, implementarea ​**funcțiilor template** în fișiere separate reprezintă o provocare datorită mecanismului de instanțiere a template-urilor la momentul compilării,​ implementarile trebuie să fie vizibile în orice fișier care le utilizează. De aceea, vom explora modalități ​pentru a organiza corect template-urile în fișiere separate, păstrându-le accesibile la compilare și în același timp menținând o structură modulară. 
 + 
 +Ca și exemplu vom scrie funcția de **adunare** în fișiere **header** și **.cpp** pentru a vedea exact cum trebuie procedat astfel încât să ne putem folosi de ea în orice fișier. 
 + 
 +== Declararea funcției == 
 + 
 +Pentru început vom muta antetul funcției într-un fișier header după cum urmează.
  
 <code cpp> <code cpp>
-class Animal +#pragma once 
-+#include <​iostream>​
-public:+
  
- virtual void afisare(const; // metoda afisare este acum virtuala ceea ce inseamna ca vom putea avea legari dinamice +template <​typename T> 
-};+T adunare(const ​T& x, const T& y);
 </​code>​ </​code>​
  
-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''​**.+== Implementarea funcției == 
 + 
 +Fiind o **funcție template** va trebui să înștiințăm compilatorul acest lucru după cum urmează.
  
 <code cpp> <code cpp>
 +#include "​Template.h"​
 +
 +template<​typename T>
 +T adunare(const T& x, const T& y)
 +{
 + return x + y;
 +}
 +</​code>​
 +
 +== Apelarea funcției în alt fișier ==
 +
 +Apelarea funcției se face în maniera următoare.
 +
 +<code cpp>
 +#include "​Template.h"​
 +
 int main() int main()
 { {
- Animal animal+ int a = 22
- animal.afisare()// se apeleaza afisarea din Animal+ int b = 6;
  
- Caine caine; + std::cout << adunare(a, b<< '​\n'​;
- caine.afisare(); // se apeleaza afisarea din Caine+
  
- animal = caine+ return 0
- animal.afisare();​ /se apeleaza afisarea din Animal deoarece animal nu este pointer+
 +</code>
  
- Animal* pAnimal ​new Animal(); +=== ===
- pAnimal->​afisare();​ // se apeleaza afisarea din Animal+
  
- Caine* pCaine = new Caine(); +Dacă vom încerca să rulăm codul exact în maniera în care l-am scris mai sus ne vom confrunta cu o **eroare de linker**.
- pCaine->​afisare();​ // se apeleaza afisarea din Caine+
  
- Animal* pa = pCaine; +<note warning>**Eroarea de linker** apare în cazul **funcțiilor template** separate în fișiere **header** și **.cpp** ​din cauza modului în care funcționează **compilarea** template-urilor în **C++**. Spre deosebire de funcțiile obișnuite, **funcțiile template** sunt **generate la momentul compilării** pentru **fiecare** tip de date specific utilizat în cod. Așadar, compilatorul **trebuie să aibă acces** la implementarea completă a **funcției template** de fiecare dată când o utilizează cu **un nou tip de date**.
- pa->afisare(); // se apeleaza afisarea ​din Caine+
  
- delete pAnimal; +În mod normal, atunci când împărțim o funcție într-un **fișier header** pentru declarare și un **fișier .cpp** pentru implementare,​ **compilatorul** generează **codul obiect** pentru implementare în **fișierul .cpp**, iar **linker-ul** leagă acest cod în etapa finală. Însă în cazul **funcțiilor template**, această separare cauzează o **eroare de linker** deoarece în momentul compilării fișierului header, compilatorul **nu** găsește implementarea completă a **funcției template** în **fișierul .cpp** pentru tipurile de date pe care încă **nu** le-a întâlnit.</​note>​
- delete pCaine;+
  
- return ​0;+Cea mai simplă soluție este să forțăm compilatorul să genereze **funcția template** pentru tipurile de date specifice pe care dorim să le testăm. Acest lucru se poate realiza prin implementarea unei funcții **locale** sau **statice** în **fișierul .cpp** care conține implementarea **funcției template**. Funcția respectivă va apela template-ul cu diverse tipuri de date, asigurând astfel compilarea și generarea de cod pentru fiecare tip necesar. 
 + 
 +<​note>​Funcția de test **nu trebuie** neapărat apelată în codul principal, motiv pentru care este prezentă doar în **fișierul .cpp**. Rolul său este pur și simplu de a **forța compilatorul** să genereze instanțe ale **template-ului** pentru tipurile de date dorite, fără a fi nevoie să fie efectiv utilizată în alte părți ale programului.</​note>​ 
 + 
 +<code cpp> 
 +#include "​Template.h"​ 
 + 
 +template<​typename T> 
 +T adunare(const T& x, const T& y) 
 +
 + return ​x + y; 
 +
 + 
 +void testare() 
 +
 + int s1 = adunare(2, 3); 
 + float s2 = adunare(2.3f,​ 3.0f); 
 + double s3 = adunare(-1.4,​ 8.24); 
 + unsigned int s4 = adunare(2u, 3u);
 } }
 </​code>​ </​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ă**.+Prin urmare ​putem apela acum funcția ​de adunare ​pentru ​patru tipuri de date după cum urmează.
  
 <code cpp> <code cpp>
-class Caine : public Animal+#include "​Template.h"​ 
 + 
 +int main()
 { {
-public:+ std::cout << "Suma numerelor este: " << adunare(22, 8) << '​\n';​ // valid 
 + std::cout << "Suma numerelor este: " << adunare(2.2f,​ 4.5f) << '​\n';​ // valid 
 + std::cout << "Suma numerelor este: " << adunare(10.0,​ 7.5) << '​\n';​ // valid 
 + std::cout << "Suma numerelor este: " << adunare(4u, 6u) << '​\n';​ // valid 
 + /​*std::​cout << "Suma numerelor este: " << adunare(4l, 6l) << '​\n';​ // invalid*/
  
- void afisare() const override// acum este mentionat explicit faptul ca metoda din Caine o va suprascrie pe cea din Animal + return 0
-};+}
 </​code>​ </​code>​
  
-<note important>​Cuvântul cheie **override** ​în **C++** este folosit pentru ​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>​+<note important>​Datorită faptului că în funcția de testare ​**nu** am apelat o instanță ​a **funcției template** pentru tipul de date **long**, dacă se decomentează linia din codul de mai sus, va apărea o **eroare ​de linker**. Acest lucru se întâmplă deoarece compilatorul ​**nu** a generat o implementare pentru tipul **long**, care nu a fost utilizat în funcția de testare.</​note>​
  
-==== Clase abstracte ​====+==== Clase template ​====
  
-În limbajul C++, o **clasă abstractă** este o clasă care conține ​**cel puțin** **metodă virtuală pură**. **Metoda virtuală pură** este funcție declarată în clasa de bază, dar care **nu are o implementare** în această clasă, **obligând** astfel **clasele derivate** să **suprascrie**. O **clasă abstractă** este utilizată pentru a defini un comportament **general** care trebuie ​să fie specificat în **mod detaliat** în **clasele derivate**.+**Clasele template**, la fel ca **funcțiile template**, au scopul de a susține ​**programarea generică** și de a elimina duplicarea codului, oferind ​soluție flexibilă și reutilizabilă pentru gestionarea mai multor tipuri de date. Prin **clasele template**, putem crea **structuri de date** și obiecte care să funcționeze ​**indiferent** de tipul de date cu care lucrează, astfel încât codul să fie mai **ușor de întreținut** și mai **eficient**.
  
-<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 ​**clasă abstractă**.</​note>​+De exemplu, o **clasă ​template** pentru o structură de date precum un **vector** poate fi scrisă astfel încât ​să poată stoca orice tip de date, fie că este vorba de **numere întregi**, **șiruri ​de caractere** sau **obiecte complexe**. Aceasta înseamnă ​că **nu este nevoie** să redefinim întreaga ​clasă ​de fiecare dată când dorim să o utilizăm cu un alt tip de date.
  
-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ă.+Ca și exemplu de clasă template pentru acest laborator propunem clasa **Student** care are un câmp **medieAnuala** de tip template, deoarece media anuală poate fi cu sau fără virgulă.
  
 <code cpp> <code cpp>
-class Animal+#pragma once 
 +#include <​string>​ 
 +#include <​iostream>​ 
 + 
 +template <​typename T> 
 +class Student
 { {
  char* nume;  char* nume;
- int varsta;+ T medieAnuala;
  
 public: public:
  
- Animal(); + Student(const char* nume, const TmedieAnuala); 
- Animal(const char* nume, const intvarsta); + Student(const Student&​ student); 
- Animal(const ​Animalanimal); + Student&​ operator=(const ​Studentstudent); 
- ~Animal();+ ~Student();
  
  char* getNume() const;  char* getNume() const;
- int getVarsta() const;+ T getMedieAnuala() const;
  
- virtual ​void afisare() const = 0// metoda virtual pura+ void setNume(const char* nume)
 + void setMedieAnuala(const T& medieAnuala);​ 
 + 
 + template <​typename T> 
 + friend std::​ostream&​ operator<<​(std::​ostream&​ out, const Student<​T>&​ student);
 }; };
 </​code>​ </​code>​
  
-Iar clasa **Caine** moștenește clasa **Animal** și implementează metoda virtual pură din clasa de bază.+În continuare vom prezenta detaliat maniera ​de implementare a fiecărei metode în parte. 
 + 
 +=== Implementarea constructorilor ===
  
 <code cpp> <code cpp>
-class Caine public Animal+template <​typename T> 
 +Student<​T>​::​Student(const char* nume, const T& medieAnuala)
 { {
- char* rasa;+ if (nume != nullptr) 
 +
 + this->​nume = new char[strlen(nume) + 1]; 
 + strcpy(this->​nume,​ nume); 
 +
 + else 
 +
 + this->​nume = nullptr; 
 +
 +  
 + this->​medieAnuala = medieAnuala;​ 
 +}
  
-public:+template<​typename T> 
 +Student<​T>​::​Student(const Student<​T>&​ student) 
 +
 + if (student.nume != nullptr) 
 +
 + nume = new char[strlen(student.nume) + 1]; 
 + strcpy(nume,​ student.nume);​ 
 +
 + else 
 +
 + nume = nullptr; 
 + }
  
- Caine(); + medieAnuala = student.medieAnuala
- 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.+=== Implementarea operatorului de asignare ===
  
 <code cpp> <code cpp>
-Caine::Caine() : Animal()+template<​typename T> 
 +Student<​T>&​ Student<​T>​::operator=(const Student<​T>&​ student)
 { {
- rasa nullptr+ if (this == &​student) 
-}+
 + return *this
 + }
  
-Caine::​Caine(const char* nume, const int& varsta, const char* rasa) : Animal(nume,​ varsta) + if (nume != nullptr)
-+
- if (rasa != nullptr)+
  {  {
- this->​rasa ​= new char[strlen(rasa) + 1]; + delete[] nume; 
- strcpy(this->​rasarasa);+
 + 
 + if (student.nume != nullptr) 
 +
 + nume ​= new char[strlen(student.nume) + 1]; 
 + strcpy(numestudent.nume);
  }  }
  else  else
  {  {
- this->​rasa ​= nullptr;+ nume = nullptr;
  }  }
 +
 + medieAnuala = student.medieAnuala;​
 +
 + return *this;
 } }
 +</​code>​
  
-Caine::Caine(const Caine& caine) : Animal(caine)+=== Implementarea destructorului === 
 + 
 +<code cpp> 
 +template<​typename T> 
 +Student<​T>​::~Student()
 { {
- if (caine.rasa ​!= nullptr) ​+ if (nume != nullptr)
  {  {
- rasa = new char[strlen(caine.rasa) + 1]+ delete[] nume;
- strcpy(rasa,​ caine.rasa);​ +
-+
- else  +
-+
- rasa = nullptr;+
  }  }
 } }
 +</​code>​
  
-Caine::~Caine()+=== Implementarea metodelor accesor === 
 + 
 +<code cpp> 
 +template<​typename T> 
 +char* Student<​T>​::getNume() const
 { {
- if (rasa != nullptr)+ return nume; 
 +
 + 
 +template<​typename T> 
 +T Student<​T>::​getMedieAnuala() const 
 +
 + return medieAnuala;​ 
 +
 + 
 +template<​typename T> 
 +void Student<​T>::​setNume(const char* nume) 
 +
 + if (nume == nullptr)
  {  {
- delete[] rasa;+ return;
  }  }
 +
 + if (this->​nume != nullptr)
 + {
 + delete[] this->​nume;​
 + }
 +
 + this->​nume = new char[strlen(nume) + 1];
 + strcpy(this->​nume,​ nume);
 } }
  
-void Caine::afisare(const +template<​typename T> 
 +void Student<​T>​::setMedieAnuala(const ​T& medieAnuala)
 { {
- std::​cout ​<< "Nume: " << (getNume(? getNume() : "​Anonim"​) << '​\n';​ + if (medieAnuala ​<= 0
- std::cout << "​Varsta:​ " << getVarsta() << " ani\n"+
- std::cout << "Rasa: " << (rasa ? rasa : "​Necunoscuta"​) << "​\n\n"​;+ return
 +
 + 
 + this->​medieAnuala = medieAnuala;
 } }
 </​code>​ </​code>​
  
-Iar mai jos propunem un exemplu ​de testare a acestor clase.+=== Implementarea operatorului ​de afișare ===
  
 <code cpp> <code cpp>
-int main()+template <​typename T> 
 +std::​ostream&​ operator<<​(std::​ostream&​ out, const Student<​T>&​ student)
 { {
- Caine caine("Zeus", 3, "​labrador"​);​ + out << ​"Numele studentului este: ";
- caine.afisare()// valid+
  
- Animal* pAnimal ​new Caine(); + if (student.nume ​== nullptr
- pAnimal->​afisare()// valid deoarece metoda afisare este virtuala +
- + out << "​N/​A\n"​
- delete pAnimal// incorect nu se cheama si destructorul din Caine, memory leak+ } 
 + else 
 +
 + out << student.nume << '​\n'​; 
 + }
  
- pAnimal = &caine; // valid datorita relatiei de tip "is-a" + out << ​"Media anuala ​studentului este: " ​<< student.medieAnuala << '​\n'​;
- pAnimal->​afisare()// valid+
  
- return ​0;+ return ​out;
 } }
 </​code>​ </​code>​
  
-<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**. +=== Crearea ​funcției de testare ===
- +
-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>​ +
- +
-Așadar pentru a elibera memoria **corect** vom declara destructorul clasei **Animal** ca **funcție virtuală**.+
  
 <code cpp> <code cpp>
-class Animal+void testTemplate()
 { {
- char* nume+ Student<​int>​ s1("​Ion",​ 10); 
- int varsta;+ Student<​int>​ s2("​George",​ 9)
 + Student<int> s3 = s2;
  
-public:+ s3 = s1;
  
- Animal(); + s3.setNume("​Maria"​); 
- Animal(const char* nume, const int& varsta); + s3.setMedieAnuala(8);
- Animal(const Animal& animal); +
- virtual ~Animal(); // destructor virtual+
  
- char* getNume() ​const+ std::cout << s3 << '​\n';​ 
- int getVarsta() const;+ std::cout << s3.getNume() ​<< "​\n\n"​
 + std::cout << s3.getMedieAnuala() << "​\n\n"​;
  
- virtual void afisare() const = 0// metoda virtual pura + Student<​double>​ s4("​Ion",​ 10); 
-}+ Student<​double>​ s5("​George",​ 9)
-</code>+ Student<doubles6 = s5;
  
-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}}.+ s5 = s4;
  
-==== Interfețe ====+ s5.setNume("​Maria"​);​ 
 + s5.setMedieAnuala(9.9);​
  
-Î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.+ std::cout << s4 << '​\n';​ 
 + std::cout << s4.getNume() << "​\n\n";​ 
 + std::cout << s4.getMedieAnuala() << "​\n\n";​ 
 +
 +</​code>​
  
-Î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ă.+==== ====
  
-^     ​Caracteristică ​         ^     ​Interfață ​                     ^     ​Clasă abstractă ​                ^ +Având acum implementarea clasei template ​**Student** putem să o folosim ​în codul din programul principal după cum umrează.
-**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 | +
- +
-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**.+
  
 <code cpp> <code cpp>
-class FiguraGeomertica // interfata+#include "​Student.h"​ 
 + 
 +int main()
 { {
-public:+ Student<​int>​ s1("​Ion",​ 10); 
 + Student<​int>​ s2("​George",​ 9); 
 + Student<​int>​ s3 = s2;
  
- virtual float getArie() const = 0+ s3 = s1; 
- virtual float getPerimetru() const = 0; + 
-};+ s3.setNume("​Maria"​); 
 + s3.setMedieAnuala(8)
 + 
 + std::cout << s3 << '​\n';​ 
 + 
 + Student<​double>​ s4("​Ion",​ 10); 
 + Student<​double>​ s5("​George",​ 9); 
 + Student<​double>​ s6 s5; 
 + 
 + s5 = s4; 
 + 
 + s5.setNume("​Maria"​);​ 
 + s5.setMedieAnuala(9.9);​ 
 + 
 + std::cout << s4 << '​\n';​ 
 + 
 + return ​0; 
 +}
 </​code>​ </​code>​
  
 +<note tip>În principiu **clasele template** sunt folosite pentru implementarea **structurilor de date** într-o manieră **generică**,​ exemplul cu clasa **Student** fiind unul pur didactic.</​note>​
 +
 +==== Concluzii ====
 +
 +În acest laborator, am explorat conceptul de **programare generică**,​ care permite scrierea de cod reutilizabil,​ flexibil și eficient. Utilizarea template-urilor ne permite să creăm clase și funcții independente de tipul de date specific, fiind astfel mai ușor să dezvoltăm structuri de date și algoritmi care pot fi utilizați pe o varietate de tipuri. Am învățat de asemenea cum să separăm în fișiere header definițiile funcțiilor și claselor template de implementările acestora din fișierele .cpp și ce probleme pot apărea în momentul în care facem acest lucru.
  
 +Template-urile oferă o **bază flexibilă pentru extinderea funcționalităților** fără a modifica codul existent. În mod particular, prin crearea de template-uri,​ putem construi un cod care este adaptabil pentru diverse tipuri de aplicații, de la procesarea numerelor până la manipularea textului și gestionarea obiectelor complexe.
poo-is-ab/laboratoare/08.1731875401.txt.gz · Last modified: 2024/11/17 22:30 by razvan.cristea0106
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0