Supraincarcarea Functiilor si a Operatorilor

 
 

 
Supraincarcarea functiilor si a operatorilor reprezinta un mecanism esential al programarii in C++ intrucat acesta face posibil polimorfismul in timpul compilarii, conferind limbajului flexibilitate si extensibilitate.
 

Supraincarcarea Functiilor

De regula, intr-un program, 2 functii diferite au nume diferite. Insa daca functiile executa operatii similare asupra unor tipuri diferite de obiecte, de multe ori e mai bine ca aceste functii sa aiba acelasi nume. Compilatorul poate selecta functia adecvata in cazul unui apel, bazandu-se pe componenta listelor de parametri. De exemplu, se pot defini 2 functii de ridicare la putere, una pentru numere intregi si alta pentru numere reale:

Utilizarea aceluiasi nume pentru mai multe functii, in acelasi domeniu de vizibilitate, se numeste supraincarcarea numelor de functii sau, pe scurt, supraincarcare. In lucrarea 2 ne-am intalnit deja cu acest concept, in contextul constructorilor unei clase.
Tehnica supraincarcarii este aplicata implicit in C++ pentru operatiile primare. Astfel, pentru operatia de adunare se utilizeaza un singur "nume", adica +, care se aplica si la numere intregi, si la numere reale, si la pointeri.
Pentru a face posibila distinctia la nivelul compilatorului intre functii omonime (supraincarcate) trebuie ca, fie numarul, fie tipurile parametrilor sa difere.  Asadar, NU pot sa fie supraincarcate doua functii care difera doar prin tipul returnat.

Atentie: Doua declaratii de functii pot parea uneori diferite, fara ca ele sa fie in realitate distincte, ca in exemplul de mai jos:

void f(int *p); 
void f(int p[]);
Cele doua functii declarate mai sus NU sunt diferite intrucat  pentru compilator *p este acelasi lucru cu p[ ].

Ambiguitati la Supraincarcarea Functiilor
Situatia in care la un apel compilatorul nu poate alege intre doua sau mai multe functii supraincarcate se numeste ambiguitate. Instructiunile ambigui sunt tratate ca erori, iar programul nu va fi compilat. Exista in principal doua cai prin care se poate ajunge la ambiguitate:

Functiile Operator

Intre tipurile de date abstracte si cele predefinite exista unele asemanari in modul de declarare si de initializare, dar din ceea ce stim pana acum exista diferente destul de mari in ceea ce priveste modul de utilizare a lor. Ar fi ideal ca tipurile de date abstracte sa se comporte ca si cele predefinite. Ceea ce deosebeste radical cele doua tipuri de date este utlizarea operatorilor.
De exemplu, in lucrarea trecuta am schitat declaratia clasei Complex prin care se implementeaza TDA "numere complexe". Ar fi de dorit sa putem aduna doua numere complexe in acelasi mod in care adunam si doua numere intregi.  Pentru ca acest lucru sa fie posibil ar trebui ca operatorul "+" sa poata lucra si cu operanzi de tip Complex.

Pentru extinde utlizarea operatorilor si asupra tipurilor de date definite de utilizator, limbajul C++ modeleaza operatorii prin intermediul unor functii speciale numite functii operator. Ceea ce le face pe aceste functii sa fie "speciale" sunt urmatoarele:

Intrucat tratam operatorii ca functii si intrucat toti operatorii din limbajul C++ sunt deja definiti pentru unul sau multe tipuri predefinite, atunci cand definim comportarea unui operator pentru un tip de date abstract, spunem ca supraincarcam operatorul respectiv.

Restrictii
La supraincarcarea operatorilor in C++ exista urmatoarele restrictii:

Regula de "buna purtare": Desi puteti efectua orice in corpul unei functii operator ramaneti aproape de semnificatia implicita a operatorului. Aceasta va face ca programele pe care le scrieti sa fie inteligibile si pentru altii. Uneori s-ar putea totusi sa doriti disocierea unui operator de semnficatia sa initiala. Desigur, puteti face acest lucru, dar asigurati-va in prealabil ca aveti un motiv temeinic pentru aceasta. 
 

Supraincarcarea Operatorilor Aritmetici

Pentru a ilustra caracteristicile supraincarcarii operatorilor aritmetici vom relua definitia clasei Complex, implementand operatorul de adunare + si pe cel de inmultire * .
 

class Complex {  
public:  
 Complex() {re=0.0; im = 0.0;} 
 Complex(double i) {re=i;} 
 Complex(double i, double j) {re=i; im=j;} 
 Complex operator+(const Complex&);  
 Complex operator*(const Complex&);  
   bool operator==(const Complex&);  
 private: 
  double re; 
  double im; 
}; 
 
Complex Complex::operator+(const Complex& z) { 
  Complex tmp;  

  tmp.re = re + z.re;  
  tmp.im = im + z.im; 
  return tmp; 
} 

Complex Complex::operator*(const Complex& z) { 
  Complex tmp; 

  tmp.re = re*z.re - im*z.im; 
  tmp.im = re*z.im + im*z.re; 
  return tmp;  

bool Complex::operator==(const Complex& z){ 
  return ((re == z.re) && (im == z.im)); 
} 
 
 

main() { 
  Complex z1(2.0, -3), z2(1, 0.5), z3; 

  z3 = z1 * z2; 
  z2 = z1 + 0.5; 
  if (z1 == z2) { ... } 
  else { ... }    
 
 

 
Am afirmat mai sus ca parametrii operatorilor corespund operanzilor. Atunci de ce operatorii binari din exemplul prezentat au un singur parametru? Motivul este acela ca functiile care implementeaza operatorii sunt functii membru, ceea ce inseamna ca pe langa parametrul transmis explicit functiei, se mai transmite implicit pointer-ul this. Instructiunea
 
   z3 = z1 * z2;
 
se traduce prin:
 
   z3 = z1.operator*(z2);
De aici desprindem urmatoarea regula: daca un operator este implementat printr-o functie membru atunci numarul de parametri este cu 1 mai mic decat aritatea op eratorului.

Sa analizam acum ce se intampla la apelul
 

    z2 = z1 + 0.5;
In acest caz, cel de-al doilea parametru nu este un numar complex, ci un numar real. Pentru a putea aplica operatorul de adunare definit pentru aceasta clasa,  trebuie facuta conversia spre tipul Complex a parametrului. Acest lucru este asigurat prin constructorul Complex(double) care creaza un obiect temporar a carui referinta va fi apoi pasata operatorului de adunare. Daca acest constructor nu ar fi fost definit, operatia de mai sus ar fi fost considerata ilegala!
Intrebare: In absenta constructorului care asigura conversia double-Complex, cum s-ar fi putut asigura legalitatea adunarii unui numar complex cu un numar real?   

Definirea operatorilor ca functii friend
Una dintre proprietatile operatiei de adunare este comutativitatea.  Astfel ne-am astepta ca operatiile de mai jos sa fie echivalente: 
 

    z2 = z1 + 0.5;
 
    z2 = 0.5 + z1;
Sa analizam ce se intampla in cel de-al doilea caz: compilatorul incearca sa descopere o functie-operator  avand semnatura operator+(double, Complex). Intrucat o astfel de functie nu exista compilatorul va raporta o eroare de compilare. In acest caz NU se mai efectueaza o conversie a parametrului double spre tipul Complex (desi in clasa Complex ar exista un constructor corespunzator). Compilatorul incearca de fapt urmatoarele variante: 
 
   z2 = (0.5).operator+(z1); 
 
   z2 = operator+(0.5, z1); 
adica fie o functie-operator membra a "clasei" primului operand (double) care sa aiba parametrul de tip Complex, fie o functie-operator globala cu parametrii double si Complex. Intrucat nu putem interveni asupra tipului predefinit double pentru a-i adauga un operator de adunare cu parametru Complex, devine clar ca daca dorim sa asiguram comutativitatea operatiei de adunare pentru clasa Complex in orice situatie, va trebui sa definim o functie-operator globala cu parametrii indicati mai sus. Iar pentru ca aceasta functie va trebui sa aiba acces la definitia clasei, vom declara functia-operator ca friend in clasa Complex.
    Observatie: de fapt statutul de friend pentru operatorii definiti ca functii globale nu este obligatoriu. El se acorda insa din motive de eficienta a accesului la membrii operanzilor. Daca operatorii globali nu ar avea statut de friend, ei ar fi obligati sa acceseze datele membru ale operanzilor via functii de genul GetMember/SetMember, ceea ce ar reduce mult din viteza de executie.
Pentru exemplul nostru adaugirile vor arata astfel:

 
class Complex {  
public: 
 ... 
 friend Complex operator+(double, const Complex&);  
 friend Complex operator*(double, const Complex&);  
 ... 
}; 
 
Complex operator+(double r, const Complex& z) { 
  Complex tmp;  

  tmp.re = r + z.re;  
  tmp.im = z.im; 
  return tmp; 
} 

Complex operator*(double r, const Complex& z) { 
  Complex tmp; 

  tmp.re = r*z.re; 
  tmp.im = r*z.im; 
  return tmp;  
} 
 

 
Se observa ca in acest caz functia care implementeaza operatorii, fiind globala are 2 parametri. De unde regula: atunci cand un operator este implementat printr-o functie friend, numarul parametrilor este egal cu aritatea operatorului.

Supraincarcarea Operatorilor  de Comparare
Alaturi de operatorii aritmetici + si * am implementat si operatorul de test la egalitate == pentru clasa Complex. Toate aspectele discutate pentru operatorii aritmetici raman valabile si in cazul operatorilor de comparare, incluzand si discutia despre cazurile in care este necesara utilizarea unei functii friend pentru a pastra proprietatile operatorului.
 

Supraincarcarea Operatorilor de Incrementare si Decrementare ++ si --

Operatorii ++ si -- sunt operatori unari, iar supraincarcarea acestora se poate face utilizand atat functii membru non-statice, cat si functii friend. Pentru a putea distinge intre forma prefix si cea postfix a acestor operatori se aplica urmatoarea regula: O functie-membru operator++ care nu primeste nici un parametru (cu exceptia parametrului implicit this) defineste operatorul ++ postfix, in timp ce functia operator++ cu un parametru de tip int defineste operatorul ++ postfix. La apel, in cazul formei postfix, utilizatorul nu este obligat sa specifice nici un argument, valoarea transmisa implicit fiind 0.  Aceeasi regula se aplica si pentru operatorul de decrementare. Astfel:

class X { 
public: 
  X operator++() { ... } 
  X operator++(int) { ... } 
}; 

void f(X a) { 
  ++a;   // la compilare se traduce ca a.operator++(); 
  a++; // la compilare se traduce ca a.operator++(0); 

 
In cazul in care se implementeaza operatorii ++ si -- folosind functii friend, regula de deosebire  prefix/postfix enuntata mai sus ramane valabila, singura diferenta reprezentand-o transmiterea explicita a referintei obiectului caruia i se aplica operatia:
class Y { 
public: 
  friend y operator--(Y&); 
  friend Y operator--(Y&, int); 
}; 

void g(Y a) { 
  --a; //se traduce ca operator--(a); 
  a--; //se traduce ca operator--(a, 0); 

    Observatie: daca pentru o clasa se defineste numai varianta prefix a operatorilor ++/--, atunci la apel se vor putea aplica ambele forme, adica si a++ (a--) si ++a (--a), efectul fiind ca amandoua vor fi traduse de compilator ca apeluri la forma prefix; pentru a++ (a--) se va emite, in plus, un avertisment in acest sens. Daca insa este definita doar forma postfix, atunci constructia ++a (--a) va fi considerata eroare.

 

Supraincarcarea Operatorului de Atribuire =

In limbajul C++ se pot face atribuiri de obiecte care sunt instantieri ale aceleiasi clase. Daca programatorul nu defineste in mod explicit un operator de atribuire pentru o clasa, compilatorul genereaza unul implicit. Comportarea operatorului de atribuire implicit presupune ca datele membru ale obiectului din dreapta operatorului = se atribuie la datele membru corespunzatoare ale obiectului din partea stanga a operatorului, dupa care se returneaza o referinta la obiectul modificat. Aceasta copiere este de tip "membru la membru" asa cum se intampla si in cazul constructorului de copiere generat de compilator. Putem descrie deci functionarea operatorului de atribuire generat de compilator, sub forma:

unde member_i sunt date membru non-statice ale clasei MyClass.
    O observatie foarte importanta care trebuie facuta aici este ca daca una dintre datele membru este o instanta a unei alte clase, care are operator de atribuire explicit, atunci pentru data membru respectiva se va apela acel operator de atribuire.

Operatorul de Atribuire si Constructorul de Copiere
La fel ca si constructorul de copiere implicit, si operatorul de atribuire implicit este, pentru majoritatea claselor nebanale, insuficient. Motivele sunt acelasi: in cazul unor date membru alocate dinamic in memoria heap, prin copiere membru la membru se vor obtine doi pointeri spre aceeasi zona de memorie, si nu o copiere a datelor aflate la adresa indicata de pointer  intr-o  alta zona de memorie distincta. Rezulta ca  operatorul de atribuire se va implementa intr-un mod similar cu constructorul de copiere.

Utilizare: Operatorul de atribuire se va defini pentru toate clasele care au date-membru alocate dinamic pe heap. Intotdeauna cand definim un constructor de copiere trebuie sa definim si un operator de atribuire si reciproc.

Intre cele doua functii exista cateva diferente:

Semnatura si Caracteristicile Operatorului de Atribuire
Se recomanda urmatoarea semnatura pentru supraincarcarea operatorului de atribuire:
nume_clasa& nume_clasa::operator=(const nume_clasa &operand_dreapta);

Obiectul curent este operandul stang al atribuirii.  Urmatoarele lucruri sunt importante atunci cand definim un operator de atribuire:

  • Supraincarcarea operatorului de atribuire se poate realiza numai printr-o functie membru nestatica.
  • Operandul drept al operatorului se poate transfera prin valoare sau prin referinta.
  • Componentele obiectului din stanga operatorului de atribuire, care sunt alocate dinamic (pe heap) vor trebui eliberate inainte de a aloca zone noi in vederea copierii elementelor corespunzaotarea obiectului din dreapta. Spunem ca obiectul din stanga trebuie "curatat" inainte de a se atribui valorile obiectului din dreapta.
  • Atribuirile de forma ob1 = ob1 nu au nici un efect. De aceea, in corpul functiei pentru supraincarcarea operatorului de atribuire trebuie testat daca obiectul curent este diferit fata de cel transmis ca parametru, si numai in acest caz se executa corpul functiei. In caz contrar, se returneaza pur si simplu o referinta la  obiectul curent.
Vom exemplifica supraincarcarea operatorului de atribuire, adaugand  acest operator pentru clasa Stack definita in lucrarea 2.
class Stack { 
 int dim;  // dimensiunea alocata pt. stiva 
public : 
 ... 
 Stack& operator=(const Stack &);  
}; 

Stack& Stack::operator=(const Stack &st) { 
  if(this != &st) { //nu avem situatia ob=ob 
    delete []items; // dezaloca datele alocate
         //inainte de atribuire 
 
    items = new int[st.dim]; 
    sp = s.sp;  
    for(int i = 0;i<=sp;items[i++] = st.items[i]);   
  } 
  return *this; 
}

 

Tipuri de Date Concrete (TDC). Forma Ortodox-Canonica a unei Clase
Tipurile de date concrete se deosebesc fata de restul claselor prin aceea ca un TDC se comporta corect in toate situatiile in care tipurile predefinite din C se comporta corect. Clasele care definesc TDC au o anumita morfologie care extinde sistemul de tipuri al  compilatorului, astfel incat acesta din urma sa poata genera un cod eficient si sigur pentru abstractiuni oricat de complexe. Numim aceasta forma a unei clase forma ortodox-canonica [Cop91]. Forma se numeste canonica intrucat ofera compilatorului un sablon de reguli pe care acesta sa le utilizeze la generarea de cod, si se numeste ortodoxa intrucat este forma cea mai bine inteleasa si direct sustinuta de limbaj.  Utilizarea acestei "retete" la definirea claselor ne asigura ca obiectele instantiate din aceasta clasa pot fi atribuite, declarate si transmise ca argumente functiilor exact ca si orica alta variabila din C.

Forma ortodox-canonica a unei clase X este caracterizata prin prezenta urmatoarelor elemente:

    • Constructor Implicit:            X::X()
    • Constructor de Copiere       X::X(const X&)
    • Operator de Atribuire          X& X::operator=(const X&)
    • Destructor                          X::~X()
Utilizare: Trebuie sa utilizam forma canonic-ortodoxa pentru o clasa daca:
      • dorim sa asiguram atribuirea obiectelor clasei, sau transmiterea lor ca argumente (prin valoare) unor functii
      • obiectele clasei contin pointeri sau destructorul clasei foloseste delete pe date-membru ale clasei
Se recomanda utilizarea formei ortodox-canonice pentru orice clasa ne-triviala, pentru a asigura unformitatea la nivelul claselor si pentru a gestiona mai bine cresterea complexitatii fiecarei clase pe parcursul evolutiei programului.

Supraincarcarea Operatorilor de Atribuire Compusi (+= , -=, *= etc)
Operatorii compusi se supraincarca folosind functii membru nestatice care au un prototip similar cu functiile pentru supraincarcarea operatorului de atribuire. In cazul in care pentru o clasa avem deja implementat operatorul de atribuire si unul dintre operatorii aritmetici, atunci operatorul compus format din cei doi va fi deobicei supraincarcat folosind "alogritmica" operatiei de la operatorul aritmetic, si tehnica modificarii obiectului curent implementata in operatorul de atribuire.

Operatori cu Semantica Predefinita
In cazul functiilor operator definiti de utilizator exista putine prezumtii in ceea ce priveste semantica. Aceasta afirmatie implica 2 aspecte:

    • (1)Pe de o parte, data fiind o clasa X, cu un membru non-static m de tip int, se poate defini functia operator+ pentru ea, astfel incat a+b sa insemne de fapt scaderea a.m-b.m (e adevarat, insa, ca asa ceva ar fi total nerecomandat!).
    • (2)Pe de alta parte, exista operatori predefiniti ai limbajului care au rezultat ca o combinatie de alti operatori. De exemplu: daca x este de tip int, expresia x++ este <=> cu x+=1 si <=> cu x=x+1. Pentru operatorii omologi definiti de utilizator asemenea reguli predefinite nu se mai aplica automat. De exemplu, daca utilizatorul a definit operatorii + si = pentru o clasa X, compilatorul nu va interpreta automat o expresie a+=b ca fiind a=a+b (a si b de tip X), ci este necesara definirea explicita a operatorului +=. Este adevarat ca definitia acestui operator se poate baza pe apelurile la = si +, dar ea trebuie sa existe.
    Operatorii = (atribuire), &(adresa) si ,(secventializare) sunt o categorie aparte, in sensul ca ei sunt singurii care au o semantica predefinita si atunci cand sunt aplicati obiectelor unei clase. Aceasta inseamna ca, daca utilizatorul nu a definit explicit acesti operatori pentru o anumita clasa, ei pot totusi sa fie utilizati, dar vor avea un comportament prestabilit.
     
     
     

    Supraincarcarea Operatorului de Indexare  [ ]

    Operatorul predefinit [ ] se utilizeaza pentru a face acces la elementele unui tablou, in constructii de forma tablou[expr]. Aceasta constructie poate fi privita ca o expresie formata din operanzii tablou si expr, carora li se aplica operatorul [ ].  Putem supraincarca acest operator pentru a da sens constructiilor de indexare si pentru cazul in care operanzii sunt obiecte.

    Intrucat expresiile tablou[expr] sunt expresii lvalue (se pot utiliza ca parte stanga intr-o atribuire) , la supraincarcare operatorului [ ] pentru tipuri abstracte, ca si la supraincarcarea operatorului de atribuire, se va utiliza o functie membru nestatica care sa returneze o referinta la elementul selectat pentru functia respectiva. Forma generala a functiei de supraincarcare a operatorului [ ] are forma generala:

    tip& nume_clasa::operator[](tip_indice)
    Utilizare: Acest operator va fi supraincarcat in mod special pentru a exprima operatia de selectare a unui element dintr-o colectie de astfel de elemente.

    Vom prezenta in continuare modul in care acest operator este supraincarcat in cazul unei liste simplu-inlantuite:
     

    typedef int InfoType;  

    struct Element { 
      InfoType info; 
      Element *next; 
      Element(InfoType i = 0, Element *el = NULL) : info(i), next(el) { }  
      Element& operator= (const Element &el) { info = el.info;  return *this; } 
      ... 
    }; 

    class Container { 
    private: 
      Element *head; 
    public:  
      Element& operator[](int index); 
      void add(InfoType); 
    }; 

    void Container::add(InfoType i) { 
      if(!head) 
        head = new Element(i); 
      else 
        head = new Element(i, head); 
    } 
     

    Element& Container::operator[] (int index) { 
      Element *tmp; 

      for(tmp = head; tmp && (index > 1); tmp = tmp->next, index--); 
      return *tmp; 
    } 

    ... 

                     //modul de utilizare 
    main() { 
     Container list; 
     Element *q = new Element(77); 

     list.add(1);   list.add(2); list.add(3); 

     //...  

     cout << list[2]->info << endl;          // 2 
     list[2] = *q; 
     cout << list[2]->info << endl;          // 77  
    }

     
    Pe langa observatiile deja facute trebuie remarcat rolul pe care il joaca supraincarcarea operatorului de atribuire pentru executia corecta a instructiunii cont[2] = *q; In cazul in care nu ar fi fost supraincarcat acest operator,  atribuirea s-ar fi facut folosind operatorul de atribuire implicit, adica s-ar fi produs pe langa copiere campului info si copierea campului next (care este NULL) ceea ce ar fi distrus legatura acestui element cu restul listei.
     

    Supraincarcarea Operatorului de Apel Functie ()

    Stim ca o functie se apeleaza printr-o constructie de forma:  nume_functie(lista_parametrilor_de_apel). Pana acum am privit aceasta constructie ca pe un tot unitar reprezentand un operator pentru o anumita operatie. In realitate, aceasta constructie este formata din doi operanzi nume_functie si lista_parametrilor_de_apel la care se aplica operatorul binar ().

    Specific pentru aceasta constructie este ca spre deosebire de ceilalti operatori binari, in acest caz al doilea parametru poate fi si vid (corespunzand unui apel de functie fara parametri).

    Operatorul () se poate supraincarca asa incat primul operand sa fie un obiect, ceea ce rezulta intr-o constructie de forma:

    obiect(lista_parametrilor_efectivi)
    O astfel de expresie este echivalenta cu:
    obiect.operator()(lista_parametrilor_efectivi)

    Functia care supraincarca operatorul() trebuie sa fie o functie membru nestatica.

    Utilizare: Principala utilizare a supraincarcarii acestui operator o constituie constructia iteratorilor.

    Clase Iterator
    Iteratorii se utilizeaza in legatura cu TDA care contin colectii de elemente. Aceste TDA-uri sunt numite adese containere. (de ex: listele, arborii, tabelele de hash etc.) Problema cautarii elementelor dintr-o colectie de elemente protejate se rezolva cel mai simplu cu ajutorul iteratorilor. Acestia  asigura pe de o parte protectia datelor si pe de alta parte acces la elementele colectiei fara a intra in detalii legate de implementarea acesteia. Astfel se realizeaza o independenta ridicata a utilizarii colectiei de implementarea acesteia.

    In principiu un iterator se realizeaza printr-o clasa speciala atasata unui TDA-container. Vom schita mai jos o clasa iterator atasata unei listei simplu prezentata in sectiunea precedenta:
     

    struct Element { 
      TipInfo info; 
      Element *next;  
    }; 

    class Container { 
    private: 
      Element *head; 
      ... 
    public: 
      Element* operator[](int index); 
      friend class Iterator; 
    }; 

    class Iterator { 
      Container *theContainer; 
      Element *crtElement; 
    public: 
      Iterator(Container &cont, int start = 0) { 
         theContainer = &cont; 
         crtElement = &cont[start]; 
      } 
      ... 
      Element* operator() (); 
    }; 

    Element* Iterator::operator() (){ 
      Element *tmp; 

      tmp = crtElement;                // se retine elementul curent  
      if(crtElement) 
       crtElement = crtElement->next;   // se trece la urmatorul element 
      return tmp;                      // se returneaza elementul curent 
    } 

    ... 
               // utilizarea iteratorului 

    main() { 
     Container cont; 
     Iterator iterate(cont, 0); 
     Element *p; 

     while((p = iterate()) != NULL) { 
     ... 
     } 

    }

     

    Supraincarcarea Operatorului  ->

    Supraincarcarea operatorului -> se face printr-o functie membru nestatica. La supraincarcare acest operator este considerat ca fiind un operator unar care  se aplica la operandul care il precede.

    Fie expresia:

    A obj_a; 
    ... 
    a->expresie; 
    Aceasta expresie este echivalenta cu
    (a.operator->())->expresie; 
    Daca apelul operataorului -> returneaza un pointer la o clasa(structura) care contine un membru public cu numele expresie atunci se 
    apeleaza operatorul predefinit -> selectandu-se componenta definita de.  Daca apelul operatorului -> returneaza un obiect atunci se aplica operatorul -> supraincarcat pentru clasa obiectului returnat.

    Utilizare: Se recomanda utilizarea acestui operator in principal pentru a inlocui folosirea in cascada a operatorilor de selectie ("." si "->") printr-o selectare aparent directa.

    In exemplul de mai jos
     

    struct Info { 
      char nume[80]; 
      int varsta;   
    }; 

    class Catalog { 
    public: 
      Info *persoane;  //un tablou de pointeri la informatii 
      ...  
      Info *operator->() { return persoane; } 
      ... 
    }; 

    ... 

    main() { 
      Catalog unCatalog; 
      ... 
      strcpy(unCatalog->nume, "Yetti"); 
      unCatalog->varsta = 1001; 
      ... 
    }

    Se observa asadar ca prin implementare operatorului -> operam cu campurile structurii Info ca si cum unCatalog ar fi declarat ca pointer la Info! In lipsa supraincarcarii operatorului -> ar fi trebuit sa efectuam operatiile din main() astfel:
     
    main() { 
      strcpy((unCatalog.persoane)->nume, "Yetti"); 
      (unCatalog.persoane)->varsta = 1001; 
    }
     
     

    Supraincarcarea Operatorului de Conversie (cast)

    Am vazut deja cum pentru o  clasa conversiile de la un tip de date oarecare la acea clasa se rezolva prin intermediul constructorilor. Problema pe care o rezolva supraincarcarea operatorului de conversie este problema inversa, si anume conversia unui tip declarat prin intermediul unei clase date spre un tip de date predefinit. Supraincarcarea operatorului de conversie se realizeaza printr-o functie membru nestatica avand forma: nume_clasa::operator nume_tip_predefinit().
    Acest operator are cateva particularitati:

      • Fiind un operator unar implementat printr-o functie membru, nu are parametrii
      • Aceasta functie nu are specificat un tip  returnat intrucat acesta este dat automat  de nume_tip_predefinit.
      • Acest operator se apeleaza pentru instantele clasei atat implicit (acolo unde contextul impune acest lucru) cat si explicit prin constructii de tipul (nume_tip_predefinit)obiect sau  nume_tip_predefinit(obiect)
    Supraincarcarea Operatorilor new si delete
    In C++ prin intermediul operatorilor new si delete sunt alocate resp. dezalocate dinamic datele  in si resp. din memoria heap.  Acesti operatori pot fi utilizati atat pentru a gestiona date de tipuri predefinite cat si obiecte.

    Caracteristici ale Supraincarcarii operatorului new.

      • Operatorul new se supraincarca utilizand o functie membru statica. Specificarea explicita a faptului ca functia este statica nu este necesara, ea fiind implicita asimilata de catre compilator astfel.
      • Antetul functiei-operator new este:
        •                 void *operator new(size_t lung);
        Functia returneaza un pointer universal a carui valoare este adresa de inceput a zonei de memorie heap rezervata obiectului creat.
      • La aplicarea operatorului new nu se indica in mod normal nici o valoarea a parametrului, intrucat compilatorul calculeaza in mod automat dimensiunea obiectului pentru care se rezerva zona de memorie si aceasta dimensiune se atribuie parametrului lung.
      • Aplicarea operatorului new supraincarcat apeleaza in mod automat constructorul corespunzator clasei, sau pe cel implicit daca clasa nu are constructori. De aceea, la aplicarea operatorului new, pot sa fie prezenti parametrii pentru constructorii clasei.
    Caracteristici ale Supraincarcarii operatorului delete.
      • La fel ca si operatorul new, si operatroul delete se supraincarca utilizand o functie membru statica. Specificarea explicita a faptului ca functia este statica nu este necesara, ea fiind implicita asimilata de catre compilator astfel.
      • Antetul functiei-operator delete este:
        •                 void operator delete(void* p);
        Operatorul ia ca parametru un pointer catre obiectul pe care il dezaloca.
      • La aplicarea operatorului delete se apeleaza automat destructorul clasei definit pentru aceea clasa, sau in cazul in care acesta nu a fost definit explicit, se va apela destructorul definit implicit de catre compilator.
    Operatorii new si delete definiti de utilizator au un avantaj fata de cei standard  si anume sunt mai economici dpdv al timpului de executie si al memoriei consumate pentru gestiunea obiectelor dinamice.
    Atentie: La definirea operatorului delete de catre utilizator trebuie avut mare grija, mai ales daca in clasa respectiva este definit si destructorul, deoarece foarte usor se poate ajunge la situatia de apel in lant infinit a  acestor 2 functii.

    Exemplu - Clasa Singleton [Gamma95]
     

    class Singleton {
    public: 
      void* operator new(size_t n=0);
      void operator delete(void*);
      // ... restul interfetei
    private:
      static Singleton* ob;
      Singleton();       // initializarea datelor
      ~Singleton();
      // ...  alte date
    };

    void* Singleton::operator new(size_t n = 0) {
      if(ob == NULL) 
        ob= ::new Singleton;
      return ob;
    }

    void Singleton::operator delete(void *p) { 
      if(ob != NULL) { 
        ::delete ob;
        ob=NULL;
      }
     

    Singleton* Singleton::ob=NULL;
     
    void main() {
       Singleton *ob1=Single::operator new();
       Singleton *ob2=Single::operator new();

       //...
     
      delete ob1; //se sterge efectiv obiectul
      delete ob2; //nu se face nimic
    }

    Observatii:
            1. Constructorul clasei Single a fost pus in sectiunea 'private' si in felul acesta, nicaieri in program nu se pot crea obiecte Single prin simpla declarare si nici prin alocare dinamica, folosind operatorul new global; orice tentativa de acest gen va fi sanctionata de compilator; singura cale de a crea  obiecte Single este prin intermediul lui Single::operator new;
            2. Operatorul 'new' pentru clasa Single returneaza de fiecare data adresa aceluiasi obiect care este creat ca obiect static local; oricate apeluri ale operatorului new ar aparea in program, nu se creaza noi obiecte;

    Probleme
    1. Sa se implementeze TDC String care ascunde detaliile de reprezentarea ale sirurilor din C (char*). Cerinte specifice:
                    - Clasa String va respecta forma ortodox-canonica;
                    - urmatoarea secventa de cod sa fie valida sintactic si semantic:
     

    main() { 
      String s1 = "Hello world"; 
      String s2 = s1(6,6);           // vezi explicatia mai jos. 
      String s3; 
     
      s3 = s1(1,5) + ("fascinating" + s2) + "of C++ !";      // concatenare  
      
      cout << "Al treilea carcater din s1 este " << s1[3] << endl; 
      s2[3] = 'e';   
      s2[5] = 'f';  // s2 devine "woolf"  

      if(s3 == s1) cout << "Sirurile sunt identice\n";    //comparare de siruri 
      else cout << "Sirurile nu sunt identice\n" 
      strcmp(s2, "woolf"); 
    }

     
     
    Expresia s(i,j) are ca efect extragerea din sirul s a subsirului care incepe de la pozitia i si are lungimea de j caractere. Primul caracter din sir se considera a fi pe pozitia 1. Daca i < 1 se considera ca subsirul incepe de la primul caracter. Daca i este mai mare decat ultima pozitie din sir se va returna sirul vid. Daca i+j depasesc lungimea sirului se va returna subsirul incepand la pozitia i si care tine pana la sfarsitul sirului.
    2. Cum credeti ca se realizeza in mod normal conversiile de la un TDA la alt TDA?
    3*. Una din criticile severe aduse limbajului C/C++ este lucrul prea "descoperit" cu pointerii. Implementati o lista dublu-inlantuita (DoubleList) in care sa "dispara" din implementarea clasei DoubleList operarea directa cu pointerii la clasa Nod. Acestia (adica, pointerii) sa fie incapsulati intr-o clasa NodP, iar DoubleList sa opereze cu instante ale acest tip.  Folositi cu incredere supraincarcarea operatorillor!

    Bibliografie

    [Cop91] J. Coplien  - "Advanced C++ Programming Styles and Idioms",  Addison-Wesley, 1991
                  M.Ellis, B.Stroustrup - "The Annotated C++ Reference Manual,  Addison-Wesley, 1990
    [Gam95] E.Gamma, R.Helm, R.Johnson, J.Vlissides - Design Patterns,  Addison-Wesley, 1995
                  L. Negrescu - "Limbajele C/C++ pentru Incepatori vol. II", Editura Microinformatica Cluj, 1994
                  H. Shildt - "C++. Manual complet", Editura TEORA,  1997

    Hosted by www.Geocities.ws

    1