In cadrul acestui laborator, vom aprofunda concepte de baza ale programarii obiectuale, cu precadere pe Functii Friend si Supradefinirea Operatorilor. Aceste concepte, odata intelese pe deplin, vor reprezenta un pas important catre stapanirea artei Programarii Orientate pe Obiecte!
Ca referinte externe, recomandam urmatorul capitol din Absolute C++:
Sintaxa prin care specificam ca o functie este friend, arata in felul urmator:
class NumeClasa { //cod friend TIP_RETURNAT NUME_FUNCTIE(ARGUMENTE); //cod };
Conform expresiei: Un exemplu de cod face cat 1000 de definitii, haideti sa analizam urmatoarea secventa.
class Distanta { private: int metri; public: //Constructor fara parametri (default) Distanta(); // Functie Friend // (ATENTIE: AICI DOAR SPECIFICAM CE FUNCTIE AVEM CA FRIEND - adunaCinciMetri) friend int adunaCinciMetri(Distanta); };
Functia adunaCinciMetri() primeste o distanta (un obiect de tip Distanta), la care aduna inca 5 metri, returnand noua valoare (int).
// Distanta - specifica clasa (scopul/contextul apelului) // Distanta() - constructor fara parametri Distanta :: Distanta() { metri = 0; } // DECLARAREA FUNCTIEI FRIEND int adunaCinciMetri(Distanta d) { //accesam atributele private ale clasei, din functie friend d.metri += 5; return d.metri; }
int main() { Distanta D; // metri = 0 cout << "Distanta: " << adunaCinciMetri(D) << endl; return 0; }
Pentru a rula codul intreg, apasa aici.
#include <iostream> using namespace std; class Distanta { private: int metri; public: //Constructor fara parametri (default) Distanta(); // Functie Friend // (ATENTIE: AICI DOAR SPECIFICAM CE FUNCTIE AVEM CA FRIEND - adunaCinciMetri) friend int adunaCinciMetri(Distanta); }; // Distanta - specifica clasa (scopul/contextul apelului) // Distanta() - constructor fara parametri Distanta :: Distanta() { metri = 0; } // DECLARAREA FUNCTIEI FRIEND int adunaCinciMetri(Distanta d) { //accesam atributele private ale clasei, din functie friend d.metri += 5; return d.metri; } int main() { Distanta D; cout << "Distanta: " << adunaCinciMetri(D) << endl; return 0; }
Miza finala este sa putem manipula obiecte definite de noi, in aceeasi maniera in care lucram cu variabile ce sunt tipuri de baza:
// Instantiem obiecte ClasaDefinitaDeNoi obj1(/*argumente constructor*/), obj2(/*argumente constructor*/); ClasaDefinitaDeNoi obj3 = obj1 + obj2; // Afisam obiecte cout << obj3 << endl; dimensiune = 3; // Alocam memorie pentru un vector ce contine elemente de tip ClasaDefinitaDeNoi ClasaDefinitaDeNoi *vectObj = new ClasaDefinitaDeNoi [dimensiune]; //etc
class NumeleClasei { ... .. ... public: TIP_RETURNAT operator symbol (argumente); ... .. ... ... .. ... }; TIP_RETURNAT NumeleClasei::operator symbol(argumente) { ... }
O clasa se poate defini împreuna cu un set de operatori asociati, obtinuti prin supraîncarcarea operatorilor existenti. In acest fel, se efectueaza operatii specifice noului tip, la fel de simplu ca în cazul tipurilor standard, printr-o semantica naturala (ex: adunarea a doua numere complexe).
Procedeul consta în definirea unei functii cu numele: operator <symbol>
Tipurile de operatori sunt:
In exemplul de mai jos, vom vedeam ambele tipuri de supradefinire pentru operatorul ++. Considerentele si rationamentele supradefinirii sunt abordate in sectiunile urmatoare.
// Supradefinirea ++ ca 'prefix' si 'postfix' class NumaramBani { private: int lei; public: // Constructor care initializeaza lei = 5 Count() : lei(5) {} // Supradefinim ++ ca prefix; adica, ++obiect; void operator ++ () { ++lei; } // Supradefinim ++ ca postfix; adica, obiect++ void operator ++ (int) { lei++; } void afisare() { cout << "lei: " << lei << endl; } }; int main() { NumaramBani numarator; // Apelam functia "void operator ++ ()" ++numarator; numarator.afisare(); return 0; }
In exemplul de mai sus, remarcam conventia postfix 'void operator ++(int)' pentru a distinge dintre cele 2 tipuri de incrementare unara. Conventia este subinteleasa de limbajul C++, permitand compilatorului sa aplice operatia potrivita aferenta incrementarii.
In cazul Operatorilor Binari, avem 2 abordari distincte, care indeplinesc acelasi scop general, anume:
Cu toate acestea, exista diferente de interpretare din partea compilatorului dintre cele 2 abordari.
Pentru a exemplifica ideile principale, vom lucra cu urmatoarea clasa ce implementeaza functionalitatile unui numar complex.
class complex { private: double real; double imaginar; public: // Aici definim Constructori si Operatori };
In cazul functiilor membre apar urmatoarele constrangeri:
Definim in felul urmator:
complex& operator +(const complex& c); complex& operator -(const complex& c); complex& operator *(const complex& c); complex& operator /(const complex& c);
Exemplu de implementare:
complex& complex::operator +(const complex& c) { this->real += c.real; this->imaginar += c.imaginar; return *this; }
Definim in felul urmator:
bool operator >(const complex& c); bool operator <(const complex& c); bool operator ==(const complex& c); bool operator !=(const complex& c);
Exemplu de implementare:
bool complex::operator ==(const complex& c) { if(real == c.real && imaginar == c.imaginar) return true; return false; }
In cazul functiilor friend, eliminam din constrangerile ce apar la functii membre:
Definim in felul urmator:
friend const complex operator +(const complex& c1, const complex & c2); friend const complex operator -(const complex& c1, const complex & c2); friend const complex operator *(const complex& c1, const complex & c2); friend const complex operator /(const complex& c1, const complex& c2); // Operator citire friend istream& operator >>(istream& citire, complex& c); // Operator afisare friend ostream& operator <<(ostream& afisare, const complex& c);
Exemplu de implementare:
const complex operator +(const complex& c1, const complex& c2) { int Real; int Imaginar; Real = c1.real + c2.real; Imaginar = c1.imaginar + c2.imaginar; return complex(Real, Imaginar); } istream& operator >>(istream& citire, complex& c) { citire >> c.real >> c.imaginar; return citire; } ostream& operator <<(ostream& afisare, const complex& c) { if(c.imaginar > 0) { afisare << c.real << " " << "+" << c.imaginar <<"i"<< endl; } else { afisare << c.real << " " << c.imaginar << "i" << endl; } return afisare; }
Definim in felul urmator:
friend bool operator == (const complex& a, const complex& b)
Exemplu de implementare:
bool operator == (const complex& a, const complex& b) { if (a.real == b.real && a.imaginar == b.imaginar) { return true; } return false; }
1. Se pot supradefini numai operatori existenti, deci simbolul asociat functiei operator trebuie sa fie deja definit ca operator pentru tipurile standard. Nu e permisa introducerea unor simboluri noi de operatori (ex |x|).
2. Nu se pot modifica:
3. Operatorul definit ca functie friend trebuie sa aiba cel putin un parametru de tipul clasa caruia ii este asociat operatorul respectiv. Aceasta restrictie implica faptul ca supradefinirea operatorilor e posibila numai pentru tipurile clasa definite in program, pentru tipurile standard operatorii isi pastreaza definitia.
//Operator + pentru tipul complex, cu al doilea operand de tip double //Varianta functie membra const complex complex:: operator + (const double& a) const { return complex (re+a , im); } //Varianta functie friend friend const complex operator + (const complex& c1, const double& c2) { return complex (c1.re+c2, c1.im); }
4. Pentru a putea fi transformata in functie membra, un operator definit ca functie friend trebuie sa aiba ca prim parametru un obiect de tipul clasei.
// Operatorul +, ce are parametrii double si complex friend const complex operator + (const double& c1, const complex& c2) { return complex (c1+c2.re, c2.im) }
5.Functiile operator pot fi implementate ca si functii membre ale clasei sau ca si functii prietene ale clasei, atunci cand este necesar.
//Operator + pentru tipul complex //Varianta ca functie membra const complex complex::operator + (const complex& a ) const { return complex (re + a.re, im + a.im) } //Varianta ca functie friend (preferata, deoarece mimeaza cel mai bine +) friend const complex operator + (const complex & a const complex & b) { return complex (a.re + b. re, a.im + b.im) }
Deoarece exemplul este relativ amplu, ar ocupa o mare parte a laboratorului. Pentru a rula codul si a-l accesa in totalitatea sa, click aici.
Daca o clasa defineste vreuna din urmatoarele, cel mai probabil e necesara definirea tuturor 3:
Pentru o descriere mai ampla, click aici
Mentionam ca exista si 'Rule of Five', care nu va fi necesara pentru acest semestru.
x.operator op(y); // functie membra
SAU
operator op(x,y); // functie friend
x.operator op(); //functie membra
SAU
operator op(x); //functie friend
Urmatoarele considerente tin de 'o buna conduita' in programarea obiectuala. Exista si alte abordari 'care merg', dar dorim sa clarificam cele mai bune standarde. Aceste concepte sunt complexe, iar mai jos aveti doar o prezentare a acestora. Pentru o intelegere mai buna, inspectati resursele externe.
Este recomandat ca operatorii sa fie supradefiniti astfel:
Operatori | Supradefiniti ca: |
---|---|
toti operatorii unari | functii membre |
=, [], () | trebuie sa fie functii membre |
+=, -=, *=, … | functii membre |
toti ceilalti operatori binari | functii friend |
1.Daca se doreste citirea dintr-un parametru/argument fara modificarea acestuia, atunci ar trebui transmis in functie ca referinta constanta (astfel voi putea sa transmit ca parametri si obiecte temporare).
//Operator = pentru numere complexe complex& operator = (const complex& a ) { re = a.re; im = a.im; return *this; }
Apelul operatorului + va arata asa:
// Exemplu c = a + b; ///SAU/// c = a.operator + (b)
2. Tipul returnat depinde de 'intelesul' operatorului: Daca efectul operatorului este acela de a produce o noua valoare, va trebui sa fie creat/generat un nou obiect si returnat prin valoare.
//Operator - pentru tipul complex //Varianta ca functie membra const complex complex::operator - (const complex& a ) const { return complex (re - a.re, im - a.im) //este creat un nou obiect si returnat prin valoare } //Varianta ca functie friend friend const complex operator - (const complex & a const complex & b) { return complex (a.re - b. re, a.im - b.im) //este creat un nou obiect si returnat prin valoare }
Acest nou obiect este retunat prin valoare ca si constanta, astfel incat rezultatul sa nu poata fi modificat in cadrul unei operatii in care el ar fi operandul din stanga( lvalue):
Ganditi-va la tipurile de date de baza; nu avem voie sa facem asa ceva:
int a=2,b=3,c=6; a+b=d; //ERROR non-lvalue in assignment
complex a(2,2),b(2,2); (a-b).modifica(7,7); //modificarea asupra obiectului returnat de + se pierde.
3. Pentru a permite ca rezultatul atribuirii sa fie folosit intr-o expresie in lant: a=b=c, este asteptat ca operatorul sa intoarca o referinta catre operandul de tip lvalue pe care tocmai l-a modificat.
complex& operator = (const complex& a) { re = a.re; im = a.im; return *this; //returneaza o referita catre obiectul care apeleaza functia (a = b <=> a.operator = (b)) }
Atribuirea se face de la dreapta la stanga: a=(b=c), deci nu ar trebui neaparat intoarsa o referinta constanta.
Mai mult, uneori se doreste realizarea unei modificari asupra obiectului care tocmai a fost modificat prin atribuire:
(a=b).modifica();
Modificarea sa trebuie pastrata in a. In acest caz, tipul returnat de toti operatorii de atribuire ar trebui sa fie o referinta neconstanta catre lvalue.
4. Operatorii de incrementare si decrementare atat in versiune prefixata cat si in versiune postfixata modifica obiectul, deci nu pot sa fie functii constante.
//Forma prefixata const complex& operator ++() { this->re++; this->im++; return *this //returneaza referinta catre obiectul curent, MODIFICAT } //Forma postfixata const complex operator++(int i) // i reprezinta formularea standard ce diferentiaza cei 2 operatori { complex aux(*this) //fac o copie a obiectului nemodificat, folosind constructorul de copiere this->re++; //modific valorile obiectului care apeleaza operatorul this->im++; return aux; //returnez COPIA cu valorile originale, in timp ce obiectul in sine se modifica }
Rezultatul intors ar trebui sa fie const sau nu?
Daca nu este const si avem ceva de genul:
Cea mai buna solutie e ca amandoua versiunile sa returneze o constanta (sau versiunea prefixata returneaza non const si cea postfixata const)
5. Pentru operatorii logici, toata lumea se asteapta sa primeasca in cel mai rau caz un rezultat de tip int si in cel mai bun caz un rezultat de tip bool.
//Operatorul == pentru complex, ca functie friend //Returneaza int friend int operator == (const complex& a, const complex& b) { if (a.re == b.re && a.im = b.im) return 1; else return 0; } //Returneaza bool friend bool operator == (const complex& a, const complex& b) { return ((a.re == b.re) && (a.im == b.im)) }