This shows you the differences between two versions of the page.
poo-is-ab:laboratoare:05 [2024/10/13 17:03] razvan.cristea0106 [Moștenirea între două clase] |
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. |
+ | 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. | ||
+ | ==== Operatorii limbajului C++ ==== | ||
- | ==== Moștenirea între două clase ==== | + | 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. |
- | 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. | + | **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ă)**. |
- | Î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)**. | + | **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 (==, !=, <, >)**. |
- | <code cpp> | + | **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. |
- | #pragma once | + | |
- | #include <cstring> | + | |
- | #include <iostream> | + | |
- | class Locuinta | + | Î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. |
- | { | + | |
- | float pret; | + | |
- | char* adresa; | + | |
- | public: | + | ^ Categoria de operatori ^ Simbolurile operatorilor ^ Mod de asociere (aplicare) ^ |
+ | | **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 | | ||
- | 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> |
- | Locuinta(const float& pret, const char* adresa); | + | |
- | Locuinta(const Locuinta& locuinta); | + | |
- | Locuinta& operator=(const Locuinta& locuinta); | + | |
- | ~Locuinta(); | + | |
- | float getPret() const; | + | ==== Funcții friend ==== |
- | char* getAdresa() const; | + | |
- | void setPret(const float& pret); | + | Î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. |
- | void setAdresa(const char* adresa); | + | |
- | friend std::ostream& operator<<(std::ostream& out, const Locuinta& locuinta); | + | <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> |
- | }; | + | |
- | </code> | + | |
- | Iar implementările pentru funcțiile membre și cea friend le putem observa în codul de mai jos. | + | 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. |
+ | |||
+ | 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; |
- | } | + | |
+ | public: | ||
+ | |||
+ | Persoana(const int& varsta, const char* nume); | ||
+ | ~Persoana(); | ||
+ | |||
+ | friend void afisarePersoana(const Persoana& persoana); // functie friend pentru afisarea datelor unei persoane | ||
+ | }; | ||
- | Locuinta::Locuinta(const float& pret, const char* adresa) | + | 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\n"; | + | |
- | } | + | |
- | else | + | |
- | { | + | |
- | out << "Adresa locuintei este: inexistenta\n\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**. |
+ | |||
+ | == Supraîncărcarea operatorului ! == | ||
+ | |||
+ | 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> | <code cpp> | ||
- | #ifndef LOCUINTA_H | ||
- | #define LOCUINTA_H | ||
- | |||
- | #include <cstring> | ||
#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 |
+ | |||
+ | NrComplex operator!() const; | ||
+ | |||
+ | NrComplex& operator+=(const NrComplex& z); // operatorul compus pentru adunarea a doua numere complexe | ||
+ | |||
+ | NrComplex& operator++(); | ||
+ | NrComplex operator++(int); | ||
+ | |||
+ | bool operator==(const NrComplex& z) const; | ||
+ | bool operator!=(const NrComplex& z) const; | ||
}; | }; | ||
</code> | </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**. | + | Iar implementarea acestui operator o vom regăsi în secvența de cod următoare. |
- | === Implementarea metodelor și a funcțiilor friend în clasa derivată === | + | <code cpp> |
+ | NrComplex& NrComplex::operator+=(const NrComplex& z) | ||
+ | { | ||
+ | this->real += z.real; | ||
+ | this->imaginar += z.imaginar; | ||
- | Î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**. | + | return *this; |
+ | } | ||
+ | </code> | ||
- | == Implementarea constructorilor clasei derivate == | + | <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> |
- | 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**. | + | ==== ==== |
- | Să urmărim cu atenție mai jos implemetarea constructorului fără parametri pentru clasa **Apartament**. | + | <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() : Locuinta() | + | #include <iostream> |
+ | |||
+ | class NrComplex | ||
{ | { | ||
- | numarCamere = 0; | + | double real; |
- | numeProprietar = nullptr; | + | double imaginar; |
- | } | + | |
- | </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> | + | public: |
+ | |||
+ | NrComplex(const double& real = 0.0, const double& imaginar = 0.0); | ||
+ | |||
+ | double getReal() const; | ||
+ | double getImaginar() const; | ||
+ | |||
+ | 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> | ||
- | În continuare vom implementa constructorul cu parametri pentru clasa copil urmând același principiu ca la constructorul fără parametri. | + | Iar implementările pentru cei trei operatori le putem observa în codul ce urmează. |
<code cpp> | <code cpp> | ||
- | Apartament::Apartament(const float& pret, const char* adresa, const int& numarCamere, const char* numeProprietar) : Locuinta(pret, adresa) | + | NrComplex operator+(const NrComplex& z1, const NrComplex& z2) |
{ | { | ||
- | this->numarCamere = numarCamere; | + | NrComplex z; |
- | if (numeProprietar != nullptr) | + | z.real = z1.real + z2.real; |
- | { | + | z.imaginar = z1.imaginar + z2.imaginar; |
- | this->numeProprietar = new char[strlen(numeProprietar) + 1]; | + | |
- | strcpy(this->numeProprietar, numeProprietar); | + | return z; |
- | } | + | |
- | 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 operator+(const NrComplex& z1, const double& numar) |
+ | { | ||
+ | NrComplex z; | ||
- | <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> | + | z.real = z1.real + numar; |
+ | z.imaginar = z1.imaginar; | ||
- | În manieră simlară se implementează și constructorul de copiere al clasei derivate. | + | return z; |
+ | } | ||
- | <code cpp> | + | NrComplex operator+(const double& numar, const NrComplex& z1) |
- | Apartament::Apartament(const Apartament& apartament) : Locuinta(apartament) | + | |
{ | { | ||
- | numarCamere = apartament.numarCamere; | + | NrComplex z; |
- | if (apartament.numeProprietar != nullptr) | + | z.real = numar + z1.real; |
- | { | + | z.imaginar = z1.imaginar; |
- | numeProprietar = new char[strlen(apartament.numeProprietar) + 1]; | + | |
- | strcpy(numeProprietar, apartament.numeProprietar); | + | return z; |
- | } | + | |
- | else | + | |
- | { | + | |
- | numeProprietar = nullptr; | + | |
- | } | + | |
} | } | ||
</code> | </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. | + | <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 destructorului în clasa derivată == | + | == Supraîncărcarea operatorilor de flux >> și << == |
- | 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. | + | **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() | + | #include <iostream> |
+ | using namespace std; | ||
+ | |||
+ | class NrComplex | ||
{ | { | ||
- | if (numeProprietar != nullptr) | + | double real; |
- | { | + | double imaginar; |
- | delete[] numeProprietar; | + | |
- | } | + | |
- | } | + | |
- | </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> | + | public: |
- | == Implementarea operatorului de asignare în clasa derivată == | + | NrComplex(const double& real = 0.0, const double& imaginar = 0.0); |
+ | |||
+ | double getReal() const; | ||
+ | double getImaginar() const; | ||
+ | |||
+ | 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> | ||
+ | |||
+ | <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> | ||
- | 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 ca în cazul constructorilor. | + | Să urmărim implementarea celor doi operatori de flux în exemplul de cod de mai jos. |
<code cpp> | <code cpp> | ||
- | Apartament& Apartament::operator=(const Apartament& apartament) | + | istream& operator>>(istream& in, NrComplex& z) |
{ | { | ||
- | if (this == &apartament) | + | std::cout << "Introduceti partea reala a numarului complex: "; |
- | { | + | in >> z.real; |
- | return *this; | + | |
- | } | + | |
- | this->Locuinta::operator=(apartament); // se apeleaza operatorul de asignare din clasa parinte | + | std::cout << "Introduceti partea imaginara a numarului complex: "; |
- | /*(Locuinta&)(*this) = apartament; // este echivalent cu linia de mai sus doar ca este o alta forma de apel*/ | + | in >> z.imaginar; |
- | numarCamere = apartament.numarCamere; | + | return in; |
+ | } | ||
- | if (apartament.numeProprietar != nullptr) | + | ostream& operator<<(ostream& out, const NrComplex& z) |
- | { | + | { |
- | numeProprietar = new char[strlen(apartament.numeProprietar) + 1]; | + | out << "Partea reala a numarului complex este: " << z.real << '\n'; |
- | strcpy(numeProprietar, apartament.numeProprietar); | + | out << "Partea imaginara a numarului complex este: " << z.imaginar << "\n\n"; |
- | } | + | |
- | else | + | |
- | { | + | |
- | numeProprietar = nullptr; | + | |
- | } | + | |
- | return *this; | + | return out; |
} | } | ||
</code> | </code> | ||
- | == Implementarea operatorului << în clasa derivată == | + | ==== ==== |
+ | |||
+ | Codul complet cu implementările operatorilor prezentați pentru clasa **NrComplex** poate fi descărcat de {{:poo-is-ab:laboratoare:complex_overloading.zip|aici}}. | ||
+ | |||
+ | <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> | ||
+ | ==== Concluzii ==== | ||
+ | Î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. | ||
+ | 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**. |