Supraincarcarea functiilor si a operatorilor (overloading) sunt mecanisme
importante in C++ care ofera flexibilitate si extensibilitate limbajului.
Pentru tipurile fundamentale ale limbajului sunt definite seturi de operatori
care permit operatii de baza executate intr-un mod convenabil. Dar, dupa
cum este cunoscut, in limbaj sunt definite prea putine tipuri de date
ca date fundamentale, iar pentru reprezentarea altor tipuri care sunt necesare
in diferite domenii (cum ar fi aritmetica numerelor complexe, algebra
matricilor, etc.), se definesc clase care contin functii ce pot opera asupra
acestor tipuri. Definirea operatorilor care sa opereze asupra obiectelor unei
clase permite un mod mult mai convenabil de a manipula obiectele decat
prin folosirea unor functii ale clasei. z6v11vb
O functie care defineste pentru o clasa o operatie echivalenta operatiei efectuate
de un operator asupra unui tip predefinit este numita functie operator. Majoritatea
operatorilor limbajului C++ pot fi supraincarcati, si anume:
new delete () ai
+ - * / % ^ & | I
! = < > += -= *= /= %=
^= &= |= << >> >>= <<= == !=
<= >= && || ++ -- , ->* ->
Operatorul () este apelul unei functii, iar operatorul ai este operatorul de
indexare. Urmatorii operatori nu se pot supraincarca:
. .* :: ?: sizeof
Exista cateva reguli care trebuie sa fie respectate la supraincarcarea
operatorilor:
• Functiile operator=(), operator()() si operatorai() trebuie sa fie
membri nestatici ai clasei.
• Cu exceptia functiei operator=(), toate celelate functii operator pot
fi mostenite.
• Nu pot fi supraincarcati operatorii pentru tipurile predefinite
ale limbajului.
• Functiile operator nu pot avea argumente implicite.
Functiile operator pentru o anumita clasa pot sa fie sau nu functii membre
ale clasei. Daca nu sunt functii membre ele sunt, totusi, functii friend ale
clasei si trebuie sa aiba ca argument cel putin un obiect din clasa respectiva
sau o referinta la aceasta. Exceptie fac operatorii =, (), ai, ->, care nu
pot fi supraincarcati folosind functii friend ale clasei. De asemenea,
functiile operator new() si operator delete()au implementari mai deosebite care
vor fi detaliate mai jos.
4.1 Functii operator membre ale claselor
Forma generala pentru functiile operator membre ale clasei este urmatoarea:
tip_returnat operator#(lista_argumente)A
// operatii
S
In aceasta forma generala semnul # reprezinta oricare dintre operanzii
care pot fi supraincarcati.
? Exemplul 4.1
Fie o clasa Point care descrie un vector intr-un plan bidimensional prin
doua numere de tip double, x si y. Valorile x si y reprezinta coordonatele punctului
de extremitate al vectorului. Pentru aceasta clasa se pot defini mai multe operatii
cu vectori, ca de exemplu:
• Suma a doi vectori
• Diferenta a doi vectori
• Produsul scalar a doi vectori
• Multiplicarea unui vector cu o constanta (scalare)
• Incrementarea/decrementarea componentelor vectorului
• Oglindire (negarea fiecarei componente).
Aceste operatii se pot implementa prin supraincarcarea corespunzatoare
a operatorilor. Pentru inceput se vor defini functiile operator+() si
operator-;() pentru calculul sumei, respectiv a diferentei a doi vectori.
class PointA double x; double y; public:
Point()A x = 0; y = 0;
S
Point(double a, double b)A x = a; y = b;
S void Display() A cout << x << " " << y << endl;
S
Point operator+(Point op2); // suma a doi vectori
Point operator-(Point op2); // diferenta a doi vect double operator*(Point op2);// produs scalar
Point& operator*(double v); // multipl. cu o const.
S;
Point Point::operator+(Point op2)A point temp; temp.x = x + op2.x; temp.y = y + op2.y; return temp;
S
Point Point::operator-(Point op2)A point temp; temp.x = x + op2.x; temp.y = y + op2.y; return temp;
S double Point::operator*(Point op2)A return x*op2.y + y*op2.x;
S
Point& Point::operator*(double v)A x *=v; y *=v; return *this;
S void f1()A
Punct pct1(10,20);
Punct pct2(30,40);
Punct pct3; pct1.Display(); // afiseaza 10 20 pct2.Display(); // afiseaza 30 40 pct3 = pct1 + pct2; pct3.Display(); // afiseaza 40 60 pct3 = pct2 -; pct1; pct3.Display(); // afiseaza 20 20
S
Functia operator+() are un singur argument, chiar daca ea supraincarca
un operator binar (operatorul +), care necesita doi operanzi. Argumentul transmis
functiei este operandul din dreapta operatiei, iar operandul din stanga
este chiar obiectul pentru care se apeleaza functia operator. Expresia: pct3 = pct1 + pct2; semnifica, de fapt: pct3 = pct1.operator+(pct2); si chiar poate fi apelata astfel. Acest lucru inseamna ca obiectul din
stanga operatorului este cel pentru care se apeleaza functia operator,
care are acces la acesta prin pointerul this pasat implicit functiei operator
membra a clasei. Pentru functia operator+() ordinea operanzilor nu are importanta,
dar aceasta conventie de apel este importanta pentru alte functii, ca de exemplu
functia operator-().
Pentru acelasi operator se pot defini mai multe functii supraincarcate,
cu conditia ca selectia uneia dintre ele in functie de numarul si tipul
argumentelor sa nu fie ambigua. In clasa Point s-a supraincarcat
operatorul * cu doua functii: prima pentru calculul produsului scalar a doi
vectori, cealalta pentru multiplicarea vectorului cu o constanta. ?
In implementarea prezentata, functia operator+() creaza un obiect temporar,
care este distrus dupa returnare. In acest fel, ea nu modifica nici unul
dintre operanzi, asa cum nici operatorul + pentru tipurile predefinite nu modifica
operanzii.
Intr-o functie operator se pot efectua orice fel de operatii, dar, in
mod obisnuit, se pastreaza semnificatia actiunii operatorului respectiv. Cu
exceptia functiilor operator new (), operator delete() si operator->(), valoarea
returnata de o functie operator poate fi de orice tip, dar, tot pentru pastrarea
contextului utilizarii normale, de obicei se returneaza un obiect din aceeasi
clasa, sau o referinta la aceasta. Acest lucru este important pentru utilizarea
operatorului in expresii, asa cum este cea scrisa mai sus.
In expresia scrisa mai sus, pct3 = pct1 + pct2, se executa, pe langa
operatia de adunare, si o operatie de asignare pentru o variabila de tip Point.
Pentru clasa Point, aceasta operatie se executa corect, chiar daca nu a fost
supraincarcata functia operator=(), deoarece asignarea implicita se executa
printr-o copiere membru cu membru si in acest caz nu produce erori. Conditiile
in care este absolut necesara supraincarcarea functiei operator=()
vor fi discutate in subsectiunea urmatoare.
In general, un operator binar poate fi supraincarcat fie printr-o
functie membra nestatica cu un argument, fie printr-o functie nemembra cu doua
argumente.
Un operator unar poate fi supraincarcat fie printr-o functie membra nestatica
fara nici un argument, fie printr-o functie nemembra cu un argument. La supraincarcarea
operatorilor de incrementare sau decrementare (++, --) se poate diferentia un
operator prefix de un operator postfix folosind doua versiuni ale functiei operator.
In continuare sunt prezentate cateva functii operator ale clasei
Point pentru operatori unari. class PointA
//…………… public:
Point operator!();
Point operator++();
Point operator—();
Point operator++(int x);
Point operator—(int x);
S;
Point operator!()A x = -x; y = -y; return *this;
S
Point Point::operator++()A x++; y++; return *this;
S
Point Point::operator--()A x--; y--; return *this;
S
Point Point::operator ++(int x)A
++x;
++y; return *this;
S
Point Point::operator --(int x)A
--x;
--y; return *this;
S
Daca ++ precede operandul, este apelata functia operator++(); daca ++ urmeaza
operandului, atunci este apelata functia operator++(int x), iar x are valoarea
0.
4.2 Functii operator friend
La supraincarcarea unui operator folosind o functie care nu este membra
a clasei este necesar sa fie transmisi toti operanzii necesari, deoarece nu
mai exista un obiect al carui pointer (this) sa fie transferat implicit functiei.
Din aceasta cauza, functiile operator binar necesita doua argumente de tip clasa
sau referinta la clasa, iar functiile operator unar necesita un argument de
tip clasa sau referinta la clasa. In cazul operatorilor binari, primul
argument transmis este operandul stanga, iar al doilea argument este operandul
dreapta.
In exemplul urmator se reiau unele functiile operator ale clasei Point,
implementate ca functii friend ale clasei.
? Exemplul 4.3
class Point
A int x; int y; public:
//………………………. friend Point operator+(Point op1, Point op2); friend Point operator-(Point op1, Point op2); friend Point operator++(Point &p); friend Point operator++(Point &p, int x); friend Point operator--(Point &p); friend Point operator--(Point &p, int x);
S;
Point operator+(Point op1, Point op2)A
Point temp; temp.x = op1.x + op2.x; temp.y = op1.y + op2.y; return temp;
S
Point operator-(Point op1, Point op2)A
Point temp; temp.x = op1.x - op2.x; temp.y = op1.y - op2.y; return temp;
S
Point operator++(Point &p)
A p.x++; p.y++; return p;
S
Point operator ++(Point &p, int x)
A
++p.x;
++p.y; return p;
S
Point operator--(Point &p)
A p.x--; p.y--; return p;
S
Point operator --(Point &p, int x)
A
--p.x;
--p.y; return p;
S
void f2()A
Punct pct1(10,20);
Punct pct2(30,40);
Punct pct3; pct1.Display(); // afiseaza 10 20 pct2.Display(); // afiseaza 30 40 pct3 = pct1 + pct2; pct3.Display(); // afiseaza 40 60 pct3 = pct2 -; pct1; pct3.Display(); // afiseaza 20 20 pct1++; pct1.Display(); // afiseaza 11 21
S
?
Referitor la supraincarcarea operatorilor folosind functii nemembre ale
clasei se mai pot face cateva observatii.
Daca functia operator nu ar fi declarata functie friend a clasei, ea nu ar avea
acces la variabilele protejate ale clasei. Problema s-ar putea rezolva prin
adaugarea unor functii publice de citire si scriere ale datelor membre ale clasei
respective, care sa fie apelate in functia operator. Dar o astfel de solutie
este incomoda, ineficienta si nu aduce nici un avantaj. Se poate evidentia acest
aspect daca se incearca o modificare a functiei operator+() a clasei Point
astfel:
class Point
A public:
//………………………. double getx() Areturn x;S double gety() Areturn y;S void setx(double a) Ax = a;S void sety(double b) Ay = b;S
S;
Point operator+(Point op1, Point op2)A // nu este friend
Point temp; temp.setx(op1.getx()+op2.getx()); temp.sety(op1.gety()+op2.gety()); return temp;
S
Se poate observa ca, daca se folosesc functii nemembre ale clasei pentru supraincarcarea
operatorilor, atunci este mult mai eficient si mai comod ca acestea sa fie declarate
functii friend ale clasei respective.
O alta observatie referitoare la supraincarcarea operatorilor folosind
functii friend este aceea ca pentru operatorii care trebuie sa modifice operandul
(cum sunt operatorii de incrementare, decrementare, complement, etc) este necesar
transmiterea operandului ca parametru prin referinta, ceea ce permite modificarea
lui in functia operator. Acest mod de apel se observa la definirea functiilor
operator++() si operator--() din Exemplul 4.2.
4.3 Supraincarcarea operatorului de asignare
Exista mai multi operatori de asignare: =, *=, /=, %=, +=, -=, >>=, <<=,
&=, ^=, |=, dintre care primul este operatorul simplu de asignare, ceilalti
fiind combinatie cu alti operatori.
In asignarea simpla (=), valoarea expresiei care reprezinta operandul
dreapta inlocuieste valoarea operandului stanga. Daca ambii operanzi
sunt de tip aritmetic, operandul dreapta este convertit la tipul operandului
stanga, dupa care are loc atribuirea valorii. Nu exista o conversie implicita
la tipul enumerare, astfel incat, daca operandul stanga este
o enumerare, cel din dreapta trebuie sa fie de acelasi tip.
Pentru tipurile definite de utilizator, functia operator de asignare (operator=())
trebuie sa fie o functie membra nestatica a clasei si nu poate fi mostenita.
In multe privinte, functia operator=() seamana mai mult cu constructorii
decat cu ceilalti operatori, dar este, totusi diferita de operatia de
initializare pe care o efectueaza constructorii. In lipsa unei functii
operator=() definita de utilizator pentru o clasa X, este utilizata definitia
implicita de asignare prin copierea membru cu membru astfel:
class X A
//…….
S;
X& X::operator=(const X& op2)A
// copiere membru cu membru
S
void f()A
X a,b; a = b; // se foloseste asignarea implicita
S
Ceilalti operatori de asignare (+=, -=,*=, etc) nu au o semnificatie predefinita
pentru clase, deci, pentru a putea fi folositi, trebuie sa fie definiti de utilizator.
In cazul claselor de obiecte care nu contin date alocate dinamic la initializare
sau prin intermediul altor functii membre, asignarea prin copiere membru cu
membru functioneaza corect si, in general, nu mai este necesar sa fie
supraincarcat operatorul de asignare. Acest lucru s-a remarcat si in
executiile din Exemplele 4.1 sau 4.2.
Nu acelasi lucru este valabil pentru clasele care contin date alocate dinamic.
Situatia este foarte asemanatoare celei prezentate in cazul constructorilor
de copiere: daca o clasa contine date alocate dinamic, copierea membru cu membru
care se executa implicit la asignare sau la constructia prin copiere, are ca
efect copierea pointerilor la datele alocate dinamic, deci doi pointeri din
doua obiecte vor indica catre aceeasi zona din memoria heap. Acesta situatie
conduce la numeroase si subtile erori care se vor analiza mai sugestiv pe un
exemplu de clasa care modeleaza un sir de caractere, clasa String.
? Exemplul 4.3
Fie clasa String care implementeaza un sir de caractere, folosind constante
de tip sir:
#include <string.h> class StringA char *str; int size; public:
String();
String(const char *p);
String(const String& r);
IString(); friend ostream& operator <<(ostream &stream, const String &r); friend istream& operator >>(istream &stream,
String &r);
String& operator=(const String &op2);
String& perator=(const char* p);
S;
Clasa String contine un pointer la un sir de caractere, str, si o variabila
de tip intreg size care memoreaza dimensiunea vectorului de caractere
corespunzator, deci inclusiv spatiul necesar pentru caracterul 0 de la sfarsitul
sirului. Constructorii si destructorul sunt simplu de implementat. Constructorul
implicit creaza un obiect String cu un sir de caractere de lungime zero. Constructorul
de initializare construiestc un obiect String avand ca argument un sir
de caractere terminat cu nul. Dimensiunea tabloului de caractere pe care il
aloca in memoria heap este strlen(p)+1, pentru a se insera caracterul
0 de la sfarsit. Constructorul de copiere este absolut necesar, pentru
a aloca un sir nou in memoria heap si a evita astfel erorile care ar putea
apare prin copierea pointerului, care ar indica catre acelasi sir in memoria
heap. Se asemenea este absolut necesar sa fie definit destructorul, pentru eliminarea
sirului de caractere alocat dinamic in heap.
String::String()A cout << "Constructor implicit\n"; str = 0; size = 0;
S
String::String(const char *p)A cout << "Constructor init\n"; size = strlen(p) + 1; str = new charasizei; strcpy(str, p);
S
String::String(const String& r)A cout << "Constructor copiere\n"; size = r.size; str = new charasizei; strcpy(str, r.str);
S
String::IString()A cout << "Destructor\n"; if (str)A delete aistr; str = 0;
S
S
Functiile operator <<() si operator >>() care definesc operatiile
de inserare si de extragere a unui obiect String dintr-un stream definite in
acest punct vor fi prezentate detaliat in sectiunea 6.
ostream& operator <<(ostream &stream, const String &r)A stream << r.str; return stream;
S istream& operator >>(istream &stream, String &r)A char bufa256i; cin.get(buf,256); r = buf; return stream;
S
Operatorul de asignare este asemanator constructorului de copiere: nu efectueaza
copierea membru cu membru a datelor ci aloca un spatiu nou in memoria
heap pentru sirul de caractere str si efectueaza copierea continutului acestuia.
In plus, la asignare, mai este necesara stergerea sirului pe care obiectul
String pentru care se executa operatia de asignare ar fi putut sa-l aiba alocat
in memoria heap. S-au definit doua functii operator=(), cu argumente de
tip diferit (o referinta la clasa String si un pointer si un sir de caractere),
selectia intre aceste functii efectuandu-se pe baza tipului argumentului
de apel.
String& String::operator=(const String &op2)A cout << "Operator=(String&)\n"; if (str) delete aistr; size = op2.size; str = new charasizei; strcpy(str, op2.str); return *this;
S
String& String::operator=(const char* p)A cout << "Operator = (char*) \n"; if (str) delete aistr; size = strlen(p) + 1; str = new charasizei; strcpy(str, p); return *this;
S
Se pot studia situatiile in care se folosesc diferiti constructori sau
operatori de asignare pentru obiecte din clasa String. Fie functia f3():
void f3 ()A
String sir1("123456"); // constr. initializ cout << sir1 <<endl;
String sir2 = "abcd"; // constr. initializ cout << sir2 << endl;
String sir3 = sir1; // constr. copiere cout << sir3 << endl; sir3 = sir2; // operator=(String&) cout << sir3 << endl; sir3 = "mnp"; // operator=(char*) cout << sir3 << endl;
S
La executia acestei functii, la consola se afiseaza mesajele:
Constructor init
123456
Constructor init abcd
Constructor copiere
123456
Operator=(String&) abcd
Operator=(char*) mnp
Destructor
Destructor
Destructor
care indica modul in care sunt apelati constructorii, destructorul si
functiile operator de asignare. S-au creat trei obiecte din clasa String, obiectele
sir1 si sir2 prin constructori de initializare, iar obiectul sir3 prin constructorul
de copiere. Asignarile catre obiectul sir3 utilizeaza acea functie operator=()
care se potriveste tipului argumentului. La iesirea din blocul funtiei, cele
trei obiecte sunt distruse si se apeleaza de fiecare data destructorul, care
eliminina din memoria heap sirul de caractere str corespunzator fiecarui obiect.
Aceasta este executia corecta a programului, asigurata de definirea corecta
a constructorilor, destructorului si a functiilor de copiere. Lipsa unora dintre
aceste functii poate avea urmari dintre cele mai grave.
De exemplu, lipsa functiei operator:
String& operator=(const String &op2); are ca efect folosirea operatorului implicit de asignare, care copiaza in
variabila str a obiectului sir3 valoarea pointerului str din obiectul sir2,
deci ambii pointeri indica acelasi sir de caractere “abcd”: sir2.str
= sir3.str. Eroarea se evidentiaza la iesirea din functia f1(): eliminarea obiectului
sir3 produce stergerea sirului de caractere “abcd”, acelasi pe care
incearca sa-l steearga apoi si destructorul apelat pentru obiectul sir2.
Aceasta operatie de stergere a unor date care au fost deja sterse din memorie
provoaca executia anormala a programului si la consola va apare un mesaj de
eroare. Mai sunt posibile si alte erori de executie provenite din aceasta asignare
eronata, care pot fi studiate in exercitiile propuse.
Tot ca exercitii se vor studia si alte situatii care evidentiaza comportamentul
functiilor operator de asignare.
4.4 Supraincarcarea operatorului de indexare
O functie operatorai() poate fi folosita pentru a defini o operatie de indexare
pentru obiecte de tipuri definite de utilizator (clase). Ca si functia operator
de asignare, functia operator de indexare nu poate fi decat functie membra
nestatica a clasei respective. Argumentul functiei reprezinta al doilea operand
al operatiei de indexare si este un indice. Acesta argument poate fi orice tip
de date, spre deosebire de indicii in tablouri care nu pot avea decat
valori intregi. Primul argument al functiei este obiectul de tip String
pentru care se executa operatia de indexare si pointerul la acesta (pointerul
this) este transmis implicit functiei operator de asignare care este membra
a clasei.
In clasa String se poate adauga functia operator ai(int i), care returneaza
referinta la caracterul din pozitia i a sirului de caractere str continut de
un obiect String:
char& String::operatorai(int i)A return straii;
S
Se poate remarca faptul ca aceasta implementare este cea mai simpla posibila,
dar pot apare erori de executie atunci cand se executa indexarea pentru
valori ale argumentului care depasesc dimensiunea sirului de caractere. Modul
cum se trateaza astfel de erori in C++ este prezentat in sectiunea
8.
? Exemplul 4.4
Fie functia f4():
void f4()A char vai = "123456789";
String sir(v); int s = strlen(v); for(int i=0;i<s;i++) cout << siraii; cout << endl; for(i=0;i<s;i++) siraii = 65 + i; cout << sir << endl;
S
La executia acesteia, la consola se afiseaza urmatoarele rezultate:
Constructor init
123456789
ABCDEFGH
Destructor
Datorita faptului ca functia operator de indexare returneaza o referinta la
un element al sirului, a fost posibila folosirea indexarii atat pentru
un membru dreapta cat si pentru un membru stanga al unei expresii.
4.5 Supraincarcarea operatorilor new si delete
Operatorii new si delete sunt utilizati pentru alocarea dinamica a datelor
in memoria heap. Pentru supraincarcarea acestor operatori se pot
folosi numai functii membre statice ale clasei. Prototipul functiilor de supraincarcare
a operatorilor new si delete sunt:
void* operator new(size_t lungime); void operator delete(void* p);
In ambele situatii, functiile sunt implicit statice, fara sa fie nevoie
de utilizarea specificatorului static.
Tipul size_t este un tip definit in fisierul de biblioteca stdlib.h (intreg
fara semn), iar lungime este dimensiunea in numar de octeti a zonei de
memorie care trebuie sa fie alocata. Aceasta valoare (lungime) nu trebuie sa
fie specificata la apelul unei functii operator new, deoarece compilatorul calculeaza
in mod automat dimensiunea obiectului pentru care se aloca zona de memorie.
Pointerul universal (void*) returnat are ca valoare adresa de inceput
a zonei de memorie alocata.
Functia operator delete() primeste un pointer catre regiunea de memorie pe care
trebuie sa o elibereze.
Operatorii new si delete pot fi supraincarcati global, astfel incat
orice utilizare a lor sa foloseasca versiunea supraincarcata, sau pot
fi supraincarcati pentru o anumita clasa, si in aceasta situatie
functiile operator trebuie sa fie membre statice ale clasei.
La intalnirea unuia dintre operatorii new sau delete pentru un anumit
tip, compilatorul verifica mai intai daca acesta a fost supraincarcat
pentru clasa respectiva. Daca a fost supraincarcat, se foloseste versiunea
supraincarcata a clasei; daca nu, este apelat operatorul new sau delete
global. Daca acestea au fost supraincarcate, se utilizeaza aceste versiuni.
? Exemplul 4.5
Se considera supraincarcarea operatorilor new si delete pentru clasa
Point. In acest exemplu simplu operatorul supraincarcat nu modifica
modul de alocare sau de dezalocare a memoriei, ci doar adauga un mesaj la consola.
class PointA
//……………. public:
//………………. void* operator new(size_t dim); void operator delete(void *p);
S;
void* Point::operator new(size_t dim)A cout << "Creare punct nou\n"; return ::new Point;
S void Point::operator delete(void* p)A cout << "Distrugere punct\n";
::delete p;
S void f5()A
Point *p1 = new Point; p1->Display(); delete p1; p1 = new Point(7); p1->Display(); delete p1; p1=new Point(5,8); delete p1;
Point *p2 = new Pointa7i; delete aip2;
S
Expresiile de forma ::new sau ::delete se refera la versiunea predefinita
(globala) a operatorilor de alocare dinamica, folosind operatorul de rezolutie
::. Daca si aceste versiuni au fost supraincarcate, atunci este apelata
functia supraincarcata, dar aceasta este o practica mai putin frecventa.
La executia functiei f5(), la consola sunt afisate urmatoarele mesaje:
Creare punct nou
0 0
Distrugere punct
Creare punct nou
7 7
Distrugere punct
Creare punct nou
5 8
Distrugere punct
Operatorul new supraincarcat se apeleaza la fel ca operatorul new predefinit:
X* pX = new X(arg1, arg2, …);
Daca in clasa X operatorul new a fost supraincarcat, atunci este
apelata versiunea supraincarcata a acestuia, iar argumerntele arg1, arg2,…
sunt folosite pentru selectia constructorului clasei X, apelat implicit de functia
operator new().
In functia f5() s-au apelat trei constructori diferiti (constructorul
implicit, cu un argument si cu doua argumente) pentru primele trei alocari a
cate unui obiect din clasa Point si, de fiecare data, la distrugere a
fost apelata functia operator delete supraincarcata a clasei Point. Mesajele
afisate evidentiaza acest luccru.
?
Din exemplul prezentat mai sus se poate observa ca la constructia unui tablou
de obiecte de tip Point, (new Pointa7i) este folosit operatorul new predefinit
(global) si nu functia operator new a clasei Point. La fel, la stergerea tabloului
se foloseste operatorul global delete ai.
Aceasta situatie apare datorita faptului ca, pentru alocarea dinamica a tablourilor
de obiecte folosind operatori supraincarcati, trebuie sa fie supraincarcati
operatorii new si delete in versiunea pentru tablouri de obiecte care
arata astfel:
void* operator newai(size_t lungime); void operator deleteai(void* p);
4.6 Supraincarcarea operatorului de apel functie
Operatorul de apel al unei functii poate fi considerat o expresie binara de
forma: nume_functie(lista_argumente) unde lista_argumente introduce argumentele efective de apel ale functiei cu
numele nume_functie. In aceasta expresie binara operandul stanga
este numele functiei, iar operandul dreapta este lista de argumente de apel.
Operatorul () poate fi supraincarcat pentru o clasa data folosind o functie
membra nestatica a clasei printr-o constructie de forma: tip_returnat operator() (lista_argumente);
Lista de argumente este evaluata si utilizata dupa regulile obisnuite de transfer
ale argumentelor. Functia operator de apel poate fi supraincarcata pentru
orice clasa, dar ea este utila in special pentru acele clase care au fie
o singura functie, fie una dintre functiile membre este predominanta ca utilizare.
De exemplu, supraincarcarea functiei operator de apel pentru clasa Point,
poate arata astfel:
class PointA
//………….. public:
//………….
Point operator()(double a, double b)A cout << "Operator () " << endl; x = a; y = b; return *this;
S
S;
Pentru functia operator()() s-a ales o operatie de atribuire a unor valori
datelor membre ale clasei si utilizarea ei intr-o functie oarecare f6()
poate arata astfel:
void f6()A
Point p3(11,22); p3.Display(); // afiseaza 11 22 p3(4,5); // apel functie afiseaza Operator () p3.Display(); // afiseaza 4 5
S
Supraincarcarea operatorului de apel este frecvent utilizata in
definirea iteratorilor, care permit parcurgerea in ordinea dorita a elementelor
unei colectii, fara ca aceasta ordine sa depinda de modul de ordonare interna
a elementelor colectiei. Un astfel de exemplu este dat in sectiunea 8.
De asemenea, functia operator () () mai este utilizata in operatii cu
subsiruri si ca operator de indexare in tablourile multidimensionale.
4.7 Supraincarcarea operatorului pointer
Operatorul pointer -> poate fi considerat un operator unar postfix, care
se aplica operandului care il precede. Supraincarcarea acestui operator
se poate face printr-o functie nestatica membra a clasei, cu acceasi sintaxa
ca cea prezentata la inceputul acestei sectiuni. Fie o operatie de selectie
membru cu forma generala: obiect->expresie unde obiect este o instanta a unei clase oarecare X pentru care s-a supraincarcat
operatorul pointer. Evaluarea pentru expresie se executa in functie de
tipul de data returnat de functia supraincarcata a clasei X operator ->()astfel:
a) Daca functia operator->() a unei clase X returneaza un obiect de tipul
X, atunci se acceseaza elementul corespunzator (obtinut prin evaluarea expresie)
a clasei X.
b) Daca functia operator->() a unei clase Y returneaza un pointer la o data
de un tip oarecare, atunci se aplica operatorul -> predefinit, adica se selecteaza
o componenta a obiectului catre care indica pointerul returnat.
? Exemplul 4.6
Fie doua clase X si Y in care se supraincarca in mod deosebit
operatorul de pointer: in clasa X functia operator->() returneaza un
pointer la tipul X, iar in clasa Y functia operator->()returneaza un
pointer la tipul X (deci diferit de tipul Y al clasei respective).
class XA public: int x;
X* operator->() Areturn this;S void Display()A cout << x << endl;
S friend ifstream& operator>>(ifstream& stream,
X& obX);
S; ifstream& operator>>(ifstream& stream, X& obX)A stream >> obX.x; return stream;
S
X* read_from_disk(const char *p)A ifstream file(p);
X* pX = new X; file >> (*pX); return pX;
S class YA
X* pX; const char* name; public:
Y(const char *p):name(p) ApX=0;S
X* operator->() A if (pX==0) pX = read_from_disk(name); return pX;
S
S; void f7()A
X obX; obX->x = 10; cout << obX->x <<" " << obX.x << endl;
//10 10
Y obY("test.txt"); obY->Display(); //afiseaza valoarea citita
S
In acest exemplu implementarea functiei read_from_disk() folosind operatii
cu streamuri este mai putin interesanta (ea poate fi inteleasa mai bine
dupa prcurgerea operatiilor cu streamuri), ccea ce intereseaza este faptul ca
returneaza un pointer la un obiect de tipul X, obiect creat in memoria
heap si initializat prin citirea unor date dintr-un fisier de pe disc.
Din acest exemplu se pot observa cele doua modalitati de utilizare a functiei
operator->() supraincarcate in cele doua clase X si Y.
Functia operator->() din clasa X returneaza un pointer la X (chiar pointerul
this), de aceea poate fi utilizata pentru a accesa o data membra a clasei X.
De aceea, operatiile obX->x si obX.x sunt echivalente si mesajele afisate
la consola sunt identice, indicand valoarea datei membre x a obiectului
obX de tip X.
Functia operator->() din clasa Y returneaza un pointer la X, de aceea poate
fi utilizata pentru accesul la o data membra a clasei X, si anume functia Display().
La constructia obiectului obY, sunt initializate datele membre name = “test.txt”
si pX=0. Apelul functiei operator de dereferentiere pentru obiectul obY din
clasa Y apeleaza functia read_from_disk() care creaza un obiect de clasa X in
memoria heap, il initializeaza cu date citite din fisierul cu numele “test.txt”,
returneaza pointerul la acest obiect creat. Acest pointer este folosit pentru
selectarea functiei Display() a obiectului. La consola se afiseaza valoarea
numarului intreg citit din fisierul al carui nume este transferat ca argument
constructorului obiectului obY (“test.txt”)
?
Supraincarcarea operatorului pointer este o trasatura puternica a limbajului
C++, care permite construirea de programe interesante si flexibile.
4.8 Supraincarcarea operatorilor de conversie
Utilizarea unui constructor pentru a specifica o conversie de tip este posibila
pentru un numar limitat de situatii deoarece:
a) Nu exista o conversie implicita de la un tip definit de utilizator (clasa)
la un tip predefinit. b) Nu se poate defini o conversie de la un tip nou de date la un tip definit
mai inainte, fara modificarea tipului vechi.
Aceste probleme se pot rezolva prin definirea unui operator de conversie al
clasei. Intr-o clasa X, o functie membra X::operator T(), unde T este
un nume de tip (predefinit sau definit de utilizator) realizeaza conversia de
la tipul X la tipul T, in modul descris de functia operator. Se pot diferentia
doua categorii de operatori de conversie: conversie dintr-un tip definit de
utilizator intr-un tip predefinit si conversie dintr-un tip definit de
utilizator in alt tip definit de utilizator.
4.8.1 Conversia dintr-un tip definit de utilizator intr-un tip predefinit
Una din utilizarile frecvente ale operatorilor de conversie este aceea de
a converti un tip definit de utilizator (clasa) intr-un tip predefinit
printr-o functie membra nestatica a clasei respective. O astfel de functie nu
are nici un argument, dat fiind ca operatorul de conversie este un operator
unar (foloseste numai obiectul pentru care a fost apelat, al carui pointer this
il primeste implicit).
Pentru o clasa data se pot supraincarca mai multi operatori de conversie
de la clasa respectiva la unul din tipurile predefinite. Conversia definita
prin supraincarcarea operatorului de conversie poate fi apelata explicit
sau implicit. Apelul explicit al operatorului de conversie pentru un obiect
din clasa X catre tipul predefinit T poate avea doua forme:
T(obiect) sau T(obiect)
Conversia implicita (deci apelul implicit al functiei operator de conversie)
are loc la utilizarea unui obiect dintr-o clasa in care s-a definit o
astfel de functie intr-o expresie aritmetica sau conditionala. Daca sunt
definite mai multe functii operatori de conversie pot sa apara ambiguitati in
selectarea uneia dintre acestea in cazul conversiei implicite. Cateva
situatii de conversie sunt prezentate in exemplul urmator.
? Exemplul 4.7
Se completeaza clasa Point cu definirea catorva operatori de conversie,
care apoi sunt apelati in diferite modalitati.
class PointA
//……….. public: operator double(); operator void*();
//…………
S;
Point::operator double()A return sqrt(x*x + y*y);
S
Point::operator void*()A if (sqrt(x*x + y*y)) return &x; else return 0;
S void f8()A
Point p1(3, 4); double x = p1; cout << x << endl; // afiseaza 5 cout << (double)p1 << endl; // afiseaza 5 cout << double(p1) << endl; // afiseaza 5 void *pp1 = p1; cout << p1 <<" "<< pp1 <<endl;// afiseaza
o adresa if (p1) // eroare, conversie ambigua
// intr-o expresie conditionala cout << p1;
S
Se poate evita eroarea de compilare determinata de ambiguitatea la conversia
implicita din instructiunea if(p1), fortand in mod explicit unul
dintre operatorii de conversie ai clasei, de exemplu astfel: if ((void*)p1)…
4.8.2 Conversia dintr-un tip definit de utilizator intr-un alt tip definit
de utilizator
Fie doua clase X si Y. Conversia obiectelor de tip X in obiecte de tip
Y se poate realiza fie prin utilizarea constructorilor, fie prin supraincarcarea
operatorului de conversie in clasa Y.
Daca in clasa Y se defineste un constructor de tipul Y(X ob), atunci se
poate realiza o conversie a unui obiect de tip X intr-un obiect de tip
Y. Pentru accesul la datele private sau protected ale clasei X, este necesar
declararea friend class Y in clasa X.
? Exemplul 4.8
Se considera clasa Point, definita in aceasta sectiune si clasa Complex,
definita in sectiunea 2. Conversia datelor intre cele doua tipuri
are si o semnificatie matematica bine precizata, dat fiind ca un numar complex
poate fi reprezentat printr-un punct intr-un plan bidimensional (imaginea
numarului complex). Modul in care se realizeaza conversia datelor de tip
Complex in date de tip Point, deci calculul imaginii unui numar complex,
este prezentata in continuare.
class Complex A double re; double im; public:
Complex()Are = 0; im = 0;S
Complex(double r, double i) Are = r; im = i;S friend class Point;
S; class Point A
//………………… public:
Point(Complex c)A x = c.re; y = c.im;
S
S;
void f9()A
Complex c1(2.3, 9.7);
Point p1(c1);
Point p2 = c1; // alternativa de apel cout << p1; // afiseaza 2.3 9.7
S
?
Conversia dintr-un tip definit de utilizator intr-un alt tip definit
de utilizator se poate realiza si prin supraincarcarea operatorului de
conversie. Pentru conversia obiectelor de tip X in obiecte de tip Y, in
clasa X se defineste functia operator de conversie:
X::operator Y();
Pentru ca aceasta functie membra nestatica a clasei X sa aiba acces la date
private sau protejate ale clasei Y, ea se declara functie friend in clasa
Y.
In exemplul urmator se reia operatia de conversie din clasa Complex in
clasa Point prin supraincarcarea operatorului cu conversie in clasa
Complex.
? Exemplul 4.9
class Point; class ComplexA double re; double im; public:
Complex()Are = 0; im = 0;S
Complex(double r, double i) Are = r; im = i;S operator Point();
S; class Point A
//………….. public: friend Complex::operator Point();
S;
Complex::operator Point()A
Point tmp; tmp.x = re; tmp.y = im; return tmp;
S void f10()A
Complex c1(4,7);
Point p1; p1 = c1; cout << p1; // afiseaza 4 7
S
Se construieste mai intai obiectul p1 folosind constructorul implicit;
instructiunea p1 = c1 apeleaza operatorul de conversie la tipul Point definit
in clasa Complex; dupa aceasta conversie mai este apelata functia operator
de asignare a clasei Point, care, la returnare, construieste un obiect temporar
folosind constructorul de copiere al clasei Point.
?
Este de mentionat faptul ca este admisa definirea unei singure conversii de
la un tip de date la altul. Daca in Exemplul 4.9 s-ar pastra si constructorul
de conversie definit in Exemplul 4.8, atunci ar apare o eroare de compilare.
Se poate observa ca, in unele cazuri, o valoare de un tip dorit poate
fi construita prin utilizarea repetata a constructorilor si operatorilor de
conversie. Dintre acestea, o singura conversie implicita definita de utilizator
este legala. Situatiile in care un obiect poate fi construit in
mai multe feluri sunt ilegale (mesajul de eroare de compilare se refera la o
ambiguitate).