Laborator 04 - Supraîncărcarea operatorilor

Autor: Răzvan Cristea

Obiective Specifice

Studentul va fi capabil la finalul acestui laborator să:

  • recunoască și să înțeleagă conceptul de supraîncărcare (overloading)
  • știe când un operator trebuie supraîncărcat fie ca funcție membră fie ca funcție friend
  • supraîncarce operatorii aritmetici pentru o clasă
  • supraîncarce operatorii logici de comparație
  • supraîncarce operatorii de flux pentru citire și afișare

Introducere

Î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.

Operatorii limbajului C++

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

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.

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.

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.

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++.

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 înștinț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++.

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.

#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;
}

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ă.

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.

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.

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.

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.

#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;
}

Se poate observa ca la forma postfixată avem un parametru de care nu ne folosim. Acel parametru este doar pentru a asigura polimorfismul, compilatorul facând distincția între cele două variante de operator de incrementare.

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.

#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 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.

Supraîncărcarea operatorilor == și !=

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;
}
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.

#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 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.

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.

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.

#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;
}

Î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.

Supraîncărcarea operatorilor de flux >> și <<

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
};

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).

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 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)

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.

poo-is-ab/laboratoare/04.txt · Last modified: 2024/11/11 10:36 by razvan.cristea0106
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0