This is an old revision of the document!
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) }
!! EXEMPLE COD (Operatori membri + FRIEND)
Daca o clasa defineste defineste vreuna din urmatoarele, cel mai probabil e necesara definirea tuturor 3:
* Destructor
* Copy Constructor (Constructor de copiere)
* Assignment Operator (Operatorul
Pentru o descriere mai ampla, click aici
Mentionam ca exista si 'Rule of Five', care nu va fi necesara pentru acest semestru.
</note>
* Pentru un operator binar op, expresia x op y este interpretata:
<code c++>
x.operator op(y); functie membra
</code>
SAU
<code c++>
operator op(x,y); functie friend
</code>
* Pentru un operator unar op, expresia x op sau op x este interpretata:
<code c++>
x.operator op(); functie membra
</code>
SAU
<code c++>
operator op(x); functie friend
</code>
====2.8. Observatii legate de tipul argumentelor si tipul returnat ====
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).
<code cpp>
Operator = pentru numere complexe
complex& operator = (const complex& a )
{
re = a.re;
im = a.im;
return *this;
}
</code>
<note tip>Implementarea de mai sus permite o atribuire de tipul:
complex c;
c = complex(1,3)
Daca parametrul ar fi fost o referinta neconstanta, compilatorul nu ar fi permis transmiterea unui obiect temporar prin referinta</note>
Apelul operatorului + va arata asa:
<code c++>
Exemplu
c = a + b;
/SAU/
c = a.operator + (b)
</code>
<note tip>Operatiile aritmetice (+,-,etc) si cele logice(==,>,etc) nu vor modifica valorile parametrilor. In acest caz, daca operatorul este implementat ca functie membra va fi o functie membra constanta (nu modifica atributele obiectelor care apeleaza functia)
Exemplu: const complex complex::operator - (const complex& a ) const </note>
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.
<code cpp>
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
}
</code>
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:
<code c++>
int a=2,b=3,c=6;
a+b=d; ERROR non-lvalue in assignment
</code>
<note>
De ce nu? S-ar modifica un obiect temporar – iar modificarea s-ar pierde:
</note>
<code>
complex a(2,2),b(2,2);
(a-b).modifica(7,7); modificarea asupra obiectului returnat de + se pierde.
</code>
<note important>
Este de preferat sa pot apela doar functii constante – de tip afisare, etc care garanteaza ca nu vor face modificari.
</note>
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.
<code cpp>
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))
}
</code>
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:
<code>
(a=b).modifica();
</code>
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.
* versiunea prefixata (++a) returneaza valoarea obiectului dupa modificare: return *this; (ca referinta).
* versiunea postfixata intoarce valoarea inainte de modificare, ceea ce inseamna crearea unui nou obiect, deci va intoarce un obiect prin valoare.
<code cpp>
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
}
</code>
Rezultatul intors ar trebui sa fie const sau nu?
Daca nu este const si avem ceva de genul:
* (++a).funct(), funct() va opera asupra lui a (o referinta)
* dar in cazul (a++).funct(), va opera asupra unui obiect temporar returnat de varianta postfixata a operatorului. Obiectele temporare sunt constante, ceea ce va fi sesizat de compilator (nu pot chema decat functii const).
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.
<code cpp>
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 1)
}
</code>