This shows you the differences between two versions of the page.
poo-is-ab:laboratoare:08 [2024/12/17 09:06] razvan.cristea0106 [Overloading vs Overriding] |
poo-is-ab:laboratoare:08 [2025/09/23 20:06] (current) razvan.cristea0106 |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ===== Laborator 09 - 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ă o 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 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. | + | Î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 generic, pentru 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ă 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 pune în evidență 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ți 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 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. | + | ==== 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 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ă. | + | **Funcțiile template** sunt similare cu funcțiile obișnuite, însă oferă un avantaj important: permit crearea de funcții generice, care 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 o 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 a 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& a, const 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ă** 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**. | + | 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(2, 3); | ||
+ | float sumaFloaturi = adunare(3.5f, 8.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. |
- | <code cpp> | + | === Declararea și implementarea funcțiilor template === |
- | class Animal | + | |
- | { | + | |
- | public: | + | |
- | void afisare() const; | + | Î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**. |
- | }; | + | |
- | void Animal::afisare() const | + | <code cpp> |
+ | template <typename T> | ||
+ | T adunare(const T& a, const T& b) | ||
{ | { | ||
- | std::cout << "Sunt un animal!\n"; | + | return a + b; |
} | } | ||
</code> | </code> | ||
- | Și respectiv clasa **Caine**. | + | <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> | <code cpp> | ||
- | class Caine : public Animal | + | #include <iostream> |
+ | |||
+ | template <typename T> | ||
+ | T adunare(const T& a, const T& b) | ||
{ | { | ||
- | public: | + | return a + b; |
- | + | } | |
- | void afisare() const; | + | |
- | }; | + | |
- | void Caine::afisare() const | + | int main() |
{ | { | ||
- | std::cout << "Sunt un caine!\n"; | + | 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> | ||
- | Î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ă. | + | Dacă am vrea să adunăm două tipuri diferite de date și 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> | ||
- | int main() | + | template <typename T1, typename T2, typename T3> |
+ | T1 adunare(const T2& a, const T3& b) | ||
{ | { | ||
- | Animal animal; | + | return (T1)a + b; |
- | animal.afisare(); // se apeleaza afisarea din Animal | + | } |
+ | </code> | ||
- | Caine caine; | + | **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. |
- | caine.afisare(); // se apeleaza afisarea din Caine | + | |
- | animal = caine; | + | <code cpp> |
- | animal.afisare(); // se apeleaza afisarea din Animal | + | #include <iostream> |
- | Animal* pAnimal = new Animal(); | + | template <typename T1, typename T2, typename T3> |
- | pAnimal->afisare(); // se apeleaza afisarea din Animal | + | T1 adunare(const T2& a, const T3& b) |
+ | { | ||
+ | return (T1)a + b; | ||
+ | } | ||
- | Caine* pCaine = new Caine(); | + | int main() |
- | pCaine->afisare(); // se apeleaza afisarea din Caine | + | { |
- | + | double suma = adunare<double>(2, 7.5f); | |
- | Animal* pa = pCaine; | + | /*double suma = adunare<double, int, float>(2, 7.5f); // corect de asemenea*/ |
- | pa->afisare(); // se apeleaza afisarea din Animal desi ar fi trebui sa se apeleze cea din Caine | + | /*double suma = adunare(2, 7.5f); // incorect deoarece compilatorul nu stie ce tip de return sa utilizeze*/ |
- | delete pAnimal; | + | std::cout << "Suma este: " << suma << '\n'; |
- | delete pCaine; | + | |
return 0; | return 0; | ||
Line 113: | Line 136: | ||
</code> | </code> | ||
- | Soluția este să marcăm metoda de **''afisare''** ca fiind virtuală pentru a putea permite **legături întârziate**. | + | 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. |
- | <code cpp> | + | <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> |
- | class Animal | + | |
- | { | + | |
- | public: | + | |
- | virtual void afisare() const; // metoda afisare este acum virtuala ceea ce inseamna ca vom putea avea legari dinamice | + | === Supraîncărcarea unei funcții template === |
- | }; | + | |
- | </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''**. | + | 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 deoarece animal nu este pointer | + | *x = *y; |
+ | *y = aux; | ||
+ | } | ||
- | Animal* pAnimal = new Animal(); | + | int main() |
- | pAnimal->afisare(); // se apeleaza afisarea din Animal | + | { |
+ | int a = 22; | ||
+ | int b = 6; | ||
- | Caine* pCaine = new Caine(); | + | interschimbare(a, b); |
- | pCaine->afisare(); // se apeleaza afisarea din Caine | + | |
- | Animal* pa = pCaine; | + | std::cout << "a = " << a << '\n'; |
- | pa->afisare(); // se apeleaza afisarea din Caine | + | std::cout << "b = " << b << '\n'; |
- | delete pAnimal; | + | interschimbare(&a, &b); |
- | delete pCaine; | + | |
+ | std::cout << "\na = " << a << '\n'; | ||
+ | std::cout << "b = " << b << '\n'; | ||
return 0; | return 0; | ||
Line 154: | Line 189: | ||
</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 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. |
- | <code cpp> | + | === Separarea declarației de implementărea unei funcții template === |
- | class Caine : public Animal | + | |
- | { | + | |
- | public: | + | |
- | void afisare() const override; // acum este mentionat explicit faptul ca metoda din Caine o va suprascrie pe cea din Animal | + | 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. |
- | }; | + | |
- | </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> | + | Î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ă. |
- | ==== Clase abstracte ==== | + | 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. |
- | Î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**. | + | == Declararea funcției == |
- | <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> | + | Pentru început vom muta antetul funcției într-un fișier header după cum urmează. |
- | + | ||
- | 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> | <code cpp> | ||
- | class Animal | + | #pragma once |
- | { | + | #include <iostream> |
- | char* nume; | + | |
- | int varsta; | + | |
- | public: | + | template <typename T> |
+ | T adunare(const T& x, const T& y); | ||
+ | </code> | ||
- | Animal(); | + | == Implementarea funcției == |
- | Animal(const char* nume, const int& varsta); | + | |
- | Animal(const Animal& animal); | + | |
- | ~Animal(); | + | |
- | char* getNume() const; | + | Fiind o **funcție template** va trebui să înștiințăm compilatorul acest lucru după cum urmează. |
- | int getVarsta() const; | + | |
- | virtual void afisare() const = 0; // metoda virtual pura | + | <code cpp> |
- | }; | + | #include "Template.h" |
+ | |||
+ | template<typename T> | ||
+ | T adunare(const T& x, const T& y) | ||
+ | { | ||
+ | return x + y; | ||
+ | } | ||
</code> | </code> | ||
- | Iar clasa **Caine** moștenește clasa **Animal** și implementează metoda virtual pură din clasa de bază. | + | == Apelarea funcției în alt fișier == |
+ | |||
+ | Apelarea funcției se face în maniera următoare. | ||
<code cpp> | <code cpp> | ||
- | class Caine : public Animal | + | #include "Template.h" |
+ | |||
+ | int main() | ||
{ | { | ||
- | char* rasa; | + | int a = 22; |
+ | int b = 6; | ||
- | public: | + | std::cout << adunare(a, b) << '\n'; |
- | Caine(); | + | return 0; |
- | 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> | + | 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**. |
- | Caine::Caine() : Animal() | + | |
- | { | + | |
- | rasa = nullptr; | + | |
- | } | + | |
- | Caine::Caine(const char* nume, const int& varsta, const char* rasa) : Animal(nume, varsta) | + | <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**. |
- | { | + | |
- | if (rasa != nullptr) | + | |
- | { | + | |
- | this->rasa = new char[strlen(rasa) + 1]; | + | |
- | strcpy(this->rasa, rasa); | + | |
- | } | + | |
- | else | + | |
- | { | + | |
- | this->rasa = nullptr; | + | |
- | } | + | |
- | } | + | |
- | Caine::Caine(const Caine& caine) : Animal(caine) | + | Î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> |
- | { | + | |
- | if (caine.rasa != nullptr) | + | 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. |
- | { | + | |
- | rasa = new char[strlen(caine.rasa) + 1]; | + | |
- | strcpy(rasa, caine.rasa); | + | |
- | } | + | |
- | else | + | |
- | { | + | |
- | rasa = nullptr; | + | |
- | } | + | |
- | } | + | |
- | Caine::~Caine() | + | <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) | ||
{ | { | ||
- | if (rasa != nullptr) | + | return x + y; |
- | { | + | |
- | delete[] rasa; | + | |
- | } | + | |
} | } | ||
- | void Caine::afisare() const | + | void testare() |
{ | { | ||
- | std::cout << "Nume: " << (getNume() ? getNume() : "Anonim") << '\n'; | + | int s1 = adunare(2, 3); |
- | std::cout << "Varsta: " << getVarsta() << " ani\n"; | + | float s2 = adunare(2.3f, 3.0f); |
- | std::cout << "Rasa: " << (rasa ? rasa : "Necunoscuta") << "\n\n"; | + | double s3 = adunare(-1.4, 8.24); |
+ | unsigned int s4 = adunare(2u, 3u); | ||
} | } | ||
</code> | </code> | ||
- | Iar mai jos propunem un exemplu de testare a acestor clase. | + | Prin urmare putem apela acum funcția de adunare pentru patru tipuri de date după cum urmează. |
<code cpp> | <code cpp> | ||
+ | #include "Template.h" | ||
+ | |||
int main() | int main() | ||
{ | { | ||
- | Caine caine("Zeus", 3, "labrador"); | + | std::cout << "Suma numerelor este: " << adunare(22, 8) << '\n'; // valid |
- | caine.afisare(); // 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 | |
- | Animal* pAnimal = new Caine(); | + | std::cout << "Suma numerelor este: " << adunare(4u, 6u) << '\n'; // valid |
- | pAnimal->afisare(); // valid deoarece metoda afisare este virtuala | + | /*std::cout << "Suma numerelor este: " << adunare(4l, 6l) << '\n'; // invalid*/ |
- | + | ||
- | delete pAnimal; // incorect nu se cheama si destructorul din Caine, memory leak | + | |
- | + | ||
- | pAnimal = &caine; // valid datorita relatiei de tip "is-a" | + | |
- | pAnimal->afisare(); // valid | + | |
return 0; | return 0; | ||
Line 283: | Line 290: | ||
</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**. | + | <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> |
- | 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> | + | ==== Clase template ==== |
- | Așadar pentru a elibera memoria **corect** vom declara destructorul clasei **Animal** ca **funcție virtuală**. | + | **Clasele template**, la fel ca **funcțiile template**, au scopul de a susține **programarea generică** și de a elimina duplicarea codului, oferind o 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**. |
+ | |||
+ | 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. | ||
+ | |||
+ | 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 T& medieAnuala); |
- | Animal(const char* nume, const int& varsta); | + | Student(const Student& student); |
- | Animal(const Animal& animal); | + | Student& operator=(const Student& student); |
- | virtual ~Animal(); // destructor virtual | + | ~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 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}}. | + | În continuare vom prezenta detaliat maniera de implementare a fiecărei metode în parte. |
- | ==== Interfețe ==== | + | === Implementarea constructorilor === |
- | + | ||
- | Î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 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ă ^ | + | |
- | | **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 | + | template <typename T> |
+ | Student<T>::Student(const char* nume, const T& medieAnuala) | ||
{ | { | ||
- | public: | + | if (nume != nullptr) |
+ | { | ||
+ | this->nume = new char[strlen(nume) + 1]; | ||
+ | strcpy(this->nume, nume); | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | this->nume = nullptr; | ||
+ | } | ||
+ | |||
+ | this->medieAnuala = medieAnuala; | ||
+ | } | ||
- | virtual float getArie() const = 0; | + | template<typename T> |
- | virtual float getPerimetru() const = 0; | + | 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; | ||
+ | } | ||
+ | |||
+ | medieAnuala = student.medieAnuala; | ||
+ | } | ||
</code> | </code> | ||
- | Iar în continuare vom prezenta clasele **Cerc** și **Patrat**. | + | === Implementarea operatorului de asignare === |
<code cpp> | <code cpp> | ||
- | class Cerc : public FiguraGeomertica | + | template<typename T> |
+ | Student<T>& Student<T>::operator=(const Student<T>& student) | ||
{ | { | ||
- | int raza; | + | if (this == &student) |
+ | { | ||
+ | return *this; | ||
+ | } | ||
- | public: | + | if (nume != nullptr) |
+ | { | ||
+ | delete[] nume; | ||
+ | } | ||
- | Cerc(const int& raza = 0); | + | if (student.nume != nullptr) |
+ | { | ||
+ | nume = new char[strlen(student.nume) + 1]; | ||
+ | strcpy(nume, student.nume); | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | nume = nullptr; | ||
+ | } | ||
- | float getArie() const override; | + | medieAnuala = student.medieAnuala; |
- | float getPerimetru() const override; | + | |
- | }; | + | return *this; |
+ | } | ||
</code> | </code> | ||
- | Iar mai jos sunt prezentate implementările metodelor aferente clasei **Cerc**. | + | === Implementarea destructorului === |
<code cpp> | <code cpp> | ||
- | Cerc::Cerc(const int& raza) | + | template<typename T> |
+ | Student<T>::~Student() | ||
{ | { | ||
- | this->raza = raza; | + | if (nume != nullptr) |
+ | { | ||
+ | delete[] nume; | ||
+ | } | ||
} | } | ||
+ | </code> | ||
- | float Cerc::getArie() const | + | === Implementarea metodelor accesor === |
+ | |||
+ | <code cpp> | ||
+ | template<typename T> | ||
+ | char* Student<T>::getNume() const | ||
{ | { | ||
- | return 3.14f * raza * raza; | + | return nume; |
} | } | ||
- | float Cerc::getPerimetru() const | + | template<typename T> |
+ | T Student<T>::getMedieAnuala() const | ||
{ | { | ||
- | return 2 * 3.14f * raza; | + | return medieAnuala; |
} | } | ||
- | </code> | ||
- | Declarația clasei **Patrat** se poate observa mai jos. | + | template<typename T> |
- | + | void Student<T>::setNume(const char* nume) | |
- | <code cpp> | + | |
- | class Patrat : public FiguraGeomertica | + | |
{ | { | ||
- | int latura; | + | if (nume == nullptr) |
+ | { | ||
+ | return; | ||
+ | } | ||
- | public: | + | if (this->nume != nullptr) |
+ | { | ||
+ | delete[] this->nume; | ||
+ | } | ||
- | Patrat(const int& latura = 0); | + | this->nume = new char[strlen(nume) + 1]; |
+ | strcpy(this->nume, nume); | ||
+ | } | ||
- | float getArie() const override; | + | template<typename T> |
- | float getPerimetru() const override; | + | void Student<T>::setMedieAnuala(const T& medieAnuala) |
- | }; | + | { |
+ | if (medieAnuala <= 0) | ||
+ | { | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | this->medieAnuala = medieAnuala; | ||
+ | } | ||
</code> | </code> | ||
- | Iar implementările metodelor sunt disponibile mai jos. | + | === Implementarea operatorului de afișare === |
<code cpp> | <code cpp> | ||
- | Patrat::Patrat(const int& latura) | + | template <typename T> |
+ | std::ostream& operator<<(std::ostream& out, const Student<T>& student) | ||
{ | { | ||
- | this->latura = latura; | + | out << "Numele studentului este: "; |
- | } | + | |
- | float Patrat::getArie() const | + | if (student.nume == nullptr) |
- | { | + | { |
- | return(float) latura * latura; | + | out << "N/A\n"; |
- | } | + | } |
+ | else | ||
+ | { | ||
+ | out << student.nume << '\n'; | ||
+ | } | ||
- | float Patrat::getPerimetru() const | + | out << "Media anuala a studentului este: " << student.medieAnuala << '\n'; |
- | { | + | |
- | return(float) 4 * latura; | + | return out; |
} | } | ||
</code> | </code> | ||
- | Iar ca și exemplu de testare a funcționalităților în funcția main avem: | + | === Crearea funcției de testare === |
<code cpp> | <code cpp> | ||
- | int main() | + | void testTemplate() |
{ | { | ||
- | FiguraGeomertica* pfg1 = new Cerc(4); | + | Student<int> s1("Ion", 10); |
+ | Student<int> s2("George", 9); | ||
+ | Student<int> s3 = s2; | ||
- | std::cout << "Aria cercului este: " << pfg1->getArie() << '\n'; | + | s3 = s1; |
- | std::cout << "Perimetrul cercului este: " << pfg1->getPerimetru() << '\n'; | + | |
- | FiguraGeomertica* pfg2 = new Patrat(5); | + | s3.setNume("Maria"); |
+ | s3.setMedieAnuala(8); | ||
- | std::cout << "\nAria patratului este: " << pfg2->getArie() << '\n'; | + | std::cout << s3 << '\n'; |
- | std::cout << "Perimetrul patratului este: " << pfg2->getPerimetru() << '\n'; | + | std::cout << s3.getNume() << "\n\n"; |
+ | std::cout << s3.getMedieAnuala() << "\n\n"; | ||
- | delete pfg1; | + | Student<double> s4("Ion", 10); |
- | delete pfg2; | + | Student<double> s5("George", 9); |
+ | Student<double> s6 = s5; | ||
- | return 0; | + | s5 = s4; |
+ | |||
+ | s5.setNume("Maria"); | ||
+ | s5.setMedieAnuala(9.9); | ||
+ | |||
+ | std::cout << s4 << '\n'; | ||
+ | std::cout << s4.getNume() << "\n\n"; | ||
+ | std::cout << s4.getMedieAnuala() << "\n\n"; | ||
} | } | ||
</code> | </code> | ||
- | <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> | + | ==== ==== |
- | ==== Concluzii ==== | + | Având acum implementarea clasei template **Student** putem să o folosim în codul din programul principal după cum umrează. |
- | 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**. | + | <code cpp> |
+ | #include "Student.h" | ||
- | == Virtualizarea în C++ == | + | int main() |
+ | { | ||
+ | Student<int> s1("Ion", 10); | ||
+ | Student<int> s2("George", 9); | ||
+ | Student<int> s3 = s2; | ||
- | * **Metodele virtuale** oferă flexibilitatea necesară pentru implementarea **polimorfismului dinamic**, unde comportamentul funcțiilor este stabilit în timpul execuției (**late binding**). | + | s3 = s1; |
- | * **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. | + | s3.setNume("Maria"); |
+ | s3.setMedieAnuala(8); | ||
- | == Overloading vs Overriding == | + | std::cout << s3 << '\n'; |
- | * **Overriding** este asociat cu polimorfismul dinamic (**run-time polymorphism**) și presupune redefinirea comportamentului unei metode virtuale din clasa de bază în clasele derivate. | + | Student<double> s4("Ion", 10); |
+ | Student<double> s5("George", 9); | ||
+ | Student<double> s6 = s5; | ||
- | * **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. | + | s5 = s4; |
- | * 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. | + | s5.setNume("Maria"); |
+ | s5.setMedieAnuala(9.9); | ||
- | == Clase Abstracte și Metode Virtual Pure == | + | std::cout << s4 << '\n'; |
- | * **Clasele abstracte** oferă un punct de plecare pentru proiectarea ierarhiilor complexe, combinând funcționalitățile comune și abstractizarea. | + | return 0; |
+ | } | ||
+ | </code> | ||
- | * 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ă. | + | <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> |
- | * Destructorii virtuali sunt obligatorii în clasele abstracte pentru a asigura o eliberare corectă a resurselor în ierarhiile de moștenire. | + | ==== Concluzii ==== |
- | + | ||
- | == Interfețele în C++ == | + | |
- | * **Interfețele** sunt un caz particular de clase abstracte care conțin **doar** metode virtual pure. | + | Î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. |
- | + | ||
- | * 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 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**. | + | 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. |