Autor: Răzvan Cristea
Studentul va fi capabil la finalul acestui laborator să:
Î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 laboratorul 2, 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.
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.
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.
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ă).
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 (==, !=, <, >).
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.
Î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.
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 |
Î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.
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ă.
#include <iostream> #include <cstring> class Persoana { int varsta; char* nume; 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->varsta = varsta; if (nume != nullptr) { this->nume = new char[strlen(nume) + 1]; strcpy(this->nume, nume); } else { this->nume = nullptr; } } Persoana::~Persoana() { if (nume != nullptr) { delete[] nume; } } void afisarePersoana(const Persoana& persoana) { std::cout << "Numele persoanei este: " << persoana.nume << '\n'; std::cout << "Varsta persoanei este: " << persoana.varsta << "\n\n"; } int main() { Persoana persoana(22, "Andrei"); afisarePersoana(persoana); return 0; }
Î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++.
Vom folosi foarte mult acest tip de funcții după cum vom vedea în cele ce urmează la supraîncărcarea operatorilor limbajului C++.
Î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.
#include <iostream> class NrComplex { double real; double imaginar; 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); };
Iar implementările funcțiilor membre sunt vizibile în codul de mai jos.
#include "Complex.h" NrComplex::NrComplex(const double& real, const double& imaginar) { this->real = real; this->imaginar = imaginar; } double NrComplex::getReal() const { return real; } double NrComplex::getImaginar() const { return imaginar; } void NrComplex::setReal(const double& real) { this->real = real; } void NrComplex::setImaginar(const double& imaginar) { this->imaginar = imaginar; }
Î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.
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.
#include <iostream> class NrComplex { double real; double imaginar; 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++(); // forma prefixata NrComplex operator++(int); // forma postfixata };
Iar implementările corespunzătoare pentru cele două forme ale operatorului de incrementare le putem vedea mai jos.
NrComplex& NrComplex::operator++() { this->real++; this->imaginar++; return *this; } NrComplex NrComplex::operator++(int) { NrComplex copie = *this; this->real++; this->imaginar++; return copie; }
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.
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.
#include <iostream> class NrComplex { double real; double imaginar; 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; // operatorul de negare logica (in cazul acestei clase va conjuga un numar complex) NrComplex& operator++(); NrComplex operator++(int); };
Iar implementarea acestui operator se poate observa mai jos.
NrComplex NrComplex::operator!() const { NrComplex conjugat = *this; conjugat.imaginar = -conjugat.imaginar; return conjugat; }
Operatorul == este folosit pentru a testa egaliatetea 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.
#include <iostream> class NrComplex { double real; double imaginar; 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 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 };
Iar implementările pentru cei doi operatori le putem observa în codul de mai jos.
bool NrComplex::operator==(const NrComplex& z) const { return this->real == z.real && this->imaginar == z.imaginar; } bool NrComplex::operator!=(const NrComplex& z) const { return this->real != z.real || this->imaginar != z.imaginar; }
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.
#include <iostream> class NrComplex { double real; double imaginar; 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); // 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; };
Iar implementarea acestui operator o vom regăsi în secvența de cod următoare.
NrComplex& NrComplex::operator+=(const NrComplex& z) { this->real += z.real; this->imaginar += z.imaginar; return *this; }
Î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.
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.
#include <iostream> class NrComplex { double real; double imaginar; 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 };
Iar implementările pentru cei trei operatori le putem observa în codul ce urmează.
NrComplex operator+(const NrComplex& z1, const NrComplex& z2) { NrComplex z; z.real = z1.real + z2.real; z.imaginar = z1.imaginar + z2.imaginar; return z; } NrComplex operator+(const NrComplex& z1, const double& numar) { NrComplex z; z.real = z1.real + numar; z.imaginar = z1.imaginar; return z; } NrComplex operator+(const double& numar, const NrComplex& z1) { NrComplex z; z.real = numar + z1.real; z.imaginar = z1.imaginar; return z; }
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.
#include <iostream> using namespace std; class NrComplex { double real; double imaginar; 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); 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 };
Să urmărim implementarea celor doi operatori de flux în exemplul de cod de mai jos.
istream& operator>>(istream& in, NrComplex& z) { cout << "Introduceti partea reala a numarului complex: "; in >> z.real; cout << "Introduceti partea imaginara a numarului complex: "; in >> z.imaginar; return in; } ostream& operator<<(ostream& out, const NrComplex& z) { out << "Partea reala a numarului complex este: " << z.real << '\n'; out << "Partea imaginara a numarului complex este: " << z.imaginar << "\n\n"; return out; }
Codul complet cu implementările operatorilor prezentați pentru clasa NrComplex poate fi descărcat de aici.
Î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.