Laboratorul 04: Functii Friend. Supradefinirea Operatorilor

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

  • Capitolul 8 (Chapter 8: Operator Overloading, Friends, and references, pag. 321-367)

1. Functii Friend

O functie de tip friend este o functie ce se defineste in exteriorul clasei, dar care are acces la toate atributele private sau protected ale clasei. Aceasta NU se afla in scopul/contextul clasei.

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.

1.1. Definim Clasa

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

Functia 'adunaCinciMetri' nu face parte din clasa 'Distanta', dar poate accesa campul metri. In cadrul clasei, doar se specifica relatia de 'friend' (de prietenie). Mai departe, vom vedea implementarea functiei 'adunaCinciMetri' in exteriorul clasei.

Analogie: Doar fiindca un prieten/o prietena vine la voi in vizita, nu inseamna ca face parte din familie. Asa cum o functie de tip friend are acces la membri privati ai clasei, nu inseamna ca face parte din clasa.

1.2. Implementam Constructorii, Metodele si Functiile Friend

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

A se observa diferenta dintre header-ul constructorului si cel al clasei Friend. Constructorul are 'atasat' si contextul (numele clasei), pe cand functia friend este de sine statatoare. Asadar, o functie ar putea sa fie FRIEND cu mai multe clase.

Functiile friend NU se mostenesc.

Analogie: Doar fiindca voi sunteti prieteni cu o persoana, nu inseamna ca persoana aceea nu mai are alti prieteni la randul sau. De asemenea, prietenii parintilor vostri nu sunt neaparat si prietenii vostri (precum in viata reala, prietenii nu se mostenesc). Ducand acest exemplu mai departe, un prieten de-al vostru poate sa fie si prietenul 'dusmanului' vostru. Functiile friend pot duce la 'leak-uri' de informatii, asa cum un prieten poate 'scapa' o barfa despre voi unde nu trebuie.

1.3. Cum apelam?

int main() {
    Distanta D; // metri = 0
    cout << "Distanta: " << adunaCinciMetri(D) << endl;
    return 0;
}

1.4. Codul intreg

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

2. Supradefinirea Operatorilor

Functiile operator constituie un tip special de functii. Sunt utilizate pentru redefinirea operatorilor si pentru alte tipuri de date (definite de utilizator) in afara celor de baza.

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

2.1. Sintaxa Generala

class NumeleClasei {
    ... .. ...
    public:
       TIP_RETURNAT operator symbol (argumente);
           ... .. ...
    ... .. ...
};
 
TIP_RETURNAT NumeleClasei::operator symbol(argumente)
{
    ...
}

2.2. De ce se supradefinesc operatorii?

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:

  • Operatori unari (cu un argument/operand). Ex: ++b, b++, -a(cu sensul de inversul lui a)
  • Operatori binari (cu doua argumente/operanzi). Ex: a + b, c == d, a = b

Toti operatorii pot fi supradefiniti cu urmatoarele cateva exceptii:

  • operatorul de acces la membrii clasei .
  • operatorul de rezolutie ::
  • operatorul conditional ? :
  • operatorul sizeof

2.3. Supradefinirea Operatorilor Unari

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.

2.4. Supradefinirea Operatorilor Binari

In cazul Operatorilor Binari, avem 2 abordari distincte, care indeplinesc acelasi scop general, anume:

  1. Functii Membre
  2. Functii Friend

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

2.4.1. Supradefinirea operatorilor prin Functii Membre

In cazul functiilor membre apar urmatoarele constrangeri:

  1. Operatorul supradefinit trebuie sa fie o functie membra a operatorului stang.
  2. Operatorul stang devine implicit obiectul *this.
  3. Ceilalti operanzi devin parametri ai functiei
Operanzi ce returneaza un obiect

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

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

Atentie! In ceea ce priveste numerele complexe, pentru a determina ce numar este mai mare sau mai mic, folosim metode matematice (aplificare conjugata, inmultiri etc).

2.4.2. Supradefinirea operatorilor prin Functii Friend

In cazul functiilor friend, eliminam din constrangerile ce apar la functii membre:

  • Nu vom mai avea *this, acesta fiind inlocuit de operandul stang.
  • Orice referinta catre *this poate fi inlocuita cu primul operand (operandul stang).
  • Nu vom mai fi restrictionati de tipul operandului stang pentru a efectua operatia.
  • Mentionam ca Functia Friend este 'mai lenta' din punct de vedere viteza a rularii codului.
Operanzi ce returneaza un obiect

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

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

2.5. Dupa ce reguli se realizeaza supradefinirea?

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:

  • pluralitatea (un operator unar nu poate fi supradefinit ca unul binar sau invers)
  • precedenta
  • asociativitatea.

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

Nu putem transforma functia de mai sus in functie membra, deoarece ar insemna sa supradefinim operatorul + pentru tipul standard double

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

In particular, pentru operatorii: = ,[], (), functia operator trebuie sa fie membra a clasei.

2.6. Cum arata codul complet?

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.

2.7. Observatii Generale

Operatorul = este generat intotdeauna de catre compilator. Totusi, este indicat ca orice clasa sa defineasca propriul operator de atribuire, pentru a evita confuziile si comportamentele nedorite/default, mai ales in cazul alocarii si eliberarii de memorie (ex: copii superficiale in cazul atributelor de tip pointer)

The Rule of 3

O regula de buna etica in Programare Obiectuala in C++, care este definita in felul urmator:

Daca o clasa 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.

  • Pentru un operator binar op, expresia x op y este interpretata:
x.operator op(y); // functie membra

SAU

operator op(x,y); // functie friend
  • Pentru un operator unar op, expresia x op sau op x este interpretata:
x.operator op(); //functie membra

SAU

operator op(x); //functie friend

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

//Operator = pentru numere complexe
complex& operator = (const complex& a )
{
 re = a.re;
 im = a.im;
 return *this;
}

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

Apelul operatorului + va arata asa:

// Exemplu
c = a + b;
 
///SAU///
 
c = a.operator + (b)

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

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 

De ce nu? S-ar modifica un obiect temporar – iar modificarea s-ar pierde:

complex a(2,2),b(2,2);
(a-b).modifica(7,7); //modificarea asupra obiectului returnat de + se pierde.

Este de preferat sa pot apela doar functii constante – de tip afisare, etc care garanteaza ca nu vor face modificari.

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.

  • 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.
//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:

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

//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))
}
poo-is/laboratoare/04.txt · Last modified: 2021/01/07 17:43 by alexandru.ionita99
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