This shows you the differences between two versions of the page.
poo-is-ab:laboratoare:05 [2024/10/27 16:20] razvan.cristea0106 [Introducere] |
poo-is-ab:laboratoare:05 [2025/10/11 20:12] (current) razvan.cristea0106 [Supraîncărcarea operatorilor] |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ===== Laborator 05 - Moștenire simplă ===== | + | ===== Laborator 05 - Supraîncărcarea operatorilor ===== |
**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ă înțeleagă conceptul de moștenire între două clase | + | * recunoască și să înțeleagă conceptul de supraîncărcare (overloading) |
- | * construiască legături între clase folosind relația de tip "is-a" (relație de specializare) | + | * știe când un operator trebuie supraîncărcat fie ca funcție membră fie ca funcție friend |
- | * folosească membrii marcați cu protected și să înțeleagă diferențele dintre accesul public, privat și protejat în moștenire | + | * supraîncarce operatorii aritmetici pentru o clasă |
- | * aplice principiile de reutilizare a codului prin extinderea funcționalității clasei de bază în clasa derivată | + | * supraîncarce operatorii logici de comparație |
+ | * supraîncarce operatorii de flux pentru citire și afișare | ||
==== Introducere ==== | ==== Introducere ==== | ||
- | În acest laborator vom extinde lucrul cu clase, introducând conceptul de **relații între clase**. Vom construi și analiza legături între două clase, folosind relațiile de tip **"is-a"** și **"has-a"**, dar vom pune accentul mai mult pe relația de **"is-a"**. Relația de tip **"is-a"** reprezintă o formă de moștenire, un principiu fundamental al **POO**, prin care o **clasă derivată (subclasă)** preia **proprietățile** și **comportamentele** unei **clase de bază (superclasă)**. | + | În acest laborator, ne vom concentra pe aprofundarea conceptului de **overloading (supraîncărcare)**, un aspect esențial al **POO**. Așa cum am introdus deja în [[poo-is-ab:laboratoare:03|laboratorul 3]], atunci când am discutat despre **polimorfism**, am înțeles că supraîncărcarea se referă la posibilitatea de a defini mai multe funcții cu același nume, dar cu semnături diferite. Acest mecanism se aplică atât funcțiilor libere, cât și metodelor în cadrul unei clase. |
- | Acest tip de relație ne permite să definim **ierarhii de clase**, să **reutilizăm codul** și să **extindem** funcționalitățile claselor, oferind un model de organizare flexibil și scalabil. **Moștenirea** funcționează similar cu cea din viața reală: **clasa derivată** preia proprietățile și comportamentele **clasei de bază**, dar poate adăuga și **comportamente noi** sau **modifica** pe cele existente. Astfel, **moștenirea** facilitează **extensibilitatea** și **întreținerea** codului, reducând duplicarea și oferind un mod eficient de a gestiona complexitatea în proiecte de mari dimensiuni. | + | Este important de subliniat că supraîncărcarea nu schimbă comportamentul fundamental al unei **funcții** sau **metode**, ci oferă alternative prin care acestea pot fi apelate, în funcție de tipul și numărul parametrilor. Cu alte cuvinte, **funcția** sau **metoda** își păstrează scopul de bază, dar poate trata diverse scenarii sau tipuri de date fără a necesita **nume diferite**. Această flexibilitate contribuie la creșterea lizibilității codului și la reducerea redundanței, permițând programatorilor să scrie cod mai clar și mai modular. |
- | ==== Moștenirea între două clase ==== | + | |
- | Așa cum am menționat anterior, **moștenirea** este un principiu fundamental al **POO** care permite unei **clase derivate** să preia atât **proprietățile (atributele)** cât și **comportamentele (funcțiile membre)** unei **clase părinte**. Prin acest mecanism, **clasa derivată** poate să extindă funcționalitatea moștenită prin adăugarea de noi atribute și metode sau prin redefinirea celor existente. Scopul principal al **moștenirii** este de a promova **reutilizarea codului** și de a permite o **extensie** naturală a funcționalităților inițiale, astfel încât să se creeze o structură mai **flexibilă** și mai **ușor de întreținut** în cadrul aplicațiilor. | + | Pe parcursul acestui laborator, vom explora în detaliu cum funcționează **supraîncărcarea operatorilor** în C++ și cum poate fi utilizată eficient în cadrul claselor pentru a îmbunătăți funcționalitatea și flexibilitatea aplicațiilor la care lucrăm. |
- | Înainte de a explica moștenirea între două clase vom face o scurtă recapitulare a noțiunilor deja învățate în cadrul laboratoarelor anterioare. Pentru acest laborator propunem clasa **Locuinta** care are ca și membri **pret (de tip float)** și **adresa (șir de caractere alocat dinamic)**. | + | ==== Operatorii limbajului C++ ==== |
- | <code cpp> | + | Când discutăm despre operatori în C++, primul lucru de care trebuie să ținem cont este faptul că aceștia operează asupra unor **operanzi**. În funcție de **numărul de operanzi** asupra cărora acționează, operatorii pot fi clasificați în trei mari categorii dupa cum urmează în descrierea de mai jos. |
- | #pragma once | + | |
- | #include <cstring> | + | |
- | #include <iostream> | + | |
- | class Locuinta | + | **Operatori Unari**: acești operatori au nevoie de **un singur** operand pentru a-și îndeplini funcția. Ei sunt folosiți în mod frecvent pentru operații simple precum negarea, incrementarea sau decrementarea valorii operandului. Exemple comune includ **++ (incrementare)**, **%%--%% (decrementare)**, **! (negare logică)** și **- (negare aritmetică)**. |
- | { | + | |
- | float pret; | + | |
- | char* adresa; | + | |
- | public: | + | **Operatori Binari**: aceștia necesită **doi** operanzi și sunt cei mai utilizați operatori în programare. Acești operatori includ **adunarea (+)**, **scăderea (-)**, **înmulțirea (*)**, **împărțirea (/)**, dar și operatori logici precum **&& (și logic)**, **|| (sau logic)** și **operatori de comparație (==, !=, <, >)**. |
- | Locuinta(); | + | **Operatorul Ternar**: există un **singur** operator ternar în C++, cunoscut sub numele de **operator condițional (?:)**. Acesta utilizează **trei** operanzi și este folosit pentru a evalua o condiție și a alege între două valori, în funcție de rezultatul acelei condiții. |
- | Locuinta(const float& pret, const char* adresa); | + | |
- | Locuinta(const Locuinta& locuinta); | + | |
- | Locuinta& operator=(const Locuinta& locuinta); | + | |
- | ~Locuinta(); | + | |
- | float getPret() const; | + | În continuare vom prezenta un tabel cu operatorii existenți în limbajul C++ pentru a putea vedea atât simbolurile cât și modul de asociere (aplicare) al acestora. |
- | char* getAdresa() const; | + | |
- | void setPret(const float& pret); | + | ^ Categoria de operatori ^ Simbolurile operatorilor ^ Mod de asociere (aplicare) ^ |
- | void setAdresa(const char* adresa); | + | | **Primari** | ( ), [ ], ., %%->%% | stânga - dreapta | |
+ | | **Unari** | ++, %%--%%, -, !, ~, (tip), &, *, sizeof | **dreapta - stânga** | | ||
+ | | **Multiplicativi** | *, /, % | stânga - dreapta | | ||
+ | | **Adunare, scădere** | +, - | stânga - dreapta | | ||
+ | | **Deplasare (nivel bit)** | %%<<%%, %%>>%% | stânga - dreapta | | ||
+ | | **Relaționali** | <, >, %%<=%%, %%>=%% | stânga - dreapta | | ||
+ | | **Testare egalitate** | ==, != | stânga - dreapta | | ||
+ | | **ȘI (nivel bit)** | & | stânga - dreapta | | ||
+ | | **SAU exclusiv (nivel bit)** | %%^%% | stânga - dreapta | | ||
+ | | **SAU inclusiv (nivel bit)** | %%|%% | stânga - dreapta | | ||
+ | | **ȘI logic** | %%&&%% | stânga - dreapta | | ||
+ | | **SAU logic** | %%||%% | stânga - dreapta | | ||
+ | | **Condițional (ternar)**| ?: | stânga - dreapta | | ||
+ | | **Atribuire** | =, +=, -=, *=, /=, %=, %%<<=%%, %%>>=%%, &=, %%^=%%, %%|=%% | **dreapta - stânga** | | ||
+ | | **Virgulă** | , | stânga - dreapta | | ||
- | friend std::ostream& operator<<(std::ostream& out, const Locuinta& locuinta); | + | <note important>Pentru operatorii din tabelul de mai sus am ales această ordine pentru a putea stabili cu ușurință **prioritățile** fiecăruia dintre ei. Astfel cea mai **mică** prioritate o are operatorul **virgulă**, în timp ce cea mai **mare** prioritate o au **operatorii primari**.</note> |
- | }; | + | |
- | </code> | + | ==== Funcții friend ==== |
+ | |||
+ | În C++ funcțiile **friend** sunt acele funcții care au aces la zona **privată** a clasei, dar și la cea **protected**. Deși o funcție friend strică **principiul încapsulării datelor**, trebuie menționat că acest lucru este controlat. | ||
+ | |||
+ | <note tip>Pentru a înțelege mai bine rolul funcțiilor **friend**, imaginați-vă următoarea analogie: fiecare dintre voi are un set de gânduri și sentimente care sunt **private**, **inaccesibile** celorlalți. Totuși, ca ființe sociale, avem prieteni cărora alegem să le împărtășim aceste gânduri. Deși prietenii noștri **au acces** la informații personale, acest lucru **nu** înseamnă că oricine poate avea acces la ele. Voi sunteți cei care **dețineți controlul total** asupra a ceea ce **dezvăluiți** și **cui**.</note> | ||
+ | |||
+ | Pentru a declara o funcție de tip **friend**, folosim cuvântul cheie **friend**. Aceste funcții sunt declarate în interiorul clasei și au cuvântul cheie **friend** plasat **înaintea tipului de return** al funcției. | ||
- | Iar implementările pentru funcțiile membre și cea friend le putem observa în codul de mai jos. | + | Să urmărim exemplul de cod de mai jos unde am declarat și am implementat o **funcție friend** care afișază datele despre o persoană. |
<code cpp> | <code cpp> | ||
- | #include "Locuinta.h" | + | #include <iostream> |
+ | #include <cstring> | ||
- | Locuinta::Locuinta() | + | class Persoana |
{ | { | ||
- | pret = 0.0f; | + | int varsta; |
- | adresa = nullptr; | + | char* nume; |
- | } | + | |
- | Locuinta::Locuinta(const float& pret, const char* adresa) | + | public: |
+ | |||
+ | Persoana(const int& varsta, const char* nume); | ||
+ | ~Persoana(); | ||
+ | |||
+ | friend void afisarePersoana(const Persoana& persoana); // functie friend pentru afisarea datelor unei persoane | ||
+ | }; | ||
+ | |||
+ | Persoana::Persoana(const int& varsta, const char* nume) | ||
{ | { | ||
- | this->pret = pret; | + | this->varsta = varsta; |
- | if (adresa != nullptr) | + | if (nume != nullptr) |
{ | { | ||
- | this->adresa = new char[strlen(adresa) + 1]; | + | this->nume = new char[strlen(nume) + 1]; |
- | strcpy(this->adresa, adresa); | + | strcpy(this->nume, nume); |
} | } | ||
else | else | ||
{ | { | ||
- | this->adresa = nullptr; | + | this->nume = nullptr; |
} | } | ||
} | } | ||
- | Locuinta::Locuinta(const Locuinta& locuinta) | + | Persoana::~Persoana() |
{ | { | ||
- | pret = locuinta.pret; | + | if (nume != nullptr) |
- | + | ||
- | if (locuinta.adresa != nullptr) | + | |
{ | { | ||
- | adresa = new char[strlen(locuinta.adresa) + 1]; | + | delete[] nume; |
- | strcpy(adresa, locuinta.adresa); | + | |
- | } | + | |
- | else | + | |
- | { | + | |
- | adresa = nullptr; | + | |
} | } | ||
} | } | ||
- | Locuinta& Locuinta::operator=(const Locuinta& locuinta) | + | void afisarePersoana(const Persoana& persoana) |
{ | { | ||
- | if (this == &locuinta) | + | std::cout << "Numele persoanei este: " << persoana.nume << '\n'; |
- | { | + | std::cout << "Varsta persoanei este: " << persoana.varsta << "\n\n"; |
- | return *this; | + | } |
- | } | + | |
- | if (adresa != nullptr) | + | int main() |
- | { | + | { |
- | delete[] adresa; | + | Persoana persoana(22, "Andrei"); |
- | } | + | |
- | pret = locuinta.pret; | + | afisarePersoana(persoana); // apelarea functiei friend este identica cu a unei functii clasice din C/C++ |
- | if (locuinta.adresa != nullptr) | + | return 0; |
- | { | + | |
- | adresa = new char[strlen(locuinta.adresa) + 1]; | + | |
- | strcpy(adresa, locuinta.adresa); | + | |
- | } | + | |
- | else | + | |
- | { | + | |
- | adresa = nullptr; | + | |
- | } | + | |
- | + | ||
- | return *this; | + | |
} | } | ||
+ | </code> | ||
- | Locuinta::~Locuinta() | + | În mod evident puteam declara și implementa o metodă simplă de afișare în loc să optăm pentru o funcție **friend**. Trebuie însă menționat faptul că este doar un exemplu didactic pentru a putea înțelege cum putem folosi funcțiile **friend** în limbajul C++. |
+ | |||
+ | <note warning>Deși sunt declarate în interiorul clasei **funcțiile friend** se numesc **funcții** și **nu metode** datorită faptului că **nu** primesc **pointerul this** în lista de parametri. Cuvântul cheie **friend** se utilizează **doar** la declararea funcției pentru a anunța compilatorul că este vorba despre o **funcție** și **nu** despre o **metodă**, iar implementarea acesteia este **identică** cu a unei **funcții clasice din C/C++**.</note> | ||
+ | |||
+ | Vom folosi foarte mult acest tip de funcții după cum vom vedea în cele ce urmează la supraîncărcarea operatorilor limbajului C++. | ||
+ | |||
+ | ==== Supraîncărcarea operatorilor ==== | ||
+ | |||
+ | În această secțiune vom învăța cum vom putea specializa diverși operatori pentru o clasă astfel încât obiectele acesteia să se comporte similar cu variabilele care sunt declarate folosind **tipuri de date primitive (int, float, char, long, double, etc.)**. Clasa cu care vom lucra este **NrComplex** în care ne dorim să punem în evidență diferite variante de operatori supraîncărcați. | ||
+ | |||
+ | Structura clasei **NrComplex** poate fi observată în codul de mai jos. | ||
+ | |||
+ | <code cpp> | ||
+ | #include <iostream> | ||
+ | |||
+ | class NrComplex | ||
{ | { | ||
- | if (adresa != nullptr) | + | double real; |
- | { | + | double imaginar; |
- | delete[] adresa; | + | |
- | } | + | public: |
+ | |||
+ | NrComplex(const double& real = 0.0, const double& imaginar = 0.0); // constructor cu parametri cu valori implicite | ||
+ | |||
+ | double getReal() const; | ||
+ | double getImaginar() const; | ||
+ | |||
+ | void setReal(const double& real); | ||
+ | void setImaginar(const double& imaginar); | ||
+ | }; | ||
+ | </code> | ||
+ | |||
+ | Iar implementările funcțiilor membre sunt vizibile în codul de mai jos. | ||
+ | |||
+ | <code cpp> | ||
+ | #include "Complex.h" | ||
+ | |||
+ | NrComplex::NrComplex(const double& real, const double& imaginar) | ||
+ | { | ||
+ | this->real = real; | ||
+ | this->imaginar = imaginar; | ||
} | } | ||
- | float Locuinta::getPret() const | + | double NrComplex::getReal() const |
{ | { | ||
- | return pret; | + | return real; |
} | } | ||
- | char* Locuinta::getAdresa() const | + | double NrComplex::getImaginar() const |
{ | { | ||
- | return adresa; | + | return imaginar; |
} | } | ||
- | void Locuinta::setPret(const float& pret) | + | void NrComplex::setReal(const double& real) |
{ | { | ||
- | if (pret <= 0.0f) | + | this->real = real; |
- | { | + | } |
- | return; | + | |
- | } | + | |
- | this->pret = pret; | + | void NrComplex::setImaginar(const double& imaginar) |
+ | { | ||
+ | this->imaginar = imaginar; | ||
} | } | ||
+ | </code> | ||
+ | |||
+ | <note>După cum am putut observa în fișierul **header** pentru clasa **NrComplex** am declarat un constructor cu parametri cu **valori implicite**. Acest **constructor special** ține locul de fapt a trei constructori. Dacă **nu** vom specifica niciun parametru câmpurile vor primi valoarea 0.0 care este cea **default**. Dacă specificăm un parametru atunci **doar** partea imaginară va primi valoarea 0.0 care este cea **implicită**.</note> | ||
+ | |||
+ | <note warning>Atunci când definim **funcții** sau **metode** care conțin parametri cu **valori implicite**, este esențial ca acești parametri să fie plasați **după** parametrii fără valori implicite în lista de argumente. Dacă această regulă **nu** este respectată, compilatorul va genera o **eroare de ambiguitate**, deoarece **nu** va putea să determine **corect** care dintre parametri trebuie să primească valoarea implicită și care valoare este furnizată explicit la apelul funcției. Această regulă asigură **claritatea** și **predictibilitatea** modului în care sunt procesate argumentele funcției.</note> | ||
+ | |||
+ | === Operatori supraîncărcați ca funcții membre în clasă === | ||
+ | |||
+ | În general alegem această variantă de supraîncărcare atunci când vrem să avem acces **direct** la membrii unei clase. Astfel, se pot manipula **direct** datele interne ale obiectului fără a fi necesare **metode acccesor** de tipul **getter/setter** sau mecanisme suplimentare pentru accesarea datelor private. | ||
+ | |||
+ | <note important>Operatorii **unari** sunt supraîncărcați **doar** ca **funcții membre**, deoarece aceștia operează întotdeauna asupra unui singur obiect, respectiv **obiectul curent**. În acest caz, obiectul curent este accesat **implicit** prin intermediul **pointerului this**, iar modificările sunt aplicate **direct** asupra sa. Această abordare oferă un control eficient asupra stării interne a obiectului, fără a necesita acces din exterior la membrii clasei.</note> | ||
+ | |||
+ | == Supraîncărcarea operatorului ++ == | ||
+ | |||
+ | După cum bine știm există două variante pentru acest operator și anume **forma prefixată**, care presupune modificarea valorii **înainte** de a trece la următoarea operație, și respectiv **forma postfixată**, unde valoarea este **mai întâi folosită** și **ulterior modificată**. | ||
+ | |||
+ | Prin urmare trebuie să avem **două** metode care reprezintă cele **două** forme ale acestui **operator unar**. Putem face acest lucru folosindu-ne de **polimorfism** după cum urmează în exemplul de cod de mai jos. | ||
+ | |||
+ | <code cpp> | ||
+ | #include <iostream> | ||
- | void Locuinta::setAdresa(const char* adresa) | + | class NrComplex |
{ | { | ||
- | if (adresa == nullptr) | + | double real; |
- | { | + | double imaginar; |
- | return; | + | |
- | } | + | |
- | if (this->adresa != nullptr) | + | public: |
- | { | + | |
- | delete[] this->adresa; | + | |
- | } | + | |
- | this->adresa = new char[strlen(adresa) + 1]; | + | NrComplex(const double& real = 0.0, const double& imaginar = 0.0); |
- | strcpy(this->adresa, adresa); | + | |
+ | double getReal() const; | ||
+ | double getImaginar() const; | ||
+ | |||
+ | void setReal(const double& real); | ||
+ | void setImaginar(const double& imaginar); | ||
+ | |||
+ | // supraincarcarea operatorilor ca functii membre | ||
+ | |||
+ | NrComplex& operator++(); // forma prefixata | ||
+ | NrComplex operator++(int); // forma postfixata | ||
+ | }; | ||
+ | </code> | ||
+ | |||
+ | Iar implementările corespunzătoare pentru cele două forme ale operatorului de incrementare le putem vedea mai jos. | ||
+ | |||
+ | <code cpp> | ||
+ | NrComplex& NrComplex::operator++() | ||
+ | { | ||
+ | this->real++; | ||
+ | this->imaginar++; | ||
+ | |||
+ | return *this; | ||
} | } | ||
- | std::ostream& operator<<(std::ostream& out, const Locuinta& locuinta) | + | NrComplex NrComplex::operator++(int) |
{ | { | ||
- | out << "Pretul locuintei este: " << locuinta.pret << " ron\n"; | + | NrComplex copie = *this; |
- | if (locuinta.adresa != nullptr) | + | this->real++; |
- | { | + | this->imaginar++; |
- | out << "Adresa locuintei este: " << locuinta.adresa << "\n"; | + | |
- | } | + | |
- | else | + | |
- | { | + | |
- | out << "Adresa locuintei este: inexistenta\n"; | + | |
- | } | + | |
- | return out; | + | return copie; |
} | } | ||
</code> | </code> | ||
- | <note>Pe prima linie a **fișierului header** în care este definită clasa **Locuinta**, putem observa utilizarea directivei **''#pragma once''**. Aceasta este o instrucțiune specifică compilatorului care indică faptul că fișierul respectiv trebuie inclus și compilat o **singură dată**, chiar dacă este referit în mod repetat în alte fișiere prin intermediul directivelor **''#include''**. Astfel, se previn **multiplele incluziuni** ale aceluiași fișier header, care ar putea duce la erori de compilare, cum ar fi redefinirea claselor sau funcțiilor. Directiva **''#pragma once''** este o alternativă modernă și mai simplă la gardienii clasici ai fișierelor header, adică acele secvențe de cod cu **''#ifndef''**, **''#define''** și **''#endif''** care au același scop.</note> | + | <note important>Se poate observa că la **forma postfixată** avem un parametru de care **nu** ne folosim. Acel parametru este **doar** pentru a asigura **polimorfismul**, compilatorul făcând distincția între cele două variante de operator de incrementare.</note> |
- | Dacă voiam să folosim varianta tradițională de scriere a unui fișier header am fi procedat în maniera următoare. | + | Pentru **operatorul de decrementare** se aplică aceleași **exact** aceeași pași, încercați să îl implementați voi pentru a putea înțelege mai bine cum funcționează conceptul de **overloading**. |
- | <code cpp> | + | == Supraîncărcarea operatorului ! == |
- | #ifndef LOCUINTA_H | + | |
- | #define LOCUINTA_H | + | |
- | #include <cstring> | + | Acest operator are rolul de a **nega** o **expresie logică**. Deși un număr complex **nu** poate fi negat în sensul unei **negații logice**, putem interpreta negația ca **operație de conjugare**. Prin urmare, utilizarea **operatorului de negare logică (!)** ar putea fi relevantă pentru clasa **NrComplex**, în sensul că am putea implementa acest operator pentru a **returna** conjugatul unui numărul complex. |
+ | |||
+ | În continuare vom declara în clasa **NrComplex** acest operator pentru a realiza ceea ce ne dorim, și anume conjugarea unui număr complex. | ||
+ | |||
+ | <code cpp> | ||
#include <iostream> | #include <iostream> | ||
- | class Locuinta | + | class NrComplex |
{ | { | ||
- | float pret; | + | double real; |
- | char* adresa; | + | double imaginar; |
public: | public: | ||
- | Locuinta(); | + | NrComplex(const double& real = 0.0, const double& imaginar = 0.0); |
- | Locuinta(const float& pret, const char* adresa); | + | |
- | Locuinta(const Locuinta& locuinta); | + | |
- | Locuinta& operator=(const Locuinta& locuinta); | + | |
- | ~Locuinta(); | + | |
- | float getPret() const; | + | double getReal() const; |
- | char* getAdresa() const; | + | double getImaginar() const; |
- | void setPret(const float& pret); | + | void setReal(const double& real); |
- | void setAdresa(const char* adresa); | + | void setImaginar(const double& imaginar); |
- | friend std::ostream& operator<<(std::ostream& out, const Locuinta& locuinta); | + | // supraincarcarea operatorilor ca functii membre |
- | }; | + | |
- | #endif // LOCUINTA_H | + | NrComplex operator!() const; // operatorul de negare logica (in cazul acestei clase va conjuga un numar complex) |
+ | |||
+ | NrComplex& operator++(); | ||
+ | NrComplex operator++(int); | ||
+ | }; | ||
</code> | </code> | ||
- | === Relația de "is-a" între două clase === | + | Iar implementarea acestui operator se poate observa mai jos. |
- | Acest tip de relație ne permite să implementăm **moștenirea** între clase. În acest context, când discutăm despre moștenire, întâlnim următorii termeni esențiali: **clasă părinte (denumită și clasă de bază sau superclasă)** și **clasă derivată (denumită și clasă copil sau subclasă)**. **Clasa părinte** reprezintă clasa de la care dorim să **preluăm** atribute și metode, având posibilitatea să **reutilizăm** codul existent, în timp ce **clasa derivată** **extinde** această funcționalitate, **adăugând** noi comportamente și caracteristici. | + | <code cpp> |
+ | NrComplex NrComplex::operator!() const | ||
+ | { | ||
+ | NrComplex conjugat = *this; | ||
- | <note tip>Atunci când dorim să implementăm **moștenirea** între două clase, este important să respectăm un set clar de reguli. În primul rând, trebuie să stabilim care dintre clase va fi **clasa părinte** și care va fi **clasa copil**. Prin procesul de **moștenire**, afirmăm că un **obiect** al **clasei derivate** este, implicit, și un obiect al **clasei părinte**, datorită relației de tip **"is-a"** dintre cele două clase. Această relație trebuie să aibă o **coerență logică**, adică orice instanță a **clasei derivate** este automat și o instanță a **superclasei**. De exemplu, omul este un mamifer, iar afirmația că toți oamenii sunt mamifere este corectă. Însă reciproca nu este valabilă, deoarece nu toate mamiferele sunt oameni. Prin urmare, relația de tip **"is-a"** se aplică doar de la **clasa copil** către **clasa părinte**, și nu invers. | + | conjugat.imaginar = -conjugat.imaginar; |
- | </note> | + | |
- | În continuare vom căuta o clasă care poate să extindă și să respecte relația de **"is-a"** cu clasa **Locuinta**. Vom propune clasa **Apartament** care respectă regulile descrise mai sus, deoarece orice apartament este o locuință. Clasa **Apartament** are ca și atribute **numarCamere (de tip întreg)** și **numeProprietar (șir de caractere alocat dinamic)**. | + | return conjugat; |
+ | } | ||
+ | </code> | ||
- | În limbajul C++ pentru ca o clasă să moștenească o altă clasă se folosește următoarea sintaxă: | + | <note warning>Operatorul de negare logică **nu** trebuie să modifice **this-ul**, acesta a fost și motivul pentru care am returnat o **copie modificată** în loc să modificăm **direct** obiectul curent.</note> |
- | **''class NumeClasaDerivata : specificator de acces pentru moștenire NumeClasaParinte''** și apoi corpul clasei derivate. | + | |
- | Să urmărim în cod cum putem face clasa **Apartament** să moștenească clasa **Locuință**. | + | == Supraîncărcarea operatorilor == și != == |
+ | |||
+ | **Operatorul %%==%%** este folosit pentru a testa egalitatea dintre doi operanzi, deci prin urmare trebuie să returneze o valoare de adevăr (**true** sau **false**). Îl supraîncârcăm ca funcție membră, deoarece avem deja un parametru existent, și anume **pointerul this**, la care mai adăugăm un alt parametru care reprezintă **obiectul cu care facem comparația**. | ||
+ | |||
+ | Același lucru putem spune și despre **operatorul %%!=%%**, numai că el face exact **opusul** a ceea ce face operatorul de testare a egalității între doi operanzi, adică verifică dacă valorile celor doi termeni sunt **diferite**. | ||
+ | |||
+ | Vom declara după cum urmează aceste două metode în clasa **NrComplex**. | ||
<code cpp> | <code cpp> | ||
- | #pragma once | + | #include <iostream> |
- | #include "Locuinta.h" | + | |
- | class Apartament : public Locuinta // clasa Apartament mosteneste clasa Locuinta | + | class NrComplex |
{ | { | ||
- | int numarCamere; | + | double real; |
- | char* numeProprietar; | + | double imaginar; |
public: | public: | ||
- | Apartament(); | + | NrComplex(const double& real = 0.0, const double& imaginar = 0.0); |
- | Apartament(const float& pret, const char* adresa, const int& numarCamere, const char* numeProprietar); | + | |
- | Apartament(const Apartament& apartament); | + | |
- | Apartament& operator=(const Apartament& apartament); | + | |
- | ~Apartament(); | + | |
- | int getNumarCamere() const; | + | double getReal() const; |
- | char* getNumeProprietar() const; | + | double getImaginar() const; |
- | void setNumarCamere(const int& numarCamere); | + | void setReal(const double& real); |
- | void setNumarCamere(const char* numeProprietar); | + | void setImaginar(const double& imaginar); |
- | friend std::ostream& operator<<(std::ostream& out, const Apartament& apartament); | + | // supraincarcarea operatorilor functii membre |
+ | |||
+ | NrComplex operator!() const; | ||
+ | |||
+ | NrComplex& operator++(); | ||
+ | NrComplex operator++(int); | ||
+ | |||
+ | bool operator==(const NrComplex& z) const; // operatorul de testare a egalitatii intre doua numere complexe | ||
+ | bool operator!=(const NrComplex& z) const; // operatorul de testare a diferentei intre doua numere complexe | ||
}; | }; | ||
</code> | </code> | ||
- | <note warning>Deși clasa **Apartament** moștenește clasa **Locuinta** din cauza faptului că membrii clasei **Locuinta** sunt marcați din default cu **private** nu vom avea acces la ei în **clasa derivată**.</note> | + | Iar implementările pentru cei doi operatori le putem observa în codul de mai jos. |
- | === Specificatorul de acces protected === | + | <code cpp> |
+ | bool NrComplex::operator==(const NrComplex& z) const | ||
+ | { | ||
+ | return this->real == z.real && this->imaginar == z.imaginar; | ||
+ | } | ||
- | Pentru a putea face câmpurile clasei **Locuinta** să fie vizibile în clasa copil, dar să nu poată fi în continuare accesate din exterior de oricine le vom marca cu **protected** după cum urmează în codul de mai jos. | + | bool NrComplex::operator!=(const NrComplex& z) const |
+ | { | ||
+ | return this->real != z.real || this->imaginar != z.imaginar; | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | == Supraîncărcarea operatorului += == | ||
+ | |||
+ | Acest operator este o **variantă prescurtată** pentru adunarea a două variabile de același tip. În cazul a două numere complexe vom prezenta în codul de mai jos cum putem declara acest operator în fișierul **header**. | ||
<code cpp> | <code cpp> | ||
- | #pragma once | ||
- | #include <cstring> | ||
#include <iostream> | #include <iostream> | ||
- | class Locuinta | + | class NrComplex |
{ | { | ||
- | protected: // specificatorul de acces protected | + | double real; |
- | + | double imaginar; | |
- | float pret; | + | |
- | char* adresa; | + | |
public: | public: | ||
- | Locuinta(); | + | NrComplex(const double& real = 0.0, const double& imaginar = 0.0); |
- | Locuinta(const float& pret, const char* adresa); | + | |
- | Locuinta(const Locuinta& locuinta); | + | |
- | Locuinta& operator=(const Locuinta& locuinta); | + | |
- | ~Locuinta(); | + | |
- | float getPret() const; | + | double getReal() const; |
- | char* getAdresa() const; | + | double getImaginar() const; |
- | void setPret(const float& pret); | + | void setReal(const double& real); |
- | void setAdresa(const char* adresa); | + | void setImaginar(const double& imaginar); |
- | friend std::ostream& operator<<(std::ostream& out, const Locuinta& locuinta); | + | // supraincarcarea operatorilor ca functii membre |
- | }; | + | |
- | </code> | + | |
- | Astfel acum câmpurile **pret** și **adresa** vor fi vizibile și în clasa derivată, dar vor rămâne **inaccesibile** în funcția **main** sau în orice altă clasă care **nu** moștenește clasa **Locuinta**. | + | NrComplex operator!() const; |
- | === Implementarea metodelor și a funcțiilor friend în clasa derivată === | + | NrComplex& operator+=(const NrComplex& z); // operatorul compus pentru adunarea a doua numere complexe |
- | În continuare vom prezenta modul în care trebuiesc implementate toate funcționalitățile clasei **Apartament** astfel încât realția de **"is-a"** să fie satisfăcută și să reutilizăm codul din clasa **Locuinta**. | + | NrComplex& operator++(); |
+ | NrComplex operator++(int); | ||
- | == Implementarea constructorilor clasei derivate == | + | bool operator==(const NrComplex& z) const; |
- | + | bool operator!=(const NrComplex& z) const; | |
- | Atunci când dorim să construim un obiect de tipul **clasei derivate**, trebuie să ținem cont de faptul că, **mai întâi**, trebuie inițializate și gestionate toate datele și resursele **moștenite** din **clasa părinte**. Constructorul **clasei derivate** va apela constructorul **clasei părinte** pentru a asigura corectitudinea inițializării **componentelor moștenite**. Astfel, acest proces garantează că toate proprietățile părintelui sunt gestionate corespunzător înainte de a se trece la inițializarea specifică **clasei derivate**. | + | }; |
+ | </code> | ||
- | Să urmărim cu atenție mai jos implemetarea constructorului fără parametri pentru clasa **Apartament**. | + | Iar implementarea acestui operator o vom regăsi în secvența de cod următoare. |
<code cpp> | <code cpp> | ||
- | Apartament::Apartament() : Locuinta() | + | NrComplex& NrComplex::operator+=(const NrComplex& z) |
{ | { | ||
- | numarCamere = 0; | + | this->real += z.real; |
- | numeProprietar = nullptr; | + | this->imaginar += z.imaginar; |
+ | |||
+ | return *this; | ||
} | } | ||
</code> | </code> | ||
- | <note important>Se poate observa că, înainte de a inițializa câmpurile specifice clasei **Apartament**, am apelat **constructorul fără parametri** al clasei **Locuință** în **lista de inițializare** a constructorului **clasei derivate**. Astfel, am **reutilizat** codul din **clasa părinte** printr-un simplu apel, asigurând inițializarea corectă a membrilor moșteniți. Este important de menționat că această **listă de inițializare** poate fi utilizată **exclusiv** în cazul **constructorilor**, fiind o metodă eficientă de a gestiona inițializarea **clasei părinte**.</note> | + | <note tip>În mod similar, putem proceda pentru **operatorii -=, /=, *= și %=**. Totuși, este important să ne întrebăm dacă aceste operații au o **semnificație logică** în contextul numerelor complexe. Atunci când supraîncărcăm operatori, **trebuie** să ne asigurăm că operațiile respective **au sens logic** în raport cu clasa pentru care le implementăm.</note> |
- | În continuare vom implementa constructorul cu parametri pentru clasa copil urmând același principiu ca la constructorul fără parametri. | + | ==== ==== |
+ | |||
+ | <note important>Nu doar **operatorii unari** sunt supraîncărcați ca funcții membre, mai putem supraîncărca și operatori precum cel de **indexare ("[]")**, operatorul de **apel de funcție ("()")**, operatorul **săgeată ("%%->%%")** și operatorul de **asignare ("=")**. Cei din urmă trebuie implementați **exclusiv** ca funcții membre în clasă. De asemenea, și operatorii de comparație pot fi supraîncărcați ca **metode** ale clasei, dar în contextul numerelor complexe aceștia **nu au sens**, deoarece în matematică **nu** există **relație de ordine** în mulțimea numerelor complexe.</note> | ||
+ | |||
+ | === Operatori supraîncărcați ca funcții friend === | ||
+ | |||
+ | În general, alegem să supraîncărcăm operatorii ca **funcții friend** atunci când dorim să permitem ca aceștia să primească ca parametri tipuri de date **diferite** de **clasa** pentru care facem supraîncărcarea. Această abordare este utilă mai ales când operatorul trebuie să acceseze **membrii privați** ai clasei, dar implică și **alte** tipuri de date în operații. | ||
+ | |||
+ | == Supraîncărcarea operatorului + == | ||
+ | |||
+ | Aici putem avea mai multe situații spre exemplu: adunarea a două numere complexe, adunarea dintre un număr complex și unul real și adunarea dintre un număr real și unul complex. Prin urmare avem **trei** situații dintre care două necesită un **alt tip de date** primit ca parametru. Prin urmare vom supraîncărca acest operator ca **funcție friend** punând la dispoziție cele **trei** variante despre care am discutat anterior. | ||
+ | |||
+ | Să urmărim codul de mai jos unde am pus la dispoziție cele trei forme pentru **operatorul de adunare**. | ||
<code cpp> | <code cpp> | ||
- | Apartament::Apartament(const float& pret, const char* adresa, const int& numarCamere, const char* numeProprietar) : Locuinta(pret, adresa) | + | #include <iostream> |
+ | |||
+ | class NrComplex | ||
{ | { | ||
- | this->numarCamere = numarCamere; | + | double real; |
+ | double imaginar; | ||
- | if (numeProprietar != nullptr) | + | public: |
- | { | + | |
- | this->numeProprietar = new char[strlen(numeProprietar) + 1]; | + | |
- | strcpy(this->numeProprietar, numeProprietar); | + | |
- | } | + | |
- | else | + | |
- | { | + | |
- | this->numeProprietar = nullptr; | + | |
- | } | + | |
- | } | + | |
- | </code> | + | |
- | Se poate observa din implementarea anterioară că am deschis lista de inițializare pentru acest constructor unde am chemat constructorul cu parametri al clasei părinte (clasa **Locuinta**). | + | NrComplex(const double& real = 0.0, const double& imaginar = 0.0); |
- | <note warning>Constructorul cu parametri al **clasei derivate** include în lista sa de argumente și parametrii necesari pentru a apela **constructorul corespunzător din clasa părinte**. Acești parametri sunt transmiși în lista de inițializare a constructorului **clasei copil** atunci când este apelat constructorul din **superclasă**, facilitând astfel **inițializarea corectă** a **membrilor moșteniți** din **clasa părinte**. Acest mecanism permite transmiterea valorilor necesare direct către **clasa părinte**, asigurând o **organizare clară** și o **reutilizare eficientă** a codului.</note> | + | double getReal() const; |
+ | double getImaginar() const; | ||
- | În manieră similară se implementează și constructorul de copiere al clasei derivate. | + | void setReal(const double& real); |
+ | void setImaginar(const double& imaginar); | ||
+ | |||
+ | // supraincarcarea operatorilor ca functii membre | ||
+ | |||
+ | NrComplex operator!() const; | ||
+ | |||
+ | NrComplex& operator+=(const NrComplex& z); | ||
+ | |||
+ | NrComplex& operator++(); | ||
+ | NrComplex operator++(int); | ||
+ | |||
+ | bool operator==(const NrComplex& z) const; | ||
+ | bool operator!=(const NrComplex& z) const; | ||
+ | |||
+ | // operatori supraincarcati ca functii friend | ||
+ | |||
+ | friend NrComplex operator+(const NrComplex& z1, const NrComplex& z2); // operator pentru adunarea a doua numere complexe | ||
+ | friend NrComplex operator+(const NrComplex& z1, const double& numar); // operator pentru adunarea unui numar complex cu un numar real | ||
+ | friend NrComplex operator+(const double& numar, const NrComplex& z1); // operator pentru adunarea unui numar real cu un numar complex | ||
+ | }; | ||
+ | </code> | ||
+ | |||
+ | Iar implementările pentru cei trei operatori le putem observa în codul ce urmează. | ||
<code cpp> | <code cpp> | ||
- | Apartament::Apartament(const Apartament& apartament) : Locuinta(apartament) | + | NrComplex operator+(const NrComplex& z1, const NrComplex& z2) |
{ | { | ||
- | numarCamere = apartament.numarCamere; | + | NrComplex z; |
- | if (apartament.numeProprietar != nullptr) | + | z.real = z1.real + z2.real; |
- | { | + | z.imaginar = z1.imaginar + z2.imaginar; |
- | numeProprietar = new char[strlen(apartament.numeProprietar) + 1]; | + | |
- | strcpy(numeProprietar, apartament.numeProprietar); | + | return z; |
- | } | + | |
- | else | + | |
- | { | + | |
- | numeProprietar = nullptr; | + | |
- | } | + | |
} | } | ||
- | </code> | ||
- | Astfel, am evidențiat **reutilizarea codului** prin faptul că, în momentul în care construim un obiect de tipul **clasei derivate**, nu este necesar să redefinim sau să duplicăm funcționalitățile și datele din **clasa părinte**. Prin simplul apel al constructorului **clasei părinte** în lista de inițializare a constructorului **clasei derivate**, putem păstra claritatea și concizia codului sursă. Aceasta reprezintă un avantaj major al **moștenirii** în **POO**, permițând extinderea funcționalității fără a compromite principiile de reutilizare și organizare. | + | NrComplex operator+(const NrComplex& z1, const double& numar) |
+ | { | ||
+ | NrComplex z; | ||
- | == Implementarea destructorului în clasa derivată == | + | z.real = z1.real + numar; |
+ | z.imaginar = z1.imaginar; | ||
- | Destructorul clasei derivate se implementează la fel ca un destructor obișnuit, adică vom elibera memoria alocată dinamic pentru membrii care sunt de tip pointer. | + | return z; |
+ | } | ||
- | <code cpp> | + | NrComplex operator+(const double& numar, const NrComplex& z1) |
- | Apartament::~Apartament() | + | |
{ | { | ||
- | if (numeProprietar != nullptr) | + | NrComplex z; |
- | { | + | |
- | delete[] numeProprietar; | + | z.real = numar + z1.real; |
- | } | + | z.imaginar = z1.imaginar; |
+ | |||
+ | return z; | ||
} | } | ||
</code> | </code> | ||
- | <note warning>În destructorul clasei derivate **nu** apelăm destructorul clasei părinte. Acest lucru va fi realizat **automat** de către **compilator** în mod corect fără a fi nevoie de intervenția noastră.</note> | + | <note tip>În manieră similară se poate proceda și pentru ceilalți **operatori aritmetici (-, *, /, %)**. Din nou, **trebuie** să ne punem întrebarea **dacă** operațiile respective au **sens logic** pentru clasa unde supraîncărcăm operatorii.</note> |
- | == Implementarea operatorului de asignare în clasa derivată == | + | == Supraîncărcarea operatorilor de flux >> și << == |
- | La fel ca în cazul constructorilor va trebui să găsim o modalitate prin care **mai întâi** ne ocupăm de atributele **clasei părinte** și pe urmă prelucram datele **clasei copil**. Operatorul de asignare **nefiind** un constructor **nu** are listă de inițializare și va trebui să îl apelăm explicit pe cel din **clasa părinte** pentru a respecta ordinea pașilor exact la fel ca în cazul constructorilor. | + | **Operatorul %%>>%%** este folosit în C++ pentru citirea datelor fie de la tastatură fie dintr-un fișier, în timp ce **operatorul %%<<%%** este folosit pentru afișarea datelor fie în consolă fie într-un fișier. În general cei doi operatori sunt supraîncărcați ca **funcții friend**, dar mai pot fi întâlniți și ca **funcții globale obișnuite (adică nu sunt marcate ca funcții friend)** unde operațiile de citire și afișare se realizează cu ajutorul **metodelor accesor** de tip **get** și **set**. |
+ | |||
+ | Să urmărim cu atenție modul în care declarăm acesți operatori ca **funcții friend** în clasa **NrComplex**. | ||
<code cpp> | <code cpp> | ||
- | Apartament& Apartament::operator=(const Apartament& apartament) | + | #include <iostream> |
+ | using namespace std; | ||
+ | |||
+ | class NrComplex | ||
{ | { | ||
- | if (this == &apartament) | + | double real; |
- | { | + | double imaginar; |
- | return *this; | + | |
- | } | + | |
- | this->Locuinta::operator=(apartament); // se apeleaza operatorul de asignare din clasa parinte | + | public: |
- | /*(Locuinta&)(*this) = apartament; // este echivalent cu linia de mai sus doar ca este o alta forma de apel*/ | + | |
- | numarCamere = apartament.numarCamere; | + | NrComplex(const double& real = 0.0, const double& imaginar = 0.0); |
- | if (apartament.numeProprietar != nullptr) | + | double getReal() const; |
- | { | + | double getImaginar() const; |
- | numeProprietar = new char[strlen(apartament.numeProprietar) + 1]; | + | |
- | strcpy(numeProprietar, apartament.numeProprietar); | + | |
- | } | + | |
- | else | + | |
- | { | + | |
- | numeProprietar = nullptr; | + | |
- | } | + | |
- | return *this; | + | void setReal(const double& real); |
- | } | + | void setImaginar(const double& imaginar); |
+ | |||
+ | // supraincarcarea operatorilor ca functii membre | ||
+ | |||
+ | NrComplex operator!() const; | ||
+ | |||
+ | NrComplex& operator+=(const NrComplex& z); | ||
+ | |||
+ | NrComplex& operator++(); | ||
+ | NrComplex operator++(int); | ||
+ | |||
+ | bool operator==(const NrComplex& z) const; | ||
+ | bool operator!=(const NrComplex& z) const; | ||
+ | |||
+ | // operatori supraincarcati ca functii friend | ||
+ | |||
+ | friend NrComplex operator+(const NrComplex& z1, const NrComplex& z2); | ||
+ | friend NrComplex operator+(const NrComplex& z1, const double& numar); | ||
+ | friend NrComplex operator+(const double& numar, const NrComplex& z1); | ||
+ | |||
+ | friend istream& operator>>(istream& in, NrComplex& z); // operator pentru citirea unui numar complex | ||
+ | friend ostream& operator<<(ostream& out, const NrComplex& z); // operator pentru afisarea unui numar complex | ||
+ | }; | ||
</code> | </code> | ||
- | == Implementarea operatorului << în clasa derivată == | + | <note>Se poate observa că cele două **funcții friend** primesc câte un parametru special de tip **stream**. Tipurile de date **istream** și **ostream** anunță compilatorul că vrem să lucrăm cu fluxuri de date. **Istream** vine de la **input stream (flux de intrare)** în timp ce **ostream** vine de la **output stream (flux de ieșire)**. |
+ | </note> | ||
- | Vom prezenta în continuare modul de implementare a operatorului de afișare pentru obiectele de tip **Apartament** respectând în continuare relația de **"is-a"**. | + | Să urmărim implementarea celor doi operatori de flux în exemplul de cod de mai jos. |
<code cpp> | <code cpp> | ||
- | std::ostream& operator<<(std::ostream& out, const Apartament& apartament) | + | istream& operator>>(istream& in, NrComplex& z) |
{ | { | ||
- | operator<<(out, (Locuinta&)apartament); // chemam operatorul << din clasa parinte | + | std::cout << "Introduceti partea reala a numarului complex: "; |
+ | in >> z.real; | ||
- | out << "Numarul de camere din apartament este: " << apartament.numarCamere << " ron\n"; | + | std::cout << "Introduceti partea imaginara a numarului complex: "; |
+ | in >> z.imaginar; | ||
- | if (apartament.numeProprietar != nullptr) | + | return in; |
- | { | + | } |
- | out << "Numele proprietarului este: " << apartament.numeProprietar << "\n"; | + | |
- | } | + | ostream& operator<<(ostream& out, const NrComplex& z) |
- | else | + | { |
- | { | + | out << "Partea reala a numarului complex este: " << z.real << '\n'; |
- | out << "Numele proprietarului este: N/A\n"; | + | out << "Partea imaginara a numarului complex este: " << z.imaginar << "\n\n"; |
- | } | + | |
return out; | return out; | ||
} | } | ||
</code> | </code> | ||
- | |||
- | <note warning>Funcțiile **friend** dintr-o clasă **nu** se moștenesc automat de către **clasa derivată**, motiv pentru care trebuie să apelăm explicit **operatorul %%<<%%** definit în **clasa de bază**. Pentru a înțelege mai bine acest comportament, putem face următoarea analogie: prietenii părinților voștri **nu sunt neapărat** și prietenii voștri. Relația de prietenie este specifică **doar** între părinții voștri și acele persoane, iar aceasta **nu se extinde automat** asupra voastră. La fel, funcțiile **friend** sunt prietene ale **clasei părinte**, dar **nu** devin prietene implicit și pentru **clasa derivată**.</note> | ||
==== ==== | ==== ==== | ||
- | Acum că am înțeles conceptul de **moștenire** între două clase, vom putea avansa către implementarea unor ierarhii mai complexe începând cu următorul laborator. Moștenirea ne permite să construim structuri **ierarhice**, în care clasele pot extinde și reutiliza funcționalități din **clasele părinte**. Astfel, vom fi capabili să dezvoltăm sisteme mai robuste, eficiente și ușor de întreținut, în care fiecare clasă va adăuga comportamente și atribute specifice, păstrând în același timp funcționalitatea de bază moștenită. Aceste ierarhii de clase vor facilita gestionarea mai bună a codului și îmbunătățirea scalabilității aplicațiilor noastre. | + | Codul complet cu implementările operatorilor prezentați pentru clasa **NrComplex** poate fi descărcat de {{:poo-is-ab:laboratoare:complex_overloading.zip|aici}}. |
- | ==== Concluzii ==== | + | <note warning>În limbajul C++ **nu** este permisă supraîncărcarea următorilor operatori: |
+ | * de rezoluție **"::"** | ||
+ | * de acces la membrii unei clase/structuri **"."** | ||
+ | * condițional/ternar **"?:"** | ||
+ | * **sizeof** (returnează dimensiunea în bytes a unui tip de date) | ||
+ | * **typeid** (folosit pentru a obține informații despre tipul dinamic al unui obiect la run time) | ||
+ | * **alignof** (similar cu operatorul **sizeof**) | ||
+ | </note> | ||
- | În cadrul acestui laborator, am învățat și aprofundat conceptul de **moștenire** și am văzut cum poate fi implementată între **două clase**. Am înțeles că **moștenirea** este o metodă esențială pentru a **reutiliza** și **extinde** codul existent, oferind un cadru flexibil și scalabil pentru dezvoltarea aplicațiilor **OOP**. Prin utilizarea moștenirii, o **clasă derivată** poate prelua proprietățile unei **clase părinte**, oferind astfel posibilitatea de a adăuga sau modifica funcționalități specifice. | + | ==== Concluzii ==== |
- | + | ||
- | Un aspect important pe care l-am discutat este faptul că **funcțiile friend nu se moștenesc**. Aceste funcții, deși pot accesa membri privați sau protejați ai unei clase, **nu** sunt automat apelate în **clasa derivată**. Pentru a înțelege acest comportament, am făcut o analogie simplă: prietenii părinților voștri nu sunt în mod automat și prietenii voștri direcți. Astfel, în cazul în care dorim să accesăm funcționalitățile unei funcții friend dintr-o **clasă părinte**, va trebui **să o apelăm explicit** în **clasa derivată**. | + | |
- | + | ||
- | De asemenea, am explorat rolul specificatorului de acces **protected**, care permite membrilor clasei să fie accesibili în cadrul **claselor derivate**, dar să rămână inaccesibili din exterior. Această abordare oferă un echilibru între **încapsulare** și **moștenire**, protejând datele interne ale **clasei părinte**, dar permițând totuși **clasei copil** să le utilizeze. | + | |
- | + | ||
- | Un alt concept esențial a fost utilizarea **listei de inițializare a constructorului** în clasele derivate. În momentul în care instanțiem un obiect din clasa derivată, trebuie să avem grijă să inițializăm corect și **membrii clasei părinte**. Aceasta se realizează prin apelarea **explicită** a **constructorului părinte** în **lista de inițializare a constructorului clasei derivate**. Am subliniat importanța acestui mecanism, deoarece **doar** constructorii pot fi apelați în această manieră. | + | |
- | În plus, pentru a accesa metode sau funcții din **clasa părinte** care nu sunt constructori, trebuie să apelăm **explicit** funcția dorită folosind sintaxa: **''numeClasăPărinte::numeMetodă()''**. Acest apel este necesar pentru a ne asigura că executăm **corect** comportamentul definit în **clasa părinte** asupra obiectelor din **clasa fiu**. | + | În cadrul acestui laborator, am descoperit **importanța supraîncărcării operatorilor** într-o clasă și modul în care acest proces ne permite să efectuăm diverse operații într-un mod **intuitiv**, la fel cum procedăm și cu **tipurile de date standard** (**int**, **float**, **char**,...). Prin **supraîncărcarea operatorilor**, am reușit să **îmbunătățim lizibilitatea și ușurința** în utilizarea claselor personalizate, oferind posibilitatea de a efectua **operații** cum ar fi **adunarea**, **scăderea** sau **compararea** obiectelor de tipul definit de noi. |
- | Prin toate aceste concepte și tehnici, am făcut un pas important în **utilizarea eficientă** a **moștenirii** în limbajul C++, și suntem pregătiți să explorăm ierarhii mai complexe de clase în laboratoarele viitoare. | + | Am înțeles, de asemenea, când este necesar să **supraîncărcăm** un operator ca **funcție membră** a unei clase și când este mai potrivit să îl supraîncărcăm ca **funcție friend**. Operatorii care au nevoie de **acces direct** la membrii clasei, cum ar fi **operatorii unari** sau **operatorul de asignare**, sunt adesea implementați ca **funcții membre**. În schimb, operatorii care implică obiecte de **diferite tipuri** (de exemplu, un obiect al clasei noastre și un tip fundamental precum **int** sau **double**) pot fi implementați mai eficient ca **funcții friend**, pentru a permite accesul din exterior la membri privați **fără** a compromite **încapsularea datelor clasei**. |